Merge pull request #10342 from misskey-dev/develop

Release: 13.10.0
This commit is contained in:
syuilo 2023-03-22 09:55:38 +09:00 committed by GitHub
commit 1e67e9c661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
330 changed files with 8271 additions and 2446 deletions

View File

@ -4,14 +4,20 @@ Thank you for your PR! Before creating a PR, please check the contribution guide
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
--> -->
# What ## What
<!-- このPRで何をしたのか どう変わるのか? --> <!-- このPRで何をしたのか どう変わるのか? -->
<!-- What did you do with this PR? How will it change things? --> <!-- What did you do with this PR? How will it change things? -->
# Why ## Why
<!-- なぜそうするのか? どういう意図なのか? 何が困っているのか? --> <!-- なぜそうするのか? どういう意図なのか? 何が困っているのか? -->
<!-- Why do you do it? What are your intentions? What is the problem? --> <!-- Why do you do it? What are your intentions? What is the problem? -->
# Additional info (optional) ## Additional info (optional)
<!-- テスト観点など --> <!-- テスト観点など -->
<!-- Test perspective, etc --> <!-- Test perspective, etc -->
## Checklist
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
- [ ] Test working in a local environment
- [ ] (If needed) Update CHANGELOG.md
- [ ] (If possible) Add tests

View File

@ -4,14 +4,20 @@ Thank you for your PR! Before creating a PR, please check the contribution guide
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
--> -->
# What ## What
<!-- このPRで何をしたのか どう変わるのか? --> <!-- このPRで何をしたのか どう変わるのか? -->
<!-- What did you do with this PR? How will it change things? --> <!-- What did you do with this PR? How will it change things? -->
# Why ## Why
<!-- なぜそうするのか? どういう意図なのか? 何が困っているのか? --> <!-- なぜそうするのか? どういう意図なのか? 何が困っているのか? -->
<!-- Why do you do it? What are your intentions? What is the problem? --> <!-- Why do you do it? What are your intentions? What is the problem? -->
# Additional info (optional) ## Additional info (optional)
<!-- テスト観点など --> <!-- テスト観点など -->
<!-- Test perspective, etc --> <!-- Test perspective, etc -->
## Checklist
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
- [ ] Test working in a local environment
- [ ] (If needed) Update CHANGELOG.md
- [ ] (If possible) Add tests

23
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,23 @@
<!-- お読みください / README
PRありがとうございます PRを作成する前に、コントリビューションガイドをご確認ください:
Thank you for your PR! Before creating a PR, please check the contribution guide:
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
-->
## What
<!-- このPRで何をしたのか どう変わるのか? -->
<!-- What did you do with this PR? How will it change things? -->
## Why
<!-- なぜそうするのか? どういう意図なのか? 何が困っているのか? -->
<!-- Why do you do it? What are your intentions? What is the problem? -->
## Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->
## Checklist
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
- [ ] Test working in a local environment
- [ ] (If needed) Update CHANGELOG.md
- [ ] (If possible) Add tests

59
.github/workflows/test-backend.yml vendored Normal file
View File

@ -0,0 +1,59 @@
name: Test (backend)
on:
push:
branches:
- master
- develop
pull_request:
jobs:
jest:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
services:
postgres:
image: postgres:13
ports:
- 54312:5432
env:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:6
ports:
- 56312:6379
steps:
- uses: actions/checkout@v3.3.0
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .github/misskey/test.yml .config
- name: Build
run: pnpm build
- name: Test
run: pnpm jest-and-coverage
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json

View File

@ -1,4 +1,4 @@
name: Test name: Test (frontend)
on: on:
push: push:
@ -8,26 +8,13 @@ on:
pull_request: pull_request:
jobs: jobs:
jest: vitest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [18.x] node-version: [18.x]
services:
postgres:
image: postgres:13
ports:
- 54312:5432
env:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:6
ports:
- 56312:6379
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0
with: with:
@ -51,12 +38,12 @@ jobs:
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Test - name: Test
run: pnpm jest-and-coverage run: pnpm --filter frontend test-and-coverage
- name: Upload Coverage - name: Upload Coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json files: ./packages/frontend/coverage/coverage-final.json
e2e: e2e:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -1,15 +1,71 @@
<!-- <!--
## 13.x.x (unreleased) ## 13.x.x (unreleased)
### Improvements ### General
- -
### Bugfixes ### Client
x -
### Server
-
You should also include the user name that made the change.
--> -->
## 13.10.0
### General
- ユーザーごとにRenoteをミュートできるように
- ノートごとに絵文字リアクションを受け取るか設定できるように
- クリップをお気に入りに登録できるように
- ノート検索の利用可否をロールで制御可能に(デフォルトでオフ)
- ロールの並び順を設定可能に
- カスタム絵文字にライセンス情報を付与できるように
- 指定した文字列を含む投稿の公開範囲をホームにできるように
- 使われてないアンテナは自動停止されるように
### Client
- 設定から自分のロールを確認できるように
- 広告一覧ページを追加
- ドライブクリーナーを追加
- DM作成時にメンションも含むように
- フォロー申請のボタンのデザインを改善
- 付箋ウィジェットの高さを設定可能に
- APオブジェクトを入力してフェッチする機能とユーザーやートの検索機能を分離
- ナビゲーションバーの項目に「プロフィール」を追加できるように
- ナビゲーションバーのカスタマイズをドラッグ&ドロップで行えるように
- ジョブキューの再試行をワンクリックでできるように
- AiScriptを0.13.1に更新
- oEmbedをサポートしているウェブサイトのプレビューができるように
- YouTubeをoEmbedでロードし、プレビューで共有ボタンを押すとOSの共有画面がでるように
- ([FirefoxでSpotifyのプレビューを開けるとフルサイズじゃなくプレビューサイズだけ再生できる問題](https://bugzilla.mozilla.org/show_bug.cgi?id=1792395)があります)
- (すでにブラウザーでキャッシュされたリンクに対しては以前のプレビュー行動が行われてます。その場合、ブラウザーのキャッシュをクリアしてまた試してください。)
- プロフィールで設定した情報が削除できない問題を修正
- ロールで広告を無効にするとadmin/adsでプレビューがでてこない問題を修正
- /api-consoleページにアクセスすると404が出る問題を修正
- Safariでプラグインが複数ある場合に正常に読み込まれない問題を修正
- Bookwyrmのユーザーのプロフィールページで「リモートで表示」をタップしても反応がない問題を修正
- 非ログイン時の「Misskeyについて」の表示を修正
- PC版にて「設定」「コントロールパネル」のリンクを2度以上続けてクリックした際に空白のページが表示される問題を修正
### Server
- OpenAPIエンドポイントを復旧
- WebP/AVIF/JPEGのweb公開用画像は、サーバーサイドではJPEGではなくWebPに変換するように
- アニメーション画像のサムネイルを生成するように
- アクティブユーザー数チャートの記録上限値を拡張
- Playのソースコード上限文字数を2倍に拡張
- 配送先サーバーが410 Goneで応答してきた場合は自動で配送停止をするように
- avatarBlurHash/bannerBlurHashの型をstringに限定
- タイムライン取得時のパフォーマンスを改善
- SMTP Login id length is too short
- API上で`visibility`を`followers`に設定してrenoteすると連合や削除で不具合が発生する問題を修正
- AWS S3からのファイル削除でNoSuchKeyエラーが出ると進めらない状態になる問題を修正
- `disableCache: true`を設定している場合に絵文字管理操作でエラーが出る問題を修正
- リテンション分析が上手く機能しないことがあるのを修正
- 空のアンテナが作成できないように修正
- 特定の条件で通報が見れない問題を修正
- 絵文字の名前に任意の文字が使用できる問題を修正
## 13.9.2 (2023/03/06) ## 13.9.2 (2023/03/06)
### Improvements ### Improvements
@ -246,8 +302,8 @@ You should also include the user name that made the change.
## 13.3.2 (2023/02/04) ## 13.3.2 (2023/02/04)
### Improvements ### Improvements
- 外部メディアプロキシへの対応を強化しました - 外部メディアプロキシへの対応を強化しました
外部メディアプロキシのFastify実装を作りました 外部メディアプロキシのFastify実装を作りました
https://github.com/misskey-dev/media-proxy https://github.com/misskey-dev/media-proxy
- Server: improve performance - Server: improve performance
@ -410,7 +466,7 @@ You should also include the user name that made the change.
- ユーザーごとのドライブ容量設定はロールに統合されました。 - ユーザーごとのドライブ容量設定はロールに統合されました。
- インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールもしくはコンディショナルロールでドライブ容量を編集してください。 - インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールもしくはコンディショナルロールでドライブ容量を編集してください。
- LTL/GTLの解放状態はロールに統合されました。 - LTL/GTLの解放状態はロールに統合されました。
- Dockerの実行をrootで行わないようにしました。Dockerかつオブジェクトストレージを使用していない場合は`chown -hR 991.991 ./files`を実行してください。 - Dockerの実行をrootで行わないようにしました。Dockerかつオブジェクトストレージを使用していない場合は`chown -hR 991.991 ./files`を実行してください。
https://github.com/misskey-dev/misskey/pull/9560 https://github.com/misskey-dev/misskey/pull/9560
#### For users #### For users
@ -638,7 +694,7 @@ You should also include the user name that made the change.
## 12.112.2 (2022/07/08) ## 12.112.2 (2022/07/08)
### Bugfixes ### Bugfixes
- Fix Docker doesn't work @mei23 - Fix Docker doesn't work @mei23
Still not working on arm64 environment. (See 12.112.0) Still not working on arm64 environment. (See 12.112.0)
## 12.112.1 (2022/07/07) ## 12.112.1 (2022/07/07)
@ -680,7 +736,7 @@ same as 12.112.0
- Improve player detection in URL preview @mei23 - Improve player detection in URL preview @mei23
- Add Badge Image to Push Notification #8012 @tamaina - Add Badge Image to Push Notification #8012 @tamaina
- Server: Improve performance - Server: Improve performance
- Server: Supports IPv6 on Redis transport. @mei23 - Server: Supports IPv6 on Redis transport. @mei23
IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`. IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`.
- Server: Add possibility to log IP addresses of users @syuilo - Server: Add possibility to log IP addresses of users @syuilo
- Add additional drive capacity change support @CyberRex0 - Add additional drive capacity change support @CyberRex0

View File

@ -52,13 +52,30 @@ describe('After setup instance', () => {
cy.intercept('POST', '/api/signup').as('signup'); cy.intercept('POST', '/api/signup').as('signup');
cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-username] input').type('alice'); cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-password] input').type('alice1234'); cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-password-retype] input').type('alice1234'); cy.get('[data-cy-signup-password-retype] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('not.be.disabled');
cy.get('[data-cy-signup-submit]').click(); cy.get('[data-cy-signup-submit]').click();
cy.wait('@signup'); cy.wait('@signup');
}); });
it('signup with duplicated username', () => {
cy.registerUser('alice', 'alice1234');
cy.visitHome();
// ユーザー名が重複している場合の挙動確認
cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('be.disabled');
});
}); });
describe('After user signup', () => { describe('After user signup', () => {

View File

@ -29,17 +29,17 @@ describe('After user signed in', () => {
it('first widget should be removed', () => { it('first widget should be removed', () => {
cy.get('.mk-widget-edit').click(); cy.get('.mk-widget-edit').click();
cy.get('.data-cy-customize-container:first-child .data-cy-customize-container-remove._button').click(); cy.get('[data-cy-customize-container]:first-child [data-cy-customize-container-remove]._button').click();
cy.get('.data-cy-customize-container').should('have.length', 2); cy.get('[data-cy-customize-container]').should('have.length', 2);
}); });
function buildWidgetTest(widgetName) { function buildWidgetTest(widgetName) {
it(`${widgetName} widget should get added`, () => { it(`${widgetName} widget should get added`, () => {
cy.get('.mk-widget-edit').click(); cy.get('.mk-widget-edit').click();
cy.get('.mk-widget-select select').select(widgetName, { force: true }); cy.get('.mk-widget-select select').select(widgetName, { force: true });
cy.get('.data-cy-bg._modalBg.data-cy-transparent').click({ multiple: true, force: true }); cy.get('[data-cy-bg]._modalBg[data-cy-transparent]').click({ multiple: true, force: true });
cy.get('.mk-widget-add').click({ force: true }); cy.get('.mk-widget-add').click({ force: true });
cy.get(`.data-cy-mkw-${widgetName}`).should('exist'); cy.get(`[data-cy-mkw-${widgetName}]`).should('exist');
}); });
} }

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "Als nicht NSFW markieren"
enterFileName: "Dateinamen eingeben" enterFileName: "Dateinamen eingeben"
mute: "Stummschalten" mute: "Stummschalten"
unmute: "Stummschaltung aufheben" unmute: "Stummschaltung aufheben"
renoteMute: "Renotes stummschalten"
renoteUnmute: "Renote-Stummschaltung aufheben"
block: "Blockieren" block: "Blockieren"
unblock: "Blockierung aufheben" unblock: "Blockierung aufheben"
suspend: "Sperren" suspend: "Sperren"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "Antworten in der Chronik anzeigen"
flagShowTimelineRepliesDescription: "Ist diese Option aktiviert, so werden Antworten von Benutzern auf die Notizen anderer Benutzer in der Chronik angezeigt." flagShowTimelineRepliesDescription: "Ist diese Option aktiviert, so werden Antworten von Benutzern auf die Notizen anderer Benutzer in der Chronik angezeigt."
autoAcceptFollowed: "Follow-Anfragen von Benutzern, denen du folgst, automatisch akzeptieren" autoAcceptFollowed: "Follow-Anfragen von Benutzern, denen du folgst, automatisch akzeptieren"
addAccount: "Benutzerkonto hinzufügen" addAccount: "Benutzerkonto hinzufügen"
reloadAccountsList: "Benutzerkontoliste aktualisieren"
loginFailed: "Anmeldung fehlgeschlagen" loginFailed: "Anmeldung fehlgeschlagen"
showOnRemote: "Auf Ursprungsinstanz ansehen" showOnRemote: "Auf Ursprungsinstanz ansehen"
general: "Allgemein" general: "Allgemein"
@ -544,6 +547,10 @@ userSuspended: "Dieser Benutzer wurde gesperrt."
userSilenced: "Dieser Benutzer wurde instanzweit stummgeschaltet." userSilenced: "Dieser Benutzer wurde instanzweit stummgeschaltet."
yourAccountSuspendedTitle: "Dieses Benutzerkonto ist gesperrt" yourAccountSuspendedTitle: "Dieses Benutzerkonto ist gesperrt"
yourAccountSuspendedDescription: "Dieses Benutzerkonto wurde gesperrt, da es gegen die Nutzungsbedingungen dieses Servers verstoßen hat. Trete mit dem Betreiber in Kontakt, falls du weitere Details erfahren möchtest. Bitte erstelle kein neues Benutzerkonto." yourAccountSuspendedDescription: "Dieses Benutzerkonto wurde gesperrt, da es gegen die Nutzungsbedingungen dieses Servers verstoßen hat. Trete mit dem Betreiber in Kontakt, falls du weitere Details erfahren möchtest. Bitte erstelle kein neues Benutzerkonto."
tokenRevoked: "Ungültiger Token"
tokenRevokedDescription: "Der Token ist abgelaufen. Bitte melde dich erneut an."
accountDeleted: "Benutzerkonto wurde gelöscht"
accountDeletedDescription: "Dieses Konto wurde gelöscht."
menu: "Menü" menu: "Menü"
divider: "Trenner" divider: "Trenner"
addItem: "Element hinzufügen" addItem: "Element hinzufügen"
@ -959,6 +966,18 @@ invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst ein
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"
cannotBeChangedLater: "Kann später nicht mehr geändert werden." cannotBeChangedLater: "Kann später nicht mehr geändert werden."
reactionAcceptance: "Reaktionsannahme"
likeOnly: "Nur \"Gefällt mir\""
likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen"
rolesAssignedToMe: "Mir zugewiesene Rollen"
resetPasswordConfirm: "Wirklich Passwort zurücksetzen?"
sensitiveWords: "Sensible Wörter"
sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden."
notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar."
license: "Lizenz"
unfavoriteConfirm: "Wirklich aus Favoriten entfernen?"
myClips: "Meine Clips"
drivecleaner: "Drive-Reiniger"
_achievements: _achievements:
earnedAt: "Freigeschaltet am" earnedAt: "Freigeschaltet am"
_types: _types:
@ -1106,7 +1125,7 @@ _achievements:
title: "Beliebt" title: "Beliebt"
description: "Die Anzahl deiner Follower hat 100 überschritten" description: "Die Anzahl deiner Follower hat 100 überschritten"
_followers300: _followers300:
title: "Stellt euch bitte in einer Reihe auf" title: "Eine geordnete Reihe, bitte!"
description: "Die Anzahl deiner Follower hat 300 überschritten" description: "Die Anzahl deiner Follower hat 300 überschritten"
_followers500: _followers500:
title: "Funkmast" title: "Funkmast"
@ -1218,6 +1237,8 @@ _role:
iconUrl: "Icon-URL" iconUrl: "Icon-URL"
asBadge: "Als Abzeichen anzeigen" asBadge: "Als Abzeichen anzeigen"
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt." descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
displayOrder: "Position"
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"
descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten."
priority: "Priorität" priority: "Priorität"
@ -1243,6 +1264,7 @@ _role:
rateLimitFactor: "Versuchsanzahl" rateLimitFactor: "Versuchsanzahl"
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
canHideAds: "Kann Werbung ausblenden" canHideAds: "Kann Werbung ausblenden"
canSearchNotes: "Nutzung der Notizsuchfunktion"
_condition: _condition:
isLocal: "Lokaler Benutzer" isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz" isRemote: "Benutzer fremder Instanz"
@ -1844,3 +1866,9 @@ _deck:
_dialog: _dialog:
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}" charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}" charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}"
_disabledTimeline:
title: "Chronik deaktiviert"
description: "Mit deinen jetzigen Rollen ist diese Chronik nicht verfügbar."
_drivecleaner:
orderBySizeDesc: "Absteigende Dateigrößen"
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"

View File

@ -67,7 +67,7 @@ import: "Import"
export: "Export" export: "Export"
files: "Files" files: "Files"
download: "Download" download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted." driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
unfollowConfirm: "Are you sure you want to unfollow {name}?" unfollowConfirm: "Are you sure you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
importRequested: "You've requested an import. This may take a while." importRequested: "You've requested an import. This may take a while."
@ -122,6 +122,8 @@ unmarkAsSensitive: "Unmark as NSFW"
enterFileName: "Enter filename" enterFileName: "Enter filename"
mute: "Mute" mute: "Mute"
unmute: "Unmute" unmute: "Unmute"
renoteMute: "Mute Renotes"
renoteUnmute: "Unmute Renotes"
block: "Block" block: "Block"
unblock: "Unblock" unblock: "Unblock"
suspend: "Suspend" suspend: "Suspend"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "Show replies in timeline"
flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on." flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on."
autoAcceptFollowed: "Automatically approve follow requests from users you're following" autoAcceptFollowed: "Automatically approve follow requests from users you're following"
addAccount: "Add account" addAccount: "Add account"
reloadAccountsList: "Reload account list"
loginFailed: "Failed to sign in" loginFailed: "Failed to sign in"
showOnRemote: "View on remote instance" showOnRemote: "View on remote instance"
general: "General" general: "General"
@ -527,7 +530,7 @@ nothing: "There's nothing to see here"
installedDate: "Authorized at" installedDate: "Authorized at"
lastUsedDate: "Last used at" lastUsedDate: "Last used at"
state: "State" state: "State"
sort: "Sort" sort: "Sorting order"
ascendingOrder: "Ascending" ascendingOrder: "Ascending"
descendingOrder: "Descending" descendingOrder: "Descending"
scratchpad: "Scratchpad" scratchpad: "Scratchpad"
@ -544,6 +547,10 @@ userSuspended: "This user has been suspended."
userSilenced: "This user is being silenced." userSilenced: "This user is being silenced."
yourAccountSuspendedTitle: "This account is suspended" yourAccountSuspendedTitle: "This account is suspended"
yourAccountSuspendedDescription: "This account has been suspended due to breaking the server's terms of services or similar. Contact the administrator if you would like to know a more detailed reason. Please do not create a new account." yourAccountSuspendedDescription: "This account has been suspended due to breaking the server's terms of services or similar. Contact the administrator if you would like to know a more detailed reason. Please do not create a new account."
tokenRevoked: "Invalid token"
tokenRevokedDescription: "This token has expired. Please log in again."
accountDeleted: "Account deleted"
accountDeletedDescription: "This account has been deleted."
menu: "Menu" menu: "Menu"
divider: "Divider" divider: "Divider"
addItem: "Add Item" addItem: "Add Item"
@ -959,6 +966,18 @@ invitationRequiredToRegister: "This instance is invite-only. You must enter a va
emailNotSupported: "This instance does not support sending emails" emailNotSupported: "This instance does not support sending emails"
postToTheChannel: "Post to channel" postToTheChannel: "Post to channel"
cannotBeChangedLater: "This cannot be changed later." cannotBeChangedLater: "This cannot be changed later."
reactionAcceptance: "Reaction Acceptance"
likeOnly: "Only likes"
likeOnlyForRemote: "Only likes for remote instances"
rolesAssignedToMe: "Roles assigned to me"
resetPasswordConfirm: "Really reset your password?"
sensitiveWords: "Sensitive words"
sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks."
notesSearchNotAvailable: "Note search is unavailable."
license: "License"
unfavoriteConfirm: "Really remove from favorites?"
myClips: "My clips"
drivecleaner: "Drive Cleaner"
_achievements: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:
@ -1218,6 +1237,8 @@ _role:
iconUrl: "Icon URL" iconUrl: "Icon URL"
asBadge: "Show as badge" asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
displayOrder: "Position"
descriptionOfDisplayOrder: "The higher the number, the higher its UI position."
canEditMembersByModerator: "Allow moderators to edit the list of members for this role" canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users."
priority: "Priority" priority: "Priority"
@ -1243,6 +1264,7 @@ _role:
rateLimitFactor: "Rate limit" rateLimitFactor: "Rate limit"
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
canHideAds: "Can hide ads" canHideAds: "Can hide ads"
canSearchNotes: "Usage of note search"
_condition: _condition:
isLocal: "Local user" isLocal: "Local user"
isRemote: "Remote user" isRemote: "Remote user"
@ -1844,3 +1866,9 @@ _deck:
_dialog: _dialog:
charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}." charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}."
charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}." charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}."
_disabledTimeline:
title: "Timeline disabled"
description: "You cannot use this timeline under your current roles."
_drivecleaner:
orderBySizeDesc: "Descending Filesizes"
orderByCreatedAtAsc: "Ascending Dates"

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "Desmarcar como sensible"
enterFileName: "Ingrese el nombre del archivo" enterFileName: "Ingrese el nombre del archivo"
mute: "Silenciar" mute: "Silenciar"
unmute: "Dejar de silenciar" unmute: "Dejar de silenciar"
renoteMute: "Silenciar renota"
renoteUnmute: "Desilenciar renota"
block: "Bloquear" block: "Bloquear"
unblock: "Dejar de bloquear" unblock: "Dejar de bloquear"
suspend: "Suspender" suspend: "Suspender"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "Mostrar respuestas a las notas en la biografía"
flagShowTimelineRepliesDescription: "Cuando se marca, la línea de tiempo muestra respuestas a otras notas además de las notas del usuario" flagShowTimelineRepliesDescription: "Cuando se marca, la línea de tiempo muestra respuestas a otras notas además de las notas del usuario"
autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues" autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues"
addAccount: "Agregar Cuenta" addAccount: "Agregar Cuenta"
reloadAccountsList: "Recargar lista de cuentas"
loginFailed: "Error al iniciar sesión." loginFailed: "Error al iniciar sesión."
showOnRemote: "Ver en una instancia remota" showOnRemote: "Ver en una instancia remota"
general: "General" general: "General"
@ -506,6 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
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"
showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)"
newNoteRecived: "Tienes una nota nueva" newNoteRecived: "Tienes una nota nueva"
sounds: "Sonidos" sounds: "Sonidos"
sound: "Sonidos" sound: "Sonidos"
@ -543,6 +547,10 @@ userSuspended: "Este usuario ha sido suspendido."
userSilenced: "Este usuario ha sido silenciado." userSilenced: "Este usuario ha sido silenciado."
yourAccountSuspendedTitle: "Esta cuenta ha sido suspendida" yourAccountSuspendedTitle: "Esta cuenta ha sido suspendida"
yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones de los términos de servicio del servidor y otras razones. Para más información, póngase en contacto con el administrador. Por favor, no cree una nueva cuenta." yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones de los términos de servicio del servidor y otras razones. Para más información, póngase en contacto con el administrador. Por favor, no cree una nueva cuenta."
tokenRevoked: "Token inválido"
tokenRevokedDescription: "Este token expiró, vuelve a iniciar sesión."
accountDeleted: "Cuenta borrada"
accountDeletedDescription: "Esta cuenta ha sido borrada."
menu: "Menú" menu: "Menú"
divider: "Divisor" divider: "Divisor"
addItem: "Agregar elemento" addItem: "Agregar elemento"
@ -955,6 +963,16 @@ 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." 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."
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"
postToTheChannel: "Publicar en el canal"
cannotBeChangedLater: "Esto no podrá ser cambiado después."
reactionAcceptance: "Aceptación de reacciones"
likeOnly: "Sólo 'me gusta'"
likeOnlyForRemote: "Sólo reacciones de instancias remotas"
rolesAssignedToMe: "Roles asignados a mí"
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
sensitiveWords: "Palabras sensibles"
sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea"
_achievements: _achievements:
earnedAt: "Desbloqueado el" earnedAt: "Desbloqueado el"
_types: _types:
@ -1214,6 +1232,8 @@ _role:
iconUrl: "URL del ícono" iconUrl: "URL del ícono"
asBadge: "Mostrar como emblema" asBadge: "Mostrar como emblema"
descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo." descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo."
displayOrder: "Posición"
descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz."
canEditMembersByModerator: "Permitir a los moderadores editar los miembros" canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo." descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo."
priority: "Prioridad" priority: "Prioridad"
@ -1239,6 +1259,7 @@ _role:
rateLimitFactor: "Limitador" rateLimitFactor: "Limitador"
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos" descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
canHideAds: "Puede ocultar anuncios" canHideAds: "Puede ocultar anuncios"
canSearchNotes: "Uso de la búsqueda de notas"
_condition: _condition:
isLocal: "Usuario local" isLocal: "Usuario local"
isRemote: "Usuario remoto" isRemote: "Usuario remoto"
@ -1840,3 +1861,6 @@ _deck:
_dialog: _dialog:
charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}." charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}."
charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}." charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}."
_disabledTimeline:
title: "Línea de tiempo deshabilitada"
description: "No puedes usar esta línea de tiempo con tus roles actuales."

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "Segna come non sensibile"
enterFileName: "Nome del file" enterFileName: "Nome del file"
mute: "Silenzia" mute: "Silenzia"
unmute: "Riattiva l'audio" unmute: "Riattiva l'audio"
renoteMute: "Silenzia i Rinota"
renoteUnmute: "Non silenziare i Rinota"
block: "Blocca" block: "Blocca"
unblock: "Sblocca" unblock: "Sblocca"
suspend: "Sospendi" suspend: "Sospendi"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline."
flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le risposte alle altre note dell'utente oltre a quelle dell'utente stesso." flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le risposte alle altre note dell'utente oltre a quelle dell'utente stesso."
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui" autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui"
addAccount: "Aggiungi profilo" addAccount: "Aggiungi profilo"
reloadAccountsList: "Ricarica l'elenco dei profili"
loginFailed: "Accesso non riuscito" loginFailed: "Accesso non riuscito"
showOnRemote: "Leggi sull'istanza remota" showOnRemote: "Leggi sull'istanza remota"
general: "Generali" general: "Generali"
@ -544,6 +547,10 @@ userSuspended: "L'utente è in sospensione"
userSilenced: "L'utente è silenziat@." userSilenced: "L'utente è silenziat@."
yourAccountSuspendedTitle: "Questo profilo è sospeso" yourAccountSuspendedTitle: "Questo profilo è sospeso"
yourAccountSuspendedDescription: "Questo profilo è stato sospeso a causa di una violazione del regolamento. Per informazioni, contattare l'amministrazione. Si prega di non creare un nuovo account." yourAccountSuspendedDescription: "Questo profilo è stato sospeso a causa di una violazione del regolamento. Per informazioni, contattare l'amministrazione. Si prega di non creare un nuovo account."
tokenRevoked: "Il token non è valido"
tokenRevokedDescription: "Il token di accesso è scaduto. Per favore, accedi nuovamente."
accountDeleted: "Profilo eliminato"
accountDeletedDescription: "Questo profilo è stato eliminato."
menu: "Menù" menu: "Menù"
divider: "Linea di separazione" divider: "Linea di separazione"
addItem: "Aggiungi elemento" addItem: "Aggiungi elemento"
@ -959,6 +966,17 @@ invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi i
emailNotSupported: "L'istanza non supporta l'invio di email" emailNotSupported: "L'istanza non supporta l'invio di email"
postToTheChannel: "Pubblica sul canale" postToTheChannel: "Pubblica sul canale"
cannotBeChangedLater: "Non sarà più modificabile" cannotBeChangedLater: "Non sarà più modificabile"
reactionAcceptance: "Accettazione reazioni"
likeOnly: "Solo i Like"
likeOnlyForRemote: "Solo Like remoti"
rolesAssignedToMe: "I miei ruoli"
resetPasswordConfirm: "Vuoi reimpostare la password?"
sensitiveWords: "Parole sensibili"
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."
license: "Licenza"
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
myClips: "Le mie Clip"
_achievements: _achievements:
earnedAt: "Data di conseguimento" earnedAt: "Data di conseguimento"
_types: _types:
@ -1218,6 +1236,8 @@ _role:
iconUrl: "URL dell'icona" iconUrl: "URL dell'icona"
asBadge: "Mostra come badge" asBadge: "Mostra come badge"
descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo." descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo."
displayOrder: "Ordine di visualizzazione"
descriptionOfDisplayOrder: "I valori più alti vengono visualizzati per primi"
canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo" canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo"
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
priority: "Priorità" priority: "Priorità"
@ -1243,6 +1263,7 @@ _role:
rateLimitFactor: "Limite del rapporto" rateLimitFactor: "Limite del rapporto"
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
canHideAds: "Può nascondere i banner" canHideAds: "Può nascondere i banner"
canSearchNotes: "Ricercare nelle Note"
_condition: _condition:
isLocal: "Profilo locale" isLocal: "Profilo locale"
isRemote: "Profilo remoto" isRemote: "Profilo remoto"
@ -1844,3 +1865,6 @@ _deck:
_dialog: _dialog:
charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})" charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})"
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})" charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
_disabledTimeline:
title: "Timeline disabilitata"
description: "Il tuo ruolo non ha i permessi per accedere a questa timeline"

