diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index 91dce35155..3907615f73 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -2,6 +2,19 @@ # Misskey configuration #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# ┌────────────────────────┐ +#───┘ Initial Setup Password └───────────────────────────────────────────────────── + +# Password to initiate setting up admin account. +# It will not be used after the initial setup is complete. +# +# Be sure to change this when you set up Misskey via the Internet. +# +# The provider of the service who sets up Misskey on behalf of the customer should +# set this value to something unique when generating the Misskey config file, +# and provide it to the customer. +setupPassword: example_password_please_change_this_or_you_will_get_hacked + # ┌─────┐ #───┘ URL └───────────────────────────────────────────────────── diff --git a/.config/example.yml b/.config/example.yml index 7080159117..60a6a0aa71 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -59,6 +59,20 @@ # # publishTarballInsteadOfProvideRepositoryUrl: true +# ┌────────────────────────┐ +#───┘ Initial Setup Password └───────────────────────────────────────────────────── + +# Password to initiate setting up admin account. +# It will not be used after the initial setup is complete. +# +# Be sure to change this when you set up Misskey via the Internet. +# +# The provider of the service who sets up Misskey on behalf of the customer should +# set this value to something unique when generating the Misskey config file, +# and provide it to the customer. +# +# setupPassword: example_password_please_change_this_or_you_will_get_hacked + # ┌─────┐ #───┘ URL └───────────────────────────────────────────────────── diff --git a/.github/misskey/test.yml b/.github/misskey/test.yml index 7a4aa4ae6c..3c807e8b9e 100644 --- a/.github/misskey/test.yml +++ b/.github/misskey/test.yml @@ -1,5 +1,7 @@ url: 'http://misskey.local' +setupPassword: example_password_please_change_this_or_you_will_get_hacked + # ローカルでテストするときにポートを被らないようにするためデフォルトのものとは変える(以下同じ) port: 61812 diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml index 5afd7d2714..f26c9a4d45 100644 --- a/.github/workflows/check-misskey-js-autogen.yml +++ b/.github/workflows/check-misskey-js-autogen.yml @@ -21,6 +21,7 @@ jobs: uses: actions/checkout@v4.1.1 with: submodules: true + persist-credentials: false ref: refs/pull/${{ github.event.pull_request.number }}/merge - name: setup pnpm @@ -57,7 +58,7 @@ jobs: name: generated-misskey-js path: packages/misskey-js/generator/built/autogen - # pull_request_target safety: permissions: read-all, and there are no secrets used in this job + # pull_request_target safety: permissions: read-all, and no user codes are executed get-actual-misskey-js: runs-on: ubuntu-latest permissions: @@ -68,6 +69,7 @@ jobs: uses: actions/checkout@v4.1.1 with: submodules: true + persist-credentials: false ref: refs/pull/${{ github.event.pull_request.number }}/merge - name: Upload From Merged @@ -131,3 +133,7 @@ jobs: mode: delete message: "Thank you!" create_if_not_exists: false + + - name: Make failure if changes are detected + if: steps.check-changes.outputs.changes == 'true' + run: exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 249c559d3c..813c3b1329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## 2024.10.0 + +### Note +- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません) + - ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。 + - なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。 +- ユーザーデータを読み込む際の型が一部変更されました。 + - `twoFactorEnabled`, `usePasswordLessLogin`, `securityKeys`: 自分とモデレーター以外のユーザーからは取得できなくなりました + +### General +- Feat: サーバー初期設定時に初期パスワードを設定できるように +- Feat: 通報にモデレーションノートを残せるように +- Feat: 通報の解決種別を設定できるように +- Enhance: 通報の解決と転送を個別に行えるように +- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました +- Enhance: 依存関係の更新 +- Enhance: l10nの更新 +- Enhance: Playの「人気」タブで10件以上表示可能に #14399 +- Fix: 連合のホワイトリストが正常に登録されない問題を修正 + +### Client +- Enhance: デザインの調整 +- Enhance: ログイン画面の認証フローを改善 + +### Server +- Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように +- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように +- Enhance: 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるように ( #14697 ) + ## 2024.9.0 ### General @@ -6,6 +35,8 @@ - Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください - Feat: パスキーでログインボタンを実装 (#14574) +- Feat: フォローされた際のメッセージを設定できるように +- Feat: 連合をホワイトリスト制にできるように - Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) - Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680) @@ -21,6 +52,8 @@ - Enhance: ScratchpadにUIインスペクターを追加 - Enhance: Play編集画面の項目の並びを少しリデザイン - Enhance: 各種メニューをドロワー表示するかどうか設定可能に +- Enhance: AiScriptのMk:C:containerのオプションに`borderStyle`と`borderRadius`を追加 +- Enhance: CWでも絵文字をクリックしてメニューを表示できるように - Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 - Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正 - Fix: 月の違う同じ日はセパレータが表示されないのを修正 diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index d2525e0a7d..d2efbf709c 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -23,6 +23,7 @@ describe('Before setup instance', () => { cy.intercept('POST', '/api/admin/accounts/create').as('signup'); + cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked'); cy.get('[data-cy-admin-username] input').type('admin'); cy.get('[data-cy-admin-password] input').type('admin1234'); cy.get('[data-cy-admin-ok]').click(); @@ -119,11 +120,16 @@ describe('After user signup', () => { it('signin', () => { cy.visitHome(); - cy.intercept('POST', '/api/signin').as('signin'); + cy.intercept('POST', '/api/signin-flow').as('signin'); cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-username] input').type('alice'); - // Enterキーでサインインできるかの確認も兼ねる + + cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); + // Enterキーで続行できるかの確認も兼ねる + cy.get('[data-cy-signin-username] input').type('alice{enter}'); + + cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 }); + // Enterキーで続行できるかの確認も兼ねる cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); cy.wait('@signin'); @@ -138,8 +144,9 @@ describe('After user signup', () => { cy.visitHome(); cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-username] input').type('alice'); - cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); + + cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); + cy.get('[data-cy-signin-username] input').type('alice{enter}'); // TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 281f2e6ccd..197ff963ac 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -48,16 +48,19 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => { cy.request('POST', route, { username: username, password: password, + ...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}), }).its('body').as(username); }); Cypress.Commands.add('login', (username, password) => { cy.visitHome(); - cy.intercept('POST', '/api/signin').as('signin'); + cy.intercept('POST', '/api/signin-flow').as('signin'); cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-username] input').type(username); + cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); + cy.get('[data-cy-signin-username] input').type(`${username}{enter}`); + cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 }); cy.get('[data-cy-signin-password] input').type(`${password}{enter}`); cy.wait('@signin').as('signedIn'); diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/idea/MkAbuseReport.stories.impl.ts similarity index 88% rename from packages/frontend/src/components/MkAbuseReport.stories.impl.ts rename to idea/MkAbuseReport.stories.impl.ts index cf09c96fd4..717bceb23d 100644 --- a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts +++ b/idea/MkAbuseReport.stories.impl.ts @@ -7,8 +7,8 @@ import { action } from '@storybook/addon-actions'; import { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; -import { abuseUserReport } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; +import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js'; +import { commonHandlers } from '../packages/frontend/.storybook/mocks.js'; import MkAbuseReport from './MkAbuseReport.vue'; export const Default = { render(args) { diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index b6bfbfa682..24b15ee693 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1533,6 +1533,7 @@ _notification: reaction: "التفاعل" receiveFollowRequest: "طلبات المتابعة" followRequestAccepted: "طلبات المتابعة المقبولة" + login: "لِج" app: "إشعارات التطبيقات المرتبطة" _actions: followBack: "تابعك بالمثل" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 6fb51ea5d8..642fdf2b73 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -451,7 +451,6 @@ or: "অথবা" language: "ভাষা" uiLanguage: "UI এর ভাষা" aboutX: "{x} সম্পর্কে" -disableDrawer: "ড্রয়ার মেনু প্রদর্শন করবেন না" noHistory: "কোনো ইতিহাস নেই" signinHistory: "প্রবেশ করার ইতিহাস" doing: "প্রক্রিয়া করছে..." @@ -1314,6 +1313,7 @@ _notification: pollEnded: "পোল শেষ" receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" + login: "প্রবেশ করুন" app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি" _actions: followBack: "ফলো ব্যাক করেছে" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 49ea2bb64f..bcea736e7a 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -236,6 +236,8 @@ silencedInstances: "Instàncies silenciades" silencedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols silenciar. Tots els comptes de les instàncies llistades s'establiran com silenciades i només podran fer sol·licitacions de seguiment, i no podran mencionar als comptes locals si no els segueixen. Això no afectarà les instàncies bloquejades." mediaSilencedInstances: "Instàncies amb els arxius silenciats" mediaSilencedInstancesDescription: "Llista els noms dels servidors que vulguis silenciar els arxius, un servidor per línia. Tots els comptes que pertanyin als servidors llistats seran tractats com sensibles i no podran fer servir emojis personalitzats. Això no tindrà efecte sobre els servidors blocats." +federationAllowedHosts: "Llista de servidors federats" +federationAllowedHostsDescription: "Llista dels servidors amb els quals es federa." muteAndBlock: "Silencia i bloca" mutedUsers: "Usuaris silenciats" blockedUsers: "Usuaris bloquejats" @@ -334,6 +336,7 @@ renameFolder: "Canvia el nom de la carpeta" deleteFolder: "Elimina la carpeta" folder: "Carpeta " addFile: "Afegeix un fitxer" +showFile: "Mostrar fitxer" emptyDrive: "La teva unitat és buida" emptyFolder: "La carpeta està buida" unableToDelete: "No es pot eliminar" @@ -509,7 +512,10 @@ uiLanguage: "Idioma de l'interfície" aboutX: "Respecte a {x}" emojiStyle: "Estil d'emoji" native: "Nadiu" -disableDrawer: "No mostrar els menús en calaixos" +menuStyle: "Estil de menú" +style: "Estil" +drawer: "Calaix" +popup: "Emergent" showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor" showReactionsCount: "Mostra el nombre de reaccions a les publicacions" noHistory: "No hi ha un registre previ" @@ -1269,6 +1275,15 @@ fromX: "De {x}" genEmbedCode: "Obtenir el codi per incrustar" noteOfThisUser: "Notes d'aquest usuari" clipNoteLimitExceeded: "No es poden afegir més notes a aquest clip." +performance: "Rendiment" +modified: "Modificat" +discard: "Descarta" +thereAreNChanges: "Hi ha(n) {n} canvi(s)" +signinWithPasskey: "Inicia sessió amb Passkey" +unknownWebAuthnKey: "Passkey desconeguda" +passkeyVerificationFailed: "La verificació a fallat" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya." +messageToFollower: "Missatge als meus seguidors" _delivery: status: "Estat d'entrega " stop: "Suspés" @@ -2236,6 +2251,9 @@ _profile: changeBanner: "Canviar el bàner " verifiedLinkDescription: "Escrivint una adreça URL que enllaci a aquest perfil, una icona de propietat verificada es mostrarà al costat del camp." avatarDecorationMax: "Pot afegir un màxim de {max} decoracions." + followedMessage: "Missatge als nous seguidors" + followedMessageDescription: "Es pot configurar un missatge curt que es mostra a l'altra persona quan comença a seguir-te." + followedMessageDescriptionForLockedAccount: "Si comencen a seguir-te es mostra un missatge de quan es permet aquesta sol·licitud. " _exportOrImport: allNotes: "Totes les publicacions" favoritedNotes: "Notes preferides" @@ -2374,6 +2392,7 @@ _notification: renotedBySomeUsers: "L'han impulsat {n} usuaris" followedBySomeUsers: "Et segueixen {n} usuaris" flushNotification: "Netejar notificacions" + exportOfXCompleted: "Completada l'exportació de {n}" _types: all: "Tots" note: "Notes noves" @@ -2388,6 +2407,9 @@ _notification: followRequestAccepted: "Petició de seguiment acceptada" roleAssigned: "Rol donat" achievementEarned: "Assoliment desbloquejat" + exportCompleted: "Exportació completada" + login: "Iniciar sessió" + test: "Prova la notificació" app: "Notificacions d'aplicacions" _actions: followBack: "t'ha seguit també" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 7db7424762..1e391fcc31 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -471,7 +471,6 @@ uiLanguage: "Jazyk uživatelského rozhraní" aboutX: "O {x}" emojiStyle: "Styl emoji" native: "Výchozí" -disableDrawer: "Nepoužívat šuplíkové menu" showNoteActionsOnlyHover: "Zobrazit akce poznámky jenom při naběhnutí myši" noHistory: "Žádná historie" signinHistory: "Historie přihlášení" @@ -1963,6 +1962,7 @@ _notification: receiveFollowRequest: "Obdržené žádosti o sledování" followRequestAccepted: "Přijaté žádosti o sledování" achievementEarned: "Úspěch odemčen" + login: "Přihlásit se" app: "Oznámení z propojených aplikací" _actions: followBack: "vás začal sledovat zpět" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 8e44a3bbd4..871ed87564 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -491,7 +491,6 @@ uiLanguage: "Sprache der Benutzeroberfläche" aboutX: "Über {x}" emojiStyle: "Emoji-Stil" native: "Nativ" -disableDrawer: "Keine ausfahrbaren Menüs verwenden" showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen" noHistory: "Kein Verlauf gefunden" signinHistory: "Anmeldungsverlauf" @@ -2142,6 +2141,7 @@ _notification: receiveFollowRequest: "Erhaltene Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen" achievementEarned: "Errungenschaft freigeschaltet" + login: "Anmelden" app: "Benachrichtigungen von Apps" _actions: followBack: "folgt dir nun auch" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 5eca348e18..4657842ca5 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -378,6 +378,7 @@ _notification: renote: "Κοινοποίηση σημειώματος" quote: "Παράθεση" reaction: "Αντιδράσεις" + login: "Σύνδεση" _actions: reply: "Απάντηση" renote: "Κοινοποίηση σημειώματος" diff --git a/locales/en-US.yml b/locales/en-US.yml index 61e45628ae..7af6d65ea4 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -8,6 +8,9 @@ search: "Search" notifications: "Notifications" username: "Username" password: "Password" +initialPasswordForSetup: "Initial password for setup" +initialPasswordIsIncorrect: "Initial password for setup is incorrect" +initialPasswordForSetupDescription: "Use the password you entered in the configuration file if you installed Misskey yourself.\n If you are using a Misskey hosting service, use the password provided.\n If you have not set a password, leave it blank to continue." forgotPassword: "Forgot password" fetchingAsApObject: "Fetching from the Fediverse..." ok: "OK" @@ -236,6 +239,8 @@ silencedInstances: "Silenced instances" silencedInstancesDescription: "List the host names of the servers that you want to silence, separated by a new line. All accounts belonging to the listed servers will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked servers." mediaSilencedInstances: "Media-silenced servers" mediaSilencedInstancesDescription: "List the host names of the servers that you want to media-silence, separated by a new line. All accounts belonging to the listed servers will be treated as sensitive, and can't use custom emojis. This will not affect the blocked servers." +federationAllowedHosts: "Federation allowed servers" +federationAllowedHostsDescription: "Specify the hostnames of the servers you want to allow federation separated by line breaks." muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" @@ -334,6 +339,7 @@ renameFolder: "Rename this folder" deleteFolder: "Delete this folder" folder: "Folder" addFile: "Add a file" +showFile: "Show files" emptyDrive: "Your Drive is empty" emptyFolder: "This folder is empty" unableToDelete: "Unable to delete" @@ -509,7 +515,10 @@ uiLanguage: "User interface language" aboutX: "About {x}" emojiStyle: "Emoji style" native: "Native" -disableDrawer: "Don't use drawer-style menus" +menuStyle: "Menu style" +style: "Style" +drawer: "Drawer" +popup: "Pop up" showNoteActionsOnlyHover: "Only show note actions on hover" showReactionsCount: "See the number of reactions in notes" noHistory: "No history available" @@ -592,6 +601,8 @@ ascendingOrder: "Ascending" descendingOrder: "Descending" scratchpad: "Scratchpad" scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Misskey in it." +uiInspector: "UI inspector" +uiInspectorDescription: "You can see the UI component server list on memory. UI component will be generated by Ui:C: function." output: "Output" script: "Script" disablePagesScript: "Disable AiScript on Pages" @@ -1126,7 +1137,7 @@ options: "Options" specifyUser: "Specific user" lookupConfirm: "Do you want to look up?" openTagPageConfirm: "Do you want to open a hashtag page?" -specifyHost: "Specify a host" +specifyHost: "Specific host" failedToPreviewUrl: "Could not preview" update: "Update" rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction" @@ -1267,6 +1278,15 @@ fromX: "From {x}" genEmbedCode: "Generate embed code" noteOfThisUser: "Notes by this user" clipNoteLimitExceeded: "No more notes can be added to this clip." +performance: "Performance" +modified: "Modified" +discard: "Discard" +thereAreNChanges: "There are {n} change(s)" +signinWithPasskey: "Sign in with Passkey" +unknownWebAuthnKey: "Unknown Passkey" +passkeyVerificationFailed: "Passkey verification has failed." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled." +messageToFollower: "Message to followers" _delivery: status: "Delivery status" stop: "Suspended" @@ -1401,6 +1421,7 @@ _serverSettings: fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability." fanoutTimelineDbFallback: "Fallback to database" fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved." + reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase." inquiryUrl: "Inquiry URL" inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." _accountMigration: @@ -1734,6 +1755,11 @@ _role: canSearchNotes: "Usage of note search" canUseTranslator: "Translator usage" avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" + canImportAntennas: "Allow importing antennas" + canImportBlocking: "Allow importing blocking" + canImportFollowing: "Allow importing following" + canImportMuting: "Allow importing muting" + canImportUserLists: "Allow importing lists" _condition: roleAssignedTo: "Assigned to manual roles" isLocal: "Local user" @@ -2228,6 +2254,9 @@ _profile: changeBanner: "Change banner" verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field." avatarDecorationMax: "You can add up to {max} decorations." + followedMessage: "Message when you are followed" + followedMessageDescription: "You can set a short message to be displayed to the recipient when they follow you." + followedMessageDescriptionForLockedAccount: "If you have set up that follow requests require approval, this will be displayed when you grant a follow request." _exportOrImport: allNotes: "All notes" favoritedNotes: "Favorite notes" @@ -2366,6 +2395,8 @@ _notification: renotedBySomeUsers: "Renote from {n} users" followedBySomeUsers: "Followed by {n} users" flushNotification: "Clear notifications" + exportOfXCompleted: "Export of {x} has been completed" + login: "Someone logged in" _types: all: "All" note: "New notes" @@ -2380,6 +2411,9 @@ _notification: followRequestAccepted: "Accepted follow requests" roleAssigned: "Role given" achievementEarned: "Achievement unlocked" + exportCompleted: "The export has been completed" + login: "Sign In" + test: "Notification test" app: "Notifications from linked apps" _actions: followBack: "followed you back" @@ -2446,6 +2480,7 @@ _webhookSettings: abuseReportResolved: "When resolved abuse report" userCreated: "When user is created" deleteConfirm: "Are you sure you want to delete the Webhook?" + testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data." _abuseReport: _notificationRecipient: createRecipient: "Add a recipient for abuse reports" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 45ad792717..10966a77b6 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -502,7 +502,6 @@ uiLanguage: "Idioma de visualización de la interfaz" aboutX: "Acerca de {x}" emojiStyle: "Estilo de emoji" native: "Nativo" -disableDrawer: "No mostrar los menús en cajones" showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor" showReactionsCount: "Mostrar el número de reacciones en las notas" noHistory: "No hay datos en el historial" @@ -2344,6 +2343,7 @@ _notification: followRequestAccepted: "El seguimiento fue aceptado" roleAssigned: "Rol asignado" achievementEarned: "Logro desbloqueado" + login: "Iniciar sesión" app: "Notificaciones desde aplicaciones" _actions: followBack: "Te sigue de vuelta" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index c1e2555d0d..d15fadcb1c 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -493,7 +493,6 @@ uiLanguage: "Langue d’affichage de l’interface" aboutX: "À propos de {x}" emojiStyle: "Style des émojis" native: "Natif" -disableDrawer: "Les menus ne s'affichent pas dans le tiroir" showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol" showReactionsCount: "Afficher le nombre de réactions des notes" noHistory: "Pas d'historique" @@ -2038,6 +2037,7 @@ _notification: followRequestAccepted: "Demande d'abonnement acceptée" roleAssigned: "Rôle reçu" achievementEarned: "Déverrouillage d'accomplissement" + login: "Se connecter" app: "Notifications provenant des apps" _actions: followBack: "Suivre" diff --git a/locales/hu-HU.yml b/locales/hu-HU.yml index 023a91494d..acc27ed092 100644 --- a/locales/hu-HU.yml +++ b/locales/hu-HU.yml @@ -96,6 +96,7 @@ _notification: renote: "Renote" quote: "Idézet" reaction: "Reakciók" + login: "Bejelentkezés" _actions: renote: "Renote" _deck: diff --git a/locales/id-ID.yml b/locales/id-ID.yml index b579d3261b..4c2040dd07 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -504,7 +504,6 @@ uiLanguage: "Bahasa antarmuka pengguna" aboutX: "Tentang {x}" emojiStyle: "Gaya emoji" native: "Native" -disableDrawer: "Jangan gunakan menu bergaya laci" showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk" showReactionsCount: "Lihat jumlah reaksi dalam catatan" noHistory: "Tidak ada riwayat" @@ -2355,6 +2354,7 @@ _notification: followRequestAccepted: "Permintaan mengikuti disetujui" roleAssigned: "Peran Diberikan" achievementEarned: "Pencapaian didapatkan" + login: "Masuk" app: "Notifikasi dari aplikasi tertaut" _actions: followBack: "Ikuti Kembali" diff --git a/locales/index.d.ts b/locales/index.d.ts index 5a484c7ec9..be0f289beb 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -52,6 +52,20 @@ export interface Locale extends ILocale { * パスワード */ "password": string; + /** + * 初期設定開始用パスワード + */ + "initialPasswordForSetup": string; + /** + * 初期設定開始用のパスワードが違います。 + */ + "initialPasswordIsIncorrect": string; + /** + * Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。 + * Misskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。 + * パスワードを設定していない場合は、空欄にしたまま続行してください。 + */ + "initialPasswordForSetupDescription": string; /** * パスワードを忘れた */ @@ -964,6 +978,14 @@ export interface Locale extends ILocale { * メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。 */ "mediaSilencedInstancesDescription": string; + /** + * 連合を許可するサーバー + */ + "federationAllowedHosts": string; + /** + * 連合を許可するサーバーのホストを改行で区切って設定します。 + */ + "federationAllowedHostsDescription": string; /** * ミュートとブロック */ @@ -1816,6 +1838,10 @@ export interface Locale extends ILocale { * モデレーションノート */ "moderationNote": string; + /** + * モデレーター間でだけ共有されるメモを記入することができます。 + */ + "moderationNoteDescription": string; /** * モデレーションノートを追加する */ @@ -2876,22 +2902,10 @@ export interface Locale extends ILocale { * 通報元 */ "reporterOrigin": string; - /** - * リモートサーバーに通報を転送する - */ - "forwardReport": string; - /** - * リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。 - */ - "forwardReportIsAnonymous": string; /** * 送信 */ "send": string; - /** - * 対応済みにする - */ - "abuseMarkAsResolved": string; /** * 新しいタブで開く */ @@ -3696,6 +3710,10 @@ export interface Locale extends ILocale { * パスワードが間違っています。 */ "incorrectPassword": string; + /** + * ワンタイムパスワードが間違っているか、期限切れになっています。 + */ + "incorrectTotp": string; /** * 「{choice}」に投票しますか? */ @@ -5144,6 +5162,41 @@ export interface Locale extends ILocale { * パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。 */ "passkeyVerificationSucceededButPasswordlessLoginDisabled": string; + /** + * フォロワーへのメッセージ + */ + "messageToFollower": string; + /** + * 対象 + */ + "target": string; + "_abuseUserReport": { + /** + * 転送 + */ + "forward": string; + /** + * 匿名のシステムアカウントとして、リモートサーバーに通報を転送します。 + */ + "forwardDescription": string; + /** + * 解決 + */ + "resolve": string; + /** + * 是認 + */ + "accept": string; + /** + * 否認 + */ + "reject": string; + /** + * 内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。 + * 内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。 + */ + "resolveTutorial": string; + }; "_delivery": { /** * 配信状態 @@ -8729,6 +8782,18 @@ export interface Locale extends ILocale { * 最大{max}つまでデコレーションを付けられます。 */ "avatarDecorationMax": ParameterizedString<"max">; + /** + * フォローされた時のメッセージ + */ + "followedMessage": string; + /** + * フォローされた時に相手に表示する短いメッセージを設定できます。 + */ + "followedMessageDescription": string; + /** + * フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。 + */ + "followedMessageDescriptionForLockedAccount": string; }; "_exportOrImport": { /** @@ -9265,6 +9330,10 @@ export interface Locale extends ILocale { * {x}のエクスポートが完了しました */ "exportOfXCompleted": ParameterizedString<"x">; + /** + * ログインがありました + */ + "login": string; "_types": { /** * すべて @@ -9322,6 +9391,10 @@ export interface Locale extends ILocale { * エクスポートが完了した */ "exportCompleted": string; + /** + * ログイン + */ + "login": string; /** * 通知のテスト */ @@ -9739,6 +9812,14 @@ export interface Locale extends ILocale { * 通報を解決 */ "resolveAbuseReport": string; + /** + * 通報を転送 + */ + "forwardAbuseReport": string; + /** + * 通報のモデレーションノート更新 + */ + "updateAbuseReportNote": string; /** * 招待コードを作成 */ diff --git a/locales/it-IT.yml b/locales/it-IT.yml index f56cd4e5b2..0399ba4d9c 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -236,6 +236,8 @@ silencedInstances: "Istanze silenziate" silencedInstancesDescription: "Elenca i nomi host delle istanze che vuoi silenziare. Tutti i profili nelle istanze silenziate vengono trattati come tali. Possono solo inviare richieste di follow e menzionare soltanto i profili locali che seguono. Le istanze bloccate non sono interessate." mediaSilencedInstances: "Istanze coi media silenziati" mediaSilencedInstancesDescription: "Elenca i nomi host delle istanze di cui vuoi silenziare i media, uno per riga. Tutti gli allegati dei profili nelle istanze silenziate per via degli allegati espliciti, verranno impostati come tali, le emoji personalizzate non saranno disponibili. Le istanze bloccate sono escluse." +federationAllowedHosts: "Server a cui consentire la federazione" +federationAllowedHostsDescription: "Indica gli host dei server a cui è consentita la federazione, uno per ogni linea." muteAndBlock: "Silenziare e bloccare" mutedUsers: "Profili silenziati" blockedUsers: "Profili bloccati" @@ -334,6 +336,7 @@ renameFolder: "Rinomina cartella" deleteFolder: "Elimina cartella" folder: "Cartella" addFile: "Allega" +showFile: "Visualizza file" emptyDrive: "Il Drive è vuoto" emptyFolder: "La cartella è vuota" unableToDelete: "Eliminazione impossibile" @@ -509,7 +512,10 @@ uiLanguage: "Lingua di visualizzazione dell'interfaccia" aboutX: "Informazioni su {x}" emojiStyle: "Stile emoji" native: "Nativo" -disableDrawer: "Non mostrare il menù sul drawer" +menuStyle: "Stile menu" +style: "Stile" +drawer: "Drawer" +popup: "Popup" showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mouse" showReactionsCount: "Visualizza il numero di reazioni su una nota" noHistory: "Nessuna cronologia" @@ -1270,6 +1276,14 @@ genEmbedCode: "Ottieni il codice di incorporamento" noteOfThisUser: "Elenco di Note di questo profilo" clipNoteLimitExceeded: "Non è possibile aggiungere ulteriori Note a questa Clip." performance: "Prestazioni" +modified: "Modificato" +discard: "Scarta" +thereAreNChanges: "Ci sono {n} cambiamenti" +signinWithPasskey: "Accedi con passkey" +unknownWebAuthnKey: "Questa è una passkey sconosciuta." +passkeyVerificationFailed: "La verifica della passkey non è riuscita." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato." +messageToFollower: "Messaggio ai follower" _delivery: status: "Stato della consegna" stop: "Sospensione" @@ -2237,6 +2251,9 @@ _profile: changeBanner: "Cambia intestazione" verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo." avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni." + followedMessage: "Messaggio, quando qualcuno ti segue" + followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono." + followedMessageDescriptionForLockedAccount: "Quando approvi una richiesta di follow, verrà visualizzato questo testo." _exportOrImport: allNotes: "Tutte le note" favoritedNotes: "Note preferite" @@ -2375,6 +2392,7 @@ _notification: renotedBySomeUsers: "{n} Rinota" followedBySomeUsers: "{n} follower" flushNotification: "Azzera le notifiche" + exportOfXCompleted: "Abbiamo completato l'esportazione di {x}" _types: all: "Tutto" note: "Nuove Note" @@ -2389,6 +2407,9 @@ _notification: followRequestAccepted: "Richiesta di follow accettata" roleAssigned: "Ruolo concesso" achievementEarned: "Risultato raggiunto" + exportCompleted: "Esportazione completata" + login: "Accedi" + test: "Prova la notifica" app: "Notifiche da applicazioni" _actions: followBack: "Segui" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 219a92fbb8..d898624b5e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -9,6 +9,9 @@ reset: "リセット" notifications: "通知" username: "ユーザー名" password: "パスワード" +initialPasswordForSetup: "初期設定開始用パスワード" +initialPasswordIsIncorrect: "初期設定開始用のパスワードが違います。" +initialPasswordForSetupDescription: "Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。\nMisskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。\nパスワードを設定していない場合は、空欄にしたまま続行してください。" forgotPassword: "パスワードを忘れた" fetchingAsApObject: "連合に照会中" ok: "OK" @@ -237,6 +240,8 @@ silencedInstances: "サイレンスしたサーバー" silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。" mediaSilencedInstances: "メディアサイレンスしたサーバー" mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。" +federationAllowedHosts: "連合を許可するサーバー" +federationAllowedHostsDescription: "連合を許可するサーバーのホストを改行で区切って設定します。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" @@ -450,6 +455,7 @@ totpDescription: "認証アプリを使ってワンタイムパスワードを moderator: "モデレーター" moderation: "モデレーション" moderationNote: "モデレーションノート" +moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。" addModerationNote: "モデレーションノートを追加する" moderationLogs: "モデログ" nUsersMentioned: "{n}人が投稿" @@ -715,10 +721,7 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ reporter: "通報者" reporteeOrigin: "通報先" reporterOrigin: "通報元" -forwardReport: "リモートサーバーに通報を転送する" -forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" send: "送信" -abuseMarkAsResolved: "対応済みにする" openInNewTab: "新しいタブで開く" openInSideView: "サイドビューで開く" defaultNavigationBehaviour: "デフォルトのナビゲーション" @@ -920,6 +923,7 @@ followersVisibility: "フォロワーの公開範囲" continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" incorrectPassword: "パスワードが間違っています。" +incorrectTotp: "ワンタイムパスワードが間違っているか、期限切れになっています。" voteConfirm: "「{choice}」に投票しますか?" hide: "隠す" useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示" @@ -1282,6 +1286,16 @@ signinWithPasskey: "パスキーでログイン" unknownWebAuthnKey: "登録されていないパスキーです。" passkeyVerificationFailed: "パスキーの検証に失敗しました。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" +messageToFollower: "フォロワーへのメッセージ" +target: "対象" + +_abuseUserReport: + forward: "転送" + forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。" + resolve: "解決" + accept: "是認" + reject: "否認" + resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。" _delivery: status: "配信状態" @@ -2298,6 +2312,9 @@ _profile: changeBanner: "バナー画像を変更" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" + followedMessage: "フォローされた時のメッセージ" + followedMessageDescription: "フォローされた時に相手に表示する短いメッセージを設定できます。" + followedMessageDescriptionForLockedAccount: "フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。" _exportOrImport: allNotes: "全てのノート" @@ -2446,6 +2463,7 @@ _notification: followedBySomeUsers: "{n}人にフォローされました" flushNotification: "通知の履歴をリセットする" exportOfXCompleted: "{x}のエクスポートが完了しました" + login: "ログインがありました" _types: all: "すべて" @@ -2462,6 +2480,7 @@ _notification: roleAssigned: "ロールが付与された" achievementEarned: "実績の獲得" exportCompleted: "エクスポートが完了した" + login: "ログイン" test: "通知のテスト" app: "連携アプリからの通知" @@ -2582,6 +2601,8 @@ _moderationLogTypes: markSensitiveDriveFile: "ファイルをセンシティブ付与" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" resolveAbuseReport: "通報を解決" + forwardAbuseReport: "通報を転送" + updateAbuseReportNote: "通報のモデレーションノート更新" createInvitation: "招待コードを作成" createAd: "広告を作成" deleteAd: "広告を削除" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 98045b43ac..4f950059a7 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -509,7 +509,6 @@ uiLanguage: "UIの表示言語" aboutX: "{x}について" emojiStyle: "絵文字のスタイル" native: "ネイティブ" -disableDrawer: "メニューをドロワーで表示せえへん" showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示するで" showReactionsCount: "ノートのリアクション数を表示する" noHistory: "履歴はないわ。" @@ -2375,6 +2374,7 @@ _notification: followRequestAccepted: "フォローが受理されたで" roleAssigned: "ロールが付与された" achievementEarned: "実績の獲得" + login: "ログイン" app: "連携アプリからの通知や" _actions: followBack: "フォローバック" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index b3ad46f2b1..222599572a 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -77,6 +77,8 @@ _profile: username: "ಬಳಕೆಹೆಸರು" _notification: youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು" + _types: + login: "ಪ್ರವೇಶ" _actions: reply: "ಉತ್ತರಿಸು" _deck: diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index 77fcc9f489..f8a0d328a3 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -476,7 +476,6 @@ uiLanguage: "UI 표시 언어" aboutX: "{x}에 대해서" emojiStyle: "이모지 모양" native: "기본" -disableDrawer: "드로어 메뉴 쓰지 않기" showNoteActionsOnlyHover: "마우스 올맀을 때만 노트 액션 버턴 보이기" noHistory: "기록이 없십니다" signinHistory: "로그인 기록" @@ -814,6 +813,7 @@ _notification: mention: "멘션" quote: "따오기" reaction: "반엉" + login: "로그인" _actions: reply: "답하기" _deck: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index e9b09b0c76..b85bc048e1 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -8,6 +8,9 @@ search: "검색" notifications: "알림" username: "유저명" password: "비밀번호" +initialPasswordForSetup: "초기 설정용 비밀번호" +initialPasswordIsIncorrect: "초기 설정용 비밀번호가 올바르지 않습니다." +initialPasswordForSetupDescription: "Misskey를 직접 설치하는 경우, 설정 파일에 입력해둔 비밀번호를 사용하세요.\nMisskey 설치를 도와주는 호스팅 서비스 등을 사용하는 경우, 서비스 제공자로부터 받은 비밀번호를 사용하세요.\n비밀번호를 따로 설정하지 않은 경우, 아무것도 입력하지 않아도 됩니다." forgotPassword: "비밀번호 재설정" fetchingAsApObject: "연합에서 찾아보는 중" ok: "확인" @@ -236,6 +239,8 @@ silencedInstances: "사일런스한 서버" silencedInstancesDescription: "사일런스하려는 서버의 호스트명을 한 줄에 하나씩 입력합니다. 사일런스된 서버에 소속된 유저는 모두 '사일런스'된 상태로 취급되며, 이 서버로부터의 팔로우가 프로필 설정과 무관하게 승인제로 변경되고, 팔로워가 아닌 로컬 유저에게는 멘션할 수 없게 됩니다. 정지된 서버에는 적용되지 않습니다." mediaSilencedInstances: "미디어를 사일런스한 서버" mediaSilencedInstancesDescription: "미디어를 사일런스 하려는 서버의 호스트를 한 줄에 하나씩 입력합니다. 미디어가 사일런스된 서버의 유저가 업로드한 파일은 모두 민감한 미디어로 처리되며, 커스텀 이모지를 사용할 수 없게 됩니다. 또한, 차단한 인스턴스에는 적용되지 않습니다." +federationAllowedHosts: "연합을 허가하는 서버" +federationAllowedHostsDescription: "연합을 허가하는 서버의 호스트를 엔터로 구분해서 설정합니다." muteAndBlock: "뮤트 및 차단" mutedUsers: "뮤트한 유저" blockedUsers: "차단한 유저" @@ -334,6 +339,7 @@ renameFolder: "폴더 이름 바꾸기" deleteFolder: "폴더 삭제" folder: "폴더" addFile: "파일 추가" +showFile: "파일 표시하기" emptyDrive: "드라이브가 비어 있습니다" emptyFolder: "폴더가 비어 있습니다" unableToDelete: "삭제할 수 없습니다" @@ -509,7 +515,10 @@ uiLanguage: "UI 표시 언어" aboutX: "{x}에 대하여" emojiStyle: "이모지 스타일" native: "기본" -disableDrawer: "드로어 메뉴를 사용하지 않기" +menuStyle: "메뉴 스타일" +style: "스타일" +drawer: "서랍" +popup: "팝업" showNoteActionsOnlyHover: "마우스가 올라간 때에만 노트 동작 버튼을 표시하기" showReactionsCount: "노트의 반응 수를 표시하기" noHistory: "기록이 없습니다" @@ -1273,6 +1282,11 @@ performance: "퍼포먼스" modified: "변경 있음" discard: "파기" thereAreNChanges: "{n}건 변경이 있습니다." +signinWithPasskey: "패스키로 로그인" +unknownWebAuthnKey: "등록되지 않은 패스키입니다." +passkeyVerificationFailed: "패스키 검증을 실패했습니다." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다." +messageToFollower: "팔로워에 보낼 메시지" _delivery: status: "전송 상태" stop: "정지됨" @@ -2240,6 +2254,9 @@ _profile: changeBanner: "배너 이미지 변경" verifiedLinkDescription: "내용에 자신의 프로필로 향하는 링크가 포함된 페이지의 URL을 삽입하면 소유자 인증 마크가 표시됩니다." avatarDecorationMax: "최대 {max}개까지 장식을 할 수 있습니다." + followedMessage: "팔로우 받았을 때 메시지" + followedMessageDescription: "팔로우 받았을 때 상대방에게 보여줄 단문 메시지를 설정할 수 있습니다." + followedMessageDescriptionForLockedAccount: "팔로우를 승인제로 한 경우, 팔로우 요청을 수락했을 때 보여줍니다." _exportOrImport: allNotes: "모든 노트" favoritedNotes: "즐겨찾기한 노트" @@ -2378,6 +2395,8 @@ _notification: renotedBySomeUsers: "{n}명이 리노트했습니다" followedBySomeUsers: "{n}명에게 팔로우됨" flushNotification: "알림 이력을 초기화" + exportOfXCompleted: "{x} 추출에 성공했습니다." + login: "로그인 알림이 있습니다" _types: all: "전부" note: "사용자의 새 글" @@ -2392,6 +2411,9 @@ _notification: followRequestAccepted: "팔로우 요청이 승인되었을 때" roleAssigned: "역할이 부여 됨" achievementEarned: "도전 과제 획득" + exportCompleted: "추출을 성공함" + login: "로그인" + test: "알림 테스트" app: "연동된 앱을 통한 알림" _actions: followBack: "팔로우" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 1bead5635d..b100d0300f 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -456,6 +456,7 @@ _notification: renote: "Renote" quote: "ອ້າງອີງ" reaction: "Reaction" + login: "ເຂົ້າ​ສູ່​ລະ​ບົບ" _actions: reply: "ຕອບ​ກັບ" renote: "Renote" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index eb48cf72da..dde3035357 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -486,6 +486,7 @@ _notification: renote: "Herdelen" quote: "Quote" reaction: "Reacties" + login: "Inloggen" _actions: reply: "Antwoord" renote: "Herdelen" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index cd00ecf9ab..c5f61db745 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -701,6 +701,7 @@ _notification: renote: "Renotes" quote: "Sitater" reaction: "Reaksjoner" + login: "Logg inn" _actions: reply: "Svar" renote: "Renote" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 73eff0941a..0073628673 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -492,7 +492,6 @@ uiLanguage: "Język wyświetlania UI" aboutX: "O {x}" emojiStyle: "Styl emoji" native: "Natywny" -disableDrawer: "Nie używaj menu w stylu szuflady" showNoteActionsOnlyHover: "Pokazuj akcje notatek tylko po najechaniu myszką" showReactionsCount: "Wyświetl liczbę reakcji na notatkę" noHistory: "Brak historii" @@ -1510,6 +1509,7 @@ _notification: reaction: "Reakcja" receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji" followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji" + login: "Zaloguj się" app: "Powiadomienia z aplikacji" _actions: followBack: "zaobserwował cię z powrotem" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 41246c18ae..f5d29891df 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -509,7 +509,6 @@ uiLanguage: "Idioma de exibição da interface " aboutX: "Sobre {x}" emojiStyle: "Estilo de emojis" native: "Nativo" -disableDrawer: "Não mostrar o menu em formato de gaveta" showNoteActionsOnlyHover: "Exibir as ações da nota somente ao passar o cursor sobre ela" showReactionsCount: "Ver o número de reações nas notas" noHistory: "Ainda não há histórico" @@ -2377,6 +2376,7 @@ _notification: followRequestAccepted: "Aceitou pedidos de seguidor" roleAssigned: "Cargo dado" achievementEarned: "Conquista desbloqueada" + login: "Iniciar sessão" app: "Notificações de aplicativos conectados" _actions: followBack: "te seguiu de volta" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index b4c9b90de9..88495a41a1 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -453,7 +453,6 @@ or: "Sau" language: "Limbă" uiLanguage: "Limba interfeței" aboutX: "Despre {x}" -disableDrawer: "Nu folosi meniuri în stil sertar" noHistory: "Nu există istoric" signinHistory: "Istoric autentificări" doing: "Se procesează..." @@ -715,6 +714,7 @@ _notification: renote: "Re-notează" quote: "Citează" reaction: "Reacție" + login: "Autentifică-te" _actions: reply: "Răspunde" renote: "Re-notează" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 9610600ed0..15e33c7f4d 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -503,7 +503,6 @@ uiLanguage: "Язык интерфейса" aboutX: "Описание {x}" emojiStyle: "Стиль эмодзи" native: "Системные" -disableDrawer: "Не использовать выдвижные меню" showNoteActionsOnlyHover: "Показывать кнопки у заметок только при наведении" showReactionsCount: "Видеть количество реакций на заметках" noHistory: "История пока пуста" @@ -2047,6 +2046,7 @@ _notification: receiveFollowRequest: "Получен запрос на подписку" followRequestAccepted: "Запрос на подписку одобрен" achievementEarned: "Получение достижений" + login: "Войти" app: "Уведомления из приложений" _actions: followBack: "отвечает взаимной подпиской" diff --git a/locales/si-LK.yml b/locales/si-LK.yml index e130d68ed8..c43f3d860d 100644 --- a/locales/si-LK.yml +++ b/locales/si-LK.yml @@ -17,3 +17,6 @@ _sfx: note: "නෝට්" _profile: username: "පරිශීලක නාමය" +_notification: + _types: + login: "පිවිසෙන්න" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 41f8949196..ad004eb4e2 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -454,7 +454,6 @@ uiLanguage: "Jazyk používateľského prostredia" aboutX: "O {x}" emojiStyle: "Štýl emoji" native: "Natívne" -disableDrawer: "Nepoužívať šuflíkové menu" showNoteActionsOnlyHover: "Ovládacie prvky poznámky sa zobrazujú len po nabehnutí myši" noHistory: "Žiadna história" signinHistory: "História prihlásení" @@ -1410,6 +1409,7 @@ _notification: pollEnded: "Hlasovanie skončilo" receiveFollowRequest: "Doručené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie" + login: "Prihlásiť sa" app: "Oznámenia z prepojených aplikácií" _actions: followBack: "Sledovať späť\n" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index c1a998b8fb..5a0de660e8 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -562,6 +562,7 @@ _notification: renote: "Omnotera" quote: "Citat" reaction: "Reaktioner" + login: "Logga in" _actions: reply: "Svara" renote: "Omnotera" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 2fb4a5253a..77fea6a68e 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -509,7 +509,6 @@ uiLanguage: "ภาษาอินเทอร์เฟซผู้ใช้ง aboutX: "เกี่ยวกับ {x}" emojiStyle: "สไตล์ของเอโมจิ" native: "ภาษาแม่" -disableDrawer: "ไม่แสดงเมนูในรูปแบบลิ้นชัก" showNoteActionsOnlyHover: "แสดงการดำเนินการโน้ตเมื่อโฮเวอร์(วางเมาส์เหนือ)เท่านั้น" showReactionsCount: "แสดงจำนวนรีแอกชั่นในโน้ต" noHistory: "ไม่มีประวัติ" @@ -2375,6 +2374,7 @@ _notification: followRequestAccepted: "อนุมัติให้ติดตามแล้ว" roleAssigned: "ให้บทบาท" achievementEarned: "ปลดล็อกความสำเร็จแล้ว" + login: "เข้าสู่ระบบ" app: "การแจ้งเตือนจากแอปที่มีลิงก์" _actions: followBack: "ติดตามกลับด้วย" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index cf6729a81d..fe2f158ff6 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -446,6 +446,7 @@ _notification: reaction: "Tepkiler" receiveFollowRequest: "Takip isteği alındı" followRequestAccepted: "Takip isteği kabul edildi" + login: "Giriş Yap " _actions: reply: "yanıt" renote: "vazgeçme" diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml index e48f64511c..fef26040a5 100644 --- a/locales/ug-CN.yml +++ b/locales/ug-CN.yml @@ -17,3 +17,6 @@ _2fa: renewTOTPCancel: "ئۇنى توختىتىڭ" _widgets: profile: "profile" +_notification: + _types: + login: "كىرىش" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 36d741d30e..ef01c8186c 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -452,7 +452,6 @@ language: "Мова" uiLanguage: "Мова інтерфейсу" aboutX: "Про {x}" native: "місцевий" -disableDrawer: "Не використовувати висувні меню" noHistory: "Історія порожня" signinHistory: "Історія входів" enableAdvancedMfm: "Увімкнути розширений MFM" @@ -1588,6 +1587,7 @@ _notification: reaction: "Реакції" receiveFollowRequest: "Запити на підписку" followRequestAccepted: "Прийняті підписки" + login: "Увійти" app: "Сповіщення від додатків" _actions: reply: "Відповісти" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index ee4ab83ce7..7c5d2796f6 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -471,7 +471,6 @@ uiLanguage: "Interfeys tili" aboutX: "{x} haqida" emojiStyle: "Emoji ko'rinishi" native: "Mahalliy" -disableDrawer: "Slayd menyusidan foydalanmang" showNoteActionsOnlyHover: "Eslatma amallarini faqat sichqonchani olib borganda ko‘rsatish" noHistory: "Tarix yo'q" signinHistory: "kirish tarixi" @@ -1058,6 +1057,7 @@ _notification: quote: "Iqtibos keltirish" reaction: "Reaktsiyalar" receiveFollowRequest: "Qabul qilingan kuzatuv so'rovlari" + login: "Kirish" _actions: reply: "Javob berish" renote: "Qayta qayd qilish" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index aadbf8b16f..c84eb574f3 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -486,7 +486,6 @@ uiLanguage: "Ngôn ngữ giao diện" aboutX: "Giới thiệu {x}" emojiStyle: "Kiểu cách Emoji" native: "Bản xứ" -disableDrawer: "Không dùng menu thanh bên" showNoteActionsOnlyHover: "Chỉ hiển thị các hành động ghi chú khi di chuột" noHistory: "Không có dữ liệu" signinHistory: "Lịch sử đăng nhập" @@ -1879,6 +1878,7 @@ _notification: receiveFollowRequest: "Yêu cầu theo dõi" followRequestAccepted: "Yêu cầu theo dõi được chấp nhận" achievementEarned: "Hoàn thành Achievement" + login: "Đăng nhập" app: "Từ app liên kết" _actions: followBack: "đã theo dõi lại bạn" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index ff00a1bb8f..15f84e845d 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -8,6 +8,9 @@ search: "搜索" notifications: "通知" username: "用户名" password: "密码" +initialPasswordForSetup: "初始化密码" +initialPasswordIsIncorrect: "初始化密码不正确" +initialPasswordForSetupDescription: "如果是自己安装的 Misskey,请输入配置文件里设好的密码。\n如果使用的是 Misskey 的托管服务等,请输入服务商提供的密码。\n如果没有设置密码,请留空并继续。" forgotPassword: "忘记密码" fetchingAsApObject: "在联邦宇宙查询中..." ok: "OK" @@ -90,7 +93,7 @@ followsYou: "正在关注你" createList: "创建列表" manageLists: "管理列表" error: "错误" -somethingHappened: "出现了一些问题!" +somethingHappened: "出错了" retry: "重试" pageLoadError: "页面加载失败。" pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。" @@ -167,7 +170,7 @@ emojiUrl: "emoji 地址" addEmoji: "添加表情符号" settingGuide: "推荐配置" cacheRemoteFiles: "缓存远程文件" -cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。" +cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。" youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。" cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件" cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。" @@ -236,6 +239,8 @@ silencedInstances: "被静音的服务器" silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。" mediaSilencedInstances: "已隐藏媒体文件的服务器" mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" +federationAllowedHosts: "允许联合的服务器" +federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。" muteAndBlock: "静音/拉黑" mutedUsers: "已静音用户" blockedUsers: "已拉黑的用户" @@ -334,6 +339,7 @@ renameFolder: "重命名文件夹" deleteFolder: "删除文件夹" folder: "文件夹" addFile: "添加文件" +showFile: "显示文件" emptyDrive: "网盘中无文件" emptyFolder: "此文件夹中无文件" unableToDelete: "无法删除" @@ -509,7 +515,10 @@ uiLanguage: "显示语言" aboutX: "关于 {x}" emojiStyle: "表情符号的样式" native: "原生" -disableDrawer: "不显示抽屉菜单" +menuStyle: "菜单样式" +style: "样式" +drawer: "抽屉" +popup: "弹窗" showNoteActionsOnlyHover: "仅在悬停时显示帖子操作" showReactionsCount: "显示帖子的回应数" noHistory: "没有历史记录" @@ -915,6 +924,7 @@ followersVisibility: "关注者的公开范围" continueThread: "查看更多帖子" deleteAccountConfirm: "将要删除账户。是否确认?" incorrectPassword: "密码错误" +incorrectTotp: "一次性密码不正确或已过期" voteConfirm: "确定投给 “{choice}” ?" hide: "隐藏" useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示" @@ -1270,6 +1280,14 @@ genEmbedCode: "生成嵌入代码" noteOfThisUser: "此用户的帖子" clipNoteLimitExceeded: "无法再往此便签内添加更多帖子" performance: "性能" +modified: "有变更" +discard: "取消" +thereAreNChanges: "有 {n} 处更改" +signinWithPasskey: "使用通行密钥登录" +unknownWebAuthnKey: "此通行密钥未注册。" +passkeyVerificationFailed: "验证通行密钥失败。" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。" +messageToFollower: "给关注者的消息" _delivery: status: "投递状态" stop: "停止投递" @@ -2237,6 +2255,9 @@ _profile: changeBanner: "修改横幅" verifiedLinkDescription: "如果将内容设置为 URL,当链接所指向的网页内包含自己的个人资料链接时,可以显示一个已验证图标。" avatarDecorationMax: "最多可添加 {max} 个挂件" + followedMessage: "被关注时显示的消息" + followedMessageDescription: "可以设置被关注时向对方显示的短消息。" + followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在被请求被批准后显示。" _exportOrImport: allNotes: "所有帖子" favoritedNotes: "收藏的帖子" @@ -2375,6 +2396,8 @@ _notification: renotedBySomeUsers: "{n} 人转发了" followedBySomeUsers: "被 {n} 人关注" flushNotification: "重置通知历史" + exportOfXCompleted: "已完成 {x} 个导出" + login: "有新的登录" _types: all: "全部" note: "用户的新帖子" @@ -2389,6 +2412,9 @@ _notification: followRequestAccepted: "关注请求已通过" roleAssigned: "授予的角色" achievementEarned: "取得的成就" + exportCompleted: "已完成导出" + login: "登录" + test: "测试通知" app: "关联应用的通知" _actions: followBack: "回关" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index c684fbe628..6659efcb7a 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -8,6 +8,9 @@ search: "搜尋" notifications: "通知" username: "使用者名稱" password: "密碼" +initialPasswordForSetup: "初始設定用的密碼" +initialPasswordIsIncorrect: "初始設定用的密碼錯誤。" +initialPasswordForSetupDescription: "如果您自己安裝了 Misskey,請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼,請將其留空並繼續。" forgotPassword: "忘記密碼" fetchingAsApObject: "從聯邦宇宙取得中..." ok: "OK" @@ -236,6 +239,8 @@ silencedInstances: "被禁言的伺服器" silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。" mediaSilencedInstances: "媒體被禁言的伺服器" mediaSilencedInstancesDescription: "設定您想要對媒體設定禁言的伺服器,以換行符號區隔。來自被媒體禁言的伺服器所屬帳戶的所有檔案都會被視為敏感檔案,且自訂表情符號不能使用。被封鎖的伺服器不受影響。" +federationAllowedHosts: "允許聯邦通訊的伺服器" +federationAllowedHostsDescription: "設定允許聯邦通訊的伺服器主機,以換行符號分隔。" muteAndBlock: "靜音和封鎖" mutedUsers: "被靜音的使用者" blockedUsers: "被封鎖的使用者" @@ -334,6 +339,7 @@ renameFolder: "重新命名資料夾" deleteFolder: "刪除資料夾" folder: "資料夾" addFile: "加入附件" +showFile: "瀏覽文件" emptyDrive: "雲端硬碟為空" emptyFolder: "資料夾為空" unableToDelete: "無法刪除" @@ -509,7 +515,10 @@ uiLanguage: "介面語言" aboutX: "關於{x}" emojiStyle: "表情符號的風格" native: "原生" -disableDrawer: "不顯示下拉式選單" +menuStyle: "選單風格" +style: "風格" +drawer: "側邊欄" +popup: "彈出式視窗" showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項" showReactionsCount: "顯示貼文的反應數目" noHistory: "沒有歷史紀錄" @@ -1273,6 +1282,11 @@ performance: "性能" modified: "已變更" discard: "取消" thereAreNChanges: "有 {n} 處的變更" +signinWithPasskey: "使用密碼金鑰登入" +unknownWebAuthnKey: "未註冊的金鑰。" +passkeyVerificationFailed: "驗證金鑰失敗。" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。" +messageToFollower: "給追隨者的訊息" _delivery: status: "傳送狀態" stop: "停止發送" @@ -2240,6 +2254,9 @@ _profile: changeBanner: "變更橫幅圖像" verifiedLinkDescription: "如果輸入包含您個人資料的網站 URL,欄位旁邊將出現驗證圖示。" avatarDecorationMax: "最多可以設置 {max} 個裝飾。" + followedMessage: "被追隨時的訊息" + followedMessageDescription: "可以設定被追隨時顯示給對方的訊息。" + followedMessageDescriptionForLockedAccount: "如果追隨是需要審核的話,在允許追隨請求之後顯示。" _exportOrImport: allNotes: "所有貼文" favoritedNotes: "「我的最愛」貼文" @@ -2378,6 +2395,8 @@ _notification: renotedBySomeUsers: "{n}人做了轉發" followedBySomeUsers: "被{n}人追隨了" flushNotification: "重置通知歷史紀錄" + exportOfXCompleted: "{x} 的匯出已完成。" + login: "已登入" _types: all: "全部 " note: "使用者的最新貼文" @@ -2392,6 +2411,9 @@ _notification: followRequestAccepted: "追隨請求已接受" roleAssigned: "已授予角色" achievementEarned: "獲得成就" + exportCompleted: "已完成匯出。" + login: "登入" + test: "通知測試" app: "應用程式通知" _actions: followBack: "追隨回去" diff --git a/package.json b/package.json index 91ff93301d..50c42645d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.9.0-alpha.10", + "version": "2024.10.0-beta.5", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/assets/tabler-badges/login-2.png b/packages/backend/assets/tabler-badges/login-2.png new file mode 100644 index 0000000000..f3ca8de3dd Binary files /dev/null and b/packages/backend/assets/tabler-badges/login-2.png differ diff --git a/packages/backend/migration/1723944246767-followedMessage.js b/packages/backend/migration/1723944246767-followedMessage.js new file mode 100644 index 0000000000..fc9ad1cb85 --- /dev/null +++ b/packages/backend/migration/1723944246767-followedMessage.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class FollowedMessage1723944246767 { + name = 'FollowedMessage1723944246767'; + + async up(queryRunner) { + await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)'); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"'); + } +} diff --git a/packages/backend/migration/1727491883993-user-score.js b/packages/backend/migration/1727491883993-user-score.js new file mode 100644 index 0000000000..7292d5363c --- /dev/null +++ b/packages/backend/migration/1727491883993-user-score.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UserScore1727491883993 { + name = 'UserScore1727491883993' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "score" integer NOT NULL DEFAULT '0'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "score"`); + } +} diff --git a/packages/backend/migration/1727512908322-meta-federation.js b/packages/backend/migration/1727512908322-meta-federation.js new file mode 100644 index 0000000000..52c24df4f7 --- /dev/null +++ b/packages/backend/migration/1727512908322-meta-federation.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MetaFederation1727512908322 { + name = 'MetaFederation1727512908322' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "federation" character varying(128) NOT NULL DEFAULT 'all'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "federationHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federationHosts"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federation"`); + } +} diff --git a/packages/backend/migration/1728085812127-refine-abuse-user-report.js b/packages/backend/migration/1728085812127-refine-abuse-user-report.js new file mode 100644 index 0000000000..57cbfdcf6d --- /dev/null +++ b/packages/backend/migration/1728085812127-refine-abuse-user-report.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RefineAbuseUserReport1728085812127 { + name = 'RefineAbuseUserReport1728085812127' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`); + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 617df78267..c6e31797f8 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -67,24 +67,24 @@ "dependencies": { "@aws-sdk/client-s3": "3.620.0", "@aws-sdk/lib-storage": "3.620.0", - "@bull-board/api": "5.23.0", - "@bull-board/fastify": "5.23.0", - "@bull-board/ui": "5.23.0", + "@bull-board/api": "6.0.0", + "@bull-board/fastify": "6.0.0", + "@bull-board/ui": "6.0.0", "@discordapp/twemoji": "15.1.0", - "@fastify/accepts": "5.0.0", - "@fastify/cookie": "10.0.0", - "@fastify/cors": "10.0.0", - "@fastify/express": "4.0.0", + "@fastify/accepts": "5.0.1", + "@fastify/cookie": "10.0.1", + "@fastify/cors": "10.0.1", + "@fastify/express": "4.0.1", "@fastify/http-proxy": "10.0.0", - "@fastify/multipart": "9.0.0", - "@fastify/static": "8.0.0", - "@fastify/view": "10.0.0", + "@fastify/multipart": "9.0.1", + "@fastify/static": "8.0.1", + "@fastify/view": "10.0.1", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.1.0", "@napi-rs/canvas": "0.1.56", - "@nestjs/common": "10.4.3", - "@nestjs/core": "10.4.3", - "@nestjs/testing": "10.4.3", + "@nestjs/common": "10.4.4", + "@nestjs/core": "10.4.4", + "@nestjs/testing": "10.4.4", "@peertube/http-signature": "1.7.0", "@sentry/node": "8.20.0", "@sentry/profiling-node": "8.20.0", @@ -101,7 +101,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.13.2", + "bullmq": "5.15.0", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.3.0", @@ -149,7 +149,7 @@ "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.3.2", + "otpauth": "9.3.4", "parse5": "7.1.2", "pg": "8.13.0", "pkce-challenge": "4.1.0", @@ -166,7 +166,7 @@ "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.1", - "sanitize-html": "2.13.0", + "sanitize-html": "2.13.1", "secure-json-parse": "2.7.0", "sharp": "0.33.5", "slacc": "0.0.10", @@ -187,14 +187,14 @@ }, "devDependencies": { "@jest/globals": "29.7.0", - "@nestjs/platform-express": "10.4.3", + "@nestjs/platform-express": "10.4.4", "@simplewebauthn/types": "10.0.0", "@swc/jest": "0.2.36", "@types/accepts": "1.3.7", "@types/archiver": "6.0.2", "@types/bcryptjs": "2.4.6", "@types/body-parser": "1.19.5", - "@types/color-convert": "2.0.3", + "@types/color-convert": "2.0.4", "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.26", "@types/htmlescape": "1.1.3", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 97ba79c574..42f1033b9d 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -63,6 +63,8 @@ type Source = { publishTarballInsteadOfProvideRepositoryUrl?: boolean; + setupPassword?: string; + proxy?: string; proxySmtp?: string; proxyBypassHosts?: string[]; @@ -152,6 +154,7 @@ export type Config = { version: string; publishTarballInsteadOfProvideRepositoryUrl: boolean; + setupPassword: string | undefined; host: string; hostname: string; scheme: string; @@ -232,6 +235,7 @@ export function loadConfig(): Config { return { version, publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, + setupPassword: config.setupPassword, url: url.origin, port: config.port ?? parseInt(process.env.PORT ?? '', 10), socket: config.socket, diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts index fe2c63e7d6..fb7c7bd2c3 100644 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ b/packages/backend/src/core/AbuseReportNotificationService.ts @@ -22,6 +22,7 @@ import { RoleService } from '@/core/RoleService.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { IdService } from './IdService.js'; @Injectable() @@ -42,6 +43,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { private emailService: EmailService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, + private userEntityService: UserEntityService, ) { this.redisForSub.on('message', this.onMessage); } @@ -135,6 +137,26 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { return; } + const usersMap = await this.userEntityService.packMany( + [ + ...new Set([ + ...abuseReports.map(it => it.reporter ?? it.reporterId), + ...abuseReports.map(it => it.targetUser ?? it.targetUserId), + ...abuseReports.map(it => it.assignee ?? it.assigneeId), + ].filter(x => x != null)), + ], + null, + { schema: 'UserLite' }, + ).then(it => new Map(it.map(it => [it.id, it]))); + const convertedReports = abuseReports.map(it => { + return { + ...it, + reporter: usersMap.get(it.reporterId), + targetUser: usersMap.get(it.targetUserId), + assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null, + }; + }); + const recipientWebhookIds = await this.fetchWebhookRecipients() .then(it => it .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook') @@ -142,7 +164,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { .filter(x => x != null)); for (const webhookId of recipientWebhookIds) { await Promise.all( - abuseReports.map(it => { + convertedReports.map(it => { return this.systemWebhookService.enqueueSystemWebhook( webhookId, type, diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts index 69c51509ba..73baad5499 100644 --- a/packages/backend/src/core/AbuseReportService.ts +++ b/packages/backend/src/core/AbuseReportService.ts @@ -20,8 +20,10 @@ export class AbuseReportService { constructor( @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, + private idService: IdService, private abuseReportNotificationService: AbuseReportNotificationService, private queueService: QueueService, @@ -77,16 +79,16 @@ export class AbuseReportService { * - SystemWebhook * * @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える - * @param operator 通報を処理したユーザ + * @param moderator 通報を処理したユーザ * @see AbuseReportNotificationService.notify */ @bindThis public async resolve( params: { reportId: string; - forward: boolean; + resolvedAs: MiAbuseUserReport['resolvedAs']; }[], - operator: MiUser, + moderator: MiUser, ) { const paramsMap = new Map(params.map(it => [it.reportId, it])); const reports = await this.abuseUserReportsRepository.findBy({ @@ -99,25 +101,15 @@ export class AbuseReportService { await this.abuseUserReportsRepository.update(report.id, { resolved: true, - assigneeId: operator.id, - forwarded: ps.forward && report.targetUserHost !== null, + assigneeId: moderator.id, + resolvedAs: ps.resolvedAs, }); - if (ps.forward && report.targetUserHost != null) { - const actor = await this.instanceActorService.getInstanceActor(); - const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); - - // eslint-disable-next-line - const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment); - const contextAssignedFlag = this.apRendererService.addContext(flag); - this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false); - } - this.moderationLogService - .log(operator, 'resolveAbuseReport', { + .log(moderator, 'resolveAbuseReport', { reportId: report.id, report: report, - forwarded: ps.forward && report.targetUserHost !== null, + resolvedAs: ps.resolvedAs, }) .then(); } @@ -125,4 +117,62 @@ export class AbuseReportService { return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) }) .then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved')); } + + @bindThis + public async forward( + reportId: MiAbuseUserReport['id'], + moderator: MiUser, + ) { + const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId }); + + if (report.targetUserHost == null) { + throw new Error('The target user host is null.'); + } + + if (report.forwarded) { + throw new Error('The report has already been forwarded.'); + } + + await this.abuseUserReportsRepository.update(report.id, { + forwarded: true, + }); + + const actor = await this.instanceActorService.getInstanceActor(); + const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); + + const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment); + const contextAssignedFlag = this.apRendererService.addContext(flag); + this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false); + + this.moderationLogService + .log(moderator, 'forwardAbuseReport', { + reportId: report.id, + report: report, + }) + .then(); + } + + @bindThis + public async update( + reportId: MiAbuseUserReport['id'], + params: { + moderationNote?: MiAbuseUserReport['moderationNote']; + }, + moderator: MiUser, + ) { + const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId }); + + await this.abuseUserReportsRepository.update(report.id, { + moderationNote: params.moderationNote, + }); + + if (params.moderationNote != null && report.moderationNote !== params.moderationNote) { + this.moderationLogService.log(moderator, 'updateAbuseReportNote', { + reportId: report.id, + report: report, + before: report.moderationNote, + after: params.moderationNote, + }); + } + } } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 3b3c35f976..734d135648 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { UserSearchService } from '@/core/UserSearchService.js'; import { WebhookTestService } from '@/core/WebhookTestService.js'; +import { FlashService } from '@/core/FlashService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; @@ -217,6 +218,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; +const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; @@ -367,6 +369,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting WebhookTestService, UtilityService, FileInfoService, + FlashService, SearchService, ClipService, FeaturedService, @@ -513,6 +516,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $WebhookTestService, $UtilityService, $FileInfoService, + $FlashService, $SearchService, $ClipService, $FeaturedService, @@ -660,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting WebhookTestService, UtilityService, FileInfoService, + FlashService, SearchService, ClipService, FeaturedService, diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts new file mode 100644 index 0000000000..2a98225382 --- /dev/null +++ b/packages/backend/src/core/FlashService.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { type FlashsRepository } from '@/models/_.js'; + +/** + * MisskeyPlay関係のService + */ +@Injectable() +export class FlashService { + constructor( + @Inject(DI.flashsRepository) + private flashRepository: FlashsRepository, + ) { + } + + /** + * 人気のあるPlay一覧を取得する. + */ + public async featured(opts?: { offset?: number, limit: number }) { + const builder = this.flashRepository.createQueryBuilder('flash') + .andWhere('flash.likedCount > 0') + .andWhere('flash.visibility = :visibility', { visibility: 'public' }) + .addOrderBy('flash.likedCount', 'DESC') + .addOrderBy('flash.updatedAt', 'DESC') + .addOrderBy('flash.id', 'DESC'); + + if (opts?.offset) { + builder.skip(opts.offset); + } + + builder.take(opts?.limit ?? 10); + + return await builder.getMany(); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 89e3eafa0e..0ce57f16e6 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -218,7 +218,7 @@ export class NoteCreateService implements OnApplicationShutdown { private utilityService: UtilityService, private userBlockingService: UserBlockingService, ) { - this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount); + this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount); } @bindThis diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 3f1c6b7125..77e7b60bea 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -275,16 +275,19 @@ export class UserFollowingService implements OnModuleInit { followeeId: followee.id, followerId: follower.id, }); - - // 通知を作成 - if (follower.host === null) { - this.notificationService.createNotification(follower.id, 'followRequestAccepted', { - }, followee.id); - } } if (alreadyFollowed) return; + // 通知を作成 + if (follower.host === null) { + const profile = await this.cacheService.userProfileCache.fetch(followee.id); + + this.notificationService.createNotification(follower.id, 'followRequestAccepted', { + message: profile.followedMessage, + }, followee.id); + } + this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); const [followeeUser, followerUser] = await Promise.all([ diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 94729250a6..86082ccdcd 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -10,12 +10,16 @@ import RE2 from 're2'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; +import { MiMeta } from '@/models/Meta.js'; @Injectable() export class UtilityService { constructor( @Inject(DI.config) private config: Config, + + @Inject(DI.meta) + private meta: MiMeta, ) { } @@ -105,4 +109,19 @@ export class UtilityService { if (host == null) return null; return toASCII(host.toLowerCase()); } + + @bindThis + public isFederationAllowedHost(host: string): boolean { + if (this.meta.federation === 'none') return false; + if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false; + if (this.isBlockedHost(this.meta.blockedHosts, host)) return false; + + return true; + } + + @bindThis + public isFederationAllowedUri(uri: string): boolean { + const host = this.extractDbHost(uri); + return this.isFederationAllowedHost(host); + } } diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 0b4e107d21..4c45b95a64 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -15,8 +15,14 @@ import { QueueService } from '@/core/QueueService.js'; const oneDayMillis = 24 * 60 * 60 * 1000; -function generateAbuseReport(override?: Partial): MiAbuseUserReport { - return { +type AbuseUserReportDto = Omit & { + targetUser: Packed<'UserLite'> | null, + reporter: Packed<'UserLite'> | null, + assignee: Packed<'UserLite'> | null, +}; + +function generateAbuseReport(override?: Partial): AbuseUserReportDto { + const result: MiAbuseUserReport = { id: 'dummy-abuse-report1', targetUserId: 'dummy-target-user', targetUser: null, @@ -29,8 +35,17 @@ function generateAbuseReport(override?: Partial): MiAbuseUser comment: 'This is a dummy report for testing purposes.', targetUserHost: null, reporterHost: null, + resolvedAs: null, + moderationNote: 'foo', ...override, }; + + return { + ...result, + targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null, + reporter: result.reporter ? toPackedUserLite(result.reporter) : null, + assignee: result.assignee ? toPackedUserLite(result.assignee) : null, + }; } function generateDummyUser(override?: Partial): MiUser { @@ -68,6 +83,7 @@ function generateDummyUser(override?: Partial): MiUser { isHibernated: false, isDeleted: false, emojis: [], + score: 0, host: null, inbox: null, sharedInbox: null, @@ -267,7 +283,8 @@ const dummyUser3 = generateDummyUser({ @Injectable() export class WebhookTestService { - public static NoSuchWebhookError = class extends Error {}; + public static NoSuchWebhookError = class extends Error { + }; constructor( private userWebhookService: UserWebhookService, diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 90da032895..376c9c0151 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -290,8 +290,8 @@ export class ApInboxService { return; } - // アナウンス先をブロックしてたら中断 - if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; + // アナウンス先が許可されているかチェック + if (!this.utilityService.isFederationAllowedUri(uri)) return; const unlock = await this.appLockService.getApLock(uri); diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 98e944f347..fba8947f03 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -494,6 +494,7 @@ export class ApRendererService { name: user.name, summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, _misskey_summary: profile.description, + _misskey_followedMessage: profile.followedMessage, icon: avatar ? this.renderImage(avatar) : null, image: banner ? this.renderImage(banner) : null, tag, diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index fdef7a8ffd..ca35608d9b 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -93,7 +93,7 @@ export class Resolver { return await this.resolveLocal(value); } - if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { + if (!this.utilityService.isFederationAllowedHost(host)) { throw new Error('Instance is blocked'); } diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index feb8c42c56..3dd85b9b86 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -554,6 +554,7 @@ const extension_context_definition = { '_misskey_reaction': 'misskey:_misskey_reaction', '_misskey_votes': 'misskey:_misskey_votes', '_misskey_summary': 'misskey:_misskey_summary', + '_misskey_followedMessage': 'misskey:_misskey_followedMessage', 'isCat': 'misskey:isCat', // vcard vcard: 'http://www.w3.org/2006/vcard/ns#', diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 00acb19a0f..2d333b3634 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -336,8 +336,7 @@ export class ApNoteService { public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise { const uri = getApId(value); - // ブロックしていたら中断 - if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) { + if (!this.utilityService.isFederationAllowedUri(uri)) { throw new StatusError('blocked host', 451); } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 39c18e5e15..e042a85782 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -45,7 +45,7 @@ import type { ApNoteService } from './ApNoteService.js'; import type { ApMfmService } from '../ApMfmService.js'; import type { ApResolverService, Resolver } from '../ApResolverService.js'; import type { ApLoggerService } from '../ApLoggerService.js'; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports + import type { ApImageService } from './ApImageService.js'; import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js'; @@ -307,8 +307,8 @@ export class ApPersonService implements OnModuleInit { this.logger.error('error occurred while fetching following/followers collection', { stack: err }); } return 'private'; - }) - ) + }), + ), ); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); @@ -370,6 +370,7 @@ export class ApPersonService implements OnModuleInit { await transactionalEntityManager.save(new MiUserProfile({ userId: user.id, description: _description, + followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, url, fields, followingVisibility, @@ -494,8 +495,8 @@ export class ApPersonService implements OnModuleInit { return undefined; } return 'private'; - }) - ) + }), + ), ); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); @@ -566,6 +567,7 @@ export class ApPersonService implements OnModuleInit { url, fields, description: _description, + followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, followingVisibility, followersVisibility, birthday: bday?.[0] ?? null, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 16812b7a4d..154965b9d5 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -13,6 +13,7 @@ export interface IObject { name?: string | null; summary?: string; _misskey_summary?: string; + _misskey_followedMessage?: string | null; published?: string; cc?: ApObject; to?: ApObject; diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index a13c244c19..70ead890ab 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -53,6 +53,8 @@ export class AbuseUserReportEntityService { schema: 'UserDetailedNotMe', }) : null, forwarded: report.forwarded, + resolvedAs: report.resolvedAs, + moderationNote: report.moderationNote, }); } diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts index 4aa7104c1e..0cdcf3310a 100644 --- a/packages/backend/src/core/entities/FlashEntityService.ts +++ b/packages/backend/src/core/entities/FlashEntityService.ts @@ -5,10 +5,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; import type { MiUser } from '@/models/User.js'; import type { MiFlash } from '@/models/Flash.js'; import { bindThis } from '@/decorators.js'; @@ -20,10 +18,8 @@ export class FlashEntityService { constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, - @Inject(DI.flashLikesRepository) private flashLikesRepository: FlashLikesRepository, - private userEntityService: UserEntityService, private idService: IdService, ) { @@ -34,25 +30,36 @@ export class FlashEntityService { src: MiFlash['id'] | MiFlash, me?: { id: MiUser['id'] } | null | undefined, hint?: { - packedUser?: Packed<'UserLite'> + packedUser?: Packed<'UserLite'>, + likedFlashIds?: MiFlash['id'][], }, ): Promise> { const meId = me ? me.id : null; const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src }); - return await awaitAll({ + // { schema: 'UserDetailed' } すると無限ループするので注意 + const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me); + + let isLiked = false; + if (meId) { + isLiked = hint?.likedFlashIds + ? hint.likedFlashIds.includes(flash.id) + : await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }); + } + + return { id: flash.id, createdAt: this.idService.parse(flash.id).date.toISOString(), updatedAt: flash.updatedAt.toISOString(), userId: flash.userId, - user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意 + user: user, title: flash.title, summary: flash.summary, script: flash.script, visibility: flash.visibility, likedCount: flash.likedCount, - isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined, - }); + isLiked: isLiked, + }; } @bindThis @@ -63,7 +70,19 @@ export class FlashEntityService { const _users = flashes.map(({ user, userId }) => user ?? userId); const _userMap = await this.userEntityService.packMany(_users, me) .then(users => new Map(users.map(u => [u.id, u]))); - return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) }))); + const _likedFlashIds = me + ? await this.flashLikesRepository.createQueryBuilder('flashLike') + .select('flashLike.flashId') + .where('flashLike.userId = :userId', { userId: me.id }) + .getRawMany<{ flashLike_flashId: string }>() + .then(likes => [...new Set(likes.map(like => like.flashLike_flashId))]) + : []; + return Promise.all( + flashes.map(flash => this.pack(flash, me, { + packedUser: _userMap.get(flash.userId), + likedFlashIds: _likedFlashIds, + })), + ); } } diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 1b61a6195d..dff6968f9c 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit { async #packInternal ( src: T, meId: MiUser['id'], - // eslint-disable-next-line @typescript-eslint/ban-types + options: { checkValidNotifier?: boolean; }, @@ -159,6 +159,9 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'roleAssigned' ? { role: role, } : {}), + ...(notification.type === 'followRequestAccepted' ? { + message: notification.message, + } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), @@ -233,7 +236,7 @@ export class NotificationEntityService implements OnModuleInit { public async pack( src: MiNotification | MiGroupedNotification, meId: MiUser['id'], - // eslint-disable-next-line @typescript-eslint/ban-types + options: { checkValidNotifier?: boolean; }, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 9bf568bc90..c9939adf11 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -508,7 +508,7 @@ export class UserEntityService implements OnModuleInit { name: r.name, iconUrl: r.iconUrl, displayOrder: r.displayOrder, - })) + })), ) : undefined, ...(isDetailed ? { @@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit { publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 followersVisibility: profile!.followersVisibility, followingVisibility: profile!.followingVisibility, - twoFactorEnabled: profile!.twoFactorEnabled, - usePasswordLessLogin: profile!.usePasswordLessLogin, - securityKeys: profile!.twoFactorEnabled - ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1) - : false, 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, @@ -564,9 +559,18 @@ export class UserEntityService implements OnModuleInit { moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, } : {}), + ...(isDetailed && (isMe || iAmModerator) ? { + twoFactorEnabled: profile!.twoFactorEnabled, + usePasswordLessLogin: profile!.usePasswordLessLogin, + securityKeys: profile!.twoFactorEnabled + ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1) + : false, + } : {}), + ...(isDetailed && isMe ? { avatarId: user.avatarId, bannerId: user.bannerId, + followedMessage: profile!.followedMessage, isModerator: isModerator, isAdmin: isAdmin, injectFeaturedNote: profile!.injectFeaturedNote, @@ -635,6 +639,7 @@ export class UserEntityService implements OnModuleInit { isRenoteMuted: relation.isRenoteMuted, notify: relation.following?.notify ?? 'none', withReplies: relation.following?.withReplies ?? false, + followedMessage: relation.isFollowing ? profile!.followedMessage : undefined, } : {}), } as Promiseable>; diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts index 0615fd7eb5..cb5672e4ac 100644 --- a/packages/backend/src/models/AbuseUserReport.ts +++ b/packages/backend/src/models/AbuseUserReport.ts @@ -50,6 +50,9 @@ export class MiAbuseUserReport { }) public resolved: boolean; + /** + * リモートサーバーに転送したかどうか + */ @Column('boolean', { default: false, }) @@ -60,6 +63,21 @@ export class MiAbuseUserReport { }) public comment: string; + @Column('varchar', { + length: 8192, default: '', + }) + public moderationNote: string; + + /** + * accept 是認 ... 通報内容が正当であり、肯定的に対応された + * reject 否認 ... 通報内容が正当でなく、否定的に対応された + * null ... その他 + */ + @Column('varchar', { + length: 128, nullable: true, + }) + public resolvedAs: 'accept' | 'reject' | null; + //#region Denormalized fields @Index() @Column('varchar', { diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts index a1469a0d94..5db7dca992 100644 --- a/packages/backend/src/models/Flash.ts +++ b/packages/backend/src/models/Flash.ts @@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ import { id } from './util/id.js'; import { MiUser } from './User.js'; +export const flashVisibility = ['public', 'private'] as const; +export type FlashVisibility = typeof flashVisibility[number]; + @Entity('flash') export class MiFlash { @PrimaryColumn(id()) @@ -63,5 +66,5 @@ export class MiFlash { @Column('varchar', { length: 512, default: 'public', }) - public visibility: 'public' | 'private'; + public visibility: FlashVisibility; } diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 9ab76d373f..d29689f907 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -630,4 +630,17 @@ export class MiMeta { nullable: true, }) public urlPreviewUserAgent: string | null; + + @Column('varchar', { + length: 128, + default: 'all', + }) + public federation: 'all' | 'specified' | 'none'; + + @Column('varchar', { + length: 1024, + array: true, + default: '{}', + }) + public federationHosts: string[]; } diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 2c5b75f577..b7f8e94d69 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { userExportableEntities } from '@/types.js'; import { MiUser } from './User.js'; import { MiNote } from './Note.js'; import { MiAccessToken } from './AccessToken.js'; import { MiRole } from './Role.js'; import { MiDriveFile } from './DriveFile.js'; -import { userExportableEntities } from '@/types.js'; export type MiNotification = { type: 'note'; @@ -69,6 +69,7 @@ export type MiNotification = { id: string; createdAt: string; notifierId: MiUser['id']; + message: string | null; } | { type: 'roleAssigned'; id: string; @@ -85,6 +86,10 @@ export type MiNotification = { createdAt: string; exportedEntity: typeof userExportableEntities[number]; fileId: MiDriveFile['id']; +} | { + type: 'login'; + id: string; + createdAt: string; } | { type: 'app'; id: string; diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 9e2d7a3444..805a1e75ae 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -155,6 +155,11 @@ export class MiUser { }) public tags: string[]; + @Column('integer', { + default: 0, + }) + public score: number; + @Column('boolean', { default: false, comment: 'Whether the User is suspended.', @@ -289,5 +294,6 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr export const passwordSchema = { type: 'string', minLength: 1 } as const; export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const; +export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const; export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 7dbe0b3717..5544555296 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -42,6 +42,14 @@ export class MiUserProfile { }) public description: string | null; + // フォローされた際のメッセージ + @Column('varchar', { + length: 256, nullable: true, + }) + public followedMessage: string | null; + + // TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする + @Column('jsonb', { default: [], }) diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index bbec2e397f..cddaf4bc83 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -267,6 +267,10 @@ export const packedNotificationSchema = { optional: false, nullable: false, format: 'id', }, + message: { + type: 'string', + optional: false, nullable: true, + }, }, }, { type: 'object', @@ -318,6 +322,16 @@ export const packedNotificationSchema = { format: 'id', }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['login'], + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 947a9317d7..9cffd680f2 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: false, optional: false, enum: ['public', 'followers', 'private'], }, - twoFactorEnabled: { - type: 'boolean', - nullable: false, optional: false, - default: false, - }, - usePasswordLessLogin: { - type: 'boolean', - nullable: false, optional: false, - default: false, - }, - securityKeys: { - type: 'boolean', - nullable: false, optional: false, - default: false, - }, roles: { type: 'array', nullable: false, optional: false, @@ -370,6 +355,10 @@ export const packedUserDetailedNotMeOnlySchema = { ref: 'RoleLite', }, }, + followedMessage: { + type: 'string', + nullable: true, optional: true, + }, memo: { type: 'string', nullable: true, optional: false, @@ -378,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'string', nullable: false, optional: true, }, + twoFactorEnabled: { + type: 'boolean', + nullable: false, optional: true, + }, + usePasswordLessLogin: { + type: 'boolean', + nullable: false, optional: true, + }, + securityKeys: { + type: 'boolean', + nullable: false, optional: true, + }, //#region relations isFollowing: { type: 'boolean', @@ -437,6 +438,10 @@ export const packedMeDetailedOnlySchema = { nullable: true, optional: false, format: 'id', }, + followedMessage: { + type: 'string', + nullable: true, optional: false, + }, isModerator: { type: 'boolean', nullable: true, optional: false, @@ -622,6 +627,21 @@ export const packedMeDetailedOnlySchema = { nullable: false, optional: false, ref: 'RolePolicies', }, + twoFactorEnabled: { + type: 'boolean', + nullable: false, optional: false, + default: false, + }, + usePasswordLessLogin: { + type: 'boolean', + nullable: false, optional: false, + default: false, + }, + securityKeys: { + type: 'boolean', + nullable: false, optional: false, + default: false, + }, //#region secrets email: { type: 'string', diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index fc9078251f..9590a4fe71 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -53,8 +53,7 @@ export class DeliverProcessorService { public async process(job: Bull.Job): Promise { const { host } = new URL(job.data.to); - // ブロックしてたら中断 - if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.toPuny(host))) { + if (!this.utilityService.isFederationAllowedUri(job.data.to)) { return 'skip (blocked)'; } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 68999b5d17..a77c968395 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -59,7 +59,7 @@ export class InboxProcessorService implements OnApplicationShutdown { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('inbox'); - this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance); + this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance); } @bindThis @@ -75,8 +75,7 @@ export class InboxProcessorService implements OnApplicationShutdown { const host = this.utilityService.toPuny(new URL(signature.keyId).hostname); - // ブロックしてたら中断 - if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { + if (!this.utilityService.isFederationAllowedHost(host)) { return `Blocked request: ${host}`; } @@ -175,9 +174,8 @@ export class InboxProcessorService implements OnApplicationShutdown { throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`); } - // ブロックしてたら中断 const ldHost = this.utilityService.extractDbHost(authUser.user.uri); - if (this.utilityService.isBlockedHost(this.meta.blockedHosts, ldHost)) { + if (!this.utilityService.isFederationAllowedHost(ldHost)) { throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } } else { diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 709a044601..be63635efe 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -118,25 +118,27 @@ export class ApiServerService { 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; + 'm-captcha-response'?: string; } }>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); fastify.post<{ Body: { username: string; - password: string; + password?: string; token?: string; - signature?: string; - authenticatorData?: string; - clientDataJSON?: string; - credentialId?: string; - challengeId?: string; + credential?: AuthenticationResponseJSON; + 'hcaptcha-response'?: string; + 'g-recaptcha-response'?: string; + 'turnstile-response'?: string; + 'm-captcha-response'?: string; }; - }>('/signin', (request, reply) => this.signinApiService.signin(request, reply)); + }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply)); fastify.post<{ Body: { credential?: AuthenticationResponseJSON; + context?: string; }; }>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply)); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 4cd6095838..889a2a39b5 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -69,6 +69,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; +import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js'; +import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js'; import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; @@ -455,6 +457,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default }; const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default }; const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default }; +const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default }; +const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default }; const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default }; const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default }; const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default }; @@ -845,6 +849,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_relays_remove, $admin_resetPassword, $admin_resolveAbuseUserReport, + $admin_forwardAbuseUserReport, + $admin_updateAbuseUserReport, $admin_sendEmail, $admin_serverInfo, $admin_showModerationLogs, @@ -1229,6 +1235,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_relays_remove, $admin_resetPassword, $admin_resolveAbuseUserReport, + $admin_forwardAbuseUserReport, + $admin_updateAbuseUserReport, $admin_sendEmail, $admin_serverInfo, $admin_showModerationLogs, diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index edac9b3beb..0d24ffa56a 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -5,12 +5,14 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; -import * as OTPAuth from 'otpauth'; import { IsNull } from 'typeorm'; +import * as Misskey from 'misskey-js'; import { DI } from '@/di-symbols.js'; import type { + MiMeta, SigninsRepository, UserProfilesRepository, + UserSecurityKeysRepository, UsersRepository, } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -20,6 +22,8 @@ import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { CaptchaService } from '@/core/CaptchaService.js'; +import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; @@ -31,12 +35,18 @@ export class SigninApiService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, @@ -45,6 +55,7 @@ export class SigninApiService { private signinService: SigninService, private userAuthService: UserAuthService, private webAuthnService: WebAuthnService, + private captchaService: CaptchaService, ) { } @@ -53,9 +64,13 @@ export class SigninApiService { request: FastifyRequest<{ Body: { username: string; - password: string; + password?: string; token?: string; credential?: AuthenticationResponseJSON; + 'hcaptcha-response'?: string; + 'g-recaptcha-response'?: string; + 'turnstile-response'?: string; + 'm-captcha-response'?: string; }; }>, reply: FastifyReply, @@ -92,11 +107,6 @@ export class SigninApiService { return; } - if (typeof password !== 'string') { - reply.code(400); - return; - } - if (token != null && typeof token !== 'string') { reply.code(400); return; @@ -121,11 +131,32 @@ export class SigninApiService { } const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1); + + if (password == null) { + reply.code(200); + if (profile.twoFactorEnabled) { + return { + finished: false, + next: 'password', + } satisfies Misskey.entities.SigninFlowResponse; + } else { + return { + finished: false, + next: 'captcha', + } satisfies Misskey.entities.SigninFlowResponse; + } + } + + if (typeof password !== 'string') { + reply.code(400); + return; + } // Compare password const same = await bcrypt.compare(password, profile.password!); - const fail = async (status?: number, failure?: { id: string }) => { + const fail = async (status?: number, failure?: { id: string; }) => { // Append signin history await this.signinsRepository.insert({ id: this.idService.gen(), @@ -139,6 +170,32 @@ export class SigninApiService { }; if (!profile.twoFactorEnabled) { + if (process.env.NODE_ENV !== 'test') { + if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { + await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { + await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { + await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { + await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + } + if (same) { return this.signinService.signin(request, reply, user); } else { @@ -180,7 +237,7 @@ export class SigninApiService { id: '93b86c4b-72f9-40eb-9815-798928603d1e', }); } - } else { + } else if (securityKeysAvailable) { if (!same && !profile.usePasswordLessLogin) { return await fail(403, { id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', @@ -190,7 +247,23 @@ export class SigninApiService { const authRequest = await this.webAuthnService.initiateAuthentication(user.id); reply.code(200); - return authRequest; + return { + finished: false, + next: 'passkey', + authRequest, + } satisfies Misskey.entities.SigninFlowResponse; + } else { + if (!same || !profile.twoFactorEnabled) { + return await fail(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + } else { + reply.code(200); + return { + finished: false, + next: 'totp', + } satisfies Misskey.entities.SigninFlowResponse; + } } // never get here } diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index 70306c3113..640356b50c 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -4,13 +4,16 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import * as Misskey from 'misskey-js'; import { DI } from '@/di-symbols.js'; -import type { SigninsRepository } from '@/models/_.js'; +import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import type { MiLocalUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { bindThis } from '@/decorators.js'; +import { EmailService } from '@/core/EmailService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -19,7 +22,12 @@ export class SigninService { @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + private signinEntityService: SigninEntityService, + private emailService: EmailService, + private notificationService: NotificationService, private idService: IdService, private globalEventService: GlobalEventService, ) { @@ -28,7 +36,8 @@ export class SigninService { @bindThis public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) { setImmediate(async () => { - // Append signin history + this.notificationService.createNotification(user.id, 'login', {}); + const record = await this.signinsRepository.insertOne({ id: this.idService.gen(), userId: user.id, @@ -37,15 +46,22 @@ export class SigninService { success: true, }); - // Publish signin event this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + if (profile.email && profile.emailVerified) { + this.emailService.sendEmail(profile.email, 'New login / ログインがありました', + 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。', + 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。'); + } }); reply.code(200); return { + finished: true, id: user.id, - i: user.token, - }; + i: user.token!, + } satisfies Misskey.entities.SigninFlowResponse; } } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 1bd4a3ffb9..672d964684 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -75,6 +75,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; +import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js'; +import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js'; import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; @@ -459,6 +461,8 @@ const eps = [ ['admin/relays/remove', ep___admin_relays_remove], ['admin/reset-password', ep___admin_resetPassword], ['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport], + ['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport], + ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport], ['admin/send-email', ep___admin_sendEmail], ['admin/server-info', ep___admin_serverInfo], ['admin/show-moderation-logs', ep___admin_showModerationLogs], diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index a7e8a3b018..d30131a62f 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -12,11 +12,27 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import { ApiError } from '@/server/api/error.js'; import { Packed } from '@/misc/json-schema.js'; export const meta = { tags: ['admin'], + errors: { + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '1fb7cb09-d46a-4fff-b8df-057708cce513', + }, + + wrongInitialPassword: { + message: 'Initial password is incorrect.', + code: 'INCORRECT_INITIAL_PASSWORD', + id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62', + }, + }, + res: { type: 'object', optional: false, nullable: false, @@ -35,6 +51,7 @@ export const paramDef = { properties: { username: localUsernameSchema, password: passwordSchema, + setupPassword: { type: 'string', nullable: true }, }, required: ['username', 'password'], } as const; @@ -42,6 +59,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -52,7 +72,23 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, _me, token) => { const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; const realUsers = await this.instanceActorService.realLocalUsersPresent(); - if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied'); + + if (!realUsers && me == null && token == null) { + // 初回セットアップの場合 + if (this.config.setupPassword != null) { + // 初期パスワードが設定されている場合 + if (ps.setupPassword !== this.config.setupPassword) { + // 初期パスワードが違う場合 + throw new ApiError(meta.errors.wrongInitialPassword); + } + } else if (ps.setupPassword != null && ps.setupPassword.trim() !== '') { + // 初期パスワードが設定されていないのに初期パスワードが入力された場合 + throw new ApiError(meta.errors.wrongInitialPassword); + } + } else if ((realUsers && !me?.isRoot) || token !== null) { + // 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合 + throw new ApiError(meta.errors.accessDenied); + } const { account, secret } = await this.signupService.signup({ username: ps.username, diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts new file mode 100644 index 0000000000..3e42c91fed --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { AbuseReportService } from '@/core/AbuseReportService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:resolve-abuse-user-report', + + errors: { + noSuchAbuseReport: { + message: 'No such abuse report.', + code: 'NO_SUCH_ABUSE_REPORT', + id: '8763e21b-d9bc-40be-acf6-54c1a6986493', + kind: 'server', + httpStatusCode: 404, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + reportId: { type: 'string', format: 'misskey:id' }, + }, + required: ['reportId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + private abuseReportService: AbuseReportService, + ) { + super(meta, paramDef, async (ps, me) => { + const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); + if (!report) { + throw new ApiError(meta.errors.noSuchAbuseReport); + } + + await this.abuseReportService.forward(report.id, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 29e8bfaf14..b76ed5c524 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -495,6 +495,18 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + federation: { + type: 'string', + optional: false, nullable: false, + }, + federationHosts: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, }, }, } as const; @@ -630,6 +642,8 @@ export default class extends Endpoint { // eslint- urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, urlPreviewUserAgent: instance.urlPreviewUserAgent, urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, + federation: instance.federation, + federationHosts: instance.federationHosts, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index 9b79100fcf..554d324ff2 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -32,7 +32,7 @@ export const paramDef = { type: 'object', properties: { reportId: { type: 'string', format: 'misskey:id' }, - forward: { type: 'boolean', default: false }, + resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true }, }, required: ['reportId'], } as const; @@ -50,7 +50,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchAbuseReport); } - await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me); + await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 5a1c05f41a..655bd32bce 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -31,6 +31,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + followedMessage: { + type: 'string', + optional: false, nullable: true, + }, autoAcceptFollowed: { type: 'boolean', optional: false, nullable: false, @@ -226,6 +230,7 @@ export default class extends Endpoint { // eslint- return { email: profile.email, emailVerified: profile.emailVerified, + followedMessage: profile.followedMessage, autoAcceptFollowed: profile.autoAcceptFollowed, noCrawle: profile.noCrawle, preventAiLearning: profile.preventAiLearning, diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts new file mode 100644 index 0000000000..73d4b843f0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { AbuseReportService } from '@/core/AbuseReportService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:resolve-abuse-user-report', + + errors: { + noSuchAbuseReport: { + message: 'No such abuse report.', + code: 'NO_SUCH_ABUSE_REPORT', + id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662', + kind: 'server', + httpStatusCode: 404, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + reportId: { type: 'string', format: 'misskey:id' }, + moderationNote: { type: 'string' }, + }, + required: ['reportId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + private abuseReportService: AbuseReportService, + ) { + super(meta, paramDef, async (ps, me) => { + const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); + if (!report) { + throw new ApiError(meta.errors.noSuchAbuseReport); + } + + await this.abuseReportService.update(report.id, { + moderationNote: ps.moderationNote, + }, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 865e73f274..9ffae840b6 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -168,6 +168,16 @@ export const paramDef = { urlPreviewRequireContentLength: { type: 'boolean' }, urlPreviewUserAgent: { type: 'string', nullable: true }, urlPreviewSummaryProxyUrl: { type: 'string', nullable: true }, + federation: { + type: 'string', + enum: ['all', 'none', 'specified'], + }, + federationHosts: { + type: 'array', + items: { + type: 'string', + }, + }, }, required: [], } as const; @@ -637,6 +647,14 @@ export default class extends Endpoint { // eslint- set.urlPreviewSummaryProxyUrl = value === '' ? null : value; } + if (ps.federation !== undefined) { + set.federation = ps.federation; + } + + if (Array.isArray(ps.federationHosts)) { + set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 577ca0b24c..c52608cefb 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -19,8 +19,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '../../error.js'; -import { MiMeta } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -89,9 +87,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.meta) - private serverSettings: MiMeta, - private utilityService: UtilityService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, @@ -115,8 +110,7 @@ export default class extends Endpoint { // eslint- */ @bindThis private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> { - // ブロックしてたら中断 - if (this.utilityService.isBlockedHost(this.serverSettings.blockedHosts, this.utilityService.extractDbHost(uri))) return null; + if (!this.utilityService.isFederationAllowedUri(uri)) return null; let local = await this.mergePack(me, ...await Promise.all([ this.apDbResolverService.getUserFromApId(uri), diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts index c2d6ab5085..9a0cb461f2 100644 --- a/packages/backend/src/server/api/endpoints/flash/featured.ts +++ b/packages/backend/src/server/api/endpoints/flash/featured.ts @@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FlashService } from '@/core/FlashService.js'; export const meta = { tags: ['flash'], @@ -27,26 +28,25 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + offset: { type: 'integer', minimum: 0, default: 0 }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, required: [], } as const; @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.flashsRepository) - private flashsRepository: FlashsRepository, - + private flashService: FlashService, private flashEntityService: FlashEntityService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.flashsRepository.createQueryBuilder('flash') - .andWhere('flash.likedCount > 0') - .orderBy('flash.likedCount', 'DESC'); - - const flashs = await query.limit(10).getMany(); - - return await this.flashEntityService.packMany(flashs, me); + const result = await this.flashService.featured({ + offset: ps.offset, + limit: ps.limit, + }); + return await this.flashEntityService.packMany(result, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index a1e2fa5e4c..798bd98cf1 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; -import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; +import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; -import { notificationTypes } from '@/types.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { langmap } from '@/misc/langmap.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -134,6 +133,7 @@ export const paramDef = { properties: { name: { ...nameSchema, nullable: true }, description: { ...descriptionSchema, nullable: true }, + followedMessage: { ...followedMessageSchema, nullable: true }, location: { ...locationSchema, nullable: true }, birthday: { ...birthdaySchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, @@ -267,6 +267,7 @@ export default class extends Endpoint { // eslint- } } if (ps.description !== undefined) profileUpdates.description = ps.description; + if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 5de1f87667..dd7bb7823e 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -256,7 +256,7 @@ export class ClientServerService { }); bullBoardServerAdapter.setBasePath(bullBoardPath); - //(fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath }); + (fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath }); //#endregion fastify.register(fastifyView, { diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 5854c6b392..df3cfee171 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -17,6 +17,7 @@ * roleAssigned - ロールが付与された * achievementEarned - 実績を獲得 * exportCompleted - エクスポートが完了 + * login - ログイン * app - アプリ通知 * test - テスト通知(サーバー側) */ @@ -34,6 +35,7 @@ export const notificationTypes = [ 'roleAssigned', 'achievementEarned', 'exportCompleted', + 'login', 'app', 'test', ] as const; @@ -97,6 +99,8 @@ export const moderationLogTypes = [ 'markSensitiveDriveFile', 'unmarkSensitiveDriveFile', 'resolveAbuseReport', + 'forwardAbuseReport', + 'updateAbuseReportNote', 'createInvitation', 'createAd', 'updateAd', @@ -265,7 +269,18 @@ export type ModerationLogPayloads = { resolveAbuseReport: { reportId: string; report: any; - forwarded: boolean; + forwarded?: boolean; + resolvedAs?: string | null; + }; + forwardAbuseReport: { + reportId: string; + report: any; + }; + updateAbuseReportNote: { + reportId: string; + report: any; + before: string; + after: string; }; createInvitation: { invitations: any[]; diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 06548fa7da..48e1bababb 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -136,13 +136,7 @@ describe('2要素認証', () => { keyName: string, credentialId: Buffer, requestOptions: PublicKeyCredentialRequestOptionsJSON, - }): { - username: string, - password: string, - credential: AuthenticationResponseJSON, - 'g-recaptcha-response'?: string | null, - 'hcaptcha-response'?: string | null, - } => { + }): misskey.entities.SigninFlowRequest => { // AuthenticatorAssertionResponse.authenticatorData // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData const authenticatorData = Buffer.concat([ @@ -202,17 +196,21 @@ describe('2要素認証', () => { }, alice); assert.strictEqual(doneResponse.status, 200); - const usersShowResponse = await api('users/show', { - username, - }, alice); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); + const signinWithoutTokenResponse = await api('signin-flow', { + ...signinParam(), + }); + assert.strictEqual(signinWithoutTokenResponse.status, 200); + assert.deepStrictEqual(signinWithoutTokenResponse.body, { + finished: false, + next: 'totp', + }); - const signinResponse = await api('signin', { + const signinResponse = await api('signin-flow', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); assert.strictEqual(signinResponse.status, 200); + assert.strictEqual(signinResponse.body.finished, true); assert.notEqual(signinResponse.body.i, undefined); // 後片付け @@ -253,27 +251,23 @@ describe('2要素認証', () => { assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.name, keyName); - const usersShowResponse = await api('users/show', { - username, - }); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true); - - const signinResponse = await api('signin', { + const signinResponse = await api('signin-flow', { ...signinParam(), }); assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.i, undefined); - assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined); - assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined); - assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url')); + assert.strictEqual(signinResponse.body.finished, false); + assert.strictEqual(signinResponse.body.next, 'passkey'); + assert.notEqual(signinResponse.body.authRequest.challenge, undefined); + assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined); + assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url')); - const signinResponse2 = await api('signin', signinWithSecurityKeyParam({ + const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({ keyName, credentialId, - requestOptions: signinResponse.body, - } as any)); + requestOptions: signinResponse.body.authRequest, + })); assert.strictEqual(signinResponse2.status, 200); + assert.strictEqual(signinResponse2.body.finished, true); assert.notEqual(signinResponse2.body.i, undefined); // 後片付け @@ -315,28 +309,30 @@ describe('2要素認証', () => { }, alice); assert.strictEqual(passwordLessResponse.status, 204); - const usersShowResponse = await api('users/show', { - username, - }); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true); + const iResponse = await api('i', {}, alice); + assert.strictEqual(iResponse.status, 200); + assert.strictEqual(iResponse.body.usePasswordLessLogin, true); - const signinResponse = await api('signin', { + const signinResponse = await api('signin-flow', { ...signinParam(), password: '', }); assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.i, undefined); + assert.strictEqual(signinResponse.body.finished, false); + assert.strictEqual(signinResponse.body.next, 'passkey'); + assert.notEqual(signinResponse.body.authRequest.challenge, undefined); + assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined); - const signinResponse2 = await api('signin', { + const signinResponse2 = await api('signin-flow', { ...signinWithSecurityKeyParam({ keyName, credentialId, - requestOptions: signinResponse.body, + requestOptions: signinResponse.body.authRequest, } as any), password: '', }); assert.strictEqual(signinResponse2.status, 200); + assert.strictEqual(signinResponse2.body.finished, true); assert.notEqual(signinResponse2.body.i, undefined); // 後片付け @@ -424,11 +420,11 @@ describe('2要素認証', () => { assert.strictEqual(keyDoneResponse.status, 200); // テストの実行順によっては複数残ってるので全部消す - const iResponse = await api('i', { + const beforeIResponse = await api('i', { }, alice); - assert.strictEqual(iResponse.status, 200); - assert.ok(iResponse.body.securityKeysList); - for (const key of iResponse.body.securityKeysList) { + assert.strictEqual(beforeIResponse.status, 200); + assert.ok(beforeIResponse.body.securityKeysList); + for (const key of beforeIResponse.body.securityKeysList) { const removeKeyResponse = await api('i/2fa/remove-key', { token: otpToken(registerResponse.body.secret), password, @@ -437,17 +433,16 @@ describe('2要素認証', () => { assert.strictEqual(removeKeyResponse.status, 200); } - const usersShowResponse = await api('users/show', { - username, - }); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false); + const afterIResponse = await api('i', {}, alice); + assert.strictEqual(afterIResponse.status, 200); + assert.strictEqual(afterIResponse.body.securityKeys, false); - const signinResponse = await api('signin', { + const signinResponse = await api('signin-flow', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); assert.strictEqual(signinResponse.status, 200); + assert.strictEqual(signinResponse.body.finished, true); assert.notEqual(signinResponse.body.i, undefined); // 後片付け @@ -468,11 +463,9 @@ describe('2要素認証', () => { }, alice); assert.strictEqual(doneResponse.status, 200); - const usersShowResponse = await api('users/show', { - username, - }); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); + const iResponse = await api('i', {}, alice); + assert.strictEqual(iResponse.status, 200); + assert.strictEqual(iResponse.body.twoFactorEnabled, true); const unregisterResponse = await api('i/2fa/unregister', { token: otpToken(registerResponse.body.secret), @@ -480,10 +473,11 @@ describe('2要素認証', () => { }, alice); assert.strictEqual(unregisterResponse.status, 204); - const signinResponse = await api('signin', { + const signinResponse = await api('signin-flow', { ...signinParam(), }); assert.strictEqual(signinResponse.status, 200); + assert.strictEqual(signinResponse.body.finished, true); assert.notEqual(signinResponse.body.i, undefined); // 後片付け diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index 5aaec7f6f9..b91d77c398 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -66,9 +66,9 @@ describe('Endpoints', () => { }); }); - describe('signin', () => { + describe('signin-flow', () => { test('間違ったパスワードでサインインできない', async () => { - const res = await api('signin', { + const res = await api('signin-flow', { username: 'test1', password: 'bar', }); @@ -77,7 +77,7 @@ describe('Endpoints', () => { }); test('クエリをインジェクションできない', async () => { - const res = await api('signin', { + const res = await api('signin-flow', { username: 'test1', // @ts-expect-error password must be string password: { @@ -89,7 +89,7 @@ describe('Endpoints', () => { }); test('正しい情報でサインインできる', async () => { - const res = await api('signin', { + const res = await api('signin-flow', { username: 'test1', password: 'test1', }); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index f8aa67cfac..7efd688ec2 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -180,7 +180,6 @@ describe('Webリソース', () => { })); }); - /* queueは一時的に無効化されている describe.each([{ path: '/queue' }])('$path', ({ path }) => { test('はログインしないとGETできない。', async () => await notOk({ path, @@ -198,7 +197,6 @@ describe('Webリソース', () => { cookie: cookie(alice), })); }); - */ describe.each([{ path: '/streaming' }])('$path', ({ path }) => { test('はGETできない。', async () => await notOk({ diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts index 6ce6e47781..c98d199f35 100644 --- a/packages/backend/test/e2e/synalio/abuse-report.ts +++ b/packages/backend/test/e2e/synalio/abuse-report.ts @@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => { const webhookBody2 = await captureWebhook(async () => { await resolveAbuseReport({ reportId: webhookBody1.body.id, - forward: false, }, admin); }); @@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => { const webhookBody2 = await captureWebhook(async () => { await resolveAbuseReport({ reportId: abuseReportId, - forward: false, }, admin); }); @@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => { const webhookBody2 = await captureWebhook(async () => { await resolveAbuseReport({ reportId: webhookBody1.body.id, - forward: false, }, admin); }).catch(e => e.message); @@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => { const webhookBody2 = await captureWebhook(async () => { await resolveAbuseReport({ reportId: abuseReportId, - forward: false, }, admin); }).catch(e => e.message); @@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => { const webhookBody2 = await captureWebhook(async () => { await resolveAbuseReport({ reportId: abuseReportId, - forward: false, }, admin); }).catch(e => e.message); @@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => { const webhookBody2 = await captureWebhook(async () => { await resolveAbuseReport({ reportId: abuseReportId, - forward: false, }, admin); }).catch(e => e.message); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 61fd759932..822ca14ae6 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -7,9 +7,9 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { inspect } from 'node:util'; -import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; describe('ユーザー', () => { // エンティティとしてのユーザーを主眼においたテストを記述する @@ -83,9 +83,6 @@ describe('ユーザー', () => { publicReactions: user.publicReactions, followingVisibility: user.followingVisibility, followersVisibility: user.followersVisibility, - twoFactorEnabled: user.twoFactorEnabled, - usePasswordLessLogin: user.usePasswordLessLogin, - securityKeys: user.securityKeys, roles: user.roles, memo: user.memo, }); @@ -105,6 +102,7 @@ describe('ユーザー', () => { isRenoteMuted: user.isRenoteMuted ?? false, notify: user.notify ?? 'none', withReplies: user.withReplies ?? false, + followedMessage: user.isFollowing ? (user.followedMessage ?? null) : undefined, }); }; @@ -114,6 +112,7 @@ describe('ユーザー', () => { ...userDetailedNotMe(user), avatarId: user.avatarId, bannerId: user.bannerId, + followedMessage: user.followedMessage, isModerator: user.isModerator, isAdmin: user.isAdmin, injectFeaturedNote: user.injectFeaturedNote, @@ -147,6 +146,9 @@ describe('ユーザー', () => { achievements: user.achievements, loggedInDays: user.loggedInDays, policies: user.policies, + twoFactorEnabled: user.twoFactorEnabled, + usePasswordLessLogin: user.usePasswordLessLogin, + securityKeys: user.securityKeys, ...(security ? { email: user.email, emailVerified: user.emailVerified, @@ -341,15 +343,13 @@ describe('ユーザー', () => { assert.strictEqual(response.publicReactions, true); assert.strictEqual(response.followingVisibility, 'public'); assert.strictEqual(response.followersVisibility, 'public'); - assert.strictEqual(response.twoFactorEnabled, false); - assert.strictEqual(response.usePasswordLessLogin, false); - assert.strictEqual(response.securityKeys, false); assert.deepStrictEqual(response.roles, []); assert.strictEqual(response.memo, null); // MeDetailedOnly assert.strictEqual(response.avatarId, null); assert.strictEqual(response.bannerId, null); + assert.strictEqual(response.followedMessage, null); assert.strictEqual(response.isModerator, false); assert.strictEqual(response.isAdmin, false); assert.strictEqual(response.injectFeaturedNote, true); @@ -382,6 +382,9 @@ describe('ユーザー', () => { assert.deepStrictEqual(response.achievements, []); assert.deepStrictEqual(response.loggedInDays, 0); assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); + assert.strictEqual(response.twoFactorEnabled, false); + assert.strictEqual(response.usePasswordLessLogin, false); + assert.strictEqual(response.securityKeys, false); assert.notStrictEqual(response.email, undefined); assert.strictEqual(response.emailVerified, false); assert.deepStrictEqual(response.securityKeysList, []); @@ -413,6 +416,8 @@ describe('ユーザー', () => { { parameters: () => ({ description: 'x'.repeat(1500) }) }, { parameters: () => ({ description: 'x' }) }, { parameters: () => ({ description: 'My description' }) }, + { parameters: () => ({ followedMessage: null }) }, + { parameters: () => ({ followedMessage: 'Thank you' }) }, { parameters: () => ({ location: null }) }, { parameters: () => ({ location: 'x'.repeat(50) }) }, { parameters: () => ({ location: 'x' }) }, @@ -613,6 +618,9 @@ describe('ユーザー', () => { { label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator }, // @ts-expect-error UserDetailedNotMe doesn't include isModerator { label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined }, + { label: '自分から見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => alice, selector: (user: misskey.entities.MeDetailed) => user.twoFactorEnabled, expected: () => false }, + { label: '自分以外から見た場合に二要素認証関連のプロパティがセットされていない', user: () => alice, me: () => bob, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => undefined }, + { label: 'モデレーターから見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => false }, { label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced }, // FIXME: 落ちる //{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended }, diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts index e971659070..235af29f0d 100644 --- a/packages/backend/test/unit/AbuseReportNotificationService.ts +++ b/packages/backend/test/unit/AbuseReportNotificationService.ts @@ -5,6 +5,7 @@ import { jest } from '@jest/globals'; import { Test, TestingModule } from '@nestjs/testing'; +import { randomString } from '../utils.js'; import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { AbuseReportNotificationRecipientRepository, @@ -25,7 +26,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { randomString } from '../utils.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; process.env.NODE_ENV = 'test'; @@ -110,6 +111,9 @@ describe('AbuseReportNotificationService', () => { { provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }), }, + { + provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }), + }, { provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), }, diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts new file mode 100644 index 0000000000..12ffaf3421 --- /dev/null +++ b/packages/backend/test/unit/FlashService.ts @@ -0,0 +1,152 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import { FlashService } from '@/core/FlashService.js'; +import { IdService } from '@/core/IdService.js'; +import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalModule } from '@/GlobalModule.js'; + +describe('FlashService', () => { + let app: TestingModule; + let service: FlashService; + + // -------------------------------------------------------------------------------------- + + let flashsRepository: FlashsRepository; + let usersRepository: UsersRepository; + let userProfilesRepository: UserProfilesRepository; + let idService: IdService; + + // -------------------------------------------------------------------------------------- + + let root: MiUser; + let alice: MiUser; + let bob: MiUser; + + // -------------------------------------------------------------------------------------- + + async function createFlash(data: Partial) { + return flashsRepository.insert({ + id: idService.gen(), + updatedAt: new Date(), + userId: root.id, + title: 'title', + summary: 'summary', + script: 'script', + permissions: [], + likedCount: 0, + ...data, + }).then(x => flashsRepository.findOneByOrFail(x.identifiers[0])); + } + + async function createUser(data: Partial = {}) { + const user = await usersRepository + .insert({ + id: idService.gen(), + ...data, + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); + + await userProfilesRepository.insert({ + userId: user.id, + }); + + return user; + } + + // -------------------------------------------------------------------------------------- + + beforeEach(async () => { + app = await Test.createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + FlashService, + IdService, + ], + }).compile(); + + service = app.get(FlashService); + + flashsRepository = app.get(DI.flashsRepository); + usersRepository = app.get(DI.usersRepository); + userProfilesRepository = app.get(DI.userProfilesRepository); + idService = app.get(IdService); + + root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); + alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false }); + bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false }); + }); + + afterEach(async () => { + await usersRepository.delete({}); + await userProfilesRepository.delete({}); + await flashsRepository.delete({}); + }); + + afterAll(async () => { + await app.close(); + }); + + // -------------------------------------------------------------------------------------- + + describe('featured', () => { + test('should return featured flashes', async () => { + const flash1 = await createFlash({ likedCount: 1 }); + const flash2 = await createFlash({ likedCount: 2 }); + const flash3 = await createFlash({ likedCount: 3 }); + + const result = await service.featured({ + offset: 0, + limit: 10, + }); + + expect(result).toEqual([flash3, flash2, flash1]); + }); + + test('should return featured flashes public visibility only', async () => { + const flash1 = await createFlash({ likedCount: 1, visibility: 'public' }); + const flash2 = await createFlash({ likedCount: 2, visibility: 'public' }); + const flash3 = await createFlash({ likedCount: 3, visibility: 'private' }); + + const result = await service.featured({ + offset: 0, + limit: 10, + }); + + expect(result).toEqual([flash2, flash1]); + }); + + test('should return featured flashes with offset', async () => { + const flash1 = await createFlash({ likedCount: 1 }); + const flash2 = await createFlash({ likedCount: 2 }); + const flash3 = await createFlash({ likedCount: 3 }); + + const result = await service.featured({ + offset: 1, + limit: 10, + }); + + expect(result).toEqual([flash2, flash1]); + }); + + test('should return featured flashes with limit', async () => { + const flash1 = await createFlash({ likedCount: 1 }); + const flash2 = await createFlash({ likedCount: 2 }); + const flash3 = await createFlash({ likedCount: 3 }); + + const result = await service.featured({ + offset: 0, + limit: 2, + }); + + expect(result).toEqual([flash3, flash2]); + }); + }); +}); diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts new file mode 100644 index 0000000000..bae2b88c60 --- /dev/null +++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { IncomingHttpHeaders } from 'node:http'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { FastifyReply, FastifyRequest } from 'fastify'; +import { AuthenticationResponseJSON } from '@simplewebauthn/types'; +import { HttpHeader } from 'fastify/types/utils.js'; +import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; +import { MiUser } from '@/models/User.js'; +import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { DI } from '@/di-symbols.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiService.js'; +import { RateLimiterService } from '@/server/api/RateLimiterService.js'; +import { WebAuthnService } from '@/core/WebAuthnService.js'; +import { SigninService } from '@/server/api/SigninService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; + +const moduleMocker = new ModuleMocker(global); + +class FakeLimiter { + public async limit() { + return; + } +} + +class FakeSigninService { + public signin(..._args: any): any { + return true; + } +} + +class DummyFastifyReply { + public statusCode: number; + code(num: number): void { + this.statusCode = num; + } + header(_key: HttpHeader, _value: any): void { + } +} +class DummyFastifyRequest { + public ip: string; + public body: {credential: any, context: string}; + public headers: IncomingHttpHeaders = { 'accept': 'application/json' }; + constructor(body?: any) { + this.ip = '0.0.0.0'; + this.body = body; + } +} + +type ApiFastifyRequestType = FastifyRequest<{ + Body: { + credential?: AuthenticationResponseJSON; + context?: string; + }; +}>; + +describe('SigninWithPasskeyApiService', () => { + let app: TestingModule; + let passkeyApiService: SigninWithPasskeyApiService; + let usersRepository: UsersRepository; + let userProfilesRepository: UserProfilesRepository; + let webAuthnService: WebAuthnService; + let idService: IdService; + let FakeWebauthnVerify: ()=>Promise; + + async function createUser(data: Partial = {}) { + const user = await usersRepository + .save({ + ...data, + }); + return user; + } + + async function createUserProfile(data: Partial = {}) { + const userProfile = await userProfilesRepository + .save({ ...data }, + ); + return userProfile; + } + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + providers: [ + SigninWithPasskeyApiService, + { provide: RateLimiterService, useClass: FakeLimiter }, + { provide: SigninService, useClass: FakeSigninService }, + ], + }).useMocker((token) => { + if (typeof token === 'function') { + const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata; + const Mock = moduleMocker.generateFromMetadata(mockMetadata); + return new Mock(); + } + }).compile(); + passkeyApiService = app.get(SigninWithPasskeyApiService); + usersRepository = app.get(DI.usersRepository); + userProfilesRepository = app.get(DI.userProfilesRepository); + webAuthnService = app.get(WebAuthnService); + idService = app.get(IdService); + }); + + beforeEach(async () => { + const uid = idService.gen(); + FakeWebauthnVerify = async () => { + return uid; + }; + jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify); + + const dummyUser = { + id: uid, username: uid, usernameLower: uid.toLocaleLowerCase(), uri: null, host: null, + }; + const dummyProfile = { + userId: uid, + password: 'qwerty', + usePasswordLessLogin: true, + }; + await createUser(dummyUser); + await createUserProfile(dummyProfile); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('Get Passkey Options', () => { + it('Should return passkey Auth Options', async () => { + const req = new DummyFastifyRequest({}) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as unknown as FastifyReply; + const res_body = await passkeyApiService.signin(req, res); + expect(res.statusCode).toBe(200); + expect((res_body as any).option).toBeDefined(); + expect(typeof (res_body as any).context).toBe('string'); + }); + }); + describe('Try Passkey Auth', () => { + it('Should Success', async () => { + const req = new DummyFastifyRequest({ context: 'auth-context', credential: { dummy: [] } }) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as FastifyReply; + const res_body = await passkeyApiService.signin(req, res); + expect((res_body as any).signinResponse).toBeDefined(); + }); + + it('Should return 400 Without Auth Context', async () => { + const req = new DummyFastifyRequest({ credential: { dummy: [] } }) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as FastifyReply; + const res_body = await passkeyApiService.signin(req, res); + expect(res.statusCode).toBe(400); + expect((res_body as any).error?.id).toStrictEqual('1658cc2e-4495-461f-aee4-d403cdf073c1'); + }); + + it('Should return 403 When Challenge Verify fail', async () => { + const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as FastifyReply; + jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication') + .mockImplementation(async () => { + throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); + }); + const res_body = await passkeyApiService.signin(req, res); + expect(res.statusCode).toBe(403); + expect((res_body as any).error?.id).toStrictEqual('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); + }); + + it('Should return 403 When The user not Enabled Passwordless login', async () => { + const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as FastifyReply; + const userId = await FakeWebauthnVerify(); + const data = { userId: userId, usePasswordLessLogin: false }; + await userProfilesRepository.update({ userId: userId }, data); + const res_body = await passkeyApiService.signin(req, res); + expect(res.statusCode).toBe(403); + expect((res_body as any).error?.id).toStrictEqual('2d84773e-f7b7-4d0b-8f72-bb69b584c912'); + }); + }); +}); diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 46a626edf4..cb62191c3b 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -12,62 +12,49 @@ }, "dependencies": { "@discordapp/twemoji": "15.1.0", - "@github/webauthn-json": "2.1.1", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.0", + "@rollup/pluginutils": "5.1.2", "@tabler/icons-webfont": "3.3.0", "@twemoji/parser": "15.1.1", "@vitejs/plugin-vue": "5.1.4", - "@vue/compiler-sfc": "3.5.7", + "@vue/compiler-sfc": "3.5.11", "astring": "1.9.0", "buraha": "0.0.1", - "compare-versions": "6.1.1", - "date-fns": "2.30.0", - "escape-regexp": "0.0.1", "estree-walker": "3.0.3", - "eventemitter3": "5.0.1", - "idb-keyval": "6.2.1", - "is-file-animated": "1.0.2", "mfm-js": "0.24.0", "misskey-js": "workspace:*", "frontend-shared": "workspace:*", "punycode": "2.3.1", - "rollup": "4.22.2", - "sanitize-html": "2.13.0", - "sass": "1.79.3", - "shiki": "1.12.0", - "strict-event-emitter-types": "2.0.0", - "throttle-debounce": "5.0.2", + "rollup": "4.22.5", + "sass": "1.79.4", + "shiki": "1.21.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", "typescript": "5.6.2", "uuid": "10.0.0", "json5": "2.2.3", - "vite": "5.4.7", - "vue": "3.5.7" + "vite": "5.4.8", + "vue": "3.5.11" }, "devDependencies": { "@misskey-dev/summaly": "5.1.0", "@testing-library/vue": "8.1.0", - "@types/escape-regexp": "0.0.3", "@types/estree": "1.0.6", "@types/micromatch": "4.0.9", "@types/node": "20.14.12", "@types/punycode": "2.1.4", - "@types/sanitize-html": "2.13.0", - "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", "@types/ws": "8.5.12", "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "@vitest/coverage-v8": "1.6.0", - "@vue/runtime-core": "3.5.7", + "@vue/runtime-core": "3.5.11", "acorn": "8.12.1", "cross-env": "7.0.3", - "eslint-plugin-import": "2.30.0", + "eslint-plugin-import": "2.31.0", "eslint-plugin-vue": "9.28.0", "fast-glob": "3.3.2", "happy-dom": "10.0.3", diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue index e4149cf363..59b670cdc6 100644 --- a/packages/frontend-embed/src/components/EmCustomEmoji.vue +++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue @@ -38,8 +38,6 @@ const props = defineProps<{ host?: string | null; url?: string; useOriginalSize?: boolean; - menu?: boolean; - menuReaction?: boolean; fallbackToImage?: boolean; }>(); diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts index b2bcf4597e..59f0d495e6 100644 --- a/packages/frontend-embed/src/components/EmMfm.ts +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -6,6 +6,7 @@ import { VNode, h, SetupContext, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; +import { host } from '@@/js/config.js'; import EmUrl from '@/components/EmUrl.vue'; import EmTime from '@/components/EmTime.vue'; import EmLink from '@/components/EmLink.vue'; @@ -13,7 +14,6 @@ import EmMention from '@/components/EmMention.vue'; import EmEmoji from '@/components/EmEmoji.vue'; import EmCustomEmoji from '@/components/EmCustomEmoji.vue'; import EmA from '@/components/EmA.vue'; -import { host } from '@@/js/config.js'; function safeParseFloat(str: unknown): number | null { if (typeof str !== 'string' || str === '') return null; @@ -41,9 +41,6 @@ type MfmProps = { rootScale?: number; nyaize?: boolean | 'respect'; parsedNodes?: mfm.MfmNode[] | null; - enableEmojiMenu?: boolean; - enableEmojiMenuReaction?: boolean; - linkNavigationBehavior?: string; }; type MfmEvents = { @@ -52,8 +49,6 @@ type MfmEvents = { // eslint-disable-next-line import/no-default-export export default function (props: MfmProps, { emit }: { emit: SetupContext['emit'] }) { - provide('linkNavigationBehavior', props.linkNavigationBehavior); - const isNote = props.isNote ?? true; const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; @@ -397,8 +392,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext= 2.5, - menu: props.enableEmojiMenu, - menuReaction: props.enableEmojiMenuReaction, fallbackToImage: false, })]; } else { diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index 64e67401c2..2dbee488c5 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -91,6 +91,11 @@ export function getConfig(): UserConfig { } }, }, + preprocessorOptions: { + scss: { + api: 'modern-compiler', + }, + }, }, define: { diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index aec4a4a58b..4fe5cbb205 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -68,6 +68,7 @@ export const notificationTypes = [ 'roleAssigned', 'achievementEarned', 'exportCompleted', + 'login', 'test', 'app', ] as const; diff --git a/packages/frontend-shared/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5 index 23a9549a93..eb5fda3dc0 100644 --- a/packages/frontend-shared/themes/_dark.json5 +++ b/packages/frontend-shared/themes/_dark.json5 @@ -13,6 +13,7 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', accentedBg: ':alpha<0.15<@accent', + love: '#dd2e44', focus: ':alpha<0.3<@accent', bg: '#000', acrylicBg: ':alpha<0.5<@bg', diff --git a/packages/frontend-shared/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5 index 96b2ddd06c..e0196dcbf3 100644 --- a/packages/frontend-shared/themes/_light.json5 +++ b/packages/frontend-shared/themes/_light.json5 @@ -13,6 +13,7 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', accentedBg: ':alpha<0.15<@accent', + love: '#dd2e44', focus: ':alpha<0.3<@accent', bg: '#fff', acrylicBg: ':alpha<0.5<@bg', diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 12bd7f9999..8830523810 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -397,7 +397,18 @@ function toStories(component: string): Promise { const globs = await Promise.all([ glob('src/components/global/Mk*.vue'), glob('src/components/global/RouterView.vue'), - glob('src/components/Mk[A-E]*.vue'), + glob('src/components/MkAbuseReportWindow.vue'), + glob('src/components/MkAccountMoved.vue'), + glob('src/components/MkAchievements.vue'), + glob('src/components/MkAnalogClock.vue'), + glob('src/components/MkAnimBg.vue'), + glob('src/components/MkAnnouncementDialog.vue'), + glob('src/components/MkAntennaEditor.vue'), + glob('src/components/MkAntennaEditorDialog.vue'), + glob('src/components/MkAsUi.vue'), + glob('src/components/MkAutocomplete.vue'), + glob('src/components/MkAvatars.vue'), + glob('src/components/Mk[B-E]*.vue'), glob('src/components/MkFlashPreview.vue'), glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkSignupServerRules.vue'), diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 76af417550..3226a554a9 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -23,12 +23,12 @@ "@misskey-dev/browser-image-resizer": "2024.1.0", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.0", + "@rollup/pluginutils": "5.1.2", "@syuilo/aiscript": "0.19.0", "@tabler/icons-webfont": "3.3.0", "@twemoji/parser": "15.1.1", "@vitejs/plugin-vue": "5.1.4", - "@vue/compiler-sfc": "3.5.7", + "@vue/compiler-sfc": "3.5.11", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11", "astring": "1.9.0", "broadcast-channel": "7.0.0", @@ -39,13 +39,13 @@ "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "11.10.2", + "chromatic": "11.11.0", "compare-versions": "6.1.1", "cropperjs": "2.0.0-rc.2", "date-fns": "2.30.0", - "escape-regexp": "0.0.1", "estree-walker": "3.0.3", "eventemitter3": "5.0.1", + "frontend-shared": "workspace:*", "idb-keyval": "6.2.1", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", @@ -55,16 +55,15 @@ "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", - "frontend-shared": "workspace:*", "photoswipe": "5.4.4", "punycode": "2.3.1", - "rollup": "4.22.2", - "sanitize-html": "2.13.0", + "rollup": "4.22.5", + "sanitize-html": "2.13.1", "sass": "1.79.3", - "shiki": "1.12.0", + "shiki": "1.21.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.168.0", + "three": "0.169.0", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "tsc-alias": "1.8.10", @@ -72,32 +71,32 @@ "typescript": "5.6.2", "uuid": "10.0.0", "v-code-diff": "1.13.1", - "vite": "5.4.7", - "vue": "3.5.7", + "vite": "5.4.8", + "vue": "3.5.11", "vuedraggable": "next" }, "devDependencies": { "@misskey-dev/summaly": "5.1.0", - "@storybook/addon-actions": "8.3.2", - "@storybook/addon-essentials": "8.3.2", - "@storybook/addon-interactions": "8.3.2", - "@storybook/addon-links": "8.3.2", - "@storybook/addon-mdx-gfm": "8.3.2", - "@storybook/addon-storysource": "8.3.2", - "@storybook/blocks": "8.3.2", - "@storybook/components": "8.3.2", - "@storybook/core-events": "8.3.2", - "@storybook/manager-api": "8.3.2", - "@storybook/preview-api": "8.3.2", - "@storybook/react": "8.3.2", - "@storybook/react-vite": "8.3.2", - "@storybook/test": "8.3.2", - "@storybook/theming": "8.3.2", - "@storybook/types": "8.3.2", - "@storybook/vue3": "8.3.2", - "@storybook/vue3-vite": "8.3.2", + "@storybook/addon-actions": "8.3.4", + "@storybook/addon-essentials": "8.3.4", + "@storybook/addon-interactions": "8.3.4", + "@storybook/addon-links": "8.3.4", + "@storybook/addon-mdx-gfm": "8.3.4", + "@storybook/addon-storysource": "8.3.4", + "@storybook/blocks": "8.3.4", + "@storybook/components": "8.3.4", + "@storybook/core-events": "8.3.4", + "@storybook/manager-api": "8.3.4", + "@storybook/preview-api": "8.3.4", + "@storybook/react": "8.3.4", + "@storybook/react-vite": "8.3.4", + "@storybook/test": "8.3.4", + "@storybook/theming": "8.3.4", + "@storybook/types": "8.3.4", + "@storybook/vue3": "8.3.4", + "@storybook/vue3-vite": "8.3.4", "@testing-library/vue": "8.1.0", - "@types/escape-regexp": "0.0.3", + "@types/canvas-confetti": "^1.6.4", "@types/estree": "1.0.6", "@types/matter-js": "0.19.7", "@types/micromatch": "4.0.9", @@ -112,11 +111,11 @@ "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "@vitest/coverage-v8": "1.6.0", - "@vue/runtime-core": "3.5.7", + "@vue/runtime-core": "3.5.11", "acorn": "8.12.1", "cross-env": "7.0.3", - "cypress": "13.14.2", - "eslint-plugin-import": "2.30.0", + "cypress": "13.15.0", + "eslint-plugin-import": "2.31.0", "eslint-plugin-vue": "9.28.0", "fast-glob": "3.3.2", "happy-dom": "10.0.3", @@ -130,7 +129,7 @@ "react-dom": "18.3.1", "seedrandom": "3.0.5", "start-server-and-test": "2.0.8", - "storybook": "8.3.2", + "storybook": "8.3.4", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "vite-plugin-turbosnap": "1.0.3", "vitest": "1.6.0", diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index a28e7c2559..2f0e09fc4b 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -4,112 +4,153 @@ SPDX-License-Identifier: AGPL-3.0-only --> - diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 18e8e7542e..b50a7fea5c 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 9d9661e816..71bd5addfb 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->