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
-->
# What
## What
<!-- このPRで何をしたのか どう変わるのか? -->
<!-- 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? -->
# Additional info (optional)
## 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

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
-->
# What
## What
<!-- このPRで何をしたのか どう変わるのか? -->
<!-- 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? -->
# Additional info (optional)
## 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

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:
push:
@ -8,26 +8,13 @@ on:
pull_request:
jobs:
jest:
vitest:
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:
@ -51,12 +38,12 @@ jobs:
- name: Build
run: pnpm build
- name: Test
run: pnpm jest-and-coverage
run: pnpm --filter frontend test-and-coverage
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json
files: ./packages/frontend/coverage/coverage-final.json
e2e:
runs-on: ubuntu-latest

View File

@ -1,15 +1,71 @@
<!--
## 13.x.x (unreleased)
### Improvements
-
### General
-
### Bugfixes
x
### Client
-
### 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)
### Improvements
@ -246,8 +302,8 @@ You should also include the user name that made the change.
## 13.3.2 (2023/02/04)
### Improvements
- 外部メディアプロキシへの対応を強化しました
外部メディアプロキシのFastify実装を作りました
- 外部メディアプロキシへの対応を強化しました
外部メディアプロキシのFastify実装を作りました
https://github.com/misskey-dev/media-proxy
- Server: improve performance
@ -410,7 +466,7 @@ You should also include the user name that made the change.
- ユーザーごとのドライブ容量設定はロールに統合されました。
- インスタンスデフォルトのドライブ容量設定はロールに統合されました。アップデート後、ベースロールもしくはコンディショナルロールでドライブ容量を編集してください。
- 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
#### For users
@ -638,7 +694,7 @@ You should also include the user name that made the change.
## 12.112.2 (2022/07/08)
### Bugfixes
- Fix Docker doesn't work @mei23
- Fix Docker doesn't work @mei23
Still not working on arm64 environment. (See 12.112.0)
## 12.112.1 (2022/07/07)
@ -680,7 +736,7 @@ same as 12.112.0
- Improve player detection in URL preview @mei23
- Add Badge Image to Push Notification #8012 @tamaina
- 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`.
- Server: Add possibility to log IP addresses of users @syuilo
- 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.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-submit]').should('be.disabled');
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-submit]').should('not.be.disabled');
cy.get('[data-cy-signup-submit]').click();
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', () => {

View File

@ -29,17 +29,17 @@ describe('After user signed in', () => {
it('first widget should be removed', () => {
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').should('have.length', 2);
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);
});
function buildWidgetTest(widgetName) {
it(`${widgetName} widget should get added`, () => {
cy.get('.mk-widget-edit').click();
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(`.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"
mute: "Stummschalten"
unmute: "Stummschaltung aufheben"
renoteMute: "Renotes stummschalten"
renoteUnmute: "Renote-Stummschaltung aufheben"
block: "Blockieren"
unblock: "Blockierung aufheben"
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."
autoAcceptFollowed: "Follow-Anfragen von Benutzern, denen du folgst, automatisch akzeptieren"
addAccount: "Benutzerkonto hinzufügen"
reloadAccountsList: "Benutzerkontoliste aktualisieren"
loginFailed: "Anmeldung fehlgeschlagen"
showOnRemote: "Auf Ursprungsinstanz ansehen"
general: "Allgemein"
@ -544,6 +547,10 @@ userSuspended: "Dieser Benutzer wurde gesperrt."
userSilenced: "Dieser Benutzer wurde instanzweit stummgeschaltet."
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."
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ü"
divider: "Trenner"
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"
postToTheChannel: "In Kanal senden"
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:
earnedAt: "Freigeschaltet am"
_types:
@ -1106,7 +1125,7 @@ _achievements:
title: "Beliebt"
description: "Die Anzahl deiner Follower hat 100 überschritten"
_followers300:
title: "Stellt euch bitte in einer Reihe auf"
title: "Eine geordnete Reihe, bitte!"
description: "Die Anzahl deiner Follower hat 300 überschritten"
_followers500:
title: "Funkmast"
@ -1218,6 +1237,8 @@ _role:
iconUrl: "Icon-URL"
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."
displayOrder: "Position"
descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position."
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."
priority: "Priorität"
@ -1243,6 +1264,7 @@ _role:
rateLimitFactor: "Versuchsanzahl"
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
canHideAds: "Kann Werbung ausblenden"
canSearchNotes: "Nutzung der Notizsuchfunktion"
_condition:
isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz"
@ -1844,3 +1866,9 @@ _deck:
_dialog:
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
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"
files: "Files"
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}?"
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."
@ -122,6 +122,8 @@ unmarkAsSensitive: "Unmark as NSFW"
enterFileName: "Enter filename"
mute: "Mute"
unmute: "Unmute"
renoteMute: "Mute Renotes"
renoteUnmute: "Unmute Renotes"
block: "Block"
unblock: "Unblock"
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."
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
addAccount: "Add account"
reloadAccountsList: "Reload account list"
loginFailed: "Failed to sign in"
showOnRemote: "View on remote instance"
general: "General"
@ -527,7 +530,7 @@ nothing: "There's nothing to see here"
installedDate: "Authorized at"
lastUsedDate: "Last used at"
state: "State"
sort: "Sort"
sort: "Sorting order"
ascendingOrder: "Ascending"
descendingOrder: "Descending"
scratchpad: "Scratchpad"
@ -544,6 +547,10 @@ userSuspended: "This user has been suspended."
userSilenced: "This user is being silenced."
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."
tokenRevoked: "Invalid token"
tokenRevokedDescription: "This token has expired. Please log in again."
accountDeleted: "Account deleted"
accountDeletedDescription: "This account has been deleted."
menu: "Menu"
divider: "Divider"
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"
postToTheChannel: "Post to channel"
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:
earnedAt: "Unlocked at"
_types:
@ -1218,6 +1237,8 @@ _role:
iconUrl: "Icon URL"
asBadge: "Show as badge"
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"
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"
@ -1243,6 +1264,7 @@ _role:
rateLimitFactor: "Rate limit"
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
canHideAds: "Can hide ads"
canSearchNotes: "Usage of note search"
_condition:
isLocal: "Local user"
isRemote: "Remote user"
@ -1844,3 +1866,9 @@ _deck:
_dialog:
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}."
_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"
mute: "Silenciar"
unmute: "Dejar de silenciar"
renoteMute: "Silenciar renota"
renoteUnmute: "Desilenciar renota"
block: "Bloquear"
unblock: "Dejar de bloquear"
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"
autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues"
addAccount: "Agregar Cuenta"
reloadAccountsList: "Recargar lista de cuentas"
loginFailed: "Error al iniciar sesión."
showOnRemote: "Ver en una instancia remota"
general: "General"
@ -506,6 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)"
newNoteRecived: "Tienes una nota nueva"
sounds: "Sonidos"
sound: "Sonidos"
@ -543,6 +547,10 @@ userSuspended: "Este usuario ha sido suspendido."
userSilenced: "Este usuario ha sido silenciado."
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."
tokenRevoked: "Token inválido"
tokenRevokedDescription: "Este token expiró, vuelve a iniciar sesión."
accountDeleted: "Cuenta borrada"
accountDeletedDescription: "Esta cuenta ha sido borrada."
menu: "Menú"
divider: "Divisor"
addItem: "Agregar elemento"
@ -955,6 +963,16 @@ exploreOtherServers: "Buscar otra instancia"
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."
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:
earnedAt: "Desbloqueado el"
_types:
@ -1214,6 +1232,8 @@ _role:
iconUrl: "URL del ícono"
asBadge: "Mostrar como emblema"
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"
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"
@ -1239,6 +1259,7 @@ _role:
rateLimitFactor: "Limitador"
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
canHideAds: "Puede ocultar anuncios"
canSearchNotes: "Uso de la búsqueda de notas"
_condition:
isLocal: "Usuario local"
isRemote: "Usuario remoto"
@ -1840,3 +1861,6 @@ _deck:
_dialog:
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}."
_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"
mute: "Silenzia"
unmute: "Riattiva l'audio"
renoteMute: "Silenzia i Rinota"
renoteUnmute: "Non silenziare i Rinota"
block: "Blocca"
unblock: "Sblocca"
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."
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui"
addAccount: "Aggiungi profilo"
reloadAccountsList: "Ricarica l'elenco dei profili"
loginFailed: "Accesso non riuscito"
showOnRemote: "Leggi sull'istanza remota"
general: "Generali"
@ -544,6 +547,10 @@ userSuspended: "L'utente è in sospensione"
userSilenced: "L'utente è silenziat@."
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."
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ù"
divider: "Linea di separazione"
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"
postToTheChannel: "Pubblica sul canale"
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:
earnedAt: "Data di conseguimento"
_types:
@ -1218,6 +1236,8 @@ _role:
iconUrl: "URL dell'icona"
asBadge: "Mostra come badge"
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"
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
priority: "Priorità"
@ -1243,6 +1263,7 @@ _role:
rateLimitFactor: "Limite del rapporto"
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
canHideAds: "Può nascondere i banner"
canSearchNotes: "Ricercare nelle Note"
_condition:
isLocal: "Profilo locale"
isRemote: "Profilo remoto"
@ -1844,3 +1865,6 @@ _deck:
_dialog:
charactersExceeded: "Hai superato il limite di {max} 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: "エクスポート"
files: "ファイル"
download: "ダウンロード"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付したノートも消えます。"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した全てのコンテンツからも削除されます。"
unfollowConfirm: "{name}のフォローを解除しますか?"
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
@ -122,6 +122,8 @@ unmarkAsSensitive: "閲覧注意を解除する"
enterFileName: "ファイル名を入力"
mute: "ミュート"
unmute: "ミュート解除"
renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除"
block: "ブロック"
unblock: "ブロック解除"
suspend: "凍結"
@ -153,6 +155,7 @@ flagShowTimelineReplies: "タイムラインにノートへの返信を表示す
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。"
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示"
general: "全般"
@ -544,6 +547,10 @@ userSuspended: "このユーザーは凍結されています。"
userSilenced: "このユーザーはサイレンスされています。"
yourAccountSuspendedTitle: "アカウントが凍結されています"
yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。"
tokenRevoked: "トークンが無効です"
tokenRevokedDescription: "ログイントークンが失効しています。ログインし直してください。"
accountDeleted: "アカウントは削除されています"
accountDeletedDescription: "このアカウントは削除されています。"
menu: "メニュー"
divider: "分割線"
addItem: "項目を追加"
@ -959,6 +966,21 @@ invitationRequiredToRegister: "現在このサーバーは招待制です。招
emailNotSupported: "このサーバーではメール配信はサポートされていません"
postToTheChannel: "チャンネルに投稿"
cannotBeChangedLater: "後から変更できません。"
reactionAcceptance: "リアクションの受け入れ"
likeOnly: "いいねのみ"
likeOnlyForRemote: "リモートからはいいねのみ"
rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
notesSearchNotAvailable: "ノート検索は利用できません。"
license: "ライセンス"
unfavoriteConfirm: "お気に入り解除しますか?"
myClips: "自分のクリップ"
drivecleaner: "ドライブクリーナー"
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
_achievements:
earnedAt: "獲得日時"
@ -1220,6 +1242,8 @@ _role:
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度"
@ -1245,6 +1269,7 @@ _role:
rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
canHideAds: "広告の非表示"
canSearchNotes: "ノート検索の利用可否"
_condition:
isLocal: "ローカルユーザー"
isRemote: "リモートユーザー"
@ -1897,3 +1922,11 @@ _deck:
_dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
charactersBelow: "最小文字数を下回っています! 現在 {current} / 制限 {min}"
_disabledTimeline:
title: "無効化されたタイムライン"
description: "現在のロールでは、このタイムラインを使用することはできません。"
_drivecleaner:
orderBySizeDesc: "サイズが大きい順"
orderByCreatedAtAsc: "追加日が古い順"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "13.9.2",
"version": "13.10.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@7.27.0",
"packageManager": "pnpm@7.29.3",
"workspaces": [
"packages/frontend",
"packages/backend",
@ -31,8 +31,8 @@
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"jest": "cd packages/backend && pnpm jest",
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage",
"test": "pnpm -r test",
"test-and-coverage": "pnpm -r test-and-coverage",
"format": "pnpm exec gulp format",
"clean": "node ./scripts/clean.js",
"clean-all": "node ./scripts/clean-all.js",
@ -55,12 +55,12 @@
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.53.0",
"@typescript-eslint/parser": "5.53.0",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"cross-env": "7.0.3",
"cypress": "12.7.0",
"eslint": "8.35.0",
"start-server-and-test": "1.15.4"
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.2.0"

View File

@ -19,6 +19,6 @@
</head>
<body>
<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>
</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": {
"@swc/core-android-arm64": "^1.3.11",
"@swc/core-darwin-arm64": "^1.3.36",
"@swc/core-darwin-x64": "^1.3.36",
"@swc/core-linux-arm-gnueabihf": "^1.3.36",
"@swc/core-linux-arm64-gnu": "^1.3.36",
"@swc/core-linux-arm64-musl": "^1.3.36",
"@swc/core-linux-x64-gnu": "^1.3.36",
"@swc/core-linux-x64-musl": "^1.3.36",
"@swc/core-win32-arm64-msvc": "^1.3.36",
"@swc/core-win32-ia32-msvc": "^1.3.36",
"@swc/core-win32-x64-msvc": "^1.3.36",
"@swc/core-darwin-arm64": "^1.3.38",
"@swc/core-darwin-x64": "^1.3.38",
"@swc/core-linux-arm-gnueabihf": "^1.3.38",
"@swc/core-linux-arm64-gnu": "^1.3.38",
"@swc/core-linux-arm64-musl": "^1.3.38",
"@swc/core-linux-x64-gnu": "^1.3.38",
"@swc/core-linux-x64-musl": "^1.3.38",
"@swc/core-win32-arm64-msvc": "^1.3.38",
"@swc/core-win32-ia32-msvc": "^1.3.38",
"@swc/core-win32-x64-msvc": "^1.3.38",
"@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0"
},
"dependencies": {
"@bull-board/api": "4.12.1",
"@bull-board/fastify": "4.12.1",
"@bull-board/ui": "4.12.1",
"@bull-board/api": "5.0.0",
"@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0",
"@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0",
"@fastify/http-proxy": "8.4.0",
"@fastify/multipart": "7.4.1",
"@fastify/multipart": "7.4.2",
"@fastify/static": "6.9.0",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.3.9",
@ -54,7 +54,7 @@
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.36",
"@swc/core": "1.3.38",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
@ -74,12 +74,12 @@
"date-fns": "2.29.3",
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"fastify": "4.13.0",
"fastify": "4.14.1",
"feed": "4.2.2",
"file-type": "18.2.1",
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "12.5.3",
"got": "12.6.0",
"happy-dom": "8.9.0",
"hpagent": "1.2.0",
"ioredis": "4.28.5",
@ -102,7 +102,7 @@
"os-utils": "0.0.14",
"otpauth": "^9.0.2",
"parse5": "7.1.2",
"pg": "8.9.0",
"pg": "8.10.0",
"private-ip": "3.0.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@ -128,10 +128,10 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.17.10",
"systeminformation": "5.17.12",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.2",
"tsc-alias": "1.8.3",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.11",
@ -146,7 +146,7 @@
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.4.3",
"@jest/globals": "29.5.0",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.1",
@ -164,7 +164,7 @@
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1",
"@types/node": "18.14.1",
"@types/node": "18.15.0",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1",
@ -176,7 +176,7 @@
"@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.8.0",
"@types/sanitize-html": "2.8.1",
"@types/semver": "7.3.13",
"@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2",
@ -188,13 +188,13 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.52.0",
"@typescript-eslint/parser": "5.53.0",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"cross-env": "7.0.3",
"eslint": "8.35.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.4.3",
"jest-mock": "29.4.3"
"jest": "29.5.0",
"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 { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { NotificationService } from '@/core/NotificationService.js';
export const ACHIEVEMENT_TYPES = [
'notes1',
@ -90,7 +90,7 @@ export class AchievementService {
@Inject(DI.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,
});
}

View File

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

View File

@ -5,7 +5,6 @@ import { AntennaService } from './AntennaService.js';
import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js';
import { CaptchaService } from './CaptchaService.js';
import { CreateNotificationService } from './CreateNotificationService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js';
import { CustomEmojiService } from './CustomEmojiService.js';
import { DeleteAccountService } from './DeleteAccountService.js';
@ -82,6 +81,7 @@ import { HashtagEntityService } from './entities/HashtagEntityService.js';
import { InstanceEntityService } from './entities/InstanceEntityService.js';
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
import { MutingEntityService } from './entities/MutingEntityService.js';
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
import { NoteEntityService } from './entities/NoteEntityService.js';
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.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 $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService };
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
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 $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
@ -248,7 +248,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppLockService,
AchievementService,
CaptchaService,
CreateNotificationService,
CreateSystemUserService,
CustomEmojiService,
DeleteAccountService,
@ -325,6 +324,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
InstanceEntityService,
ModerationLogEntityService,
MutingEntityService,
RenoteMutingEntityService,
NoteEntityService,
NoteFavoriteEntityService,
NoteReactionEntityService,
@ -365,7 +365,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppLockService,
$AchievementService,
$CaptchaService,
$CreateNotificationService,
$CreateSystemUserService,
$CustomEmojiService,
$DeleteAccountService,
@ -442,6 +441,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$InstanceEntityService,
$ModerationLogEntityService,
$MutingEntityService,
$RenoteMutingEntityService,
$NoteEntityService,
$NoteFavoriteEntityService,
$NoteReactionEntityService,
@ -483,7 +483,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AppLockService,
AchievementService,
CaptchaService,
CreateNotificationService,
CreateSystemUserService,
CustomEmojiService,
DeleteAccountService,
@ -559,6 +558,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
InstanceEntityService,
ModerationLogEntityService,
MutingEntityService,
RenoteMutingEntityService,
NoteEntityService,
NoteFavoriteEntityService,
NoteReactionEntityService,
@ -599,7 +599,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AppLockService,
$AchievementService,
$CaptchaService,
$CreateNotificationService,
$CreateSystemUserService,
$CustomEmojiService,
$DeleteAccountService,
@ -675,6 +674,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$InstanceEntityService,
$ModerationLogEntityService,
$MutingEntityService,
$RenoteMutingEntityService,
$NoteEntityService,
$NoteFavoriteEntityService,
$NoteReactionEntityService,
@ -708,4 +708,4 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#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;
aliases: string[];
host: string | null;
license: string | null;
}): Promise<Emoji> {
const emoji = await this.emojisRepository.insert({
id: this.idService.genId(),
@ -55,10 +56,11 @@ export class CustomEmojiService {
originalUrl: data.driveFile.url,
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
type: data.driveFile.webpublicType ?? data.driveFile.type,
license: data.license,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) {
await this.db.queryResultCache!.remove(['meta_emojis']);
await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiAdded', {
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 { v4 as uuid } from 'uuid';
import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.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 { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type S3 from 'aws-sdk/clients/s3.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 = {
/** 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)) {
this.registerLogger.debug('web image and thumbnail not created (not an required file)');
if (!isMimeImage(type, 'sharp-convertible-image-with-bmp')) {
this.registerLogger.debug('web image and thumbnail not created (cannot convert by sharp)');
return {
webpublic: null,
thumbnail: null,
@ -284,22 +286,16 @@ export class DriveService {
let img: sharp.Sharp | null = null;
let satisfyWebpublic: boolean;
let isAnimated: boolean;
try {
img = sharp(path);
img = await sharpBmp(path, type);
const metadata = await img.metadata();
const isAnimated = metadata.pages && metadata.pages > 1;
// skip animated
if (isAnimated) {
return {
webpublic: null,
thumbnail: null,
};
}
isAnimated = !!(metadata.pages && metadata.pages > 1);
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.width && metadata.width <= 2048 &&
metadata.height && metadata.height <= 2048
@ -315,15 +311,13 @@ export class DriveService {
// #region webpublic
let webpublic: IImage | null = null;
if (generateWeb && !satisfyWebpublic) {
if (generateWeb && !satisfyWebpublic && !isAnimated) {
this.registerLogger.info('creating web image');
try {
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToJpeg(img, 2048, 2048);
} else if (['image/png'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
} else if (['image/svg+xml'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
} else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
} else {
this.registerLogger.debug('web image not created (not an required image)');
@ -333,6 +327,7 @@ export class DriveService {
}
} else {
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)');
}
// #endregion webpublic
@ -341,10 +336,10 @@ export class DriveService {
let thumbnail: IImage | null = null;
try {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(type)) {
thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 280);
if (isAnimated) {
thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
} else {
this.registerLogger.debug('thumbnail not created (not an required file)');
thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422);
}
} catch (err) {
this.registerLogger.warn('thumbnail not created (an error occured)', err as Error);
@ -476,7 +471,7 @@ export class DriveService {
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
// extを付加してデータベースの文字数制限に当たることはまずない
(name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled',
info.type.ext
info.type.ext,
);
if (user && !force) {
@ -728,10 +723,20 @@ export class DriveService {
const s3 = this.s3Service.getS3(meta);
await s3.deleteObject({
Bucket: meta.objectStorageBucket!,
Key: key,
}).promise();
try {
await s3.deleteObject({
Bucket: meta.objectStorageBucket!,
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
@ -749,7 +754,7 @@ export class DriveService {
}: UploadFromUrlArgs): Promise<DriveFile> {
// Create temp file
const [path, cleanup] = await createTemp();
try {
// write content at URL to temp file
const { filename: name } = await this.downloadService.downloadUrl(url, path);

View File

@ -16,7 +16,7 @@ import type {
UserListStreamTypes,
UserStreamTypes,
} 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 type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';

View File

@ -15,15 +15,28 @@ export type IImageStream = {
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 = {
quality: 85,
quality: 77,
alphaQuality: 95,
lossless: false,
nearLossless: false,
smartSubsample: true,
mixed: true,
effort: 2,
};
export const avifDefault: sharp.AvifOptions = {
quality: 60,
lossless: false,
effort: 2,
};
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
* with resize, remove metadata, resolve orientation, stop animation
@ -78,29 +61,22 @@ export class ImageProcessingService {
@bindThis
public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> {
const data = await sharp
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.webp(options)
.toBuffer();
const result = this.convertSharpToWebpStream(sharp, width, height, options);
return {
data,
ext: 'webp',
type: 'image/webp',
data: await result.data.toBuffer(),
ext: result.ext,
type: result.type,
};
}
@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);
}
@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
.resize(width, height, {
fit: 'inside',
@ -115,13 +91,56 @@ export class ImageProcessingService {
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
* with resize, remove metadata, resolve orientation, stop animation
*/
@bindThis
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

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

View File

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

View File

@ -57,7 +57,7 @@ export class RelayService {
const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
const activity = this.apRendererService.addContext(follow);
this.queueService.deliver(relayActor, activity, relay.inbox);
this.queueService.deliver(relayActor, activity, relay.inbox, false);
return relay;
}
@ -76,7 +76,7 @@ export class RelayService {
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const undo = this.apRendererService.renderUndo(follow, relayActor);
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);
}
@ -120,7 +120,7 @@ export class RelayService {
const signed = await this.apRendererService.attachLdSignature(copy, user);
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;
canInvite: boolean;
canManageCustomEmojis: boolean;
canSearchNotes: boolean;
canHideAds: boolean;
driveCapacityMb: number;
pinLimit: number;
@ -40,6 +41,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canPublicNote: true,
canInvite: false,
canManageCustomEmojis: false,
canSearchNotes: false,
canHideAds: false,
driveCapacityMb: 100,
pinLimit: 5,
@ -264,6 +266,7 @@ export class RoleService implements OnApplicationShutdown {
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', 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)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)),

View File

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

View File

@ -90,7 +90,7 @@ export class SignupService {
cipher: undefined,
passphrase: undefined,
},
} as any, (err, publicKey, privateKey) =>
}, (err, 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)) {
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送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox);
this.queueService.deliver(follower, content, followee.inbox, false);
}
// リモートからフォローリクエストを受けていたらReject送信
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));
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送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox);
this.queueService.deliver(follower, content, followee.inbox, false);
}
// リモートからフォローをされていたらRejectFollow送信
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox);
this.queueService.deliver(followee, content, follower.inbox, false);
}
}
@ -262,7 +262,7 @@ export class UserBlockingService implements OnApplicationShutdown {
// deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
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 { IdService } from '@/core/IdService.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 { FederatedInstanceService } from '@/core/FederatedInstanceService.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 type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -57,7 +57,7 @@ export class UserFollowingService {
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService,
private notificationService: NotificationService,
private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
@ -82,7 +82,7 @@ export class UserFollowingService {
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
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;
} 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)) {
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> {
if (follower.id === followee.id) return;
let alreadyFollowed = false as boolean;
await this.followingsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
followerId: follower.id,
followeeId: followee.id,
// 非正規化
followerHost: follower.host,
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null,
@ -169,35 +169,35 @@ export class UserFollowingService {
throw err;
}
});
const req = await this.followRequestsRepository.findOneBy({
followeeId: followee.id,
followerId: follower.id,
});
if (req) {
await this.followRequestsRepository.delete({
followeeId: followee.id,
followerId: follower.id,
});
// 通知を作成
this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', {
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
notifierId: followee.id,
});
}
if (alreadyFollowed) return;
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
//#region Increment counts
await Promise.all([
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
]);
//#endregion
//#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(i => {
@ -211,9 +211,9 @@ export class UserFollowingService {
});
}
//#endregion
this.perUserFollowingChart.update(follower, followee, true);
// Publish follow event
if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee.id, follower, {
@ -221,7 +221,7 @@ export class UserFollowingService {
}).then(async packed => {
this.globalEventService.publishUserEvent(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'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'follow', {
@ -230,12 +230,12 @@ export class UserFollowingService {
}
});
}
// Publish followed event
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(async packed => {
this.globalEventService.publishMainStream(followee.id, 'followed', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) {
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,
});
}
@ -265,16 +265,16 @@ export class UserFollowingService {
followerId: follower.id,
followeeId: followee.id,
});
if (following == null) {
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
return;
}
await this.followingsRepository.delete(following.id);
this.decrementFollowing(follower, followee);
// Publish unfollow event
if (!silent && this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee.id, follower, {
@ -282,7 +282,7 @@ export class UserFollowingService {
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'unfollow', {
@ -291,33 +291,33 @@ export class UserFollowingService {
}
});
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox);
this.queueService.deliver(follower, content, followee.inbox, false);
}
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
// local user has null host
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
private async decrementFollowing(
follower: {id: User['id']; host: User['host']; },
follower: { id: User['id']; host: User['host']; },
followee: { id: User['id']; host: User['host']; },
): Promise<void> {
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
//#region Decrement following / followers counts
await Promise.all([
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
]);
//#endregion
//#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(i => {
@ -331,7 +331,7 @@ export class UserFollowingService {
});
}
//#endregion
this.perUserFollowingChart.update(follower, followee, false);
}
@ -346,23 +346,23 @@ export class UserFollowingService {
requestId?: string,
): Promise<void> {
if (follower.id === followee.id) return;
// check blocking
const [blocking, blocked] = await Promise.all([
this.userBlockingService.checkBlocked(follower.id, followee.id),
this.userBlockingService.checkBlocked(followee.id, follower.id),
]);
if (blocking) throw new Error('blocking');
if (blocked) throw new Error('blocked');
const followRequest = await this.followRequestsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
followerId: follower.id,
followeeId: followee.id,
requestId,
// 非正規化
followerHost: follower.host,
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined,
@ -371,25 +371,25 @@ export class UserFollowingService {
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
// Publish receiveRequest event
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
this.userEntityService.pack(followee.id, followee, {
detail: true,
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
notifierId: follower.id,
followRequestId: followRequest.id,
});
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(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> {
if (this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
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({
followeeId: followee.id,
followerId: follower.id,
});
if (request == null) {
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
}
await this.followRequestsRepository.delete({
followeeId: followee.id,
followerId: follower.id,
});
this.userEntityService.pack(followee.id, followee, {
detail: true,
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
@ -440,18 +440,18 @@ export class UserFollowingService {
followeeId: followee.id,
followerId: follower.id,
});
if (request == null) {
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
}
await this.insertFollowingDoc(followee, follower);
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));
this.queueService.deliver(followee, content, follower.inbox);
this.queueService.deliver(followee, content, follower.inbox, false);
}
this.userEntityService.pack(followee.id, followee, {
detail: true,
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
@ -466,13 +466,13 @@ export class UserFollowingService {
const requests = await this.followRequestsRepository.findBy({
followeeId: user.id,
});
for (const request of requests) {
const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId });
this.acceptFollowRequest(user, follower);
}
}
/**
* 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));
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) {
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) {
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() {
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
@ -185,7 +186,7 @@ class DeliverManager {
for (const following of followers) {
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
&& recipe.to.inbox != null,
)
.forEach(recipe => inboxes.add(recipe.to.inbox!));
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
// deliver
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)) {
await this.flag(actor, activity);
} 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') {
to = [`${attributedTo}/followers`];
cc = ['https://www.w3.org/ns/activitystreams#Public'];
} else if (note.visibility === 'followers') {
to = [`${attributedTo}/followers`];
cc = [];
} else {
throw new Error('renderAnnounce: cannot render non-public note');
}
@ -116,7 +119,7 @@ export class ApRendererService {
if (block.blockee?.uri == null) {
throw new Error('renderBlock: missing blockee uri');
}
return {
type: 'Block',
id: `${this.config.url}/blocks/${block.id}`,
@ -134,10 +137,10 @@ export class ApRendererService {
published: note.createdAt.toISOString(),
object,
} as ICreate;
if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc;
return activity;
}
@ -155,7 +158,7 @@ export class ApRendererService {
public renderDocument(file: DriveFile): IApDocument {
return {
type: 'Document',
mediaType: file.type,
mediaType: file.webpublicType ?? file.type,
url: this.driveFileEntityService.getPublicUrl(file),
name: file.comment,
};
@ -297,16 +300,16 @@ export class ApRendererService {
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[];
};
let inReplyTo;
let inReplyToNote: Note | null;
if (note.replyId) {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
if (inReplyToUser != null) {
if (inReplyToNote.uri) {
inReplyTo = inReplyToNote.uri;
@ -322,24 +325,24 @@ export class ApRendererService {
} else {
inReplyTo = null;
}
let quote;
if (note.renoteId) {
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
if (renote) {
quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`;
}
}
const attributedTo = `${this.config.url}/users/${note.userId}`;
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
let to: string[] = [];
let cc: string[] = [];
if (note.visibility === 'public') {
to = ['https://www.w3.org/ns/activitystreams#Public'];
cc = [`${attributedTo}/followers`].concat(mentions);
@ -352,44 +355,44 @@ export class ApRendererService {
} else {
to = mentions;
}
const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({
id: In(note.mentions),
}) : [];
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
const mentionTags = mentionedUsers.map(u => this.renderMention(u));
const files = await getPromisedFiles(note.fileIds);
const text = note.text ?? '';
let poll: Poll | null = null;
if (note.hasPoll) {
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
}
let apText = text;
if (quote) {
apText += `\n\nRE: ${quote}`;
}
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
text: apText,
}));
const emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
const tag = [
...hashtagTags,
...mentionTags,
...apemojis,
];
const asPoll = poll ? {
type: 'Question',
content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
@ -601,7 +604,7 @@ export class ApRendererService {
if (typeof x === 'object' && x.id == null) {
x.id = `${this.config.url}/${uuid()}`;
}
return Object.assign({
'@context': [
'https://www.w3.org/ns/activitystreams',
@ -634,18 +637,18 @@ export class ApRendererService {
],
}, x as T & { id: string; });
}
@bindThis
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const ldSignature = this.ldSignatureService.use();
ldSignature.debug = false;
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
return activity;
}
/**
* Render OrderedCollectionPage
* @param id URL of self
@ -686,11 +689,11 @@ export class ApRendererService {
type: 'OrderedCollection',
totalItems,
};
if (first) page.first = first;
if (last) page.last = last;
if (orderedItems) page.orderedItems = orderedItems;
return page;
}

View File

@ -124,7 +124,7 @@ export class ApNoteService {
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)}`);
@ -180,7 +180,7 @@ export class ApNoteService {
const reply: Note | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, resolver).then(x => {
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');
} else {
return x;

View File

@ -164,6 +164,9 @@ export class ApPersonService implements OnModuleInit {
throw new Error('invalid Actor: wrong name');
}
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 (!(typeof x.summary === 'string' && x.summary.length > 0)) {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.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 { bindThis } from '@/decorators.js';
@ -37,6 +37,7 @@ export class AntennaEntityService {
notify: antenna.notify,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote,
};
}

View File

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.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 { User } from '@/models/entities/User.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 type { BlockingsRepository } from '@/models/index.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 { User } from '@/models/entities/User.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class BlockingEntityService {

View File

@ -1,13 +1,13 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.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 { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ChannelEntityService {

View File

@ -1,12 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';
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 type { Packed } from '@/misc/schema.js';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { Clip } from '@/models/entities/Clip.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class ClipEntityService {
@ -14,6 +14,9 @@ export class ClipEntityService {
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
@Inject(DI.clipFavoritesRepository)
private clipFavoritesRepository: ClipFavoritesRepository,
private userEntityService: UserEntityService,
) {
}
@ -21,25 +24,31 @@ export class ClipEntityService {
@bindThis
public async pack(
src: Clip['id'] | Clip,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Clip'>> {
const meId = me ? me.id : null;
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: clip.id,
createdAt: clip.createdAt.toISOString(),
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
userId: clip.userId,
user: this.userEntityService.pack(clip.user ?? clip.userId),
name: clip.name,
description: clip.description,
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
public packMany(
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 type { NotesRepository, DriveFilesRepository } from '@/models/index.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 type { User } from '@/models/entities/User.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
@ -89,9 +89,7 @@ export class DriveFileEntityService {
if (file.type.startsWith('video')) {
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) {
// 動画ではなくリモートかつメディアプロキシ
return this.getProxiedUrl(file.uri, 'static');
@ -106,7 +104,7 @@ export class DriveFileEntityService {
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

View File

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

View File

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.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 { Emoji } from '@/models/entities/Emoji.js';
import { bindThis } from '@/decorators.js';
@ -50,6 +50,7 @@ export class EmojiEntityService {
host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
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 type { FlashsRepository, FlashLikesRepository } from '@/models/index.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 { User } from '@/models/entities/User.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 type { FollowingsRepository } from '@/models/index.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 { User } from '@/models/entities/User.js';
import type { Following } from '@/models/entities/Following.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
type LocalFollowerFollowing = Following & {
@ -31,7 +32,6 @@ type RemoteFolloweeFollowing = Following & {
followeeInbox: string;
followeeSharedInbox: string;
};
import { bindThis } from '@/decorators.js';
@Injectable()
export class FollowingEntityService {

View File

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

View File

@ -1,11 +1,11 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.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 { Hashtag } from '@/models/entities/Hashtag.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class HashtagEntityService {

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.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 { } from '@/models/entities/Blocking.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 { NoteEntityService } from './NoteEntityService.js';
import { ModuleRef } from '@nestjs/core';
import { bindThis } from '@/decorators.js';
@Injectable()
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 type { Notification } from '@/models/entities/Notification.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 { isNotNull } from '@/misc/is-not-null.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 type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.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 { User } from '@/models/entities/User.js';
import type { Page } from '@/models/entities/Page.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
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,
asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator,
displayOrder: role.displayOrder,
policies: policies,
usersCount: assignedCount,
});

View File

@ -4,7 +4,7 @@ import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.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 { awaitAll } from '@/misc/prelude/await-all.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 { LocalUser, RemoteUser, User } 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 { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common';
@ -78,6 +78,9 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@ -195,6 +198,13 @@ export class UserEntityService implements OnModuleInit {
},
take: 1,
}).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),
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,
iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
}))) : undefined,
...(opts.detail ? {
@ -419,7 +430,7 @@ export class UserEntityService implements OnModuleInit {
userId: user.id,
}).then(result => result >= 1)
: 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,
name: role.name,
color: role.color,
@ -427,6 +438,7 @@ export class UserEntityService implements OnModuleInit {
description: role.description,
isModerator: role.isModerator,
isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder,
}))),
} : {}),
@ -493,6 +505,7 @@ export class UserEntityService implements OnModuleInit {
isBlocking: relation.isBlocking,
isBlocked: relation.isBlocked,
isMuted: relation.isMuted,
isRenoteMuted: relation.isRenoteMuted,
} : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;

View File

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

View File

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

View File

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

View File

@ -2,10 +2,10 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
const dictionary = {
'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-animation-convertible-image': ['image/jpeg', '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-animation-convertible-image-with-bmp': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/bmp'],
'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/tiff', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
'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/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);

View File

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

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
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 { Provider } from '@nestjs/common';
@ -190,6 +190,12 @@ const $mutingsRepository: Provider = {
inject: [DI.db],
};
const $renoteMutingsRepository: Provider = {
provide: DI.renoteMutingsRepository,
useFactory: (db: DataSource) => db.getRepository(RenoteMuting),
inject: [DI.db],
};
const $blockingsRepository: Provider = {
provide: DI.blockingsRepository,
useFactory: (db: DataSource) => db.getRepository(Blocking),
@ -280,6 +286,12 @@ const $clipNotesRepository: Provider = {
inject: [DI.db],
};
const $clipFavoritesRepository: Provider = {
provide: DI.clipFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(ClipFavorite),
inject: [DI.db],
};
const $antennasRepository: Provider = {
provide: DI.antennasRepository,
useFactory: (db: DataSource) => db.getRepository(Antenna),
@ -423,6 +435,7 @@ const $roleAssignmentsRepository: Provider = {
$notificationsRepository,
$metasRepository,
$mutingsRepository,
$renoteMutingsRepository,
$blockingsRepository,
$swSubscriptionsRepository,
$hashtagsRepository,
@ -438,6 +451,7 @@ const $roleAssignmentsRepository: Provider = {
$moderationLogsRepository,
$clipsRepository,
$clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository,
$antennaNotesRepository,
$promoNotesRepository,
@ -489,6 +503,7 @@ const $roleAssignmentsRepository: Provider = {
$notificationsRepository,
$metasRepository,
$mutingsRepository,
$renoteMutingsRepository,
$blockingsRepository,
$swSubscriptionsRepository,
$hashtagsRepository,
@ -504,6 +519,7 @@ const $roleAssignmentsRepository: Provider = {
$moderationLogsRepository,
$clipsRepository,
$clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository,
$antennaNotesRepository,
$promoNotesRepository,

View File

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

View File

@ -12,6 +12,12 @@ export class Clip {
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true,
})
public lastClippedAt: Date | null;
@Index()
@Column({
...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: '{}',
})
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;
@Column('varchar', {
length: 32768,
length: 65536,
})
public script: string;

View File

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

View File

@ -87,6 +87,11 @@ export class Note {
})
public localOnly: boolean;
@Column('varchar', {
length: 64, nullable: true,
})
public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | null;
@Column('smallint', {
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;
@Index({ unique: true })
@Column('varchar', {
length: 512, nullable: false,
})
public dateKey: string;
@Column({
...id(),
array: true,

View File

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

View File

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

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