View File

@ -67,7 +67,7 @@ import: "インポート"
export: "エクスポート" export: "エクスポート"
files: "ファイル" files: "ファイル"
download: "ダウンロード" download: "ダウンロード"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付したノートも消えます。" driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した全てのコンテンツからも削除されます。"
unfollowConfirm: "{name}のフォローを解除しますか?" unfollowConfirm: "{name}のフォローを解除しますか?"
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。" exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。" importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
@ -122,6 +122,8 @@ unmarkAsSensitive: "閲覧注意を解除する"
enterFileName: "ファイル名を入力" enterFileName: "ファイル名を入力"
mute: "ミュート" mute: "ミュート"
unmute: "ミュート解除" unmute: "ミュート解除"
renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除"
block: "ブロック" block: "ブロック"
unblock: "ブロック解除" unblock: "ブロック解除"
suspend: "凍結" suspend: "凍結"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "タイムラインにノートへの返信を表示す
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。" flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。"
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認" autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
addAccount: "アカウントを追加" addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗しました" loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示" showOnRemote: "リモートで表示"
general: "全般" general: "全般"
@ -544,6 +547,10 @@ userSuspended: "このユーザーは凍結されています。"
userSilenced: "このユーザーはサイレンスされています。" userSilenced: "このユーザーはサイレンスされています。"
yourAccountSuspendedTitle: "アカウントが凍結されています" yourAccountSuspendedTitle: "アカウントが凍結されています"
yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。" yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。"
tokenRevoked: "トークンが無効です"
tokenRevokedDescription: "ログイントークンが失効しています。ログインし直してください。"
accountDeleted: "アカウントは削除されています"
accountDeletedDescription: "このアカウントは削除されています。"
menu: "メニュー" menu: "メニュー"
divider: "分割線" divider: "分割線"
addItem: "項目を追加" addItem: "項目を追加"
@ -959,6 +966,21 @@ invitationRequiredToRegister: "現在このサーバーは招待制です。招
emailNotSupported: "このサーバーではメール配信はサポートされていません" emailNotSupported: "このサーバーではメール配信はサポートされていません"
postToTheChannel: "チャンネルに投稿" postToTheChannel: "チャンネルに投稿"
cannotBeChangedLater: "後から変更できません。" cannotBeChangedLater: "後から変更できません。"
reactionAcceptance: "リアクションの受け入れ"
likeOnly: "いいねのみ"
likeOnlyForRemote: "リモートからはいいねのみ"
rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
notesSearchNotAvailable: "ノート検索は利用できません。"
license: "ライセンス"
unfavoriteConfirm: "お気に入り解除しますか?"
myClips: "自分のクリップ"
drivecleaner: "ドライブクリーナー"
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
_achievements: _achievements:
earnedAt: "獲得日時" earnedAt: "獲得日時"
@ -1220,6 +1242,8 @@ _role:
iconUrl: "アイコン画像のURL" iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示" asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。" descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度" priority: "優先度"
@ -1245,6 +1269,7 @@ _role:
rateLimitFactor: "レートリミット" rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
canHideAds: "広告の非表示" canHideAds: "広告の非表示"
canSearchNotes: "ノート検索の利用可否"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
@ -1897,3 +1922,11 @@ _deck:
_dialog: _dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}" charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
charactersBelow: "最小文字数を下回っています! 現在 {current} / 制限 {min}" charactersBelow: "最小文字数を下回っています! 現在 {current} / 制限 {min}"
_disabledTimeline:
title: "無効化されたタイムライン"
description: "現在のロールでは、このタイムラインを使用することはできません。"
_drivecleaner:
orderBySizeDesc: "サイズが大きい順"
orderByCreatedAtAsc: "追加日が古い順"

View File

@ -2,7 +2,7 @@
_lang_: "日本語 (関西弁)" _lang_: "日本語 (関西弁)"
headlineMisskey: "ノートでつながるネットワーク" headlineMisskey: "ノートでつながるネットワーク"
introMisskey: "ようお越しMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀" introMisskey: "ようお越しMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀"
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>を使ったサービス(Misskeyインスタンスと呼ばれるやつや)のひとつやで。" poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつなんやで。"
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "探す" search: "探す"
notifications: "通知" notifications: "通知"
@ -15,13 +15,13 @@ gotIt: "ほい"
cancel: "やめとく" cancel: "やめとく"
noThankYou: "やめとく" noThankYou: "やめとく"
enterUsername: "ユーザー名を入れてや" enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenote" renotedBy: "{user}がRenoteしたで"
noNotes: "ノートはあらへん" noNotes: "ノートなんてあらへんで"
noNotifications: "通知はあらへん" noNotifications: "通知なんてあらへんで"
instance: "インスタンス" instance: "サーバー"
settings: "設定" settings: "設定"
basicSettings: "基本設定" basicSettings: "基本設定"
otherSettings: "その他の設定" otherSettings: "ほかの設定"
openInWindow: "ウィンドウで開くで" openInWindow: "ウィンドウで開くで"
profile: "プロフィール" profile: "プロフィール"
timeline: "タイムライン" timeline: "タイムライン"
@ -55,7 +55,7 @@ searchUser: "ユーザーを検索"
reply: "返事" reply: "返事"
loadMore: "まだまだあるで!" loadMore: "まだまだあるで!"
showMore: "まだまだあるで!" showMore: "まだまだあるで!"
showLess: "閉じる" showLess: "さいなら"
youGotNewFollower: "フォローされたで" youGotNewFollower: "フォローされたで"
receiveFollowRequest: "フォローリクエストされたで" receiveFollowRequest: "フォローリクエストされたで"
followRequestAccepted: "フォローが承認されたで" followRequestAccepted: "フォローが承認されたで"
@ -84,12 +84,12 @@ error: "エラー"
somethingHappened: "なんかアカンことが起こったで" somethingHappened: "なんかアカンことが起こったで"
retry: "もっぺんやる?" retry: "もっぺんやる?"
pageLoadError: "ページの読み込みに失敗してもうたわ…" pageLoadError: "ページの読み込みに失敗してもうたわ…"
pageLoadErrorDescription: "これは普通、ネットワークかブラウザキャッシュが原因やからね。キャッシュをクリアするか、もうちっとだけ待ってくれへんか" pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん"
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。" serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。" youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
enterListName: "リスト名を入れてや" enterListName: "リスト名を入れてや"
privacy: "プライバシー" privacy: "プライバシー"
makeFollowManuallyApprove: "他人のフォローは許可してからや!" makeFollowManuallyApprove: "ええって言わなフォローできへんようにする"
defaultNoteVisibility: "もとからの公開範囲" defaultNoteVisibility: "もとからの公開範囲"
follow: "フォロー" follow: "フォロー"
followRequest: "フォローを頼む" followRequest: "フォローを頼む"
@ -113,7 +113,7 @@ sensitive: "ちょっとアカンやつやで"
add: "増やす" add: "増やす"
reaction: "リアクション" reaction: "リアクション"
reactions: "リアクション" reactions: "リアクション"
reactionSetting: "Reaction that will be displayed in Picker. " reactionSetting: "ピッカーに出しとくリアクション"
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。" reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
rememberNoteVisibility: "公開範囲覚えといて" rememberNoteVisibility: "公開範囲覚えといて"
attachCancel: "のっけるのやめる" attachCancel: "のっけるのやめる"
@ -122,6 +122,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
enterFileName: "ファイル名を入れてや" enterFileName: "ファイル名を入れてや"
mute: "ミュート" mute: "ミュート"
unmute: "ミュートやめたる" unmute: "ミュートやめたる"
renoteMute: "リノートは見いひん"
renoteUnmute: "リノートもやっぱ見るわ"
block: "ブロック" block: "ブロック"
unblock: "ブロックやめたる" unblock: "ブロックやめたる"
suspend: "凍結" suspend: "凍結"
@ -145,20 +147,21 @@ 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: "タイムラインにノートへの返信を表示するで"
flagShowTimelineRepliesDescription: "オンにしたら、タイムラインにユーザーのノートの他にもそのユーザーの他のノートへの返信を表示するで。" flagShowTimelineRepliesDescription: "オンにしたら、タイムラインにユーザーのノートの他にもそのユーザーの他のノートへの返信を表示するで。"
autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく" autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく"
addAccount: "アカウントを追加" addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗してもうた…" loginFailed: "ログインに失敗してもうた…"
showOnRemote: "リモートで見る" showOnRemote: "リモートで見る"
general: "全般" general: "全般"
wallpaper: "壁紙" wallpaper: "壁紙"
setWallpaper: "壁紙を設定" setWallpaper: "壁紙を設定"
removeWallpaper: "壁紙を削除" removeWallpaper: "壁紙ほかす"
searchWith: "検索: {q}" searchWith: "検索: {q}"
youHaveNoLists: "リストがあらへんで?" youHaveNoLists: "リストがあらへんで?"
followConfirm: "{name}をフォローしてええか?" followConfirm: "{name}をフォローしてええか?"
@ -169,7 +172,7 @@ selectUser: "ユーザーを選ぶ"
recipient: "宛先" recipient: "宛先"
annotation: "注釈" annotation: "注釈"
federation: "連合" federation: "連合"
instances: "インスタンス" instances: "サーバー"
registeredAt: "初観測" registeredAt: "初観測"
latestRequestReceivedAt: "ちょっと前のリクエスト受信" latestRequestReceivedAt: "ちょっと前のリクエスト受信"
latestStatus: "ちょっと前のステータス" latestStatus: "ちょっと前のステータス"
@ -178,7 +181,7 @@ charts: "チャート"
perHour: "1時間ごと" perHour: "1時間ごと"
perDay: "1日ごと" perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送をやめる" stopActivityDelivery: "アクティビティの配送をやめる"
blockThisInstance: "このインスタンスをブロック" blockThisInstance: "このサーバーをブロックすんで"
operations: "操作" operations: "操作"
software: "ソフトウェア" software: "ソフトウェア"
version: "バージョン" version: "バージョン"
@ -189,28 +192,28 @@ jobQueue: "ジョブキュー"
cpuAndMemory: "CPUとメモリ" cpuAndMemory: "CPUとメモリ"
network: "ネットワーク" network: "ネットワーク"
disk: "ディスク" disk: "ディスク"
instanceInfo: "インスタンス情報" instanceInfo: "サーバー情報"
statistics: "統計" statistics: "統計"
clearQueue: "キューにさいなら" clearQueue: "キューにさいなら"
clearQueueConfirmTitle: "キューをクリアしまっか?" clearQueueConfirmTitle: "キューをクリアしまっか?"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。通常この操作を行う必要はあらへんや。" clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュにさいなら" clearCachedFiles: "キャッシュにさいなら"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?" clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "インスタンスブロック" blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定してな。ブロックされてもうたインスタンスとはもう金輪際やり取りできひんくなるで。" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー" mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"
noUsers: "ユーザーはおらん" noUsers: "ユーザーはおらん"
editProfile: "プロフィールをいじる" editProfile: "プロフィールをいじる"
noteDeleteConfirm: "このノートを削除しまっか?" noteDeleteConfirm: "このノートを削除しまっか?"
pinLimitExceeded: "これ以上ピン留めできひん" pinLimitExceeded: "これ以上ピン留めできひん"
intro: "Misskeyのインストールが完了してん!管理者アカウントを作ってや。" intro: "Misskeyのインストールが完了したで!管理者アカウントを作ってや。"
done: "でけた" done: "でけた"
processing: "処理しとる" processing: "処理しとる"
preview: "プレビュー" preview: "プレビュー"
default: "デフォルト" default: "デフォルト"
defaultValueIs: "デフォルト" defaultValueIs: "デフォルト: {value}"
noCustomEmojis: "絵文字はあらへん" noCustomEmojis: "絵文字はあらへん"
noJobs: "ジョブはあらへん" noJobs: "ジョブはあらへん"
federating: "連合しとる" federating: "連合しとる"
@ -220,17 +223,17 @@ all: "みんな"
subscribing: "購読しとる" subscribing: "購読しとる"
publishing: "配信しとる" publishing: "配信しとる"
notResponding: "応答してへんで" notResponding: "応答してへんで"
instanceFollowing: "インスタンスのフォロー" instanceFollowing: "サーバーのフォロー"
instanceFollowers: "インスタンスのフォロワー\n" instanceFollowers: "サーバーのフォロワー\n"
instanceUsers: "インスタンスのユーザー" instanceUsers: "サーバーのユーザー"
changePassword: "パスワード変える" changePassword: "パスワード変える"
security: "セキュリティ" security: "セキュリティ"
retypedNotMatch: "そやないねん。" retypedNotMatch: "入れたやつ同じになってないで。"
currentPassword: "今のパスワード" currentPassword: "今のパスワード"
newPassword: "今度のパスワード" newPassword: "のパスワード"
newPasswordRetype: "今度のパスワード(もっぺん入れて)" newPasswordRetype: "今度のパスワード(もっぺん入れて)"
attachFile: "ファイルのっける" attachFile: "ファイルのっける"
more: "他のやつ!" more: "他の"
featured: "ハイライト" featured: "ハイライト"
usernameOrUserId: "ユーザー名かユーザーID" usernameOrUserId: "ユーザー名かユーザーID"
noSuchUser: "ユーザーが見つからへんで" noSuchUser: "ユーザーが見つからへんで"
@ -238,15 +241,15 @@ lookup: "見てきて"
announcements: "お知らせ" announcements: "お知らせ"
imageUrl: "画像URL" imageUrl: "画像URL"
remove: "ほかす" remove: "ほかす"
removed: "削除したで!" removed: "ほかしたで!"
removeAreYouSure: "「{x}」はほかしてええか?" removeAreYouSure: "「{x}」はほかしてええか?"
deleteAreYouSure: "「{x}」はほかしてええか?" deleteAreYouSure: "「{x}」はほかしてええか?"
resetAreYouSure: "リセットしてええん?" resetAreYouSure: "リセットしてええん?"
saved: "保存したで!" saved: "保存したで!"
messaging: "チャット" messaging: "チャット"
upload: "アップロード" upload: "アップロード"
keepOriginalUploading: "オリジナル画像を保持するわ" keepOriginalUploading: "オリジナル画像のまんま"
keepOriginalUploadingDescription: "画像を上げるときにオリジナル版を保持するで。オフにしたら上げたときにブラウザでWeb公開用の画像を生成するで。 " keepOriginalUploadingDescription: "画像を上げるときにオリジナル版のまんまにするで。オフにしたら、上げたときにブラウザでWeb公開用の画像を生成するで。 "
fromDrive: "ドライブから" fromDrive: "ドライブから"
fromUrl: "URLから" fromUrl: "URLから"
uploadFromUrl: "URLアップロード" uploadFromUrl: "URLアップロード"
@ -272,8 +275,8 @@ yearsOld: "{age}歳"
registeredDate: "始めた日" registeredDate: "始めた日"
location: "場所" location: "場所"
theme: "テーマ" theme: "テーマ"
themeForLightMode: "ライトモードではこのテーマつこて" themeForLightMode: "ライトモードではこのテーマ使うて"
themeForDarkMode: "ダークモードではこのテーマつこて" themeForDarkMode: "ダークモードではこのテーマ使うて"
light: "ライト" light: "ライト"
dark: "ダーク" dark: "ダーク"
lightThemes: "デイゲーム" lightThemes: "デイゲーム"
@ -289,13 +292,13 @@ renameFile: "ファイル名をいらう"
folderName: "フォルダー名" folderName: "フォルダー名"
createFolder: "フォルダー作る" createFolder: "フォルダー作る"
renameFolder: "フォルダー名を変える" renameFolder: "フォルダー名を変える"
deleteFolder: "フォルダーを消してまう" deleteFolder: "フォルダーをほかす"
addFile: "ファイルを追加" addFile: "ファイルを追加"
emptyDrive: "ドライブにはなんも残っとらん" emptyDrive: "ドライブにはなんも残っとらん"
emptyFolder: "ふぉろだーにはなんも残っとらん" emptyFolder: "このフォルダーは空や"
unableToDelete: "消そうおもってんけどな、あかんかったわ" unableToDelete: "消そうおもってんけどな、あかんかったわ"
inputNewFileName: "今度のファイル名は何にするん?" inputNewFileName: "今度のファイル名は何にするん?"
inputNewDescription: "新しいキャプションを入力しましょ" inputNewDescription: "新しいキャプションを入れてや"
inputNewFolderName: "今度のフォルダ名は何にするん?" inputNewFolderName: "今度のフォルダ名は何にするん?"
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。" circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん" hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
@ -303,8 +306,8 @@ copyUrl: "URLをコピー"
rename: "名前を変えるで" rename: "名前を変えるで"
avatar: "アイコン" avatar: "アイコン"
banner: "バナー" banner: "バナー"
nsfw: "閲覧注意" nsfw: "見るんは気いつけてな"
whenServerDisconnected: "サーバーとの接続が切れたとき" whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき"
disconnectedFromServer: "サーバーが機嫌悪いねん" disconnectedFromServer: "サーバーが機嫌悪いねん"
reload: "リロード" reload: "リロード"
doNothing: "何もせんとく" doNothing: "何もせんとく"
@ -314,10 +317,10 @@ unwatch: "ウォッチやめる"
accept: "ええで" accept: "ええで"
reject: "あかん" reject: "あかん"
normal: "ええ感じ" normal: "ええ感じ"
instanceName: "インスタンス名" instanceName: "サーバー名"
instanceDescription: "インスタンスの紹介" instanceDescription: "サーバーの紹介"
maintainerName: "管理者の名前" maintainerName: "管理者はんの名前"
maintainerEmail: "管理者のメールアドレス" maintainerEmail: "管理者はんのメールアドレス"
tosUrl: "利用規約のURL" tosUrl: "利用規約のURL"
thisYear: "今年" thisYear: "今年"
thisMonth: "今月" thisMonth: "今月"
@ -329,23 +332,23 @@ pages: "ページ"
integration: "連携" integration: "連携"
connectService: "つなげるで" connectService: "つなげるで"
disconnectService: "切るで" disconnectService: "切るで"
enableLocalTimeline: "ローカルタイムラインを使えるようにする" enableLocalTimeline: "ローカルタイムラインを使えるようにする"
enableGlobalTimeline: "グローバルタイムラインを使えるようにする" enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。" disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
registration: "登録" registration: "登録"
enableRegistration: "一見さんでも誰でもいらっしゃ~い" enableRegistration: "一見さんでも誰でもいらっしゃ~い"
invite: "来てや" invite: "来てや"
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量" driveCapacityPerLocalAccount: "ローカルユーザーはんひとりあたりのドライブ容量"
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量" driveCapacityPerRemoteAccount: "リモートユーザーはんひとりあたりのドライブ容量"
inMb: "メガバイト単位" inMb: "メガバイト単位"
iconUrl: "アイコン画像のURL" iconUrl: "アイコン画像のURL"
bannerUrl: "バナー画像のURL" bannerUrl: "バナー画像のURL"
backgroundImageUrl: "背景画像のURL" backgroundImageUrl: "背景画像のURL"
basicInfo: "基本情報" basicInfo: "基本情報"
pinnedUsers: "ピン留めしたユーザー" pinnedUsers: "ピン留めしたユーザー"
pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。他ん人との名前は改行で区切ればええんやで。" pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。ユーザー毎に改行してや。"
pinnedPages: "ピン留めページ" pinnedPages: "ピン留めページ"
pinnedPagesDescription: "インスタンスのいっちゃん上にピン留めしたいページのパスを改行で区切って記述してな" pinnedPagesDescription: "サーバーのいっちゃん上にピン留めしたいページのパスを改行で区切って記述してな"
pinnedClipId: "ピン留めするクリップのID" pinnedClipId: "ピン留めするクリップのID"
pinnedNotes: "ピン留めされとるノート" pinnedNotes: "ピン留めされとるノート"
hcaptcha: "hCaptchaキャプチャ" hcaptcha: "hCaptchaキャプチャ"
@ -370,7 +373,7 @@ antennaExcludeKeywords: "除外キーワード"
antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や" antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や"
notifyAntenna: "新しいノートを通知すんで" notifyAntenna: "新しいノートを通知すんで"
withFileAntenna: "なんか添付されたノートだけ" withFileAntenna: "なんか添付されたノートだけ"
enableServiceworker: "ServiceWorkerをつこて" enableServiceworker: "ブラウザにプッシュ通知が行くようにする"
antennaUsersDescription: "ユーザー名を改行で区切ったってな" antennaUsersDescription: "ユーザー名を改行で区切ったってな"
caseSensitive: "大文字と小文字は別もんや" caseSensitive: "大文字と小文字は別もんや"
withReplies: "返信も入れたって" withReplies: "返信も入れたって"
@ -395,23 +398,23 @@ administrator: "管理者"
token: "トークン" token: "トークン"
2fa: "二要素認証" 2fa: "二要素認証"
totp: "認証アプリ" totp: "認証アプリ"
totpDescription: "認証アプリ使てワンタイムパスワードを入れる" totpDescription: "認証アプリ使てワンタイムパスワードを入れる"
moderator: "モデレーター" moderator: "モデレーター"
moderation: "モデレーション" moderation: "モデレーション"
nUsersMentioned: "{n}人が投稿" nUsersMentioned: "{n}人が投稿"
securityKeyAndPasskey: "セキュリティキー・パスキー" securityKeyAndPasskey: "セキュリティキー・パスキー"
securityKey: "セキュリティキー" securityKey: "セキュリティキー"
lastUsed: "最後につこうた日" lastUsed: "最後につこうた日"
lastUsedAt: "最後に使たん: {t}" lastUsedAt: "最後に使たん: {t}"
unregister: "登録やめる" unregister: "登録やめる"
passwordLessLogin: "パスワード無くてもログインできるようにする" passwordLessLogin: "パスワード無くてもログインできるようにする"
passwordLessLoginDescription: "パスワードやなくて、セキュリティキーとかパスキーだけでログインするわ" passwordLessLoginDescription: "パスワードなんかいらん、セキュリティキーとかパスキーだけでログインするわ"
resetPassword: "パスワードをリセット" resetPassword: "パスワードをリセット"
newPasswordIs: "今度のパスワードは「{password}」や" newPasswordIs: "今度のパスワードは「{password}」や"
reduceUiAnimation: "UIの動きやアニメーションを減らす" reduceUiAnimation: "UIの動きやアニメーションを少なする"
share: "わけわけ" share: "わけわけ"
notFound: "見つからへんね" notFound: "見つからへんね"
notFoundDescription: "指定されたURLに該当するページはあらへんやった。" notFoundDescription: "言われたURLにはまるページはなかったで。"
uploadFolder: "とりあえずアップロードしたやつ置いとく所" uploadFolder: "とりあえずアップロードしたやつ置いとく所"
cacheClear: "キャッシュをほかす" cacheClear: "キャッシュをほかす"
markAsReadAllNotifications: "通知はもう全て読んだわっ" markAsReadAllNotifications: "通知はもう全て読んだわっ"
@ -419,37 +422,37 @@ markAsReadAllUnreadNotes: "投稿は全て読んだわっ"
markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ" markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ"
help: "ヘルプ" help: "ヘルプ"
inputMessageHere: "ここにメッセージ書いてや" inputMessageHere: "ここにメッセージ書いてや"
close: "閉じる" close: "さいなら"
invites: "来てや" invites: "来てや"
members: "メンバー" members: "メンバーはん"
transfer: "譲渡" transfer: "譲渡"
title: "タイトル" title: "タイトル"
text: "テキスト" text: "テキスト"
enable: "有効にするで" enable: "有効にするで"
next: "次" next: "次"
retype: "もっかい入力" retype: "もっかい入力"
noteOf: "{user}のノート" noteOf: "{user}はんのノート"
quoteAttached: "引用付いとるで" quoteAttached: "引用付いとるで"
quoteQuestion: "引用として添付してもええか?" quoteQuestion: "引用として添付してもええか?"
noMessagesYet: "まだチャットはあらへんで" noMessagesYet: "まだチャットはあらへんで"
newMessageExists: "新しいメッセージがきたで" newMessageExists: "新しいメッセージがきたで"
onlyOneFileCanBeAttached: "すまん、メッセージに添付できるファイルはひとつだけなんや。" onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
signinRequired: "ログインしてくれへん?" signinRequired: "ログインしてくれへん?"
invitations: "来てや" invitations: "来てや"
invitationCode: "招待コード" invitationCode: "招待コード"
checking: "確認しとるで" checking: "確認しとるで"
available: "利用できる\n" available: "使えるで"
unavailable: "利用できん" unavailable: "利用できん"
usernameInvalidFormat: "a~z、A~Z、0~9、_が使えるで" usernameInvalidFormat: "a~z、A~Z、0~9、_が使えるで"
tooShort: "短すぎやろ!" tooShort: "短すぎやろ!"
tooLong: "長すぎやろ!" tooLong: "長すぎやろ!"
weakPassword: "へぼいパスワード" weakPassword: "へぼいパスワード"
normalPassword: "普通のパスワード" normalPassword: "ぼちぼちのパスワード"
strongPassword: "ええ感じのパスワード" strongPassword: "ええ感じのパスワード"
passwordMatched: "よし!一致や!" passwordMatched: "よし!一致や!"
passwordNotMatched: "一致しとらんで?" passwordNotMatched: "一致しとらんで?"
signinWith: "{x}でログイン" signinWith: "{x}でログイン"
signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみて。" signinFailed: "ログインできんかったで。もっかいユーザー名とパスワードを確認してみて。"
or: "それか" or: "それか"
language: "言語" language: "言語"
uiLanguage: "UIの表示言語" uiLanguage: "UIの表示言語"
@ -458,7 +461,7 @@ emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示せぇへん" disableDrawer: "メニューをドロワーで表示せぇへん"
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで" showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで"
noHistory: "履歴はあらへんねぇ。" noHistory: "履歴はないわ。"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "ややこしいMFMもありにする" enableAdvancedMfm: "ややこしいMFMもありにする"
enableAnimatedMfm: "動きがやかましいMFMも許したる" enableAnimatedMfm: "動きがやかましいMFMも許したる"
@ -466,12 +469,12 @@ doing: "やっとるがな"
category: "カテゴリ" category: "カテゴリ"
tags: "タグ" tags: "タグ"
docSource: "このドキュメントのソース" docSource: "このドキュメントのソース"
createAccount: "アカウントを作" createAccount: "アカウントを作るで"
existingAccount: "既存のアカウント" existingAccount: "前に作ったアカウント"
regenerate: "再生成" regenerate: "もっぺん生成するで"
fontSize: "フォントサイズ" fontSize: "字の大きさ"
noFollowRequests: "フォロー申請はあらへんで" noFollowRequests: "フォロー申請はあらへんで"
openImageInNewTab: "画像を新しいタブで開く" openImageInNewTab: "画像を新しいタブで開く"
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
local: "ローカル" local: "ローカル"
remote: "リモート" remote: "リモート"
@ -504,7 +507,7 @@ objectStorageUseProxy: "Proxyを使う"
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん" objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん"
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや" objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
serverLogs: "サーバーログ" serverLogs: "サーバーログ"
deleteAll: "全て削除してや" deleteAll: "全部ほかす"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?" showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)" showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)"
newNoteRecived: "新しいノートがあるで" newNoteRecived: "新しいノートがあるで"
@ -514,11 +517,11 @@ listen: "聴く"
none: "なし" none: "なし"
showInPage: "ページで表示" showInPage: "ページで表示"
popout: "ポップアウト" popout: "ポップアウト"
volume: "音量" volume: "やかましさ"
masterVolume: "全体の音量" masterVolume: "全体のやかましさ"
details: "もっと" details: "もっと"
chooseEmoji: "絵文字を選ぶ" chooseEmoji: "絵文字を選ぶ"
unableToProcess: "なんか作業が止まってしまったようやね" unableToProcess: "なんか奥の方で詰まってもうた"
recentUsed: "最近使ったやつ" recentUsed: "最近使ったやつ"
install: "インストール" install: "インストール"
uninstall: "アンインストール" uninstall: "アンインストール"
@ -536,14 +539,18 @@ output: "出力"
script: "スクリプト" script: "スクリプト"
disablePagesScript: "Pagesのスクリプトを無効にしてや" disablePagesScript: "Pagesのスクリプトを無効にしてや"
updateRemoteUser: "リモートユーザー情報の更新してくれん?" updateRemoteUser: "リモートユーザー情報の更新してくれん?"
deleteAllFiles: "すべてのファイルを削除" deleteAllFiles: "ファイルを全部ほかす"
deleteAllFilesConfirm: "ホンマにすべてのファイルを削除するん?消したもんはもう戻ってこんのやで?" deleteAllFilesConfirm: "ホンマにファイル全部ほかすんか?消したもんはもう戻ってこんのやで?"
removeAllFollowing: "フォローを全解除" removeAllFollowing: "フォローを全解除"
removeAllFollowingDescription: "{host}からのフォローをすべて解除するで。そのインスタンスが消えて無くなった時とかには便利な機能やで。" removeAllFollowingDescription: "{host}からのフォローをすべて解除するで。そのインスタンスが消えて無くなった時とかには便利な機能やで。"
userSuspended: "このユーザーは...凍結されとる。" userSuspended: "このユーザーは...凍結されとる。"
userSilenced: "このユーザーは...サイレンスされとる。" userSilenced: "このユーザーは...サイレンスされとる。"
yourAccountSuspendedTitle: "あんたのアカウント凍結されとるで" yourAccountSuspendedTitle: "あんたのアカウント凍結されとるで"
yourAccountSuspendedDescription: "あんたのアカウントは、サーバーの利用規約に違反したとかの理由で、凍結されとるで。細かいことは管理者までお問い合わせたってなー。絶対に新しいアカウント作ったらあかんで。絶対やで。" yourAccountSuspendedDescription: "あんたのアカウントは、サーバーの利用規約に違反したとかの理由で、凍結されとるで。細かいことは管理者までお問い合わせたってなー。絶対に新しいアカウント作ったらあかんで。絶対やで。"
tokenRevoked: "トークンが無効やで"
tokenRevokedDescription: "ログイントークンが失効しとるで。もっかいログインしてもろてもええか?"
accountDeleted: "アカウントは削除されとるで"
accountDeletedDescription: "このアカウントは削除されとるで。"
menu: "メニュー" menu: "メニュー"
divider: "分割線" divider: "分割線"
addItem: "項目を追加" addItem: "項目を追加"
@ -566,7 +573,7 @@ description: "説明"
describeFile: "キャプションを付ける" describeFile: "キャプションを付ける"
enterFileDescription: "キャプションを入力" enterFileDescription: "キャプションを入力"
author: "作者" author: "作者"
leaveConfirm: "未保存の変更があるで!ほかしてええか?" leaveConfirm: "あんた、いじったのにまだ保存してないで!ほかしてええか?"
manage: "管理" manage: "管理"
plugins: "プラグイン" plugins: "プラグイン"
preferencesBackups: "設定のバックアップ" preferencesBackups: "設定のバックアップ"
@ -600,12 +607,12 @@ smtpUser: "ユーザー名"
smtpPass: "パスワード" smtpPass: "パスワード"
emptyToDisableSmtpAuth: "ユーザー名とパスワードになんも入れんかったら、SMTP認証を無効化するで" emptyToDisableSmtpAuth: "ユーザー名とパスワードになんも入れんかったら、SMTP認証を無効化するで"
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使っとる時はオフにするで。" smtpSecureInfo: "STARTTLS使っとる時はオフにしてや。"
testEmail: "配信テスト" testEmail: "配信テスト"
wordMute: "ワードミュート" wordMute: "ワードミュート"
regexpError: "正規表現エラー" regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:"
instanceMute: "インスタンスミュート" instanceMute: "サーバーミュート"
userSaysSomething: "{name}が何か言うとるわ" userSaysSomething: "{name}が何か言うとるわ"
makeActive: "使うで" makeActive: "使うで"
display: "表示" display: "表示"
@ -624,7 +631,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
other: "その他" other: "その他"
regenerateLoginToken: "ログイントークンを再生成" regenerateLoginToken: "ログイントークンを再生成"
regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。" regenerateLoginTokenDescription: "ログインに使われる内部トークンをもっかい作るで。いつもならこれをやる必要はないで。もっかい作ると、全部のデバイスでログアウトされるで気ぃつけてなー。"
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できるで。" setMultipleBySeparatingWithSpace: "スペースで区切って何個でも設定できるで。"
fileIdOrUrl: "ファイルIDかURL" fileIdOrUrl: "ファイルIDかURL"
behavior: "動作" behavior: "動作"
sample: "サンプル" sample: "サンプル"
@ -636,7 +643,7 @@ abuseReported: "無事内容が送信されたみたいやで。おおきに〜
reporter: "通報者" reporter: "通報者"
reporteeOrigin: "通報先" reporteeOrigin: "通報先"
reporterOrigin: "通報元" reporterOrigin: "通報元"
forwardReport: "リモートインスタンスに通報を転送するで" forwardReport: "リモートサーバーに通報を転送するで"
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。" forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。"
send: "送信" send: "送信"
abuseMarkAsResolved: "対応したで" abuseMarkAsResolved: "対応したで"
@ -644,7 +651,7 @@ openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く" openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション" defaultNavigationBehaviour: "デフォルトのナビゲーション"
editTheseSettingsMayBreakAccount: "このへんの設定をようわからんままイジるとアカウントが壊れて使えんくなるかも知れへんで?" editTheseSettingsMayBreakAccount: "このへんの設定をようわからんままイジるとアカウントが壊れて使えんくなるかも知れへんで?"
instanceTicker: "ノートのインスタンス情報" instanceTicker: "ノートのサーバー情報"
waitingFor: "{x}を待っとるで" waitingFor: "{x}を待っとるで"
random: "ランダム" random: "ランダム"
system: "システム" system: "システム"
@ -655,7 +662,7 @@ createNew: "新しく作るで"
optional: "任意" optional: "任意"
createNewClip: "新しいクリップを作るで" createNewClip: "新しいクリップを作るで"
unclip: "クリップ解除するで" unclip: "クリップ解除するで"
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外したる" confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外しよか"
public: "パブリック" public: "パブリック"
i18nInfo: "Misskeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。" i18nInfo: "Misskeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。"
manageAccessTokens: "アクセストークンの管理" manageAccessTokens: "アクセストークンの管理"
@ -672,15 +679,15 @@ receivedReactionsCount: "リアクションされた数"
pollVotesCount: "アンケートに投票した数" pollVotesCount: "アンケートに投票した数"
pollVotedCount: "アンケートに投票された数" pollVotedCount: "アンケートに投票された数"
yes: "ええで" yes: "ええで"
no: "あかん" no: "あかん"
driveFilesCount: "ドライブのファイル数" driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量やで" driveUsage: "ドライブ使用量やで"
noCrawle: "クローラーによるインデックスを拒否するで" noCrawle: "クローラーによるインデックスを拒否するで"
noCrawleDescription: "検索エンジンにあんたのユーザーページ、ート、Pagesとかのコンテンツを登録(インデックス)せぇへんように頼むで。" noCrawleDescription: "検索エンジンにあんたのユーザーページ、ート、Pagesとかのコンテンツを登録(インデックス)せんように頼むで。邪魔すんねんやったら帰って〜。"
lockedAccountInfo: "フォローを承認制にしとっても、ノートの公開範囲を「フォロワー」にせぇへん限り、誰でもあんたのノートを見れるで。" lockedAccountInfo: "フォローを承認制にしとっても、ノートの公開範囲を「フォロワー」にせぇへん限り、誰でもあんたのノートを見れるで。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで" alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで"
loadRawImages: "添付画像のサムネイルをオリジナル画質にするで" loadRawImages: "添付画像のサムネイルをオリジナル画質にするで"
disableShowingAnimatedImages: "アニメーション画像を再生しやへんで" disableShowingAnimatedImages: "アニメーション画像を再生せんとくで"
verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。" verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。"
notSet: "未設定" notSet: "未設定"
emailVerified: "メールアドレスは確認されたで" emailVerified: "メールアドレスは確認されたで"
@ -690,14 +697,14 @@ pageLikedCount: "Pageにええやんと思ってくれた数"
contact: "連絡先" contact: "連絡先"
useSystemFont: "システムのデフォルトのフォントを使うで" useSystemFont: "システムのデフォルトのフォントを使うで"
clips: "クリップ" clips: "クリップ"
experimentalFeatures: "実験的機能やで" experimentalFeatures: "おためし機能やで"
developer: "開発者やで" developer: "開発者やで"
makeExplorable: "アカウントを見つけやすくするで" makeExplorable: "アカウントを見つけやすくするで"
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。" makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らんくなるで。"
showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示するで" showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示するで"
duplicate: "複製" duplicate: "複製"
left: "左" left: "左"
center: "" center: "真ん中"
wide: "広い" wide: "広い"
narrow: "狭い" narrow: "狭い"
reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?" reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?"
@ -708,7 +715,7 @@ onlineUsersCount: "{n}人が起きとるで"
nUsers: "{n}ユーザー" nUsers: "{n}ユーザー"
nNotes: "{n}ノート" nNotes: "{n}ノート"
sendErrorReports: "エラーリポートを送る" sendErrorReports: "エラーリポートを送る"
sendErrorReportsDescription: "オンにしたら、なんか変なことが起きたときにエラーの詳細がMisskeyに共有されて、ソフトウェアの品質向上に役立てられるんや。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴などが含まれるで。" sendErrorReportsDescription: "オンにしたら、変なことが起きたときにエラーの詳細がMisskeyに送られて、ソフトウェアの品質向上に使えるようになるで。エラー情報には、OSのバージョン、ブラウザの種類、行動履歴なんかが含まれるで。"
myTheme: "マイテーマ" myTheme: "マイテーマ"
backgroundColor: "背景" backgroundColor: "背景"
accentColor: "アクセント" accentColor: "アクセント"
@ -877,7 +884,7 @@ isSystemAccount: "システムが自動で作成・管理しとるアカウン
typeToConfirm: "この操作をやるんなら {x} と入力してなー" typeToConfirm: "この操作をやるんなら {x} と入力してなー"
deleteAccount: "アカウント削除するで" deleteAccount: "アカウント削除するで"
document: "ドキュメント" document: "ドキュメント"
numberOfPageCache: "ページキャッシュ数やで" numberOfPageCache: "ページ、どんだけキャッシュすんの?"
numberOfPageCacheDescription: "増やすと使いやすくなる、負荷とメモリ使用量が増えてくで。一長一短やな。" numberOfPageCacheDescription: "増やすと使いやすくなる、負荷とメモリ使用量が増えてくで。一長一短やな。"
logoutConfirm: "ログアウトしまっか?" logoutConfirm: "ログアウトしまっか?"
lastActiveDate: "最後に使った日時" lastActiveDate: "最後に使った日時"
@ -947,7 +954,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。"
thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingHome: "ホームに投稿"
thisPostMayBeAnnoyingCancel: "やめとく" thisPostMayBeAnnoyingCancel: "やめとく"
thisPostMayBeAnnoyingIgnore: "このまま投稿" thisPostMayBeAnnoyingIgnore: "このまま投稿"
collapseRenotes: "見たことあるRenoteは省略やで" collapseRenotes: "見たことあるRenoteは飛ばして表示するで"
internalServerError: "サーバー内部エラー" internalServerError: "サーバー内部エラー"
internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ" internalServerErrorDescription: "サーバー内部でよう分からんエラーやわ"
copyErrorInfo: "エラー情報をコピー" copyErrorInfo: "エラー情報をコピー"
@ -959,6 +966,17 @@ invitationRequiredToRegister: "今このサーバー招待制になってもう
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ" emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
postToTheChannel: "チャンネルに投稿" postToTheChannel: "チャンネルに投稿"
cannotBeChangedLater: "後からは変えられへんで。" cannotBeChangedLater: "後からは変えられへんで。"
reactionAcceptance: "リアクションの受け入れ"
likeOnly: "いいねだけ"
likeOnlyForRemote: "リモートからはいいねだけな"
rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワード作り直すんでええな?"
sensitiveWords: "けったいな単語"
sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。"
notesSearchNotAvailable: "ノート検索は使われへんで。"
license: "ライセンス"
unfavoriteConfirm: "ほんまに気に入らんの?"
myClips: "自分のクリップ"
_achievements: _achievements:
earnedAt: "貰った日ぃ" earnedAt: "貰った日ぃ"
_types: _types:
@ -976,7 +994,7 @@ _achievements:
title: "ノートの生駒山" title: "ノートの生駒山"
description: "ートを500回投稿した" description: "ートを500回投稿した"
_notes1000: _notes1000:
title: "ノートの山" title: "ノートの六甲山"
description: "ートを1,000回投稿した" description: "ートを1,000回投稿した"
_notes5000: _notes5000:
title: "箕面の滝からノート" title: "箕面の滝からノート"
@ -1218,6 +1236,8 @@ _role:
iconUrl: "アイコン画像のURL" iconUrl: "アイコン画像のURL"
asBadge: "バッジとして見せる" asBadge: "バッジとして見せる"
descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。" descriptionOfAsBadge: "オンにすると、ユーザー名の横んとこにロールのアイコンが表示されるで。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数がでかいほど、UI上で先に表示されるで。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになるで。オフにすると管理者のみが行えるで。"
priority: "優先度" priority: "優先度"
@ -1243,6 +1263,7 @@ _role:
rateLimitFactor: "レートリミット" rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩くなって、大きいほど制限されるで。" descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩くなって、大きいほど制限されるで。"
canHideAds: "広告を表示させへん" canHideAds: "広告を表示させへん"
canSearchNotes: "ノート検索を使わすかどうか"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
@ -1377,7 +1398,7 @@ _wordMute:
mutedNotes: "ミュートされたノート" mutedNotes: "ミュートされたノート"
_instanceMute: _instanceMute:
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。" instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription2: "改行で区切って設定するで" instanceMuteDescription2: "改行で区切って設定するんやで"
title: "設定したインスタンスのノートを隠すで。" title: "設定したインスタンスのノートを隠すで。"
heading: "ミュートするインスタンス" heading: "ミュートするインスタンス"
_theme: _theme:
@ -1464,7 +1485,7 @@ _sfx:
channel: "チャンネル通知" channel: "チャンネル通知"
_ago: _ago:
future: "未来" future: "未来"
justNow: "たった今" justNow: "ついさっき"
secondsAgo: "{n}秒前" secondsAgo: "{n}秒前"
minutesAgo: "{n}分前" minutesAgo: "{n}分前"
hoursAgo: "{n}時間前" hoursAgo: "{n}時間前"
@ -1587,7 +1608,7 @@ _weekday:
saturday: "土曜日" saturday: "土曜日"
_widgets: _widgets:
profile: "プロフィール" profile: "プロフィール"
instanceInfo: "インスタンス情報" instanceInfo: "サーバー情報"
memo: "付箋" memo: "付箋"
notifications: "通知" notifications: "通知"
timeline: "タイムライン" timeline: "タイムライン"
@ -1690,7 +1711,7 @@ _charts:
apRequest: "リクエスト" apRequest: "リクエスト"
usersIncDec: "ユーザーの増減" usersIncDec: "ユーザーの増減"
usersTotal: "ユーザーの合計" usersTotal: "ユーザーの合計"
activeUsers: "アクティブユーザー数" activeUsers: "いまおるユーザー数"
notesIncDec: "ノートの増減" notesIncDec: "ノートの増減"
localNotesIncDec: "ローカルのノートの増減" localNotesIncDec: "ローカルのノートの増減"
remoteNotesIncDec: "リモートのノートの増減" remoteNotesIncDec: "リモートのノートの増減"
@ -1844,3 +1865,6 @@ _deck:
_dialog: _dialog:
charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}" charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}"
charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}" charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}"
_disabledTimeline:
title: "使われへんタイムライン"
description: "あんたの今のロールやったら、このタイムラインは使われへんで。"

View File

@ -122,6 +122,8 @@ unmarkAsSensitive: "열람주의 해제"
enterFileName: "파일명을 입력" enterFileName: "파일명을 입력"
mute: "뮤트" mute: "뮤트"
unmute: "뮤트 해제" unmute: "뮤트 해제"
renoteMute: "리노트를 뮤트"
renoteUnmute: "리노트 뮤트 해제"
block: "차단" block: "차단"
unblock: "차단 해제" unblock: "차단 해제"
suspend: "정지" suspend: "정지"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기"
flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다." flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다."
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락" autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
addAccount: "계정 추가" addAccount: "계정 추가"
reloadAccountsList: "계정 리스트 정보 갱신"
loginFailed: "로그인에 실패했습니다" loginFailed: "로그인에 실패했습니다"
showOnRemote: "리모트에서 보기" showOnRemote: "리모트에서 보기"
general: "일반" general: "일반"
@ -506,6 +509,7 @@ objectStorageSetPublicRead: "업로드할 때 'public-read'를 설정하기"
serverLogs: "서버 로그" serverLogs: "서버 로그"
deleteAll: "모두 삭제" deleteAll: "모두 삭제"
showFixedPostForm: "타임라인 상단에 글 작성란을 표시" showFixedPostForm: "타임라인 상단에 글 작성란을 표시"
showFixedPostFormInChannel: "채널 타임라인 상단에 글 작성란을 표시"
newNoteRecived: "새 노트가 있습니다" newNoteRecived: "새 노트가 있습니다"
sounds: "소리" sounds: "소리"
sound: "소리" sound: "소리"
@ -543,6 +547,8 @@ userSuspended: "이 계정은 정지된 상태입니다."
userSilenced: "이 계정은 사일런스된 상태입니다." userSilenced: "이 계정은 사일런스된 상태입니다."
yourAccountSuspendedTitle: "계정이 정지되었습니다" yourAccountSuspendedTitle: "계정이 정지되었습니다"
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오." yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
accountDeleted: "계정이 정지되었습니다"
accountDeletedDescription: "이 계정이 삭제되었습니다."
menu: "메뉴" menu: "메뉴"
divider: "구분선" divider: "구분선"
addItem: "항목 추가" addItem: "항목 추가"
@ -955,6 +961,9 @@ exploreOtherServers: "다른 서버 둘러보기"
letsLookAtTimeline: "타임라인 구경하기" letsLookAtTimeline: "타임라인 구경하기"
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다." disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다." invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
postToTheChannel: "채널에 게시하기"
cannotBeChangedLater: "나중에 변경할 수 없습니다."
_achievements: _achievements:
earnedAt: "달성 일시" earnedAt: "달성 일시"
_types: _types:

View File

@ -2,7 +2,7 @@
_lang_: "中文(简体)" _lang_: "中文(简体)"
headlineMisskey: "通过帖子连接在一起的网络" headlineMisskey: "通过帖子连接在一起的网络"
introMisskey: "欢迎Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧📡\n通过「回应」功能可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧🚀" introMisskey: "欢迎Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧📡\n通过「回应」功能可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧🚀"
poweredByMisskeyDescription: "{name} 由开源平台 <b>Misskey</b> 驱动(也被称为 Misskey 服务器)" poweredByMisskeyDescription: "{name} 是开源平台 <b>Misskey</b> 的服务器之一。"
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "搜索" search: "搜索"
notifications: "通知" notifications: "通知"
@ -122,6 +122,8 @@ unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "请输入文件名" enterFileName: "请输入文件名"
mute: "屏蔽" mute: "屏蔽"
unmute: "解除屏蔽" unmute: "解除屏蔽"
renoteMute: "屏蔽转帖"
renoteUnmute: "解除屏蔽转帖"
block: "拉黑" block: "拉黑"
unblock: "取消拉黑" unblock: "取消拉黑"
suspend: "冻结" suspend: "冻结"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "在时间线上显示帖子的回复"
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。" flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
autoAcceptFollowed: "自动允许关注者的关注" autoAcceptFollowed: "自动允许关注者的关注"
addAccount: "添加账户" addAccount: "添加账户"
reloadAccountsList: "更新账户列表"
loginFailed: "登录失败" loginFailed: "登录失败"
showOnRemote: "转到所在服务器显示" showOnRemote: "转到所在服务器显示"
general: "常规设置" general: "常规设置"
@ -355,7 +358,7 @@ hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥" recaptchaSiteKey: "网站密钥"
recaptchaSecretKey: "reCAPTCHA 密钥" recaptchaSecretKey: "reCAPTCHA 密钥(SecretKey)"
turnstile: "Turnstile" turnstile: "Turnstile"
enableTurnstile: "启用Turnstile" enableTurnstile: "启用Turnstile"
turnstileSiteKey: "网站密钥" turnstileSiteKey: "网站密钥"
@ -489,7 +492,7 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐"
objectStorage: "对象存储" objectStorage: "对象存储"
useObjectStorage: "使用对象存储" useObjectStorage: "使用对象存储"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "用于引用的URL。如果您正在使用CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”" objectStorageBaseUrlDesc: "这里是用于参考的URL如果您正在使用CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”"
objectStorageBucket: "存储桶" objectStorageBucket: "存储桶"
objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。"
objectStoragePrefix: "前缀" objectStoragePrefix: "前缀"
@ -544,6 +547,10 @@ userSuspended: "该用户已被冻结。"
userSilenced: "该用户已被禁言。" userSilenced: "该用户已被禁言。"
yourAccountSuspendedTitle: "账户已被冻结" yourAccountSuspendedTitle: "账户已被冻结"
yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其他原因,该账户已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的账户。" yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其他原因,该账户已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的账户。"
tokenRevoked: "令牌无效"
tokenRevokedDescription: "登录令牌已经失效。请重新登录。"
accountDeleted: "帐户已删除"
accountDeletedDescription: "此帐户已经被删除。"
menu: "菜单" menu: "菜单"
divider: "分割线" divider: "分割线"
addItem: "添加项目" addItem: "添加项目"
@ -959,6 +966,17 @@ invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人
emailNotSupported: "此服务器不支持发送邮件" emailNotSupported: "此服务器不支持发送邮件"
postToTheChannel: "发布到频道" postToTheChannel: "发布到频道"
cannotBeChangedLater: "之后不能再更改。" cannotBeChangedLater: "之后不能再更改。"
reactionAcceptance: "接受表情回应"
likeOnly: "仅点赞"
likeOnlyForRemote: "远程仅点赞"
rolesAssignedToMe: "指派给自己的角色"
resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词"
sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。"
notesSearchNotAvailable: "帖子检索不可用"
license: "许可信息"
unfavoriteConfirm: "确定要取消收藏吗?"
myClips: "我的便签"
_achievements: _achievements:
earnedAt: "达成时间" earnedAt: "达成时间"
_types: _types:
@ -1218,6 +1236,8 @@ _role:
iconUrl: "图标URL" iconUrl: "图标URL"
asBadge: "作为徽章显示" asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。" descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
displayOrder: "显示顺序"
descriptionOfDisplayOrder: "数字越大,显示位置越靠前。"
canEditMembersByModerator: "允许监察者编辑成员" canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级" priority: "优先级"
@ -1243,6 +1263,7 @@ _role:
rateLimitFactor: "速率限制" rateLimitFactor: "速率限制"
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "可以隐藏广告" canHideAds: "可以隐藏广告"
canSearchNotes: "是否可以搜索帖子"
_condition: _condition:
isLocal: "是本地用户" isLocal: "是本地用户"
isRemote: "是远程用户" isRemote: "是远程用户"
@ -1517,7 +1538,7 @@ _2fa:
step4: "从现在开始,任何登录操作都将要求您提供动态口令。" step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
securityKeyNotSupported: "您的浏览器不支持安全密钥。" securityKeyNotSupported: "您的浏览器不支持安全密钥。"
registerTOTPBeforeKey: "要注册安全密钥或Passkey请先设置验证器应用程序。" registerTOTPBeforeKey: "要注册安全密钥或Passkey请先设置验证器应用程序。"
securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。" securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。"
chromePasskeyNotSupported: "目前不支持 Chrome 的Passkey。" chromePasskeyNotSupported: "目前不支持 Chrome 的Passkey。"
registerSecurityKey: "注册安全密钥或Passkey" registerSecurityKey: "注册安全密钥或Passkey"
securityKeyName: "输入密钥名称" securityKeyName: "输入密钥名称"
@ -1844,3 +1865,6 @@ _deck:
_dialog: _dialog:
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}" charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}" charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"
_disabledTimeline:
title: "时间线已禁用"
description: "您不能在当前角色使用时间线。"

View File

@ -122,14 +122,16 @@ unmarkAsSensitive: "取消標記為敏感內容"
enterFileName: "請輸入檔案名稱" enterFileName: "請輸入檔案名稱"
mute: "靜音" mute: "靜音"
unmute: "解除靜音" unmute: "解除靜音"
renoteMute: "將轉發貼文靜音"
renoteUnmute: "解除轉發貼文的靜音"
block: "封鎖" block: "封鎖"
unblock: "解除封鎖" unblock: "解除封鎖"
suspend: "凍結" suspend: "凍結"
unsuspend: "解除凍結" unsuspend: "解除凍結"
blockConfirm: "確定要封鎖此用戶?" blockConfirm: "確定要封鎖此用戶?"
unblockConfirm: "確定解除封鎖此用戶?" unblockConfirm: "確定解除封鎖此用戶?"
suspendConfirm: "確定凍結此帳" suspendConfirm: "確定凍結此帳"
unsuspendConfirm: "確定解凍此帳" unsuspendConfirm: "確定解凍此帳"
selectList: "選擇清單" selectList: "選擇清單"
selectChannel: "選擇頻道" selectChannel: "選擇頻道"
selectAntenna: "選擇天線" selectAntenna: "選擇天線"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。" flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
autoAcceptFollowed: "自動追隨中使用者的追隨請求" autoAcceptFollowed: "自動追隨中使用者的追隨請求"
addAccount: "添加帳戶" addAccount: "添加帳戶"
reloadAccountsList: "更新帳戶清單的資訊"
loginFailed: "登入失敗" loginFailed: "登入失敗"
showOnRemote: "轉到所在實例顯示" showOnRemote: "轉到所在實例顯示"
general: "一般" general: "一般"
@ -169,7 +172,7 @@ selectUser: "選取使用者"
recipient: "收件人" recipient: "收件人"
annotation: "註解" annotation: "註解"
federation: "站台聯邦" federation: "站台聯邦"
instances: "實例" instances: "伺服器"
registeredAt: "初次觀測" registeredAt: "初次觀測"
latestRequestReceivedAt: "上次收到的請求" latestRequestReceivedAt: "上次收到的請求"
latestStatus: "最後狀態" latestStatus: "最後狀態"
@ -403,7 +406,7 @@ securityKeyAndPasskey: "安全金鑰・Passkey"
securityKey: "安全金鑰" securityKey: "安全金鑰"
lastUsed: "上次使用" lastUsed: "上次使用"
lastUsedAt: "最後使用:{t}" lastUsedAt: "最後使用:{t}"
unregister: "註銷帳" unregister: "註銷帳"
passwordLessLogin: "設置無密碼登入" passwordLessLogin: "設置無密碼登入"
passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入" passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入"
resetPassword: "重置密碼" resetPassword: "重置密碼"
@ -544,6 +547,10 @@ userSuspended: "該使用者已被停用"
userSilenced: "該用戶已被禁言。" userSilenced: "該用戶已被禁言。"
yourAccountSuspendedTitle: "帳戶已被凍結" yourAccountSuspendedTitle: "帳戶已被凍結"
yourAccountSuspendedDescription: "由於違反了伺服器的服務條款或其他原因,該帳戶已被凍結。 您可以與管理員連繫以了解更多訊息。 請不要創建一個新的帳戶。" yourAccountSuspendedDescription: "由於違反了伺服器的服務條款或其他原因,該帳戶已被凍結。 您可以與管理員連繫以了解更多訊息。 請不要創建一個新的帳戶。"
tokenRevoked: "權杖無效"
tokenRevokedDescription: "登入權杖失效,請重新登入。"
accountDeleted: "帳戶已被刪除"
accountDeletedDescription: "這個帳戶已被刪除。"
menu: "選單" menu: "選單"
divider: "分割線" divider: "分割線"
addItem: "新增項目" addItem: "新增項目"
@ -872,10 +879,10 @@ recommended: "推薦"
check: "檢查" check: "檢查"
driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限" driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
driveCapOverrideCaption: "如果指定0以下的值就會被取消。" driveCapOverrideCaption: "如果指定0以下的值就會被取消。"
requireAdminForView: "必須以管理員帳登入才可以檢視。" requireAdminForView: "必須以管理員帳登入才可以檢視。"
isSystemAccount: "由系統自動建立與管理的帳。" isSystemAccount: "由系統自動建立與管理的帳。"
typeToConfirm: "要執行這項操作,請輸入 {x} " typeToConfirm: "要執行這項操作,請輸入 {x} "
deleteAccount: "刪除帳" deleteAccount: "刪除帳"
document: "文件" document: "文件"
numberOfPageCache: "快取頁面數" numberOfPageCache: "快取頁面數"
numberOfPageCacheDescription: "增加數量會提高便利性,但也會增加負荷與記憶體使用量。" numberOfPageCacheDescription: "增加數量會提高便利性,但也會增加負荷與記憶體使用量。"
@ -915,7 +922,7 @@ sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通
windowMaximize: "最大化" windowMaximize: "最大化"
windowRestore: "復原" windowRestore: "復原"
caption: "標題" caption: "標題"
loggedInAsBot: "以機器人帳登入中" loggedInAsBot: "以機器人帳登入中"
tools: "工具" tools: "工具"
cannotLoad: "無法載入" cannotLoad: "無法載入"
numberOfProfileView: "個人檔案檢視次數" numberOfProfileView: "個人檔案檢視次數"
@ -958,6 +965,14 @@ disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。" invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
emailNotSupported: "這個伺服器不支援寄送郵件" emailNotSupported: "這個伺服器不支援寄送郵件"
postToTheChannel: "發布到頻道" postToTheChannel: "發布到頻道"
cannotBeChangedLater: "之後不能變更。"
reactionAcceptance: "接受表情反應"
likeOnly: "僅限讚"
likeOnlyForRemote: "遠端僅限讚"
rolesAssignedToMe: "指派給自己的角色"
resetPasswordConfirm: "重設密碼?"
sensitiveWords: "敏感詞"
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
_achievements: _achievements:
earnedAt: "獲得日期" earnedAt: "獲得日期"
_types: _types:
@ -1217,6 +1232,8 @@ _role:
iconUrl: "圖示的URL" iconUrl: "圖示的URL"
asBadge: "顯示為徽章" asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。" descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
displayOrder: "顯示順序"
descriptionOfDisplayOrder: "數字越大顯示在UI上的越上面。"
canEditMembersByModerator: "允許編輯審查員的成員" canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級" priority: "優先級"
@ -1242,6 +1259,7 @@ _role:
rateLimitFactor: "速率限制" rateLimitFactor: "速率限制"
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
canHideAds: "不顯示廣告" canHideAds: "不顯示廣告"
canSearchNotes: "可否搜尋貼文"
_condition: _condition:
isLocal: "本地使用者" isLocal: "本地使用者"
isRemote: "遠端使用者" isRemote: "遠端使用者"
@ -1713,7 +1731,7 @@ _instanceCharts:
_timelines: _timelines:
home: "首頁" home: "首頁"
local: "本地" local: "本地"
social: "社" social: "社"
global: "公開" global: "公開"
_play: _play:
new: "新增Play" new: "新增Play"
@ -1843,3 +1861,6 @@ _deck:
_dialog: _dialog:
charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}" charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}"
charactersBelow: "低於最少字數!現在 {current} / 限制 {max}" charactersBelow: "低於最少字數!現在 {current} / 限制 {max}"
_disabledTimeline:
title: "停用的時間軸"
description: "目前的角色無法使用這個時間軸。"

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.9.2", "version": "13.10.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.27.0", "packageManager": "pnpm@7.29.3",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",
@ -31,8 +31,8 @@
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"jest": "cd packages/backend && pnpm jest", "jest": "cd packages/backend && pnpm jest",
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm jest", "test": "pnpm -r test",
"test-and-coverage": "pnpm jest-and-coverage", "test-and-coverage": "pnpm -r test-and-coverage",
"format": "pnpm exec gulp format", "format": "pnpm exec gulp format",
"clean": "node ./scripts/clean.js", "clean": "node ./scripts/clean.js",
"clean-all": "node ./scripts/clean-all.js", "clean-all": "node ./scripts/clean-all.js",
@ -55,12 +55,12 @@
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.53.0", "@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.53.0", "@typescript-eslint/parser": "5.54.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.7.0", "cypress": "12.7.0",
"eslint": "8.35.0", "eslint": "8.35.0",
"start-server-and-test": "1.15.4" "start-server-and-test": "2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.2.0" "@tensorflow/tfjs-core": "4.2.0"

View File

@ -19,6 +19,6 @@
</head> </head>
<body> <body>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc> <redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script> <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,16 @@
export class addRenoteMuting1665091090561 {
constructor() {
this.name = 'addRenoteMuting1665091090561';
}
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "renote_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_renoteMuting_id" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
}
async down(queryRunner) {
}
}

View File

@ -0,0 +1,15 @@
export class fixforeignkeyreports1675053125067 {
name = 'fixforeignkeyreports1675053125067'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`);
await queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`);
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT IF EXISTS "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`);
await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`);
}
}

View File

@ -0,0 +1,11 @@
export class perNoteReactionAcceptance1678164627293 {
name = 'perNoteReactionAcceptance1678164627293'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "reactionAcceptance" character varying(64)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "reactionAcceptance"`);
}
}

View File

@ -0,0 +1,68 @@
export class tweakVarcharLength1678426061773 {
name = 'tweakVarcharLength1678426061773'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "name" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "maintainerName" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "maintainerEmail" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "langs" TYPE character varying(1024) array`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedUsers" TYPE character varying(1024) array`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hiddenTags" TYPE character varying(1024) array`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "blockedHosts" TYPE character varying(1024) array`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "themeColor" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "bannerUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "backgroundImageUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "logoImageUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "errorImageUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "iconUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hcaptchaSiteKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "hcaptchaSecretKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "recaptchaSiteKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "recaptchaSecretKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "turnstileSiteKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "turnstileSecretKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "summalyProxy" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "email" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpHost" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpUser" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "smtpPass" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPublicKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPrivateKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "deeplAuthKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "ToSUrl" TO "termsOfServiceUrl"`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "termsOfServiceUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageBucket" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStoragePrefix" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageBaseUrl" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageEndpoint" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageRegion" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageAccessKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "objectStorageSecretKey" TYPE character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(65536)`, undefined);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___readWrite" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___read" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___write" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinWeek" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinMonth" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredWithinYear" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideWeek" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideMonth" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___registeredOutsideYear" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___readWrite" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___read" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___write" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinWeek" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinMonth" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredWithinYear" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideWeek" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideMonth" TYPE integer`);
await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___registeredOutsideYear" TYPE integer`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "termsOfServiceUrl" TO "ToSUrl"`);
}
}

View File

@ -0,0 +1,13 @@
export class removeUnused1678427401214 {
name = 'removeUnused1678427401214'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedClipId"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedClipId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`);
}
}

View File

@ -0,0 +1,11 @@
export class roleDisplayOrder1678602320354 {
name = 'roleDisplayOrder1678602320354'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" ADD "displayOrder" integer NOT NULL DEFAULT '0'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "displayOrder"`);
}
}

View File

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

View File

@ -0,0 +1,14 @@
export class retentionDateKey1678869617549 {
name = 'retentionDateKey1678869617549'
async up(queryRunner) {
await queryRunner.query(`TRUNCATE TABLE "retention_aggregation"`, undefined);
await queryRunner.query(`ALTER TABLE "retention_aggregation" ADD "dateKey" character varying(512) NOT NULL`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f7c3576b37bd2eec966ae24477" ON "retention_aggregation" ("dateKey") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_f7c3576b37bd2eec966ae24477"`);
await queryRunner.query(`ALTER TABLE "retention_aggregation" DROP COLUMN "dateKey"`);
}
}

View File

@ -0,0 +1,11 @@
export class addPropsForCustomEmoji1678945242650 {
name = 'addPropsForCustomEmoji1678945242650'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "emoji" ADD "license" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "license"`);
}
}

View File

@ -0,0 +1,23 @@
export class clipFavorite1678953978856 {
name = 'clipFavorite1678953978856'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "clip_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_1b539f43906f05ebcabe752a977" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_25a31662b0b0cc9af6549a9d71" ON "clip_favorite" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b1754a39d0b281e07ed7c078ec" ON "clip_favorite" ("userId", "clipId") `);
await queryRunner.query(`ALTER TABLE "clip" ADD "lastClippedAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`CREATE INDEX "IDX_a3eac04ae2aa9e221e7596114a" ON "clip" ("lastClippedAt") `);
await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_fce61c7986cee54393e79f1d849" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_fce61c7986cee54393e79f1d849"`);
await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711"`);
await queryRunner.query(`DROP INDEX "public"."IDX_a3eac04ae2aa9e221e7596114a"`);
await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "lastClippedAt"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b1754a39d0b281e07ed7c078ec"`);
await queryRunner.query(`DROP INDEX "public"."IDX_25a31662b0b0cc9af6549a9d71"`);
await queryRunner.query(`DROP TABLE "clip_favorite"`);
}
}

View File

@ -0,0 +1,17 @@
export class antennaActive1679309757174 {
name = 'antennaActive1679309757174'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now'`);
await queryRunner.query(`ALTER TABLE "antenna" ADD "isActive" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`CREATE INDEX "IDX_084c2abb8948ef59a37dce6ac1" ON "antenna" ("lastUsedAt") `);
await queryRunner.query(`CREATE INDEX "IDX_36ef5192a1ce55ed0e40aa4db5" ON "antenna" ("isActive") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_36ef5192a1ce55ed0e40aa4db5"`);
await queryRunner.query(`DROP INDEX "public"."IDX_084c2abb8948ef59a37dce6ac1"`);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "isActive"`);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "lastUsedAt"`);
}
}

View File

@ -23,29 +23,29 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "^1.3.11", "@swc/core-android-arm64": "^1.3.11",
"@swc/core-darwin-arm64": "^1.3.36", "@swc/core-darwin-arm64": "^1.3.38",
"@swc/core-darwin-x64": "^1.3.36", "@swc/core-darwin-x64": "^1.3.38",
"@swc/core-linux-arm-gnueabihf": "^1.3.36", "@swc/core-linux-arm-gnueabihf": "^1.3.38",
"@swc/core-linux-arm64-gnu": "^1.3.36", "@swc/core-linux-arm64-gnu": "^1.3.38",
"@swc/core-linux-arm64-musl": "^1.3.36", "@swc/core-linux-arm64-musl": "^1.3.38",
"@swc/core-linux-x64-gnu": "^1.3.36", "@swc/core-linux-x64-gnu": "^1.3.38",
"@swc/core-linux-x64-musl": "^1.3.36", "@swc/core-linux-x64-musl": "^1.3.38",
"@swc/core-win32-arm64-msvc": "^1.3.36", "@swc/core-win32-arm64-msvc": "^1.3.38",
"@swc/core-win32-ia32-msvc": "^1.3.36", "@swc/core-win32-ia32-msvc": "^1.3.38",
"@swc/core-win32-x64-msvc": "^1.3.36", "@swc/core-win32-x64-msvc": "^1.3.38",
"@tensorflow/tfjs": "4.2.0", "@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0" "@tensorflow/tfjs-node": "4.2.0"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "4.12.1", "@bull-board/api": "5.0.0",
"@bull-board/fastify": "4.12.1", "@bull-board/fastify": "5.0.0",
"@bull-board/ui": "4.12.1", "@bull-board/ui": "5.0.0",
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0", "@fastify/cors": "8.2.0",
"@fastify/http-proxy": "8.4.0", "@fastify/http-proxy": "8.4.0",
"@fastify/multipart": "7.4.1", "@fastify/multipart": "7.4.2",
"@fastify/static": "6.9.0", "@fastify/static": "6.9.0",
"@fastify/view": "7.4.1", "@fastify/view": "7.4.1",
"@nestjs/common": "9.3.9", "@nestjs/common": "9.3.9",
@ -54,7 +54,7 @@
"@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.36", "@swc/core": "1.3.38",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
@ -74,12 +74,12 @@
"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.13.0", "fastify": "4.14.1",
"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",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "12.5.3", "got": "12.6.0",
"happy-dom": "8.9.0", "happy-dom": "8.9.0",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"ioredis": "4.28.5", "ioredis": "4.28.5",
@ -102,7 +102,7 @@
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "^9.0.2", "otpauth": "^9.0.2",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.9.0", "pg": "8.10.0",
"private-ip": "3.0.0", "private-ip": "3.0.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
@ -128,10 +128,10 @@
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.17.10", "systeminformation": "5.17.12",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.3",
"tsconfig-paths": "4.1.2", "tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.11", "typeorm": "0.3.11",
@ -146,7 +146,7 @@
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.4.3", "@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.1",
@ -164,7 +164,7 @@
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5", "@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/node": "18.14.1", "@types/node": "18.15.0",
"@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",
@ -176,7 +176,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.0", "@types/sanitize-html": "2.8.1",
"@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",
@ -188,13 +188,13 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.52.0", "@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.53.0", "@typescript-eslint/parser": "5.54.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.35.0", "eslint": "8.35.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.4.3", "jest": "29.5.0",
"jest-mock": "29.4.3" "jest-mock": "29.5.0"
} }
} }

View File

@ -3,7 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.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 { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
export const ACHIEVEMENT_TYPES = [ export const ACHIEVEMENT_TYPES = [
'notes1', 'notes1',
@ -90,7 +90,7 @@ export class AchievementService {
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
) { ) {
} }
@ -114,7 +114,7 @@ export class AchievementService {
}], }],
}); });
this.createNotificationService.createNotification(userId, 'achievementEarned', { this.notificationService.createNotification(userId, 'achievementEarned', {
achievement: type, achievement: type,
}); });
} }

View File

@ -10,7 +10,7 @@ import { isUserRelated } from '@/misc/is-user-related.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js'; 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/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, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@ -71,12 +71,14 @@ export class AntennaService implements OnApplicationShutdown {
this.antennas.push({ this.antennas.push({
...body, ...body,
createdAt: new Date(body.createdAt), createdAt: new Date(body.createdAt),
lastUsedAt: new Date(body.lastUsedAt),
}); });
break; break;
case 'antennaUpdated': case 'antennaUpdated':
this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { this.antennas[this.antennas.findIndex(a => a.id === body.id)] = {
...body, ...body,
createdAt: new Date(body.createdAt), createdAt: new Date(body.createdAt),
lastUsedAt: new Date(body.lastUsedAt),
}; };
break; break;
case 'antennaDeleted': case 'antennaDeleted':
@ -217,7 +219,9 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis @bindThis
public async getAntennas() { public async getAntennas() {
if (!this.antennasFetched) { if (!this.antennasFetched) {
this.antennas = await this.antennasRepository.find(); this.antennas = await this.antennasRepository.findBy({
isActive: true,
});
this.antennasFetched = true; this.antennasFetched = true;
} }

View File

@ -5,7 +5,6 @@ import { AntennaService } from './AntennaService.js';
import { AppLockService } from './AppLockService.js'; import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js'; import { AchievementService } from './AchievementService.js';
import { CaptchaService } from './CaptchaService.js'; import { CaptchaService } from './CaptchaService.js';
import { CreateNotificationService } from './CreateNotificationService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js';
import { CustomEmojiService } from './CustomEmojiService.js'; import { CustomEmojiService } from './CustomEmojiService.js';
import { DeleteAccountService } from './DeleteAccountService.js'; import { DeleteAccountService } from './DeleteAccountService.js';
@ -82,6 +81,7 @@ import { HashtagEntityService } from './entities/HashtagEntityService.js';
import { InstanceEntityService } from './entities/InstanceEntityService.js'; import { InstanceEntityService } from './entities/InstanceEntityService.js';
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
import { MutingEntityService } from './entities/MutingEntityService.js'; import { MutingEntityService } from './entities/MutingEntityService.js';
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
import { NoteEntityService } from './entities/NoteEntityService.js'; import { NoteEntityService } from './entities/NoteEntityService.js';
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js'; import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js'; import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js';
@ -125,7 +125,6 @@ const $AntennaService: Provider = { provide: 'AntennaService', useExisting: Ante
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService };
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
@ -203,6 +202,7 @@ const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useEx
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService }; const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService }; const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService }; const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService }; const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService }; const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService }; const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
@ -248,7 +248,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppLockService, AppLockService,
AchievementService, AchievementService,
CaptchaService, CaptchaService,
CreateNotificationService,
CreateSystemUserService, CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
DeleteAccountService, DeleteAccountService,
@ -325,6 +324,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
InstanceEntityService, InstanceEntityService,
ModerationLogEntityService, ModerationLogEntityService,
MutingEntityService, MutingEntityService,
RenoteMutingEntityService,
NoteEntityService, NoteEntityService,
NoteFavoriteEntityService, NoteFavoriteEntityService,
NoteReactionEntityService, NoteReactionEntityService,
@ -365,7 +365,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppLockService, $AppLockService,
$AchievementService, $AchievementService,
$CaptchaService, $CaptchaService,
$CreateNotificationService,
$CreateSystemUserService, $CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
$DeleteAccountService, $DeleteAccountService,
@ -442,6 +441,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$InstanceEntityService, $InstanceEntityService,
$ModerationLogEntityService, $ModerationLogEntityService,
$MutingEntityService, $MutingEntityService,
$RenoteMutingEntityService,
$NoteEntityService, $NoteEntityService,
$NoteFavoriteEntityService, $NoteFavoriteEntityService,
$NoteReactionEntityService, $NoteReactionEntityService,
@ -483,7 +483,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppLockService, AppLockService,
AchievementService, AchievementService,
CaptchaService, CaptchaService,
CreateNotificationService,
CreateSystemUserService, CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
DeleteAccountService, DeleteAccountService,
@ -559,6 +558,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
InstanceEntityService, InstanceEntityService,
ModerationLogEntityService, ModerationLogEntityService,
MutingEntityService, MutingEntityService,
RenoteMutingEntityService,
NoteEntityService, NoteEntityService,
NoteFavoriteEntityService, NoteFavoriteEntityService,
NoteReactionEntityService, NoteReactionEntityService,
@ -599,7 +599,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppLockService, $AppLockService,
$AchievementService, $AchievementService,
$CaptchaService, $CaptchaService,
$CreateNotificationService,
$CreateSystemUserService, $CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
$DeleteAccountService, $DeleteAccountService,
@ -675,6 +674,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$InstanceEntityService, $InstanceEntityService,
$ModerationLogEntityService, $ModerationLogEntityService,
$MutingEntityService, $MutingEntityService,
$RenoteMutingEntityService,
$NoteEntityService, $NoteEntityService,
$NoteFavoriteEntityService, $NoteFavoriteEntityService,
$NoteReactionEntityService, $NoteReactionEntityService,
@ -708,4 +708,4 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#endregion //#endregion
], ],
}) })
export class CoreModule {} export class CoreModule { }

View File

@ -1,125 +0,0 @@
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class CreateNotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private notificationEntityService: NotificationEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
) {
}
@bindThis
public async createNotification(
notifieeId: User['id'],
type: Notification['type'],
data: Partial<Notification>,
): Promise<Notification | null> {
if (data.notifierId && (notifieeId === data.notifierId)) {
return null;
}
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type);
// Create notification
const notification = await this.notificationsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
notifieeId: notifieeId,
type: type,
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
isRead: isMuted,
...data,
} as Partial<Notification>)
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
const packed = await this.notificationEntityService.pack(notification, {});
// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
if (fresh == null) 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.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
}, () => { /* aborted, ignore it */ });
return notification;
}
// TODO
//const locales = await import('../../../../locales/index.js');
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
@bindThis
private async emailNotificationFollow(userId: User['id'], follower: User) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
@bindThis
private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
onApplicationShutdown(signal?: string | undefined): void {
this.#shutdownController.abort();
}
}

View File

@ -44,6 +44,7 @@ export class CustomEmojiService {
category: string | null; category: string | null;
aliases: string[]; aliases: string[];
host: string | null; host: string | null;
license: string | null;
}): Promise<Emoji> { }): Promise<Emoji> {
const emoji = await this.emojisRepository.insert({ const emoji = await this.emojisRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
@ -55,10 +56,11 @@ export class CustomEmojiService {
originalUrl: data.driveFile.url, originalUrl: data.driveFile.url,
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
type: data.driveFile.webpublicType ?? data.driveFile.type, type: data.driveFile.webpublicType ?? data.driveFile.type,
license: data.license,
}).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']); await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id), emoji: await this.emojiEntityService.packDetailed(emoji.id),

View File

@ -2,6 +2,7 @@ import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import sharp from 'sharp'; import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
@ -33,8 +34,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FileInfoService } from '@/core/FileInfoService.js'; import { FileInfoService } from '@/core/FileInfoService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import type S3 from 'aws-sdk/clients/s3.js';
import { correctFilename } from '@/misc/correct-filename.js'; import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import type S3 from 'aws-sdk/clients/s3.js';
type AddFileArgs = { type AddFileArgs = {
/** User who wish to add file */ /** User who wish to add file */
@ -274,8 +276,8 @@ export class DriveService {
} }
} }
if (!['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/svg+xml'].includes(type)) { if (!isMimeImage(type, 'sharp-convertible-image-with-bmp')) {
this.registerLogger.debug('web image and thumbnail not created (not an required file)'); this.registerLogger.debug('web image and thumbnail not created (cannot convert by sharp)');
return { return {
webpublic: null, webpublic: null,
thumbnail: null, thumbnail: null,
@ -284,22 +286,16 @@ export class DriveService {
let img: sharp.Sharp | null = null; let img: sharp.Sharp | null = null;
let satisfyWebpublic: boolean; let satisfyWebpublic: boolean;
let isAnimated: boolean;
try { try {
img = sharp(path); img = await sharpBmp(path, type);
const metadata = await img.metadata(); const metadata = await img.metadata();
const isAnimated = metadata.pages && metadata.pages > 1; isAnimated = !!(metadata.pages && metadata.pages > 1);
// skip animated
if (isAnimated) {
return {
webpublic: null,
thumbnail: null,
};
}
satisfyWebpublic = !!( satisfyWebpublic = !!(
type !== 'image/svg+xml' && type !== 'image/webp' && type !== 'image/avif' && type !== 'image/svg+xml' && // security reason
type !== 'image/avif' && // not supported by Mastodon and MS Edge
!(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) && !(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) &&
metadata.width && metadata.width <= 2048 && metadata.width && metadata.width <= 2048 &&
metadata.height && metadata.height <= 2048 metadata.height && metadata.height <= 2048
@ -315,15 +311,13 @@ export class DriveService {
// #region webpublic // #region webpublic
let webpublic: IImage | null = null; let webpublic: IImage | null = null;
if (generateWeb && !satisfyWebpublic) { if (generateWeb && !satisfyWebpublic && !isAnimated) {
this.registerLogger.info('creating web image'); this.registerLogger.info('creating web image');
try { try {
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) { if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToJpeg(img, 2048, 2048); webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
} else if (['image/png'].includes(type)) { } else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
} else if (['image/svg+xml'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
} else { } else {
this.registerLogger.debug('web image not created (not an required image)'); this.registerLogger.debug('web image not created (not an required image)');
@ -333,6 +327,7 @@ export class DriveService {
} }
} else { } else {
if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)'); if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)');
else if (isAnimated) this.registerLogger.info('web image not created (animated image)');
else this.registerLogger.info('web image not created (from remote)'); else this.registerLogger.info('web image not created (from remote)');
} }
// #endregion webpublic // #endregion webpublic
@ -341,10 +336,10 @@ export class DriveService {
let thumbnail: IImage | null = null; let thumbnail: IImage | null = null;
try { try {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(type)) { if (isAnimated) {
thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 280); thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
} else { } else {
this.registerLogger.debug('thumbnail not created (not an required file)'); thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422);
} }
} catch (err) { } catch (err) {
this.registerLogger.warn('thumbnail not created (an error occured)', err as Error); this.registerLogger.warn('thumbnail not created (an error occured)', err as Error);
@ -476,7 +471,7 @@ export class DriveService {
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、 // DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
// extを付加してデータベースの文字数制限に当たることはまずない // extを付加してデータベースの文字数制限に当たることはまずない
(name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled', (name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled',
info.type.ext info.type.ext,
); );
if (user && !force) { if (user && !force) {
@ -728,10 +723,20 @@ export class DriveService {
const s3 = this.s3Service.getS3(meta); const s3 = this.s3Service.getS3(meta);
await s3.deleteObject({ try {
Bucket: meta.objectStorageBucket!, await s3.deleteObject({
Key: key, Bucket: meta.objectStorageBucket!,
}).promise(); Key: key,
}).promise();
} catch (err: any) {
if (err.code === 'NoSuchKey') {
console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err);
return;
}
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
cause: err,
});
}
} }
@bindThis @bindThis
@ -749,7 +754,7 @@ export class DriveService {
}: UploadFromUrlArgs): Promise<DriveFile> { }: UploadFromUrlArgs): Promise<DriveFile> {
// Create temp file // Create temp file
const [path, cleanup] = await createTemp(); const [path, cleanup] = await createTemp();
try { try {
// write content at URL to temp file // write content at URL to temp file
const { filename: name } = await this.downloadService.downloadUrl(url, path); const { filename: name } = await this.downloadService.downloadUrl(url, path);

View File

@ -16,7 +16,7 @@ import type {
UserListStreamTypes, UserListStreamTypes,
UserStreamTypes, UserStreamTypes,
} from '@/server/api/stream/types.js'; } from '@/server/api/stream/types.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';

View File

@ -15,15 +15,28 @@ export type IImageStream = {
type: string; type: string;
}; };
export type IImageStreamable = IImage | IImageStream; export type IImageSharp = {
data: sharp.Sharp;
ext: string | null;
type: string;
};
export type IImageStreamable = IImage | IImageStream | IImageSharp;
export const webpDefault: sharp.WebpOptions = { export const webpDefault: sharp.WebpOptions = {
quality: 85, quality: 77,
alphaQuality: 95, alphaQuality: 95,
lossless: false, lossless: false,
nearLossless: false, nearLossless: false,
smartSubsample: true, smartSubsample: true,
mixed: true, mixed: true,
effort: 2,
};
export const avifDefault: sharp.AvifOptions = {
quality: 60,
lossless: false,
effort: 2,
}; };
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -37,36 +50,6 @@ export class ImageProcessingService {
) { ) {
} }
/**
* Convert to JPEG
* with resize, remove metadata, resolve orientation, stop animation
*/
@bindThis
public async convertToJpeg(path: string, width: number, height: number): Promise<IImage> {
return this.convertSharpToJpeg(await sharp(path), width, height);
}
@bindThis
public async convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
const data = await sharp
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.jpeg({
quality: 85,
progressive: true,
})
.toBuffer();
return {
data,
ext: 'jpg',
type: 'image/jpeg',
};
}
/** /**
* Convert to WebP * Convert to WebP
* with resize, remove metadata, resolve orientation, stop animation * with resize, remove metadata, resolve orientation, stop animation
@ -78,29 +61,22 @@ export class ImageProcessingService {
@bindThis @bindThis
public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> {
const data = await sharp const result = this.convertSharpToWebpStream(sharp, width, height, options);
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.webp(options)
.toBuffer();
return { return {
data, data: await result.data.toBuffer(),
ext: 'webp', ext: result.ext,
type: 'image/webp', type: result.type,
}; };
} }
@bindThis @bindThis
public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream { public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageSharp {
return this.convertSharpToWebpStream(sharp(path), width, height, options); return this.convertSharpToWebpStream(sharp(path), width, height, options);
} }
@bindThis @bindThis
public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream { public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageSharp {
const data = sharp const data = sharp
.resize(width, height, { .resize(width, height, {
fit: 'inside', fit: 'inside',
@ -115,13 +91,56 @@ export class ImageProcessingService {
type: 'image/webp', type: 'image/webp',
}; };
} }
/**
* Convert to Avif
* with resize, remove metadata, resolve orientation, stop animation
*/
@bindThis
public async convertToAvif(path: string, width: number, height: number, options: sharp.AvifOptions = avifDefault): Promise<IImage> {
return this.convertSharpToAvif(sharp(path), width, height, options);
}
@bindThis
public async convertSharpToAvif(sharp: sharp.Sharp, width: number, height: number, options: sharp.AvifOptions = avifDefault): Promise<IImage> {
const result = this.convertSharpToAvifStream(sharp, width, height, options);
return {
data: await result.data.toBuffer(),
ext: result.ext,
type: result.type,
};
}
@bindThis
public convertToAvifStream(path: string, width: number, height: number, options: sharp.AvifOptions = avifDefault): IImageSharp {
return this.convertSharpToAvifStream(sharp(path), width, height, options);
}
@bindThis
public convertSharpToAvifStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.AvifOptions = avifDefault): IImageSharp {
const data = sharp
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.avif(options);
return {
data,
ext: 'avif',
type: 'image/avif',
};
}
/** /**
* Convert to PNG * Convert to PNG
* with resize, remove metadata, resolve orientation, stop animation * with resize, remove metadata, resolve orientation, stop animation
*/ */
@bindThis @bindThis
public async convertToPng(path: string, width: number, height: number): Promise<IImage> { public async convertToPng(path: string, width: number, height: number): Promise<IImage> {
return this.convertSharpToPng(await sharp(path), width, height); return this.convertSharpToPng(sharp(path), width, height);
} }
@bindThis @bindThis

View File

@ -30,7 +30,7 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { HashtagService } from '@/core/HashtagService.js'; import { HashtagService } from '@/core/HashtagService.js';
import { AntennaService } from '@/core/AntennaService.js'; import { AntennaService } from '@/core/AntennaService.js';
@ -44,6 +44,7 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; 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';
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
@ -59,7 +60,7 @@ class NotificationManager {
constructor( constructor(
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
notifier: { id: User['id']; }, notifier: { id: User['id']; },
note: Note, note: Note,
) { ) {
@ -100,7 +101,7 @@ class NotificationManager {
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
if (!mentioneesMutedUserIds.includes(this.notifier.id)) { if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
this.createNotificationService.createNotification(x.target, x.reason, { this.notificationService.createNotification(x.target, x.reason, {
notifierId: this.notifier.id, notifierId: this.notifier.id,
noteId: this.note.id, noteId: this.note.id,
}); });
@ -125,6 +126,7 @@ type Option = {
files?: DriveFile[] | null; files?: DriveFile[] | null;
poll?: IPoll | null; poll?: IPoll | null;
localOnly?: boolean | null; localOnly?: boolean | null;
reactionAcceptance?: Note['reactionAcceptance'];
cw?: string | null; cw?: string | null;
visibility?: string; visibility?: string;
visibleUsers?: MinimumUser[] | null; visibleUsers?: MinimumUser[] | null;
@ -181,7 +183,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
private relayService: RelayService, private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private hashtagService: HashtagService, private hashtagService: HashtagService,
@ -191,11 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown {
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private roleService: RoleService, private roleService: RoleService,
private metaService: MetaService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) {} ) { }
@bindThis @bindThis
public async create(user: { public async create(user: {
@ -229,7 +232,9 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel != null) data.localOnly = true; if (data.channel != null) data.localOnly = true;
if (data.visibility === 'public' && data.channel == null) { if (data.visibility === 'public' && data.channel == null) {
if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home'; data.visibility = 'home';
} }
} }
@ -346,6 +351,7 @@ export class NoteCreateService implements OnApplicationShutdown {
emojis, emojis,
userId: user.id, userId: user.id,
localOnly: data.localOnly!, localOnly: data.localOnly!,
reactionAcceptance: data.reactionAcceptance,
visibility: data.visibility as any, visibility: data.visibility as any,
visibleUserIds: data.visibility === 'specified' visibleUserIds: data.visibility === 'specified'
? data.visibleUsers ? data.visibleUsers
@ -385,7 +391,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// 投稿を作成 // 投稿を作成
try { try {
if (insert.hasPoll) { if (insert.hasPoll) {
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.insert(Note, insert); await transactionalEntityManager.insert(Note, insert);
@ -408,7 +414,7 @@ export class NoteCreateService implements OnApplicationShutdown {
return insert; return insert;
} catch (e) { } catch (e) {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {
const err = new Error('Duplicated note'); const err = new Error('Duplicated note');
err.name = 'duplicated'; err.name = 'duplicated';
@ -552,7 +558,7 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
}); });
const nm = new NotificationManager(this.mutingsRepository, this.createNotificationService, user, note); const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
await this.createMentionedEvents(mentionedUsers, note, nm); await this.createMentionedEvents(mentionedUsers, note, nm);

View File

@ -4,7 +4,7 @@ import { In, IsNull, Not } 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 { Channel } from '@/models/entities/Channel.js';
import type { Packed } from '@/misc/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';

View File

@ -1,21 +1,37 @@
import { Inject, Injectable } from '@nestjs/common'; import { setTimeout } from 'node:timers/promises';
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 { NotificationsRepository } from '@/models/index.js'; import type { MutingsRepository, NotificationsRepository, 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';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEventService } from './GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from './PushNotificationService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { IdService } from '@/core/IdService.js';
@Injectable() @Injectable()
export class NotificationService { export class NotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor( constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.notificationsRepository) @Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository, private notificationsRepository: NotificationsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private notificationEntityService: NotificationEntityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
) { ) {
@ -67,4 +83,93 @@ export class NotificationService {
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds }); return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
} }
@bindThis
public async createNotification(
notifieeId: User['id'],
type: Notification['type'],
data: Partial<Notification>,
): Promise<Notification | null> {
if (data.notifierId && (notifieeId === data.notifierId)) {
return null;
}
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type);
// Create notification
const notification = await this.notificationsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
notifieeId: notifieeId,
type: type,
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
isRead: isMuted,
...data,
} as Partial<Notification>)
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
const packed = await this.notificationEntityService.pack(notification, {});
// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
if (fresh == null) 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.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
}, () => { /* aborted, ignore it */ });
return notification;
}
// TODO
//const locales = await import('../../../../locales/index.js');
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
@bindThis
private async emailNotificationFollow(userId: User['id'], follower: User) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
@bindThis
private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
onApplicationShutdown(signal?: string | undefined): void {
this.#shutdownController.abort();
}
} }

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import push from 'web-push'; import push from 'web-push';
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 { Packed } from '@/misc/schema'; import type { Packed } from '@/misc/json-schema';
import { getNoteSummary } from '@/misc/get-note-summary.js'; import { getNoteSummary } from '@/misc/get-note-summary.js';
import type { SwSubscriptionsRepository } from '@/models/index.js'; import type { SwSubscriptionsRepository } from '@/models/index.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets, ObjectLiteral } from 'typeorm'; import { Brackets, ObjectLiteral } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js'; import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { SelectQueryBuilder } from 'typeorm'; import type { SelectQueryBuilder } from 'typeorm';
@ -29,6 +29,9 @@ export class QueryService {
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
) { ) {
} }
@ -269,5 +272,24 @@ export class QueryService {
q.setParameters({ meId: me.id }); q.setParameters({ meId: me.id });
} }
} }
}
@bindThis
public generateMutedUserRenotesQueryForNotes(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
.select('renote_muting.muteeId')
.where('renote_muting.muterId = :muterId', { muterId: me.id });
q.andWhere(new Brackets(qb => {
qb
.where(new Brackets(qb => {
qb.where('note.renoteId IS NOT NULL');
qb.andWhere('note.text IS NULL');
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
}))
.orWhere('note.renoteId IS NULL')
.orWhere('note.text IS NOT NULL');
}));
q.setParameters(mutingQuery.getParameters());
}
}

View File

@ -26,7 +26,7 @@ export class QueueService {
) {} ) {}
@bindThis @bindThis
public deliver(user: ThinUser, content: IActivity | null, to: string | null) { public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
if (content == null) return null; if (content == null) return null;
if (to == null) return null; if (to == null) return null;
@ -36,6 +36,7 @@ export class QueueService {
}, },
content, content,
to, to,
isSharedInbox,
}; };
return this.deliverQueue.add(data, { return this.deliverQueue.add(data, {

View File

@ -9,7 +9,7 @@ import { IdService } from '@/core/IdService.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
import { emojiRegex } from '@/misc/emoji-regex.js'; import { emojiRegex } from '@/misc/emoji-regex.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
@ -79,7 +79,7 @@ export class ReactionService {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
) { ) {
} }
@ -93,15 +93,19 @@ export class ReactionService {
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
} }
} }
// check visibility // check visibility
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) { if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
} }
// TODO: cache if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
reaction = await this.toDbReaction(reaction, user.host); reaction = '❤️';
} else {
// TODO: cache
reaction = await this.toDbReaction(reaction, user.host);
}
const record: NoteReaction = { const record: NoteReaction = {
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@ -109,7 +113,7 @@ export class ReactionService {
userId: user.id, userId: user.id,
reaction, reaction,
}; };
// Create reaction // Create reaction
try { try {
await this.noteReactionsRepository.insert(record); await this.noteReactionsRepository.insert(record);
@ -119,7 +123,7 @@ export class ReactionService {
noteId: note.id, noteId: note.id,
userId: user.id, userId: user.id,
}); });
if (exists.reaction !== reaction) { if (exists.reaction !== reaction) {
// 別のリアクションがすでにされていたら置き換える // 別のリアクションがすでにされていたら置き換える
await this.delete(user, note); await this.delete(user, note);
@ -132,7 +136,7 @@ export class ReactionService {
throw e; throw e;
} }
} }
// Increment reactions count // Increment reactions count
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
@ -142,12 +146,12 @@ export class ReactionService {
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
this.perUserReactionsChart.update(user, note); this.perUserReactionsChart.update(user, note);
// カスタム絵文字リアクションだったら絵文字情報も送る // カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction); const decodedReaction = this.decodeReaction(reaction);
const emoji = await this.emojisRepository.findOne({ const emoji = await this.emojisRepository.findOne({
where: { where: {
name: decodedReaction.name, name: decodedReaction.name,
@ -155,7 +159,7 @@ export class ReactionService {
}, },
select: ['name', 'host', 'originalUrl', 'publicUrl'], select: ['name', 'host', 'originalUrl', 'publicUrl'],
}); });
this.globalEventService.publishNoteStream(note.id, 'reacted', { this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction, reaction: decodedReaction.reaction,
emoji: emoji != null ? { emoji: emoji != null ? {
@ -165,16 +169,16 @@ export class ReactionService {
} : null, } : null,
userId: user.id, userId: user.id,
}); });
// リアクションされたユーザーがローカルユーザーなら通知を作成 // リアクションされたユーザーがローカルユーザーなら通知を作成
if (note.userHost === null) { if (note.userHost === null) {
this.createNotificationService.createNotification(note.userId, 'reaction', { this.notificationService.createNotification(note.userId, 'reaction', {
notifierId: user.id, notifierId: user.id,
noteId: note.id, noteId: note.id,
reaction: reaction, reaction: reaction,
}); });
} }
//#region 配信 //#region 配信
if (this.userEntityService.isLocalUser(user) && !note.localOnly) { if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note)); const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note));
@ -183,7 +187,7 @@ export class ReactionService {
const reactee = await this.usersRepository.findOneBy({ id: note.userId }); const reactee = await this.usersRepository.findOneBy({ id: note.userId });
dm.addDirectRecipe(reactee as RemoteUser); dm.addDirectRecipe(reactee as RemoteUser);
} }
if (['public', 'home', 'followers'].includes(note.visibility)) { if (['public', 'home', 'followers'].includes(note.visibility)) {
dm.addFollowersRecipe(); dm.addFollowersRecipe();
} else if (note.visibility === 'specified') { } else if (note.visibility === 'specified') {
@ -192,7 +196,7 @@ export class ReactionService {
dm.addDirectRecipe(u as RemoteUser); dm.addDirectRecipe(u as RemoteUser);
} }
} }
dm.execute(); dm.execute();
} }
//#endregion //#endregion
@ -205,18 +209,18 @@ export class ReactionService {
noteId: note.id, noteId: note.id,
userId: user.id, userId: user.id,
}); });
if (exist == null) { if (exist == null) {
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
} }
// Delete reaction // Delete reaction
const result = await this.noteReactionsRepository.delete(exist.id); const result = await this.noteReactionsRepository.delete(exist.id);
if (result.affected !== 1) { if (result.affected !== 1) {
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
} }
// Decrement reactions count // Decrement reactions count
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
await this.notesRepository.createQueryBuilder().update() await this.notesRepository.createQueryBuilder().update()
@ -225,14 +229,14 @@ export class ReactionService {
}) })
.where('id = :id', { id: note.id }) .where('id = :id', { id: note.id })
.execute(); .execute();
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
this.globalEventService.publishNoteStream(note.id, 'unreacted', { this.globalEventService.publishNoteStream(note.id, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction, reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id, userId: user.id,
}); });
//#region 配信 //#region 配信
if (this.userEntityService.isLocalUser(user) && !note.localOnly) { if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user));
@ -246,7 +250,7 @@ export class ReactionService {
} }
//#endregion //#endregion
} }
@bindThis @bindThis
public async getFallbackReaction(): Promise<string> { public async getFallbackReaction(): Promise<string> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
@ -296,7 +300,7 @@ export class ReactionService {
// Unicode絵文字 // Unicode絵文字
const match = emojiRegex.exec(reaction); const match = emojiRegex.exec(reaction);
if (match) { if (match) {
// 合字を含む1つの絵文字 // 合字を含む1つの絵文字
const unicode = match[0]; const unicode = match[0];
// 異体字セレクタ除去 // 異体字セレクタ除去

View File

@ -57,7 +57,7 @@ export class RelayService {
const relayActor = await this.getRelayActor(); const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
const activity = this.apRendererService.addContext(follow); const activity = this.apRendererService.addContext(follow);
this.queueService.deliver(relayActor, activity, relay.inbox); this.queueService.deliver(relayActor, activity, relay.inbox, false);
return relay; return relay;
} }
@ -76,7 +76,7 @@ export class RelayService {
const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const undo = this.apRendererService.renderUndo(follow, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor);
const activity = this.apRendererService.addContext(undo); const activity = this.apRendererService.addContext(undo);
this.queueService.deliver(relayActor, activity, relay.inbox); this.queueService.deliver(relayActor, activity, relay.inbox, false);
await this.relaysRepository.delete(relay.id); await this.relaysRepository.delete(relay.id);
} }
@ -120,7 +120,7 @@ export class RelayService {
const signed = await this.apRendererService.attachLdSignature(copy, user); const signed = await this.apRendererService.attachLdSignature(copy, user);
for (const relay of relays) { for (const relay of relays) {
this.queueService.deliver(user, signed, relay.inbox); this.queueService.deliver(user, signed, relay.inbox, false);
} }
} }
} }

View File

@ -21,6 +21,7 @@ export type RolePolicies = {
canPublicNote: boolean; canPublicNote: boolean;
canInvite: boolean; canInvite: boolean;
canManageCustomEmojis: boolean; canManageCustomEmojis: boolean;
canSearchNotes: boolean;
canHideAds: boolean; canHideAds: boolean;
driveCapacityMb: number; driveCapacityMb: number;
pinLimit: number; pinLimit: number;
@ -40,6 +41,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canPublicNote: true, canPublicNote: true,
canInvite: false, canInvite: false,
canManageCustomEmojis: false, canManageCustomEmojis: false,
canSearchNotes: false,
canHideAds: false, canHideAds: false,
driveCapacityMb: 100, driveCapacityMb: 100,
pinLimit: 5, pinLimit: 5,
@ -264,6 +266,7 @@ export class RoleService implements OnApplicationShutdown {
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)),
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)), pinLimit: calc('pinLimit', vs => Math.max(...vs)),

View File

@ -19,12 +19,14 @@ export class S3Service {
@bindThis @bindThis
public getS3(meta: Meta) { public getS3(meta: Meta) {
const u = meta.objectStorageEndpoint != null const u = meta.objectStorageEndpoint
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` ? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`; : `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
return new S3({ return new S3({
endpoint: meta.objectStorageEndpoint ?? undefined, endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0
? meta.objectStorageEndpoint
: undefined,
accessKeyId: meta.objectStorageAccessKey!, accessKeyId: meta.objectStorageAccessKey!,
secretAccessKey: meta.objectStorageSecretKey!, secretAccessKey: meta.objectStorageSecretKey!,
region: meta.objectStorageRegion ?? undefined, region: meta.objectStorageRegion ?? undefined,

View File

@ -90,7 +90,7 @@ export class SignupService {
cipher: undefined, cipher: undefined,
passphrase: undefined, passphrase: undefined,
}, },
} as any, (err, publicKey, privateKey) => }, (err, publicKey, privateKey) =>
err ? rej(err) : res([publicKey, privateKey]), err ? rej(err) : res([publicKey, privateKey]),
)); ));

View File

@ -118,7 +118,7 @@ export class UserBlockingService implements OnApplicationShutdown {
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking)); const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking));
this.queueService.deliver(blocker, content, blockee.inbox); this.queueService.deliver(blocker, content, blockee.inbox, false);
} }
} }
@ -163,13 +163,13 @@ export class UserBlockingService implements OnApplicationShutdown {
// リモートにフォローリクエストをしていたらUndoFollow送信 // リモートにフォローリクエストをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
// リモートからフォローリクエストを受けていたらReject送信 // リモートからフォローリクエストを受けていたらReject送信
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@ -211,13 +211,13 @@ export class UserBlockingService implements OnApplicationShutdown {
// リモートにフォローをしていたらUndoFollow送信 // リモートにフォローをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
// リモートからフォローをされていたらRejectFollow送信 // リモートからフォローをされていたらRejectFollow送信
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@ -262,7 +262,7 @@ export class UserBlockingService implements OnApplicationShutdown {
// deliver if remote bloking // deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
this.queueService.deliver(blocker, content, blockee.inbox); this.queueService.deliver(blocker, content, blockee.inbox, false);
} }
} }

View File

@ -6,11 +6,11 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import InstanceChart from '@/core/chart/charts/instance.js'; import InstanceChart from '@/core/chart/charts/instance.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -57,7 +57,7 @@ export class UserFollowingService {
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService, private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@ -82,7 +82,7 @@ export class UserFollowingService {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
return; return;
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
@ -131,7 +131,7 @@ export class UserFollowingService {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@ -145,15 +145,15 @@ export class UserFollowingService {
}, },
): Promise<void> { ): Promise<void> {
if (follower.id === followee.id) return; if (follower.id === followee.id) return;
let alreadyFollowed = false as boolean; let alreadyFollowed = false as boolean;
await this.followingsRepository.insert({ await this.followingsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
// 非正規化 // 非正規化
followerHost: follower.host, followerHost: follower.host,
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null, followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null,
@ -169,35 +169,35 @@ export class UserFollowingService {
throw err; throw err;
} }
}); });
const req = await this.followRequestsRepository.findOneBy({ const req = await this.followRequestsRepository.findOneBy({
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
}); });
if (req) { if (req) {
await this.followRequestsRepository.delete({ await this.followRequestsRepository.delete({
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
}); });
// 通知を作成 // 通知を作成
this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', { this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
notifierId: followee.id, notifierId: followee.id,
}); });
} }
if (alreadyFollowed) return; if (alreadyFollowed) return;
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
//#region Increment counts //#region Increment counts
await Promise.all([ await Promise.all([
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1), this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1), this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
]); ]);
//#endregion //#endregion
//#region Update instance stats //#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(i => { this.federatedInstanceService.fetch(follower.host).then(i => {
@ -211,9 +211,9 @@ export class UserFollowingService {
}); });
} }
//#endregion //#endregion
this.perUserFollowingChart.update(follower, followee, true); this.perUserFollowingChart.update(follower, followee, true);
// Publish follow event // Publish follow event
if (this.userEntityService.isLocalUser(follower)) { if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
@ -221,7 +221,7 @@ export class UserFollowingService {
}).then(async packed => { }).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); 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'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'follow', { this.queueService.webhookDeliver(webhook, 'follow', {
@ -230,12 +230,12 @@ export class UserFollowingService {
} }
}); });
} }
// Publish followed event // Publish followed event
if (this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(async packed => { this.userEntityService.pack(follower.id, followee).then(async packed => {
this.globalEventService.publishMainStream(followee.id, 'followed', packed); this.globalEventService.publishMainStream(followee.id, 'followed', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'followed', { this.queueService.webhookDeliver(webhook, 'followed', {
@ -243,9 +243,9 @@ export class UserFollowingService {
}); });
} }
}); });
// 通知を作成 // 通知を作成
this.createNotificationService.createNotification(followee.id, 'follow', { this.notificationService.createNotification(followee.id, 'follow', {
notifierId: follower.id, notifierId: follower.id,
}); });
} }
@ -265,16 +265,16 @@ export class UserFollowingService {
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
}); });
if (following == null) { if (following == null) {
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
return; return;
} }
await this.followingsRepository.delete(following.id); await this.followingsRepository.delete(following.id);
this.decrementFollowing(follower, followee); this.decrementFollowing(follower, followee);
// Publish unfollow event // Publish unfollow event
if (!silent && this.userEntityService.isLocalUser(follower)) { if (!silent && this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
@ -282,7 +282,7 @@ export class UserFollowingService {
}).then(async packed => { }).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', 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'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'unfollow', { this.queueService.webhookDeliver(webhook, 'unfollow', {
@ -291,33 +291,33 @@ export class UserFollowingService {
} }
}); });
} }
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
// local user has null host // local user has null host
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@bindThis @bindThis
private async decrementFollowing( private async decrementFollowing(
follower: {id: User['id']; host: User['host']; }, follower: { id: User['id']; host: User['host']; },
followee: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; },
): Promise<void> { ): Promise<void> {
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
//#region Decrement following / followers counts //#region Decrement following / followers counts
await Promise.all([ await Promise.all([
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1), this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
]); ]);
//#endregion //#endregion
//#region Update instance stats //#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(i => { this.federatedInstanceService.fetch(follower.host).then(i => {
@ -331,7 +331,7 @@ export class UserFollowingService {
}); });
} }
//#endregion //#endregion
this.perUserFollowingChart.update(follower, followee, false); this.perUserFollowingChart.update(follower, followee, false);
} }
@ -346,23 +346,23 @@ export class UserFollowingService {
requestId?: string, requestId?: string,
): Promise<void> { ): Promise<void> {
if (follower.id === followee.id) return; if (follower.id === followee.id) return;
// check blocking // check blocking
const [blocking, blocked] = await Promise.all([ const [blocking, blocked] = await Promise.all([
this.userBlockingService.checkBlocked(follower.id, followee.id), this.userBlockingService.checkBlocked(follower.id, followee.id),
this.userBlockingService.checkBlocked(followee.id, follower.id), this.userBlockingService.checkBlocked(followee.id, follower.id),
]); ]);
if (blocking) throw new Error('blocking'); if (blocking) throw new Error('blocking');
if (blocked) throw new Error('blocked'); if (blocked) throw new Error('blocked');
const followRequest = await this.followRequestsRepository.insert({ const followRequest = await this.followRequestsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
requestId, requestId,
// 非正規化 // 非正規化
followerHost: follower.host, followerHost: follower.host,
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined, followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined,
@ -371,25 +371,25 @@ export class UserFollowingService {
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined, followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined, followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
// Publish receiveRequest event // Publish receiveRequest event
if (this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed)); this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成 // 通知を作成
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', { this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
notifierId: follower.id, notifierId: follower.id,
followRequestId: followRequest.id, followRequestId: followRequest.id,
}); });
} }
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)); const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee));
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
} }
@ -404,26 +404,26 @@ export class UserFollowingService {
): Promise<void> { ): Promise<void> {
if (this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
this.queueService.deliver(follower, content, followee.inbox); this.queueService.deliver(follower, content, followee.inbox, false);
} }
} }
const request = await this.followRequestsRepository.findOneBy({ const request = await this.followRequestsRepository.findOneBy({
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
}); });
if (request == null) { if (request == null) {
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
} }
await this.followRequestsRepository.delete({ await this.followRequestsRepository.delete({
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
}); });
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
@ -440,18 +440,18 @@ export class UserFollowingService {
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
}); });
if (request == null) { if (request == null) {
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.'); throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
} }
await this.insertFollowingDoc(followee, follower); await this.insertFollowingDoc(followee, follower);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
@ -466,13 +466,13 @@ export class UserFollowingService {
const requests = await this.followRequestsRepository.findBy({ const requests = await this.followRequestsRepository.findBy({
followeeId: user.id, followeeId: user.id,
}); });
for (const request of requests) { for (const request of requests) {
const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId }); const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId });
this.acceptFollowRequest(user, follower); this.acceptFollowRequest(user, follower);
} }
} }
/** /**
* API following/request/reject * API following/request/reject
*/ */
@ -557,7 +557,7 @@ export class UserFollowingService {
}); });
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox, false);
} }
/** /**

View File

@ -54,7 +54,7 @@ export class UserSuspendService {
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user, content, inbox); this.queueService.deliver(user, content, inbox, true);
} }
} }
} }
@ -84,7 +84,7 @@ export class UserSuspendService {
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user as any, content, inbox); this.queueService.deliver(user as any, content, inbox, true);
} }
} }
} }

View File

@ -157,7 +157,8 @@ class DeliverManager {
public async execute() { public async execute() {
if (!this.userEntityService.isLocalUser(this.actor)) return; if (!this.userEntityService.isLocalUser(this.actor)) return;
const inboxes = new Set<string>(); // The value flags whether it is shared or not.
const inboxes = new Map<string, boolean>();
/* /*
build inbox list build inbox list
@ -185,7 +186,7 @@ class DeliverManager {
for (const following of followers) { for (const following of followers) {
const inbox = following.followerSharedInbox ?? following.followerInbox; const inbox = following.followerSharedInbox ?? following.followerInbox;
inboxes.add(inbox); inboxes.set(inbox, following.followerSharedInbox === null);
} }
} }
@ -197,11 +198,12 @@ class DeliverManager {
// check that they actually have an inbox // check that they actually have an inbox
&& recipe.to.inbox != null, && recipe.to.inbox != null,
) )
.forEach(recipe => inboxes.add(recipe.to.inbox!)); .forEach(recipe => inboxes.set(recipe.to.inbox!, false));
// deliver // deliver
for (const inbox of inboxes) { for (const inbox of inboxes) {
this.queueService.deliver(this.actor, this.activity, inbox); // inbox[0]: inbox, inbox[1]: whether it is sharedInbox
this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]);
} }
} }
} }

View File

@ -140,7 +140,7 @@ export class ApInboxService {
} else if (isFlag(activity)) { } else if (isFlag(activity)) {
await this.flag(actor, activity); await this.flag(actor, activity);
} else { } else {
this.logger.warn(`unrecognized activity type: ${(activity as any).type}`); this.logger.warn(`unrecognized activity type: ${activity.type}`);
} }
} }

View File

@ -91,6 +91,9 @@ export class ApRendererService {
} else if (note.visibility === 'home') { } else if (note.visibility === 'home') {
to = [`${attributedTo}/followers`]; to = [`${attributedTo}/followers`];
cc = ['https://www.w3.org/ns/activitystreams#Public']; cc = ['https://www.w3.org/ns/activitystreams#Public'];
} else if (note.visibility === 'followers') {
to = [`${attributedTo}/followers`];
cc = [];
} else { } else {
throw new Error('renderAnnounce: cannot render non-public note'); throw new Error('renderAnnounce: cannot render non-public note');
} }
@ -116,7 +119,7 @@ export class ApRendererService {
if (block.blockee?.uri == null) { if (block.blockee?.uri == null) {
throw new Error('renderBlock: missing blockee uri'); throw new Error('renderBlock: missing blockee uri');
} }
return { return {
type: 'Block', type: 'Block',
id: `${this.config.url}/blocks/${block.id}`, id: `${this.config.url}/blocks/${block.id}`,
@ -134,10 +137,10 @@ export class ApRendererService {
published: note.createdAt.toISOString(), published: note.createdAt.toISOString(),
object, object,
} as ICreate; } as ICreate;
if (object.to) activity.to = object.to; if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc; if (object.cc) activity.cc = object.cc;
return activity; return activity;
} }
@ -155,7 +158,7 @@ export class ApRendererService {
public renderDocument(file: DriveFile): IApDocument { public renderDocument(file: DriveFile): IApDocument {
return { return {
type: 'Document', type: 'Document',
mediaType: file.type, mediaType: file.webpublicType ?? file.type,
url: this.driveFileEntityService.getPublicUrl(file), url: this.driveFileEntityService.getPublicUrl(file),
name: file.comment, name: file.comment,
}; };
@ -297,16 +300,16 @@ export class ApRendererService {
const items = await this.driveFilesRepository.findBy({ id: In(ids) }); const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
}; };
let inReplyTo; let inReplyTo;
let inReplyToNote: Note | null; let inReplyToNote: Note | null;
if (note.replyId) { if (note.replyId) {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) { if (inReplyToNote != null) {
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
if (inReplyToUser != null) { if (inReplyToUser != null) {
if (inReplyToNote.uri) { if (inReplyToNote.uri) {
inReplyTo = inReplyToNote.uri; inReplyTo = inReplyToNote.uri;
@ -322,24 +325,24 @@ export class ApRendererService {
} else { } else {
inReplyTo = null; inReplyTo = null;
} }
let quote; let quote;
if (note.renoteId) { if (note.renoteId) {
const renote = await this.notesRepository.findOneBy({ id: note.renoteId }); const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
if (renote) { if (renote) {
quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`; quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`;
} }
} }
const attributedTo = `${this.config.url}/users/${note.userId}`; const attributedTo = `${this.config.url}/users/${note.userId}`;
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
let to: string[] = []; let to: string[] = [];
let cc: string[] = []; let cc: string[] = [];
if (note.visibility === 'public') { if (note.visibility === 'public') {
to = ['https://www.w3.org/ns/activitystreams#Public']; to = ['https://www.w3.org/ns/activitystreams#Public'];
cc = [`${attributedTo}/followers`].concat(mentions); cc = [`${attributedTo}/followers`].concat(mentions);
@ -352,44 +355,44 @@ export class ApRendererService {
} else { } else {
to = mentions; to = mentions;
} }
const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({ const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({
id: In(note.mentions), id: In(note.mentions),
}) : []; }) : [];
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag)); const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
const mentionTags = mentionedUsers.map(u => this.renderMention(u)); const mentionTags = mentionedUsers.map(u => this.renderMention(u));
const files = await getPromisedFiles(note.fileIds); const files = await getPromisedFiles(note.fileIds);
const text = note.text ?? ''; const text = note.text ?? '';
let poll: Poll | null = null; let poll: Poll | null = null;
if (note.hasPoll) { if (note.hasPoll) {
poll = await this.pollsRepository.findOneBy({ noteId: note.id }); poll = await this.pollsRepository.findOneBy({ noteId: note.id });
} }
let apText = text; let apText = text;
if (quote) { if (quote) {
apText += `\n\nRE: ${quote}`; apText += `\n\nRE: ${quote}`;
} }
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, { const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
text: apText, text: apText,
})); }));
const emojis = await this.getEmojis(note.emojis); const emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.map(emoji => this.renderEmoji(emoji)); const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
const tag = [ const tag = [
...hashtagTags, ...hashtagTags,
...mentionTags, ...mentionTags,
...apemojis, ...apemojis,
]; ];
const asPoll = poll ? { const asPoll = poll ? {
type: 'Question', type: 'Question',
content: this.apMfmService.getNoteHtml(Object.assign({}, note, { content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
@ -601,7 +604,7 @@ export class ApRendererService {
if (typeof x === 'object' && x.id == null) { if (typeof x === 'object' && x.id == null) {
x.id = `${this.config.url}/${uuid()}`; x.id = `${this.config.url}/${uuid()}`;
} }
return Object.assign({ return Object.assign({
'@context': [ '@context': [
'https://www.w3.org/ns/activitystreams', 'https://www.w3.org/ns/activitystreams',
@ -634,18 +637,18 @@ export class ApRendererService {
], ],
}, x as T & { id: string; }); }, x as T & { id: string; });
} }
@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.userKeypairStoreService.getUserKeypair(user.id);
const ldSignature = this.ldSignatureService.use(); const ldSignature = this.ldSignatureService.use();
ldSignature.debug = false; ldSignature.debug = false;
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
return activity; return activity;
} }
/** /**
* Render OrderedCollectionPage * Render OrderedCollectionPage
* @param id URL of self * @param id URL of self
@ -686,11 +689,11 @@ export class ApRendererService {
type: 'OrderedCollection', type: 'OrderedCollection',
totalItems, totalItems,
}; };
if (first) page.first = first; if (first) page.first = first;
if (last) page.last = last; if (last) page.last = last;
if (orderedItems) page.orderedItems = orderedItems; if (orderedItems) page.orderedItems = orderedItems;
return page; return page;
} }

View File

@ -124,7 +124,7 @@ export class ApNoteService {
throw new Error('invalid note'); throw new Error('invalid note');
} }
const note: IPost = object as any; const note = object as IPost;
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
@ -180,7 +180,7 @@ export class ApNoteService {
const reply: Note | null = note.inReplyTo const reply: Note | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, resolver).then(x => { ? await this.resolveNote(note.inReplyTo, resolver).then(x => {
if (x == null) { if (x == null) {
this.logger.warn('Specified inReplyTo, but nout found'); this.logger.warn('Specified inReplyTo, but not found');
throw new Error('inReplyTo not found'); throw new Error('inReplyTo not found');
} else { } else {
return x; return x;

View File

@ -164,6 +164,9 @@ export class ApPersonService implements OnModuleInit {
throw new Error('invalid Actor: wrong name'); throw new Error('invalid Actor: wrong name');
} }
x.name = truncate(x.name, nameLength); x.name = truncate(x.name, nameLength);
} else if (x.name === '') {
// Mastodon emits empty string when the name is not set.
x.name = undefined;
} }
if (x.summary) { if (x.summary) {
if (!(typeof x.summary === 'string' && x.summary.length > 0)) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) {

View File

@ -195,7 +195,8 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
object && object &&
getApType(object) === 'PropertyValue' && getApType(object) === 'PropertyValue' &&
typeof object.name === 'string' && typeof object.name === 'string' &&
typeof (object as any).value === 'string'; 'value' in object &&
typeof object.value === 'string';
export interface IApMention extends IObject { export interface IApMention extends IObject {
type: 'Mention'; type: 'Mention';

View File

@ -3,15 +3,15 @@ import Chart from '../../core.js';
export const name = 'activeUsers'; export const name = 'activeUsers';
export const schema = { export const schema = {
'readWrite': { intersection: ['read', 'write'], range: 'small' }, 'readWrite': { intersection: ['read', 'write'] },
'read': { uniqueIncrement: true, range: 'small' }, 'read': { uniqueIncrement: true },
'write': { uniqueIncrement: true, range: 'small' }, 'write': { uniqueIncrement: true },
'registeredWithinWeek': { uniqueIncrement: true, range: 'small' }, 'registeredWithinWeek': { uniqueIncrement: true },
'registeredWithinMonth': { uniqueIncrement: true, range: 'small' }, 'registeredWithinMonth': { uniqueIncrement: true },
'registeredWithinYear': { uniqueIncrement: true, range: 'small' }, 'registeredWithinYear': { uniqueIncrement: true },
'registeredOutsideWeek': { uniqueIncrement: true, range: 'small' }, 'registeredOutsideWeek': { uniqueIncrement: true },
'registeredOutsideMonth': { uniqueIncrement: true, range: 'small' }, 'registeredOutsideMonth': { uniqueIncrement: true },
'registeredOutsideYear': { uniqueIncrement: true, range: 'small' }, 'registeredOutsideYear': { uniqueIncrement: true },
} as const; } as const;
export const entity = Chart.schemaToEntity(name, schema); export const entity = Chart.schemaToEntity(name, schema);

View File

@ -1,7 +1,7 @@
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 { AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
import type { Packed } from '@/misc/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';
@ -37,6 +37,7 @@ export class AntennaEntityService {
notify: antenna.notify, notify: antenna.notify,
withReplies: antenna.withReplies, withReplies: antenna.withReplies,
withFile: antenna.withFile, withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote, hasUnreadNote,
}; };
} }

View File

@ -1,7 +1,7 @@
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 { AccessTokensRepository, AppsRepository } from '@/models/index.js'; import type { AccessTokensRepository, AppsRepository } from '@/models/index.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { App } from '@/models/entities/App.js'; import type { App } from '@/models/entities/App.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';

View File

@ -2,11 +2,11 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { BlockingsRepository } from '@/models/index.js'; import type { BlockingsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { Blocking } from '@/models/entities/Blocking.js'; import type { Blocking } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class BlockingEntityService { export class BlockingEntityService {

View File

@ -1,13 +1,13 @@
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 { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js';
import type { Packed } from '@/misc/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 { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ChannelEntityService { export class ChannelEntityService {

View File

@ -1,12 +1,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { ClipsRepository } from '@/models/index.js'; import type { ClipFavoritesRepository, ClipsRepository, User } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/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 { Clip } from '@/models/entities/Clip.js'; import type { Clip } from '@/models/entities/Clip.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class ClipEntityService { export class ClipEntityService {
@ -14,6 +14,9 @@ export class ClipEntityService {
@Inject(DI.clipsRepository) @Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository, private clipsRepository: ClipsRepository,
@Inject(DI.clipFavoritesRepository)
private clipFavoritesRepository: ClipFavoritesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
) { ) {
} }
@ -21,25 +24,31 @@ export class ClipEntityService {
@bindThis @bindThis
public async pack( public async pack(
src: Clip['id'] | Clip, src: Clip['id'] | Clip,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Clip'>> { ): Promise<Packed<'Clip'>> {
const meId = me ? me.id : null;
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src }); const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: clip.id, id: clip.id,
createdAt: clip.createdAt.toISOString(), createdAt: clip.createdAt.toISOString(),
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
userId: clip.userId, userId: clip.userId,
user: this.userEntityService.pack(clip.user ?? clip.userId), user: this.userEntityService.pack(clip.user ?? clip.userId),
name: clip.name, name: clip.name,
description: clip.description, description: clip.description,
isPublic: clip.isPublic, isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined,
}); });
} }
@bindThis @bindThis
public packMany( public packMany(
clips: Clip[], clips: Clip[],
me?: { id: User['id'] } | null | undefined,
) { ) {
return Promise.all(clips.map(x => this.pack(x))); return Promise.all(clips.map(x => this.pack(x, me)));
} }
} }

View File

@ -3,7 +3,7 @@ import { DataSource, In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; import type { NotesRepository, DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
@ -89,9 +89,7 @@ export class DriveFileEntityService {
if (file.type.startsWith('video')) { if (file.type.startsWith('video')) {
if (file.thumbnailUrl) return file.thumbnailUrl; if (file.thumbnailUrl) return file.thumbnailUrl;
if (this.config.videoThumbnailGenerator == null) { return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri);
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri);
}
} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { } else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
// 動画ではなくリモートかつメディアプロキシ // 動画ではなくリモートかつメディアプロキシ
return this.getProxiedUrl(file.uri, 'static'); return this.getProxiedUrl(file.uri, 'static');
@ -106,7 +104,7 @@ export class DriveFileEntityService {
const url = file.webpublicUrl ?? file.url; const url = file.webpublicUrl ?? file.url;
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? this.getProxiedUrl(url, 'static') : null); return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? url : null);
} }
@bindThis @bindThis

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/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 { DriveFolder } from '@/models/entities/DriveFolder.js'; import type { DriveFolder } from '@/models/entities/DriveFolder.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';

View File

@ -1,7 +1,7 @@
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 { EmojisRepository } from '@/models/index.js'; import type { EmojisRepository } from '@/models/index.js';
import type { Packed } from '@/misc/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 { Emoji } from '@/models/entities/Emoji.js'; import type { Emoji } from '@/models/entities/Emoji.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -50,6 +50,7 @@ export class EmojiEntityService {
host: emoji.host, host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl, url: emoji.publicUrl || emoji.originalUrl,
license: emoji.license,
}; };
} }

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js'; import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/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 { Flash } from '@/models/entities/Flash.js'; import type { Flash } from '@/models/entities/Flash.js';

View File

@ -2,10 +2,11 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowingsRepository } from '@/models/index.js'; import type { FollowingsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/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 { Following } from '@/models/entities/Following.js'; import type { Following } from '@/models/entities/Following.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
type LocalFollowerFollowing = Following & { type LocalFollowerFollowing = Following & {
@ -31,7 +32,6 @@ type RemoteFolloweeFollowing = Following & {
followeeInbox: string; followeeInbox: string;
followeeSharedInbox: string; followeeSharedInbox: string;
}; };
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class FollowingEntityService { export class FollowingEntityService {

View File

@ -2,13 +2,13 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/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 { GalleryPost } from '@/models/entities/GalleryPost.js'; import type { GalleryPost } from '@/models/entities/GalleryPost.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class GalleryPostEntityService { export class GalleryPostEntityService {

View File

@ -1,11 +1,11 @@
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 { HashtagsRepository } from '@/models/index.js'; import type { HashtagsRepository } from '@/models/index.js';
import type { Packed } from '@/misc/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 { Hashtag } from '@/models/entities/Hashtag.js'; import type { Hashtag } from '@/models/entities/Hashtag.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class HashtagEntityService { export class HashtagEntityService {

View File

@ -1,12 +1,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { InstancesRepository } from '@/models/index.js'; import type { InstancesRepository } from '@/models/index.js';
import type { Packed } from '@/misc/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 { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '../UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UtilityService } from '../UtilityService.js';
@Injectable() @Injectable()
export class InstanceEntityService { export class InstanceEntityService {

View File

@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MutingsRepository } from '@/models/index.js'; import type { MutingsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/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 { Muting } from '@/models/entities/Muting.js'; import type { Muting } from '@/models/entities/Muting.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class MutingEntityService { export class MutingEntityService {

View File

@ -3,7 +3,7 @@ import { DataSource, In } from 'typeorm';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { nyaize } from '@/misc/nyaize.js'; import { nyaize } from '@/misc/nyaize.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
@ -314,6 +314,7 @@ export class NoteEntityService implements OnModuleInit {
cw: note.cw, cw: note.cw,
visibility: note.visibility, visibility: note.visibility,
localOnly: note.localOnly ?? undefined, localOnly: note.localOnly ?? undefined,
reactionAcceptance: note.reactionAcceptance,
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount, renoteCount: note.renoteCount,
repliesCount: note.repliesCount, repliesCount: note.repliesCount,

View File

@ -1,7 +1,8 @@
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 { NoteReactionsRepository } from '@/models/index.js'; import type { NoteReactionsRepository } from '@/models/index.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
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';
@ -10,7 +11,6 @@ import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class NoteReactionEntityService implements OnModuleInit { export class NoteReactionEntityService implements OnModuleInit {

View File

@ -5,7 +5,7 @@ import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepo
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Notification } from '@/models/entities/Notification.js'; import type { Notification } from '@/models/entities/Notification.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js'; import { isNotNull } from '@/misc/is-not-null.js';
import { notificationTypes } from '@/types.js'; import { notificationTypes } from '@/types.js';

View File

@ -2,14 +2,14 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js'; import type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/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 { Page } from '@/models/entities/Page.js'; import type { Page } from '@/models/entities/Page.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class PageEntityService { export class PageEntityService {

View File

@ -0,0 +1,47 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { RenoteMutingsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { RenoteMuting } from '@/models/entities/RenoteMuting.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class RenoteMutingEntityService {
constructor(
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
private userEntityService: UserEntityService,
) {
}
@bindThis
public async pack(
src: RenoteMuting['id'] | RenoteMuting,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'RenoteMuting'>> {
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: muting.id,
createdAt: muting.createdAt.toISOString(),
muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, {
detail: true,
}),
});
}
@bindThis
public packMany(
mutings: any[],
me: { id: User['id'] },
) {
return Promise.all(mutings.map(x => this.pack(x, me)));
}
}

View File

@ -61,6 +61,7 @@ export class RoleEntityService {
isModerator: role.isModerator, isModerator: role.isModerator,
asBadge: role.asBadge, asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator, canEditMembersByModerator: role.canEditMembersByModerator,
displayOrder: role.displayOrder,
policies: policies, policies: policies,
usersCount: assignedCount, usersCount: assignedCount,
}); });

View File

@ -4,7 +4,7 @@ import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
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 { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { Promiseable } from '@/misc/prelude/await-all.js'; import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js'; import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -78,6 +78,9 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
@ -195,6 +198,13 @@ export class UserEntityService implements OnModuleInit {
}, },
take: 1, take: 1,
}).then(n => n > 0), }).then(n => n > 0),
isRenoteMuted: this.renoteMutingsRepository.count({
where: {
muterId: me,
muteeId: target,
},
take: 1,
}).then(n => n > 0),
}); });
} }
@ -380,9 +390,10 @@ export class UserEntityService implements OnModuleInit {
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ // パフォーマンス上の理由でローカルユーザーのみ
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({ badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({
name: r.name, name: r.name,
iconUrl: r.iconUrl, iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
}))) : undefined, }))) : undefined,
...(opts.detail ? { ...(opts.detail ? {
@ -419,7 +430,7 @@ export class UserEntityService implements OnModuleInit {
userId: user.id, userId: user.id,
}).then(result => result >= 1) }).then(result => result >= 1)
: false, : false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).map(role => ({ roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id, id: role.id,
name: role.name, name: role.name,
color: role.color, color: role.color,
@ -427,6 +438,7 @@ export class UserEntityService implements OnModuleInit {
description: role.description, description: role.description,
isModerator: role.isModerator, isModerator: role.isModerator,
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder,
}))), }))),
} : {}), } : {}),
@ -493,6 +505,7 @@ export class UserEntityService implements OnModuleInit {
isBlocking: relation.isBlocking, isBlocking: relation.isBlocking,
isBlocked: relation.isBlocked, isBlocked: relation.isBlocked,
isMuted: relation.isMuted, isMuted: relation.isMuted,
isRenoteMuted: relation.isRenoteMuted,
} : {}), } : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>; } as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;

View File

@ -1,11 +1,11 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js';
import type { Packed } from '@/misc/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 { UserList } from '@/models/entities/UserList.js'; import type { UserList } from '@/models/entities/UserList.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class UserListEntityService { export class UserListEntityService {

View File

@ -36,6 +36,7 @@ export const DI = {
notificationsRepository: Symbol('notificationsRepository'), notificationsRepository: Symbol('notificationsRepository'),
metasRepository: Symbol('metasRepository'), metasRepository: Symbol('metasRepository'),
mutingsRepository: Symbol('mutingsRepository'), mutingsRepository: Symbol('mutingsRepository'),
renoteMutingsRepository: Symbol('renoteMutingsRepository'),
blockingsRepository: Symbol('blockingsRepository'), blockingsRepository: Symbol('blockingsRepository'),
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'), swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
hashtagsRepository: Symbol('hashtagsRepository'), hashtagsRepository: Symbol('hashtagsRepository'),
@ -51,6 +52,7 @@ export const DI = {
moderationLogsRepository: Symbol('moderationLogsRepository'), moderationLogsRepository: Symbol('moderationLogsRepository'),
clipsRepository: Symbol('clipsRepository'), clipsRepository: Symbol('clipsRepository'),
clipNotesRepository: Symbol('clipNotesRepository'), clipNotesRepository: Symbol('clipNotesRepository'),
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
antennasRepository: Symbol('antennasRepository'), antennasRepository: Symbol('antennasRepository'),
antennaNotesRepository: Symbol('antennaNotesRepository'), antennaNotesRepository: Symbol('antennaNotesRepository'),
promoNotesRepository: Symbol('promoNotesRepository'), promoNotesRepository: Symbol('promoNotesRepository'),

View File

@ -1,15 +1,15 @@
// 与えられた拡張子とファイル名が一致しているかどうかを確認し、 // 与えられた拡張子とファイル名が一致しているかどうかを確認し、
// 一致していない場合は拡張子を付与して返す // 一致していない場合は拡張子を付与して返す
export function correctFilename(filename: string, ext: string | null) { export function correctFilename(filename: string, ext: string | null) {
const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown'; const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown';
if (filename.endsWith(dotExt)) { if (filename.endsWith(dotExt)) {
return filename; return filename;
} }
if (ext === 'jpg' && filename.endsWith('.jpeg')) { if (ext === 'jpg' && filename.endsWith('.jpeg')) {
return filename; return filename;
} }
if (ext === 'tif' && filename.endsWith('.tiff')) { if (ext === 'tif' && filename.endsWith('.tiff')) {
return filename; return filename;
} }
return `${filename}${dotExt}`; return `${filename}${dotExt}`;
} }

View File

@ -1,4 +1,4 @@
import type { Packed } from './schema.js'; import type { Packed } from './json-schema.js';
/** /**
* 稿 * 稿

View File

@ -1,4 +1,4 @@
import type { Packed } from './schema.js'; import type { Packed } from './json-schema.js';
export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set<string>): boolean { export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set<string>): boolean {
if (mutedInstances.has(note.user.host ?? '')) return true; if (mutedInstances.has(note.user.host ?? '')) return true;

View File

@ -2,10 +2,10 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
const dictionary = { const dictionary = {
'safe-file': FILE_TYPE_BROWSERSAFE, 'safe-file': FILE_TYPE_BROWSERSAFE,
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'], 'sharp-convertible-image': ['image/jpeg', 'image/tiff', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'], 'sharp-animation-convertible-image': ['image/jpeg', 'image/tiff', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
'sharp-convertible-image-with-bmp': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'], 'sharp-convertible-image-with-bmp': ['image/jpeg', 'image/tiff', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
'sharp-animation-convertible-image-with-bmp': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'], 'sharp-animation-convertible-image-with-bmp': ['image/jpeg', 'image/tiff', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
}; };
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime); export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);

View File

@ -6,28 +6,29 @@ import {
packedMeDetailedSchema, packedMeDetailedSchema,
packedUserDetailedSchema, packedUserDetailedSchema,
packedUserSchema, packedUserSchema,
} from '@/models/schema/user.js'; } from '@/models/json-schema/user.js';
import { packedNoteSchema } from '@/models/schema/note.js'; import { packedNoteSchema } from '@/models/json-schema/note.js';
import { packedUserListSchema } from '@/models/schema/user-list.js'; import { packedUserListSchema } from '@/models/json-schema/user-list.js';
import { packedAppSchema } from '@/models/schema/app.js'; import { packedAppSchema } from '@/models/json-schema/app.js';
import { packedNotificationSchema } from '@/models/schema/notification.js'; import { packedNotificationSchema } from '@/models/json-schema/notification.js';
import { packedDriveFileSchema } from '@/models/schema/drive-file.js'; import { packedDriveFileSchema } from '@/models/json-schema/drive-file.js';
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js'; import { packedDriveFolderSchema } from '@/models/json-schema/drive-folder.js';
import { packedFollowingSchema } from '@/models/schema/following.js'; import { packedFollowingSchema } from '@/models/json-schema/following.js';
import { packedMutingSchema } from '@/models/schema/muting.js'; import { packedMutingSchema } from '@/models/json-schema/muting.js';
import { packedBlockingSchema } from '@/models/schema/blocking.js'; import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js'; import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedHashtagSchema } from '@/models/schema/hashtag.js'; import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedPageSchema } from '@/models/schema/page.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite.js'; import { packedPageSchema } from '@/models/json-schema/page.js';
import { packedChannelSchema } from '@/models/schema/channel.js'; import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedAntennaSchema } from '@/models/schema/antenna.js'; import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedClipSchema } from '@/models/schema/clip.js'; import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
import { packedFederationInstanceSchema } from '@/models/schema/federation-instance.js'; import { packedClipSchema } from '@/models/json-schema/clip.js';
import { packedQueueCountSchema } from '@/models/schema/queue.js'; import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js';
import { packedGalleryPostSchema } from '@/models/schema/gallery-post.js'; import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/schema/emoji.js'; import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
import { packedFlashSchema } from '@/models/schema/flash.js'; import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
import { packedFlashSchema } from '@/models/json-schema/flash.js';
export const refs = { export const refs = {
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
@ -48,6 +49,7 @@ export const refs = {
DriveFolder: packedDriveFolderSchema, DriveFolder: packedDriveFolderSchema,
Following: packedFollowingSchema, Following: packedFollowingSchema,
Muting: packedMutingSchema, Muting: packedMutingSchema,
RenoteMuting: packedRenoteMutingSchema,
Blocking: packedBlockingSchema, Blocking: packedBlockingSchema,
Hashtag: packedHashtagSchema, Hashtag: packedHashtagSchema,
Page: packedPageSchema, Page: packedPageSchema,
@ -93,7 +95,7 @@ export interface Schema extends OfSchema {
readonly example?: any; readonly example?: any;
readonly format?: string; readonly format?: string;
readonly ref?: keyof typeof refs; readonly ref?: keyof typeof refs;
readonly enum?: ReadonlyArray<string>; readonly enum?: ReadonlyArray<string | null>;
readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null; readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null;
readonly maxLength?: number; readonly maxLength?: number;
readonly minLength?: number; readonly minLength?: number;
@ -159,7 +161,7 @@ export type SchemaTypeDef<p extends Schema> =
p['type'] extends 'integer' ? number : p['type'] extends 'integer' ? number :
p['type'] extends 'number' ? number : p['type'] extends 'number' ? number :
p['type'] extends 'string' ? ( p['type'] extends 'string' ? (
p['enum'] extends readonly string[] ? p['enum'] extends readonly (string | null)[] ?
p['enum'][number] : p['enum'][number] :
p['format'] extends 'date-time' ? string : // Dateにする p['format'] extends 'date-time' ? string : // Dateにする
string string

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js'; import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
@ -190,6 +190,12 @@ const $mutingsRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $renoteMutingsRepository: Provider = {
provide: DI.renoteMutingsRepository,
useFactory: (db: DataSource) => db.getRepository(RenoteMuting),
inject: [DI.db],
};
const $blockingsRepository: Provider = { const $blockingsRepository: Provider = {
provide: DI.blockingsRepository, provide: DI.blockingsRepository,
useFactory: (db: DataSource) => db.getRepository(Blocking), useFactory: (db: DataSource) => db.getRepository(Blocking),
@ -280,6 +286,12 @@ const $clipNotesRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $clipFavoritesRepository: Provider = {
provide: DI.clipFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(ClipFavorite),
inject: [DI.db],
};
const $antennasRepository: Provider = { const $antennasRepository: Provider = {
provide: DI.antennasRepository, provide: DI.antennasRepository,
useFactory: (db: DataSource) => db.getRepository(Antenna), useFactory: (db: DataSource) => db.getRepository(Antenna),
@ -423,6 +435,7 @@ const $roleAssignmentsRepository: Provider = {
$notificationsRepository, $notificationsRepository,
$metasRepository, $metasRepository,
$mutingsRepository, $mutingsRepository,
$renoteMutingsRepository,
$blockingsRepository, $blockingsRepository,
$swSubscriptionsRepository, $swSubscriptionsRepository,
$hashtagsRepository, $hashtagsRepository,
@ -438,6 +451,7 @@ const $roleAssignmentsRepository: Provider = {
$moderationLogsRepository, $moderationLogsRepository,
$clipsRepository, $clipsRepository,
$clipNotesRepository, $clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository, $antennasRepository,
$antennaNotesRepository, $antennaNotesRepository,
$promoNotesRepository, $promoNotesRepository,
@ -489,6 +503,7 @@ const $roleAssignmentsRepository: Provider = {
$notificationsRepository, $notificationsRepository,
$metasRepository, $metasRepository,
$mutingsRepository, $mutingsRepository,
$renoteMutingsRepository,
$blockingsRepository, $blockingsRepository,
$swSubscriptionsRepository, $swSubscriptionsRepository,
$hashtagsRepository, $hashtagsRepository,
@ -504,6 +519,7 @@ const $roleAssignmentsRepository: Provider = {
$moderationLogsRepository, $moderationLogsRepository,
$clipsRepository, $clipsRepository,
$clipNotesRepository, $clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository, $antennasRepository,
$antennaNotesRepository, $antennaNotesRepository,
$promoNotesRepository, $promoNotesRepository,

View File

@ -13,6 +13,10 @@ export class Antenna {
}) })
public createdAt: Date; public createdAt: Date;
@Index()
@Column('timestamp with time zone')
public lastUsedAt: Date;
@Index() @Index()
@Column({ @Column({
...id(), ...id(),
@ -83,4 +87,10 @@ export class Antenna {
@Column('boolean') @Column('boolean')
public notify: boolean; public notify: boolean;
@Index()
@Column('boolean', {
default: true,
})
public isActive: boolean;
} }

View File

@ -12,6 +12,12 @@ export class Clip {
}) })
public createdAt: Date; public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true,
})
public lastClippedAt: Date | null;
@Index() @Index()
@Column({ @Column({
...id(), ...id(),

View File

@ -0,0 +1,33 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { User } from './User.js';
import { Clip } from './Clip.js';
@Entity()
@Index(['userId', 'clipId'], { unique: true })
export class ClipFavorite {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: User | null;
@Column(id())
public clipId: Clip['id'];
@ManyToOne(type => Clip, {
onDelete: 'CASCADE',
})
@JoinColumn()
public clip: Clip | null;
}

View File

@ -55,4 +55,9 @@ export class Emoji {
array: true, length: 128, default: '{}', array: true, length: 128, default: '{}',
}) })
public aliases: string[]; public aliases: string[];
@Column('varchar', {
length: 1024, nullable: true,
})
public license: string | null;
} }

View File

@ -43,7 +43,7 @@ export class Flash {
public user: User | null; public user: User | null;
@Column('varchar', { @Column('varchar', {
length: 32768, length: 65536,
}) })
public script: string; public script: string;

View File

@ -12,7 +12,7 @@ export class Meta {
public id: string; public id: string;
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 1024, nullable: true,
}) })
public name: string | null; public name: string | null;
@ -25,7 +25,7 @@ export class Meta {
* *
*/ */
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 1024, nullable: true,
}) })
public maintainerName: string | null; public maintainerName: string | null;
@ -33,7 +33,7 @@ export class Meta {
* *
*/ */
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 1024, nullable: true,
}) })
public maintainerEmail: string | null; public maintainerEmail: string | null;
@ -48,76 +48,68 @@ export class Meta {
public useStarForReactionFallback: boolean; public useStarForReactionFallback: boolean;
@Column('varchar', { @Column('varchar', {
length: 64, array: true, default: '{}', length: 1024, array: true, default: '{}',
}) })
public langs: string[]; public langs: string[];
@Column('varchar', { @Column('varchar', {
length: 256, array: true, default: '{}', length: 1024, array: true, default: '{}',
}) })
public pinnedUsers: string[]; public pinnedUsers: string[];
@Column('varchar', { @Column('varchar', {
length: 256, array: true, default: '{}', length: 1024, array: true, default: '{}',
}) })
public hiddenTags: string[]; public hiddenTags: string[];
@Column('varchar', { @Column('varchar', {
length: 256, array: true, default: '{}', length: 1024, array: true, default: '{}',
}) })
public blockedHosts: string[]; public blockedHosts: string[];
@Column('varchar', { @Column('varchar', {
length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}', length: 1024, array: true, default: '{}',
}) })
public pinnedPages: string[]; public sensitiveWords: string[];
@Column({
...id(),
nullable: true,
})
public pinnedClipId: Clip['id'] | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public themeColor: string | null; public themeColor: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
default: '/assets/ai.png',
}) })
public mascotImageUrl: string | null; public mascotImageUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public bannerUrl: string | null; public bannerUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public backgroundImageUrl: string | null; public backgroundImageUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public logoImageUrl: string | null; public logoImageUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
default: 'https://xn--931a.moe/aiart/yubitun.png',
}) })
public errorImageUrl: string | null; public errorImageUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public iconUrl: string | null; public iconUrl: string | null;
@ -150,13 +142,13 @@ export class Meta {
public enableHcaptcha: boolean; public enableHcaptcha: boolean;
@Column('varchar', { @Column('varchar', {
length: 64, length: 1024,
nullable: true, nullable: true,
}) })
public hcaptchaSiteKey: string | null; public hcaptchaSiteKey: string | null;
@Column('varchar', { @Column('varchar', {
length: 64, length: 1024,
nullable: true, nullable: true,
}) })
public hcaptchaSecretKey: string | null; public hcaptchaSecretKey: string | null;
@ -167,13 +159,13 @@ export class Meta {
public enableRecaptcha: boolean; public enableRecaptcha: boolean;
@Column('varchar', { @Column('varchar', {
length: 64, length: 1024,
nullable: true, nullable: true,
}) })
public recaptchaSiteKey: string | null; public recaptchaSiteKey: string | null;
@Column('varchar', { @Column('varchar', {
length: 64, length: 1024,
nullable: true, nullable: true,
}) })
public recaptchaSecretKey: string | null; public recaptchaSecretKey: string | null;
@ -184,13 +176,13 @@ export class Meta {
public enableTurnstile: boolean; public enableTurnstile: boolean;
@Column('varchar', { @Column('varchar', {
length: 64, length: 1024,
nullable: true, nullable: true,
}) })
public turnstileSiteKey: string | null; public turnstileSiteKey: string | null;
@Column('varchar', { @Column('varchar', {
length: 64, length: 1024,
nullable: true, nullable: true,
}) })
public turnstileSecretKey: string | null; public turnstileSecretKey: string | null;
@ -218,7 +210,7 @@ export class Meta {
public enableSensitiveMediaDetectionForVideos: boolean; public enableSensitiveMediaDetectionForVideos: boolean;
@Column('varchar', { @Column('varchar', {
length: 128, length: 1024,
nullable: true, nullable: true,
}) })
public summalyProxy: string | null; public summalyProxy: string | null;
@ -229,7 +221,7 @@ export class Meta {
public enableEmail: boolean; public enableEmail: boolean;
@Column('varchar', { @Column('varchar', {
length: 128, length: 1024,
nullable: true, nullable: true,
}) })
public email: string | null; public email: string | null;
@ -240,7 +232,7 @@ export class Meta {
public smtpSecure: boolean; public smtpSecure: boolean;
@Column('varchar', { @Column('varchar', {
length: 128, length: 1024,
nullable: true, nullable: true,
}) })
public smtpHost: string | null; public smtpHost: string | null;
@ -251,13 +243,13 @@ export class Meta {
public smtpPort: number | null; public smtpPort: number | null;
@Column('varchar', { @Column('varchar', {
length: 128, length: 1024,
nullable: true, nullable: true,
}) })
public smtpUser: string | null; public smtpUser: string | null;
@Column('varchar', { @Column('varchar', {
length: 128, length: 1024,
nullable: true, nullable: true,
}) })
public smtpPass: string | null; public smtpPass: string | null;
@ -268,19 +260,19 @@ export class Meta {
public enableServiceWorker: boolean; public enableServiceWorker: boolean;
@Column('varchar', { @Column('varchar', {
length: 128, length: 1024,
nullable: true, nullable: true,
}) })
public swPublicKey: string | null; public swPublicKey: string | null;
@Column('varchar', { @Column('varchar', {
length: 128, length: 1024,
nullable: true, nullable: true,
}) })
public swPrivateKey: string | null; public swPrivateKey: string | null;
@Column('varchar', { @Column('varchar', {
length: 128, length: 1024,
nullable: true, nullable: true,
}) })
public deeplAuthKey: string | null; public deeplAuthKey: string | null;
@ -291,20 +283,20 @@ export class Meta {
public deeplIsPro: boolean; public deeplIsPro: boolean;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public ToSUrl: string | null; public termsOfServiceUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
default: 'https://github.com/misskey-dev/misskey', default: 'https://github.com/misskey-dev/misskey',
nullable: false, nullable: false,
}) })
public repositoryUrl: string; public repositoryUrl: string;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
default: 'https://github.com/misskey-dev/misskey/issues/new', default: 'https://github.com/misskey-dev/misskey/issues/new',
nullable: true, nullable: true,
}) })
@ -328,43 +320,43 @@ export class Meta {
public useObjectStorage: boolean; public useObjectStorage: boolean;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public objectStorageBucket: string | null; public objectStorageBucket: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public objectStoragePrefix: string | null; public objectStoragePrefix: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public objectStorageBaseUrl: string | null; public objectStorageBaseUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public objectStorageEndpoint: string | null; public objectStorageEndpoint: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public objectStorageRegion: string | null; public objectStorageRegion: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public objectStorageAccessKey: string | null; public objectStorageAccessKey: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 1024,
nullable: true, nullable: true,
}) })
public objectStorageSecretKey: string | null; public objectStorageSecretKey: string | null;

View File

@ -87,6 +87,11 @@ export class Note {
}) })
public localOnly: boolean; public localOnly: boolean;
@Column('varchar', {
length: 64, nullable: true,
})
public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | null;
@Column('smallint', { @Column('smallint', {
default: 0, default: 0,
}) })

View File

@ -0,0 +1,42 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { User } from './User.js';
@Entity()
@Index(['muterId', 'muteeId'], { unique: true })
export class RenoteMuting {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Muting.',
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The mutee user ID.',
})
public muteeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public mutee: User | null;
@Index()
@Column({
...id(),
comment: 'The muter user ID.',
})
public muterId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public muter: User | null;
}

View File

@ -18,6 +18,12 @@ export class RetentionAggregation {
}) })
public updatedAt: Date; public updatedAt: Date;
@Index({ unique: true })
@Column('varchar', {
length: 512, nullable: false,
})
public dateKey: string;
@Column({ @Column({
...id(), ...id(),
array: true, array: true,

View File

@ -144,6 +144,12 @@ export class Role {
}) })
public canEditMembersByModerator: boolean; public canEditMembersByModerator: boolean;
// UIに表示する際の並び順用(大きいほど先頭)
@Column('integer', {
default: 0,
})
public displayOrder: number;
@Column('jsonb', { @Column('jsonb', {
default: { }, default: { },
}) })

View File

@ -13,6 +13,7 @@ import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js'; import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
import { Clip } from '@/models/entities/Clip.js'; import { Clip } from '@/models/entities/Clip.js';
import { ClipNote } from '@/models/entities/ClipNote.js'; import { ClipNote } from '@/models/entities/ClipNote.js';
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
import { DriveFile } from '@/models/entities/DriveFile.js'; import { DriveFile } from '@/models/entities/DriveFile.js';
import { DriveFolder } from '@/models/entities/DriveFolder.js'; import { DriveFolder } from '@/models/entities/DriveFolder.js';
import { Emoji } from '@/models/entities/Emoji.js'; import { Emoji } from '@/models/entities/Emoji.js';
@ -26,6 +27,7 @@ import { Meta } from '@/models/entities/Meta.js';
import { ModerationLog } from '@/models/entities/ModerationLog.js'; import { ModerationLog } from '@/models/entities/ModerationLog.js';
import { MutedNote } from '@/models/entities/MutedNote.js'; import { MutedNote } from '@/models/entities/MutedNote.js';
import { Muting } from '@/models/entities/Muting.js'; import { Muting } from '@/models/entities/Muting.js';
import { RenoteMuting } from '@/models/entities/RenoteMuting.js';
import { Note } from '@/models/entities/Note.js'; import { Note } from '@/models/entities/Note.js';
import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { NoteReaction } from '@/models/entities/NoteReaction.js'; import { NoteReaction } from '@/models/entities/NoteReaction.js';
@ -80,6 +82,7 @@ export {
ChannelNotePining, ChannelNotePining,
Clip, Clip,
ClipNote, ClipNote,
ClipFavorite,
DriveFile, DriveFile,
DriveFolder, DriveFolder,
Emoji, Emoji,
@ -93,6 +96,7 @@ export {
ModerationLog, ModerationLog,
MutedNote, MutedNote,
Muting, Muting,
RenoteMuting,
Note, Note,
NoteFavorite, NoteFavorite,
NoteReaction, NoteReaction,
@ -146,6 +150,7 @@ export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
export type ChannelNotePiningsRepository = Repository<ChannelNotePining>; export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
export type ClipsRepository = Repository<Clip>; export type ClipsRepository = Repository<Clip>;
export type ClipNotesRepository = Repository<ClipNote>; export type ClipNotesRepository = Repository<ClipNote>;
export type ClipFavoritesRepository = Repository<ClipFavorite>;
export type DriveFilesRepository = Repository<DriveFile>; export type DriveFilesRepository = Repository<DriveFile>;
export type DriveFoldersRepository = Repository<DriveFolder>; export type DriveFoldersRepository = Repository<DriveFolder>;
export type EmojisRepository = Repository<Emoji>; export type EmojisRepository = Repository<Emoji>;
@ -159,6 +164,7 @@ export type MetasRepository = Repository<Meta>;
export type ModerationLogsRepository = Repository<ModerationLog>; export type ModerationLogsRepository = Repository<ModerationLog>;
export type MutedNotesRepository = Repository<MutedNote>; export type MutedNotesRepository = Repository<MutedNote>;
export type MutingsRepository = Repository<Muting>; export type MutingsRepository = Repository<Muting>;
export type RenoteMutingsRepository = Repository<RenoteMuting>;
export type NotesRepository = Repository<Note>; export type NotesRepository = Repository<Note>;
export type NoteFavoritesRepository = Repository<NoteFavorite>; export type NoteFavoritesRepository = Repository<NoteFavorite>;
export type NoteReactionsRepository = Repository<NoteReaction>; export type NoteReactionsRepository = Repository<NoteReaction>;

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