Merge pull request #14675 from misskey-dev/develop

Release: 2024.10.0
This commit is contained in:
misskey-release-bot[bot] 2024-10-09 05:17:29 +00:00 committed by GitHub
commit 2518cf36d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
140 changed files with 4095 additions and 1824 deletions

View File

@ -2,6 +2,19 @@
# Misskey configuration # 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 └───────────────────────────────────────────────────── #───┘ URL └─────────────────────────────────────────────────────

View File

@ -59,6 +59,20 @@
# #
# publishTarballInsteadOfProvideRepositoryUrl: true # 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 └───────────────────────────────────────────────────── #───┘ URL └─────────────────────────────────────────────────────

View File

@ -1,5 +1,7 @@
url: 'http://misskey.local' url: 'http://misskey.local'
setupPassword: example_password_please_change_this_or_you_will_get_hacked
# ローカルでテストするときにポートを被らないようにするためデフォルトのものとは変える(以下同じ) # ローカルでテストするときにポートを被らないようにするためデフォルトのものとは変える(以下同じ)
port: 61812 port: 61812

View File

@ -21,6 +21,7 @@ jobs:
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
persist-credentials: false
ref: refs/pull/${{ github.event.pull_request.number }}/merge ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: setup pnpm - name: setup pnpm
@ -57,7 +58,7 @@ jobs:
name: generated-misskey-js name: generated-misskey-js
path: packages/misskey-js/generator/built/autogen 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: get-actual-misskey-js:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -68,6 +69,7 @@ jobs:
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
with: with:
submodules: true submodules: true
persist-credentials: false
ref: refs/pull/${{ github.event.pull_request.number }}/merge ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Upload From Merged - name: Upload From Merged
@ -131,3 +133,7 @@ jobs:
mode: delete mode: delete
message: "Thank you!" message: "Thank you!"
create_if_not_exists: false create_if_not_exists: false
- name: Make failure if changes are detected
if: steps.check-changes.outputs.changes == 'true'
run: exit 1

View File

@ -75,7 +75,7 @@ jobs:
- run: corepack enable - run: corepack enable
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- name: Restore eslint cache - name: Restore eslint cache
uses: actions/cache@v4.0.2 uses: actions/cache@v4.1.0
with: with:
path: ${{ env.eslint-cache-path }} path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}

View File

@ -1,3 +1,35 @@
## 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: ログイン画面の認証フローを改善
- Fix: クライアント上での時間ベースの実績獲得動作が実績獲得後も発動していた問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/657)
### Server
- Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように
- Enhance: 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるように ( #14697 )
- Fix: `admin/abuse-user-reports`エンドポイントのスキーマが間違っていた問題を修正
## 2024.9.0 ## 2024.9.0
### General ### General

View File

@ -23,6 +23,7 @@ describe('Before setup instance', () => {
cy.intercept('POST', '/api/admin/accounts/create').as('signup'); 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-username] input').type('admin');
cy.get('[data-cy-admin-password] input').type('admin1234'); cy.get('[data-cy-admin-password] input').type('admin1234');
cy.get('[data-cy-admin-ok]').click(); cy.get('[data-cy-admin-ok]').click();
@ -119,11 +120,16 @@ describe('After user signup', () => {
it('signin', () => { it('signin', () => {
cy.visitHome(); 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]').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.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.wait('@signin'); cy.wait('@signin');
@ -138,8 +144,9 @@ describe('After user signup', () => {
cy.visitHome(); cy.visitHome();
cy.get('[data-cy-signin]').click(); 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にブラウザの言語指定できる機能が実装され次第英語のみテストするようにする // TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi); cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);

View File

@ -48,16 +48,19 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
cy.request('POST', route, { cy.request('POST', route, {
username: username, username: username,
password: password, password: password,
...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}),
}).its('body').as(username); }).its('body').as(username);
}); });
Cypress.Commands.add('login', (username, password) => { Cypress.Commands.add('login', (username, password) => {
cy.visitHome(); 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]').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.get('[data-cy-signin-password] input').type(`${password}{enter}`);
cy.wait('@signin').as('signedIn'); cy.wait('@signin').as('signedIn');

View File

@ -7,8 +7,8 @@
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../../.storybook/fakes.js'; import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../packages/frontend/.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue'; import MkAbuseReport from './MkAbuseReport.vue';
export const Default = { export const Default = {
render(args) { render(args) {

View File

@ -626,10 +626,7 @@ abuseReported: "أُرسل البلاغ، شكرًا لك"
reporter: "المُبلّغ" reporter: "المُبلّغ"
reporteeOrigin: "أصل البلاغ" reporteeOrigin: "أصل البلاغ"
reporterOrigin: "أصل المُبلّغ" reporterOrigin: "أصل المُبلّغ"
forwardReport: "وجّه البلاغ إلى المثيل البعيد"
forwardReportIsAnonymous: "في المثيل البعيد سيظهر المبلّغ كحساب مجهول."
send: "أرسل" send: "أرسل"
abuseMarkAsResolved: "علّم البلاغ كمحلول"
openInNewTab: "افتح في لسان جديد" openInNewTab: "افتح في لسان جديد"
defaultNavigationBehaviour: "سلوك الملاحة الافتراضي" defaultNavigationBehaviour: "سلوك الملاحة الافتراضي"
editTheseSettingsMayBreakAccount: "تعديل هذه الإعدادات قد يسبب عطبًا لحسابك" editTheseSettingsMayBreakAccount: "تعديل هذه الإعدادات قد يسبب عطبًا لحسابك"
@ -1533,6 +1530,7 @@ _notification:
reaction: "التفاعل" reaction: "التفاعل"
receiveFollowRequest: "طلبات المتابعة" receiveFollowRequest: "طلبات المتابعة"
followRequestAccepted: "طلبات المتابعة المقبولة" followRequestAccepted: "طلبات المتابعة المقبولة"
login: "لِج"
app: "إشعارات التطبيقات المرتبطة" app: "إشعارات التطبيقات المرتبطة"
_actions: _actions:
followBack: "تابعك بالمثل" followBack: "تابعك بالمثل"

View File

@ -624,10 +624,7 @@ abuseReported: "আপনার অভিযোগটি দাখিল কর
reporter: "অভিযোগকারী" reporter: "অভিযোগকারী"
reporteeOrigin: "অভিযোগটির উৎস" reporteeOrigin: "অভিযোগটির উৎস"
reporterOrigin: "অভিযোগকারীর উৎস" reporterOrigin: "অভিযোগকারীর উৎস"
forwardReport: "রিমোট ইন্সত্যান্সে অভিযোগটি পাঠান"
forwardReportIsAnonymous: "আপনার তথ্য রিমোট ইন্সত্যান্সে পাঠানো হবে না এবং একটি বেনামী সিস্টেম অ্যাকাউন্ট হিসাবে প্রদর্শিত হবে।"
send: "পাঠান" send: "পাঠান"
abuseMarkAsResolved: "অভিযোগটিকে সমাধাকৃত হিসাবে চিহ্নিত করুন"
openInNewTab: "নতুন ট্যাবে খুলুন" openInNewTab: "নতুন ট্যাবে খুলুন"
openInSideView: "সাইড ভিউতে খুলুন" openInSideView: "সাইড ভিউতে খুলুন"
defaultNavigationBehaviour: "ডিফল্ট নেভিগেশন" defaultNavigationBehaviour: "ডিফল্ট নেভিগেশন"
@ -1313,6 +1310,7 @@ _notification:
pollEnded: "পোল শেষ" pollEnded: "পোল শেষ"
receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ" receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ"
followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ"
login: "প্রবেশ করুন"
app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি" app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি"
_actions: _actions:
followBack: "ফলো ব্যাক করেছে" followBack: "ফলো ব্যাক করেছে"

View File

@ -8,6 +8,8 @@ search: "Cercar"
notifications: "Notificacions" notifications: "Notificacions"
username: "Nom d'usuari" username: "Nom d'usuari"
password: "Contrasenya" password: "Contrasenya"
initialPasswordForSetup: "Contrasenya inicial per la configuració inicial"
initialPasswordIsIncorrect: "La contrasenya no és correcta."
forgotPassword: "Contrasenya oblidada" forgotPassword: "Contrasenya oblidada"
fetchingAsApObject: "Cercant en el Fediverse..." fetchingAsApObject: "Cercant en el Fediverse..."
ok: "OK" ok: "OK"
@ -236,6 +238,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." 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" 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." 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" muteAndBlock: "Silencia i bloca"
mutedUsers: "Usuaris silenciats" mutedUsers: "Usuaris silenciats"
blockedUsers: "Usuaris bloquejats" blockedUsers: "Usuaris bloquejats"
@ -334,6 +338,7 @@ renameFolder: "Canvia el nom de la carpeta"
deleteFolder: "Elimina la carpeta" deleteFolder: "Elimina la carpeta"
folder: "Carpeta " folder: "Carpeta "
addFile: "Afegeix un fitxer" addFile: "Afegeix un fitxer"
showFile: "Mostrar fitxer"
emptyDrive: "La teva unitat és buida" emptyDrive: "La teva unitat és buida"
emptyFolder: "La carpeta està buida" emptyFolder: "La carpeta està buida"
unableToDelete: "No es pot eliminar" unableToDelete: "No es pot eliminar"
@ -509,6 +514,10 @@ uiLanguage: "Idioma de l'interfície"
aboutX: "Respecte a {x}" aboutX: "Respecte a {x}"
emojiStyle: "Estil d'emoji" emojiStyle: "Estil d'emoji"
native: "Nadiu" native: "Nadiu"
menuStyle: "Estil de menú"
style: "Estil"
drawer: "Calaix"
popup: "Emergent"
showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor" showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor"
showReactionsCount: "Mostra el nombre de reaccions a les publicacions" showReactionsCount: "Mostra el nombre de reaccions a les publicacions"
noHistory: "No hi ha un registre previ" noHistory: "No hi ha un registre previ"
@ -709,10 +718,7 @@ abuseReported: "La teva denúncia s'ha enviat. Moltes gràcies."
reporter: "Denunciant " reporter: "Denunciant "
reporteeOrigin: "Origen de la denúncia " reporteeOrigin: "Origen de la denúncia "
reporterOrigin: "Origen del denunciant" reporterOrigin: "Origen del denunciant"
forwardReport: "Transferir la denúncia a una instància remota"
forwardReportIsAnonymous: "En lloc del teu compte, es farà servir un compte anònim com a denunciant al servidor remot."
send: "Envia" send: "Envia"
abuseMarkAsResolved: "Marca la denúncia com a resolta"
openInNewTab: "Obre a una pestanya nova" openInNewTab: "Obre a una pestanya nova"
openInSideView: "Obre a una vista lateral" openInSideView: "Obre a una vista lateral"
defaultNavigationBehaviour: "Navegació per defecte" defaultNavigationBehaviour: "Navegació per defecte"
@ -914,6 +920,7 @@ followersVisibility: "Visibilitat dels seguidors"
continueThread: "Veure la continuació del fil" continueThread: "Veure la continuació del fil"
deleteAccountConfirm: "Això eliminarà el teu compte irreversiblement. Procedir?" deleteAccountConfirm: "Això eliminarà el teu compte irreversiblement. Procedir?"
incorrectPassword: "Contrasenya incorrecta." incorrectPassword: "Contrasenya incorrecta."
incorrectTotp: "La contrasenya no és correcta, o ha caducat."
voteConfirm: "Confirma el teu vot \"{choice}\"" voteConfirm: "Confirma el teu vot \"{choice}\""
hide: "Amagar" hide: "Amagar"
useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calaix al mòbil " useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calaix al mòbil "
@ -1268,6 +1275,15 @@ fromX: "De {x}"
genEmbedCode: "Obtenir el codi per incrustar" genEmbedCode: "Obtenir el codi per incrustar"
noteOfThisUser: "Notes d'aquest usuari" noteOfThisUser: "Notes d'aquest usuari"
clipNoteLimitExceeded: "No es poden afegir més notes a aquest clip." 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: _delivery:
status: "Estat d'entrega " status: "Estat d'entrega "
stop: "Suspés" stop: "Suspés"
@ -2235,6 +2251,9 @@ _profile:
changeBanner: "Canviar el bàner " 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." 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." 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: _exportOrImport:
allNotes: "Totes les publicacions" allNotes: "Totes les publicacions"
favoritedNotes: "Notes preferides" favoritedNotes: "Notes preferides"
@ -2373,6 +2392,7 @@ _notification:
renotedBySomeUsers: "L'han impulsat {n} usuaris" renotedBySomeUsers: "L'han impulsat {n} usuaris"
followedBySomeUsers: "Et segueixen {n} usuaris" followedBySomeUsers: "Et segueixen {n} usuaris"
flushNotification: "Netejar notificacions" flushNotification: "Netejar notificacions"
exportOfXCompleted: "Completada l'exportació de {n}"
_types: _types:
all: "Tots" all: "Tots"
note: "Notes noves" note: "Notes noves"
@ -2387,6 +2407,9 @@ _notification:
followRequestAccepted: "Petició de seguiment acceptada" followRequestAccepted: "Petició de seguiment acceptada"
roleAssigned: "Rol donat" roleAssigned: "Rol donat"
achievementEarned: "Assoliment desbloquejat" achievementEarned: "Assoliment desbloquejat"
exportCompleted: "Exportació completada"
login: "Iniciar sessió"
test: "Prova la notificació"
app: "Notificacions d'aplicacions" app: "Notificacions d'aplicacions"
_actions: _actions:
followBack: "t'ha seguit també" followBack: "t'ha seguit també"

View File

@ -657,10 +657,7 @@ abuseReported: "Nahlášení bylo odesláno. Děkujeme převelice."
reporter: "Nahlásil" reporter: "Nahlásil"
reporteeOrigin: "Původ nahlášení" reporteeOrigin: "Původ nahlášení"
reporterOrigin: "Původ nahlasovače" reporterOrigin: "Původ nahlasovače"
forwardReport: "Přeposlat nahlášení do vzdálené instance"
forwardReportIsAnonymous: "Místo vašeho účtu se ve vzdálené instanci zobrazí anonymní systémový účet jako nahlašovač."
send: "Odeslat" send: "Odeslat"
abuseMarkAsResolved: "Označit nahlášení jako vyřešené"
openInNewTab: "Otevřít v nové kartě" openInNewTab: "Otevřít v nové kartě"
openInSideView: "Otevřít v bočním panelu" openInSideView: "Otevřít v bočním panelu"
defaultNavigationBehaviour: "Výchozí chování navigace" defaultNavigationBehaviour: "Výchozí chování navigace"
@ -1962,6 +1959,7 @@ _notification:
receiveFollowRequest: "Obdržené žádosti o sledování" receiveFollowRequest: "Obdržené žádosti o sledování"
followRequestAccepted: "Přijaté žádosti o sledování" followRequestAccepted: "Přijaté žádosti o sledování"
achievementEarned: "Úspěch odemčen" achievementEarned: "Úspěch odemčen"
login: "Přihlásit se"
app: "Oznámení z propojených aplikací" app: "Oznámení z propojených aplikací"
_actions: _actions:
followBack: "vás začal sledovat zpět" followBack: "vás začal sledovat zpět"

View File

@ -686,10 +686,7 @@ abuseReported: "Deine Meldung wurde versendet. Vielen Dank."
reporter: "Melder" reporter: "Melder"
reporteeOrigin: "Herkunft des Gemeldeten" reporteeOrigin: "Herkunft des Gemeldeten"
reporterOrigin: "Herkunft des Meldenden" reporterOrigin: "Herkunft des Meldenden"
forwardReport: "Meldung an fremde Instanz weiterleiten"
forwardReportIsAnonymous: "Anstatt deines Benutzerkontos wird bei der fremden Instanz ein anonymes Systemkonto als Melder angezeigt."
send: "Senden" send: "Senden"
abuseMarkAsResolved: "Meldung als gelöst markieren"
openInNewTab: "In neuem Tab öffnen" openInNewTab: "In neuem Tab öffnen"
openInSideView: "In Seitenansicht öffnen" openInSideView: "In Seitenansicht öffnen"
defaultNavigationBehaviour: "Standardnavigationsverhalten" defaultNavigationBehaviour: "Standardnavigationsverhalten"
@ -2141,6 +2138,7 @@ _notification:
receiveFollowRequest: "Erhaltene Follow-Anfragen" receiveFollowRequest: "Erhaltene Follow-Anfragen"
followRequestAccepted: "Akzeptierte Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen"
achievementEarned: "Errungenschaft freigeschaltet" achievementEarned: "Errungenschaft freigeschaltet"
login: "Anmelden"
app: "Benachrichtigungen von Apps" app: "Benachrichtigungen von Apps"
_actions: _actions:
followBack: "folgt dir nun auch" followBack: "folgt dir nun auch"

View File

@ -378,6 +378,7 @@ _notification:
renote: "Κοινοποίηση σημειώματος" renote: "Κοινοποίηση σημειώματος"
quote: "Παράθεση" quote: "Παράθεση"
reaction: "Αντιδράσεις" reaction: "Αντιδράσεις"
login: "Σύνδεση"
_actions: _actions:
reply: "Απάντηση" reply: "Απάντηση"
renote: "Κοινοποίηση σημειώματος" renote: "Κοινοποίηση σημειώματος"

View File

@ -8,6 +8,9 @@ search: "Search"
notifications: "Notifications" notifications: "Notifications"
username: "Username" username: "Username"
password: "Password" 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" forgotPassword: "Forgot password"
fetchingAsApObject: "Fetching from the Fediverse..." fetchingAsApObject: "Fetching from the Fediverse..."
ok: "OK" ok: "OK"
@ -109,7 +112,7 @@ enterEmoji: "Enter an emoji"
renote: "Renote" renote: "Renote"
unrenote: "Remove renote" unrenote: "Remove renote"
renoted: "Renoted." renoted: "Renoted."
renotedToX: "Renote to {name}." renotedToX: "Renoted to {name}."
cantRenote: "This post can't be renoted." cantRenote: "This post can't be renoted."
cantReRenote: "A renote can't be renoted." cantReRenote: "A renote can't be renoted."
quote: "Quote" quote: "Quote"
@ -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." 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" 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." 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" muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users" mutedUsers: "Muted users"
blockedUsers: "Blocked users" blockedUsers: "Blocked users"
@ -334,6 +339,7 @@ renameFolder: "Rename this folder"
deleteFolder: "Delete this folder" deleteFolder: "Delete this folder"
folder: "Folder" folder: "Folder"
addFile: "Add a file" addFile: "Add a file"
showFile: "Show files"
emptyDrive: "Your Drive is empty" emptyDrive: "Your Drive is empty"
emptyFolder: "This folder is empty" emptyFolder: "This folder is empty"
unableToDelete: "Unable to delete" unableToDelete: "Unable to delete"
@ -448,6 +454,7 @@ totpDescription: "Use an authenticator app to enter one-time passwords"
moderator: "Moderator" moderator: "Moderator"
moderation: "Moderation" moderation: "Moderation"
moderationNote: "Moderation note" moderationNote: "Moderation note"
moderationNoteDescription: "You can fill in notes that will be shared only among moderators."
addModerationNote: "Add moderation note" addModerationNote: "Add moderation note"
moderationLogs: "Moderation logs" moderationLogs: "Moderation logs"
nUsersMentioned: "Mentioned by {n} users" nUsersMentioned: "Mentioned by {n} users"
@ -509,6 +516,10 @@ uiLanguage: "User interface language"
aboutX: "About {x}" aboutX: "About {x}"
emojiStyle: "Emoji style" emojiStyle: "Emoji style"
native: "Native" native: "Native"
menuStyle: "Menu style"
style: "Style"
drawer: "Drawer"
popup: "Pop up"
showNoteActionsOnlyHover: "Only show note actions on hover" showNoteActionsOnlyHover: "Only show note actions on hover"
showReactionsCount: "See the number of reactions in notes" showReactionsCount: "See the number of reactions in notes"
noHistory: "No history available" noHistory: "No history available"
@ -591,6 +602,8 @@ ascendingOrder: "Ascending"
descendingOrder: "Descending" descendingOrder: "Descending"
scratchpad: "Scratchpad" 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." 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" output: "Output"
script: "Script" script: "Script"
disablePagesScript: "Disable AiScript on Pages" disablePagesScript: "Disable AiScript on Pages"
@ -707,10 +720,7 @@ abuseReported: "Your report has been sent. Thank you very much."
reporter: "Reporter" reporter: "Reporter"
reporteeOrigin: "Reportee Origin" reporteeOrigin: "Reportee Origin"
reporterOrigin: "Reporter Origin" reporterOrigin: "Reporter Origin"
forwardReport: "Forward report to remote instance"
forwardReportIsAnonymous: "Instead of your account, an anonymous system account will be displayed as reporter at the remote instance."
send: "Send" send: "Send"
abuseMarkAsResolved: "Mark report as resolved"
openInNewTab: "Open in new tab" openInNewTab: "Open in new tab"
openInSideView: "Open in side view" openInSideView: "Open in side view"
defaultNavigationBehaviour: "Default navigation behavior" defaultNavigationBehaviour: "Default navigation behavior"
@ -912,6 +922,7 @@ followersVisibility: "Visibility of followers"
continueThread: "View thread continuation" continueThread: "View thread continuation"
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
incorrectPassword: "Incorrect password." incorrectPassword: "Incorrect password."
incorrectTotp: "The one-time password is incorrect or has expired."
voteConfirm: "Confirm your vote for \"{choice}\"?" voteConfirm: "Confirm your vote for \"{choice}\"?"
hide: "Hide" hide: "Hide"
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile" useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
@ -1125,7 +1136,7 @@ options: "Options"
specifyUser: "Specific user" specifyUser: "Specific user"
lookupConfirm: "Do you want to look up?" lookupConfirm: "Do you want to look up?"
openTagPageConfirm: "Do you want to open a hashtag page?" openTagPageConfirm: "Do you want to open a hashtag page?"
specifyHost: "Specify a host" specifyHost: "Specific host"
failedToPreviewUrl: "Could not preview" failedToPreviewUrl: "Could not preview"
update: "Update" update: "Update"
rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction" rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
@ -1266,6 +1277,23 @@ fromX: "From {x}"
genEmbedCode: "Generate embed code" genEmbedCode: "Generate embed code"
noteOfThisUser: "Notes by this user" noteOfThisUser: "Notes by this user"
clipNoteLimitExceeded: "No more notes can be added to this clip." 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"
target: "Target"
_abuseUserReport:
forward: "Forward"
forwardDescription: "Forward the report to a remote server as an anonymous system account."
resolve: "Resolve"
accept: "Accept"
reject: "Reject"
resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative."
_delivery: _delivery:
status: "Delivery status" status: "Delivery status"
stop: "Suspended" stop: "Suspended"
@ -1400,6 +1428,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." 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" 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." 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" inquiryUrl: "Inquiry URL"
inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information."
_accountMigration: _accountMigration:
@ -1718,7 +1747,7 @@ _role:
canManageAvatarDecorations: "Manage avatar decorations" canManageAvatarDecorations: "Manage avatar decorations"
driveCapacity: "Drive capacity" driveCapacity: "Drive capacity"
alwaysMarkNsfw: "Always mark files as NSFW" alwaysMarkNsfw: "Always mark files as NSFW"
canUpdateBioMedia: "Allow to edit an icon or a banner image" canUpdateBioMedia: "Can edit an icon or a banner image"
pinMax: "Maximum number of pinned notes" pinMax: "Maximum number of pinned notes"
antennaMax: "Maximum number of antennas" antennaMax: "Maximum number of antennas"
wordMuteMax: "Maximum number of characters allowed in word mutes" wordMuteMax: "Maximum number of characters allowed in word mutes"
@ -1733,6 +1762,11 @@ _role:
canSearchNotes: "Usage of note search" canSearchNotes: "Usage of note search"
canUseTranslator: "Translator usage" canUseTranslator: "Translator usage"
avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" 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: _condition:
roleAssignedTo: "Assigned to manual roles" roleAssignedTo: "Assigned to manual roles"
isLocal: "Local user" isLocal: "Local user"
@ -2227,6 +2261,9 @@ _profile:
changeBanner: "Change banner" 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." 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." 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: _exportOrImport:
allNotes: "All notes" allNotes: "All notes"
favoritedNotes: "Favorite notes" favoritedNotes: "Favorite notes"
@ -2365,6 +2402,8 @@ _notification:
renotedBySomeUsers: "Renote from {n} users" renotedBySomeUsers: "Renote from {n} users"
followedBySomeUsers: "Followed by {n} users" followedBySomeUsers: "Followed by {n} users"
flushNotification: "Clear notifications" flushNotification: "Clear notifications"
exportOfXCompleted: "Export of {x} has been completed"
login: "Someone logged in"
_types: _types:
all: "All" all: "All"
note: "New notes" note: "New notes"
@ -2379,6 +2418,9 @@ _notification:
followRequestAccepted: "Accepted follow requests" followRequestAccepted: "Accepted follow requests"
roleAssigned: "Role given" roleAssigned: "Role given"
achievementEarned: "Achievement unlocked" achievementEarned: "Achievement unlocked"
exportCompleted: "The export has been completed"
login: "Sign In"
test: "Notification test"
app: "Notifications from linked apps" app: "Notifications from linked apps"
_actions: _actions:
followBack: "followed you back" followBack: "followed you back"
@ -2441,21 +2483,22 @@ _webhookSettings:
reaction: "When receiving a reaction" reaction: "When receiving a reaction"
mention: "When being mentioned" mention: "When being mentioned"
_systemEvents: _systemEvents:
abuseReport: "When received a new abuse report" abuseReport: "When received a new report"
abuseReportResolved: "When resolved abuse report" abuseReportResolved: "When resolved report"
userCreated: "When user is created" userCreated: "When user is created"
deleteConfirm: "Are you sure you want to delete the Webhook?" 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: _abuseReport:
_notificationRecipient: _notificationRecipient:
createRecipient: "Add a recipient for abuse reports" createRecipient: "Add a recipient for reports"
modifyRecipient: "Edit a recipient for abuse reports" modifyRecipient: "Edit a recipient for reports"
recipientType: "Notification type" recipientType: "Notification type"
_recipientType: _recipientType:
mail: "Email" mail: "Email"
webhook: "Webhook" webhook: "Webhook"
_captions: _captions:
mail: "Send the email to moderators' email addresses when you receive abuse." mail: "Send the email to moderators' email addresses when you receive reports."
webhook: "Send a notification to SystemWebhook when you receive or resolve abuse." webhook: "Send a notification to System Webhook when you receive or resolve reports."
keywords: "Keywords" keywords: "Keywords"
notifiedUser: "Users to notify" notifiedUser: "Users to notify"
notifiedWebhook: "Webhook to use" notifiedWebhook: "Webhook to use"
@ -2488,6 +2531,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "File marked as sensitive" markSensitiveDriveFile: "File marked as sensitive"
unmarkSensitiveDriveFile: "File unmarked as sensitive" unmarkSensitiveDriveFile: "File unmarked as sensitive"
resolveAbuseReport: "Report resolved" resolveAbuseReport: "Report resolved"
forwardAbuseReport: "Report forwarded"
updateAbuseReportNote: "Moderation note of a report updated"
createInvitation: "Invite generated" createInvitation: "Invite generated"
createAd: "Ad created" createAd: "Ad created"
deleteAd: "Ad deleted" deleteAd: "Ad deleted"
@ -2495,18 +2540,18 @@ _moderationLogTypes:
createAvatarDecoration: "Avatar decoration created" createAvatarDecoration: "Avatar decoration created"
updateAvatarDecoration: "Avatar decoration updated" updateAvatarDecoration: "Avatar decoration updated"
deleteAvatarDecoration: "Avatar decoration deleted" deleteAvatarDecoration: "Avatar decoration deleted"
unsetUserAvatar: "Unset this user's avatar" unsetUserAvatar: "User avatar unset"
unsetUserBanner: "Unset this user's banner" unsetUserBanner: "User banner unset"
createSystemWebhook: "Create SystemWebhook" createSystemWebhook: "System Webhook created"
updateSystemWebhook: "Update SystemWebhook" updateSystemWebhook: "System Webhook updated"
deleteSystemWebhook: "Delete SystemWebhook" deleteSystemWebhook: "System Webhook deleted"
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports" createAbuseReportNotificationRecipient: "Recipient for reports created"
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports" updateAbuseReportNotificationRecipient: "Recipient for reports updated"
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports" deleteAbuseReportNotificationRecipient: "Recipient for reports deleted"
deleteAccount: "Delete the account" deleteAccount: "Account deleted"
deletePage: "Delete the page" deletePage: "Page deleted"
deleteFlash: "Delete Play" deleteFlash: "Play deleted"
deleteGalleryPost: "Delete the gallery post" deleteGalleryPost: "Gallery post deleted"
_fileViewer: _fileViewer:
title: "File details" title: "File details"
type: "File type" type: "File type"

View File

@ -700,10 +700,7 @@ abuseReported: "Se ha enviado el reporte. Muchas gracias."
reporter: "Reportador" reporter: "Reportador"
reporteeOrigin: "Reportar a" reporteeOrigin: "Reportar a"
reporterOrigin: "Origen del reporte" reporterOrigin: "Origen del reporte"
forwardReport: "Transferir un informe a una instancia remota"
forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá como una cuenta anónima del sistema"
send: "Enviar" send: "Enviar"
abuseMarkAsResolved: "Marcar reporte como resuelto"
openInNewTab: "Abrir en una Nueva Pestaña" openInNewTab: "Abrir en una Nueva Pestaña"
openInSideView: "Abrir en una vista al costado" openInSideView: "Abrir en una vista al costado"
defaultNavigationBehaviour: "Navegación por defecto" defaultNavigationBehaviour: "Navegación por defecto"
@ -2343,6 +2340,7 @@ _notification:
followRequestAccepted: "El seguimiento fue aceptado" followRequestAccepted: "El seguimiento fue aceptado"
roleAssigned: "Rol asignado" roleAssigned: "Rol asignado"
achievementEarned: "Logro desbloqueado" achievementEarned: "Logro desbloqueado"
login: "Iniciar sesión"
app: "Notificaciones desde aplicaciones" app: "Notificaciones desde aplicaciones"
_actions: _actions:
followBack: "Te sigue de vuelta" followBack: "Te sigue de vuelta"

View File

@ -691,10 +691,7 @@ abuseReported: "Le rapport est envoyé. Merci."
reporter: "Signalé par" reporter: "Signalé par"
reporteeOrigin: "Origine du signalement" reporteeOrigin: "Origine du signalement"
reporterOrigin: "Signalé par" reporterOrigin: "Signalé par"
forwardReport: "Transférer le signalement à linstance distante"
forwardReportIsAnonymous: "L'instance distante ne sera pas en mesure de voir vos informations et apparaîtra comme un compte anonyme du système."
send: "Envoyer" send: "Envoyer"
abuseMarkAsResolved: "Marquer le signalement comme résolu"
openInNewTab: "Ouvrir dans un nouvel onglet" openInNewTab: "Ouvrir dans un nouvel onglet"
openInSideView: "Ouvrir en vue latérale" openInSideView: "Ouvrir en vue latérale"
defaultNavigationBehaviour: "Navigation par défaut" defaultNavigationBehaviour: "Navigation par défaut"
@ -2037,6 +2034,7 @@ _notification:
followRequestAccepted: "Demande d'abonnement acceptée" followRequestAccepted: "Demande d'abonnement acceptée"
roleAssigned: "Rôle reçu" roleAssigned: "Rôle reçu"
achievementEarned: "Déverrouillage d'accomplissement" achievementEarned: "Déverrouillage d'accomplissement"
login: "Se connecter"
app: "Notifications provenant des apps" app: "Notifications provenant des apps"
_actions: _actions:
followBack: "Suivre" followBack: "Suivre"

View File

@ -96,6 +96,7 @@ _notification:
renote: "Renote" renote: "Renote"
quote: "Idézet" quote: "Idézet"
reaction: "Reakciók" reaction: "Reakciók"
login: "Bejelentkezés"
_actions: _actions:
renote: "Renote" renote: "Renote"
_deck: _deck:

View File

@ -702,10 +702,7 @@ abuseReported: "Laporan kamu telah dikirimkan. Terima kasih."
reporter: "Pelapor" reporter: "Pelapor"
reporteeOrigin: "Yang dilaporkan" reporteeOrigin: "Yang dilaporkan"
reporterOrigin: "Pelapor" reporterOrigin: "Pelapor"
forwardReport: "Teruskan laporan ke instansi luar"
forwardReportIsAnonymous: "Untuk melindungi privasi akun kamu, akun anonim dari sistem akan digunakan sebagai pelapor pada instansi luar."
send: "Kirim" send: "Kirim"
abuseMarkAsResolved: "Tandai laporan sebagai selesai"
openInNewTab: "Buka di tab baru" openInNewTab: "Buka di tab baru"
openInSideView: "Buka di tampilan samping" openInSideView: "Buka di tampilan samping"
defaultNavigationBehaviour: "Navigasi bawaan" defaultNavigationBehaviour: "Navigasi bawaan"
@ -2354,6 +2351,7 @@ _notification:
followRequestAccepted: "Permintaan mengikuti disetujui" followRequestAccepted: "Permintaan mengikuti disetujui"
roleAssigned: "Peran Diberikan" roleAssigned: "Peran Diberikan"
achievementEarned: "Pencapaian didapatkan" achievementEarned: "Pencapaian didapatkan"
login: "Masuk"
app: "Notifikasi dari aplikasi tertaut" app: "Notifikasi dari aplikasi tertaut"
_actions: _actions:
followBack: "Ikuti Kembali" followBack: "Ikuti Kembali"

85
locales/index.d.ts vendored
View File

@ -48,6 +48,20 @@ export interface Locale extends ILocale {
* *
*/ */
"password": string; "password": string;
/**
*
*/
"initialPasswordForSetup": string;
/**
*
*/
"initialPasswordIsIncorrect": string;
/**
* Misskeyを自分でインストールした場合は使
* Misskeyのホスティングサービスなどを使用している場合は使
*
*/
"initialPasswordForSetupDescription": string;
/** /**
* *
*/ */
@ -1820,6 +1834,10 @@ export interface Locale extends ILocale {
* *
*/ */
"moderationNote": string; "moderationNote": string;
/**
*
*/
"moderationNoteDescription": string;
/** /**
* *
*/ */
@ -2880,22 +2898,10 @@ export interface Locale extends ILocale {
* *
*/ */
"reporterOrigin": string; "reporterOrigin": string;
/**
*
*/
"forwardReport": string;
/**
*
*/
"forwardReportIsAnonymous": string;
/** /**
* *
*/ */
"send": string; "send": string;
/**
*
*/
"abuseMarkAsResolved": string;
/** /**
* *
*/ */
@ -3700,6 +3706,10 @@ export interface Locale extends ILocale {
* *
*/ */
"incorrectPassword": string; "incorrectPassword": string;
/**
*
*/
"incorrectTotp": string;
/** /**
* {choice} * {choice}
*/ */
@ -5148,6 +5158,41 @@ export interface Locale extends ILocale {
* *
*/ */
"passkeyVerificationSucceededButPasswordlessLoginDisabled": string; "passkeyVerificationSucceededButPasswordlessLoginDisabled": string;
/**
*
*/
"messageToFollower": string;
/**
*
*/
"target": string;
"_abuseUserReport": {
/**
*
*/
"forward": string;
/**
*
*/
"forwardDescription": string;
/**
*
*/
"resolve": string;
/**
*
*/
"accept": string;
/**
*
*/
"reject": string;
/**
*
*
*/
"resolveTutorial": string;
};
"_delivery": { "_delivery": {
/** /**
* *
@ -9281,6 +9326,10 @@ export interface Locale extends ILocale {
* {x} * {x}
*/ */
"exportOfXCompleted": ParameterizedString<"x">; "exportOfXCompleted": ParameterizedString<"x">;
/**
*
*/
"login": string;
"_types": { "_types": {
/** /**
* *
@ -9338,6 +9387,10 @@ export interface Locale extends ILocale {
* *
*/ */
"exportCompleted": string; "exportCompleted": string;
/**
*
*/
"login": string;
/** /**
* *
*/ */
@ -9755,6 +9808,14 @@ export interface Locale extends ILocale {
* *
*/ */
"resolveAbuseReport": string; "resolveAbuseReport": string;
/**
*
*/
"forwardAbuseReport": string;
/**
*
*/
"updateAbuseReportNote": string;
/** /**
* *
*/ */

View File

@ -8,6 +8,9 @@ search: "Cerca"
notifications: "Notifiche" notifications: "Notifiche"
username: "Nome utente" username: "Nome utente"
password: "Password" password: "Password"
initialPasswordForSetup: "Password iniziale, per avviare le impostazioni"
initialPasswordIsIncorrect: "Password iniziale, sbagliata."
initialPasswordForSetupDescription: "Se hai installato Misskey di persona, usa la password che hai indicato nel file di configurazione.\nSe stai utilizzando un servizio di hosting Misskey, usa la password fornita dal gestore.\nSe non hai una password preimpostata, lascia il campo vuoto e continua."
forgotPassword: "Hai dimenticato la password?" forgotPassword: "Hai dimenticato la password?"
fetchingAsApObject: "Recuperando dal Fediverso..." fetchingAsApObject: "Recuperando dal Fediverso..."
ok: "OK" ok: "OK"
@ -236,6 +239,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." 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" 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." 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" muteAndBlock: "Silenziare e bloccare"
mutedUsers: "Profili silenziati" mutedUsers: "Profili silenziati"
blockedUsers: "Profili bloccati" blockedUsers: "Profili bloccati"
@ -714,10 +719,7 @@ abuseReported: "La segnalazione è stata inviata. Grazie."
reporter: "il corrispondente" reporter: "il corrispondente"
reporteeOrigin: "Segnalazione a" reporteeOrigin: "Segnalazione a"
reporterOrigin: "Segnalazione da" reporterOrigin: "Segnalazione da"
forwardReport: "Inoltro di un report a un'istanza remota."
forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo."
send: "Inviare" send: "Inviare"
abuseMarkAsResolved: "Risolvi segnalazione"
openInNewTab: "Apri in una nuova scheda" openInNewTab: "Apri in una nuova scheda"
openInSideView: "Apri in vista laterale" openInSideView: "Apri in vista laterale"
defaultNavigationBehaviour: "Navigazione preimpostata" defaultNavigationBehaviour: "Navigazione preimpostata"
@ -919,6 +921,7 @@ followersVisibility: "Visibilità dei profili che ti seguono"
continueThread: "Altre conversazioni" continueThread: "Altre conversazioni"
deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
incorrectPassword: "La password è errata." incorrectPassword: "La password è errata."
incorrectTotp: "Il codice OTP è sbagliato, oppure scaduto."
voteConfirm: "Votare per「{choice}」?" voteConfirm: "Votare per「{choice}」?"
hide: "Nascondere" hide: "Nascondere"
useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile" useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
@ -1281,6 +1284,7 @@ signinWithPasskey: "Accedi con passkey"
unknownWebAuthnKey: "Questa è una passkey sconosciuta." unknownWebAuthnKey: "Questa è una passkey sconosciuta."
passkeyVerificationFailed: "La verifica della passkey non è riuscita." passkeyVerificationFailed: "La verifica della passkey non è riuscita."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato." passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato."
messageToFollower: "Messaggio ai follower"
_delivery: _delivery:
status: "Stato della consegna" status: "Stato della consegna"
stop: "Sospensione" stop: "Sospensione"
@ -2248,6 +2252,9 @@ _profile:
changeBanner: "Cambia intestazione" 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." 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." 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: _exportOrImport:
allNotes: "Tutte le note" allNotes: "Tutte le note"
favoritedNotes: "Note preferite" favoritedNotes: "Note preferite"
@ -2387,6 +2394,7 @@ _notification:
followedBySomeUsers: "{n} follower" followedBySomeUsers: "{n} follower"
flushNotification: "Azzera le notifiche" flushNotification: "Azzera le notifiche"
exportOfXCompleted: "Abbiamo completato l'esportazione di {x}" exportOfXCompleted: "Abbiamo completato l'esportazione di {x}"
login: "Autenticazione avvenuta"
_types: _types:
all: "Tutto" all: "Tutto"
note: "Nuove Note" note: "Nuove Note"
@ -2402,6 +2410,7 @@ _notification:
roleAssigned: "Ruolo concesso" roleAssigned: "Ruolo concesso"
achievementEarned: "Risultato raggiunto" achievementEarned: "Risultato raggiunto"
exportCompleted: "Esportazione completata" exportCompleted: "Esportazione completata"
login: "Accedi"
test: "Prova la notifica" test: "Prova la notifica"
app: "Notifiche da applicazioni" app: "Notifiche da applicazioni"
_actions: _actions:

View File

@ -8,6 +8,9 @@ search: "検索"
notifications: "通知" notifications: "通知"
username: "ユーザー名" username: "ユーザー名"
password: "パスワード" password: "パスワード"
initialPasswordForSetup: "初期設定開始用パスワード"
initialPasswordIsIncorrect: "初期設定開始用のパスワードが違います。"
initialPasswordForSetupDescription: "Misskeyを自分でインストールした場合は、設定ファイルに入力したパスワードを使用してください。\nMisskeyのホスティングサービスなどを使用している場合は、提供されたパスワードを使用してください。\nパスワードを設定していない場合は、空欄にしたまま続行してください。"
forgotPassword: "パスワードを忘れた" forgotPassword: "パスワードを忘れた"
fetchingAsApObject: "連合に照会中" fetchingAsApObject: "連合に照会中"
ok: "OK" ok: "OK"
@ -451,6 +454,7 @@ totpDescription: "認証アプリを使ってワンタイムパスワードを
moderator: "モデレーター" moderator: "モデレーター"
moderation: "モデレーション" moderation: "モデレーション"
moderationNote: "モデレーションノート" moderationNote: "モデレーションノート"
moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。"
addModerationNote: "モデレーションノートを追加する" addModerationNote: "モデレーションノートを追加する"
moderationLogs: "モデログ" moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿" nUsersMentioned: "{n}人が投稿"
@ -716,10 +720,7 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ
reporter: "通報者" reporter: "通報者"
reporteeOrigin: "通報先" reporteeOrigin: "通報先"
reporterOrigin: "通報元" reporterOrigin: "通報元"
forwardReport: "リモートサーバーに通報を転送する"
forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
send: "送信" send: "送信"
abuseMarkAsResolved: "対応済みにする"
openInNewTab: "新しいタブで開く" openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く" openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション" defaultNavigationBehaviour: "デフォルトのナビゲーション"
@ -921,6 +922,7 @@ followersVisibility: "フォロワーの公開範囲"
continueThread: "さらにスレッドを見る" continueThread: "さらにスレッドを見る"
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
incorrectPassword: "パスワードが間違っています。" incorrectPassword: "パスワードが間違っています。"
incorrectTotp: "ワンタイムパスワードが間違っているか、期限切れになっています。"
voteConfirm: "「{choice}」に投票しますか?" voteConfirm: "「{choice}」に投票しますか?"
hide: "隠す" hide: "隠す"
useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示" useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示"
@ -1283,6 +1285,16 @@ signinWithPasskey: "パスキーでログイン"
unknownWebAuthnKey: "登録されていないパスキーです。" unknownWebAuthnKey: "登録されていないパスキーです。"
passkeyVerificationFailed: "パスキーの検証に失敗しました。" passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ"
target: "対象"
_abuseUserReport:
forward: "転送"
forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。"
resolve: "解決"
accept: "是認"
reject: "否認"
resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。"
_delivery: _delivery:
status: "配信状態" status: "配信状態"
@ -2450,6 +2462,7 @@ _notification:
followedBySomeUsers: "{n}人にフォローされました" followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"
exportOfXCompleted: "{x}のエクスポートが完了しました" exportOfXCompleted: "{x}のエクスポートが完了しました"
login: "ログインがありました"
_types: _types:
all: "すべて" all: "すべて"
@ -2466,6 +2479,7 @@ _notification:
roleAssigned: "ロールが付与された" roleAssigned: "ロールが付与された"
achievementEarned: "実績の獲得" achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した" exportCompleted: "エクスポートが完了した"
login: "ログイン"
test: "通知のテスト" test: "通知のテスト"
app: "連携アプリからの通知" app: "連携アプリからの通知"
@ -2586,6 +2600,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "ファイルをセンシティブ付与" markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決" resolveAbuseReport: "通報を解決"
forwardAbuseReport: "通報を転送"
updateAbuseReportNote: "通報のモデレーションノート更新"
createInvitation: "招待コードを作成" createInvitation: "招待コードを作成"
createAd: "広告を作成" createAd: "広告を作成"
deleteAd: "広告を削除" deleteAd: "広告を削除"

View File

@ -707,10 +707,7 @@ abuseReported: "無事内容が送信されたみたいやで。おおきに〜
reporter: "通報者" reporter: "通報者"
reporteeOrigin: "通報先" reporteeOrigin: "通報先"
reporterOrigin: "通報元" reporterOrigin: "通報元"
forwardReport: "リモートサーバーに通報を転送するで"
forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
send: "送信" send: "送信"
abuseMarkAsResolved: "対応したで"
openInNewTab: "新しいタブで開く" openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く" openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション" defaultNavigationBehaviour: "デフォルトのナビゲーション"
@ -2374,6 +2371,7 @@ _notification:
followRequestAccepted: "フォローが受理されたで" followRequestAccepted: "フォローが受理されたで"
roleAssigned: "ロールが付与された" roleAssigned: "ロールが付与された"
achievementEarned: "実績の獲得" achievementEarned: "実績の獲得"
login: "ログイン"
app: "連携アプリからの通知や" app: "連携アプリからの通知や"
_actions: _actions:
followBack: "フォローバック" followBack: "フォローバック"

View File

@ -77,6 +77,8 @@ _profile:
username: "ಬಳಕೆಹೆಸರು" username: "ಬಳಕೆಹೆಸರು"
_notification: _notification:
youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು" youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು"
_types:
login: "ಪ್ರವೇಶ"
_actions: _actions:
reply: "ಉತ್ತರಿಸು" reply: "ಉತ್ತರಿಸು"
_deck: _deck:

View File

@ -601,8 +601,6 @@ reportAbuseOf: "{name}님얼 신고하기"
reporter: "신고한 사람" reporter: "신고한 사람"
reporteeOrigin: "신고덴 사람" reporteeOrigin: "신고덴 사람"
reporterOrigin: "신고한 곳" reporterOrigin: "신고한 곳"
forwardReport: "웬겍 서버에 신고 보내기"
forwardReportIsAnonymous: "웬겍 서버서는 나으 정보럴 몬 보고 익멩으 시스템 게정어로 보입니다."
waitingFor: "{x}(얼)럴 지달리고 잇십니다" waitingFor: "{x}(얼)럴 지달리고 잇십니다"
random: "무작이" random: "무작이"
system: "시스템" system: "시스템"
@ -813,6 +811,7 @@ _notification:
mention: "멘션" mention: "멘션"
quote: "따오기" quote: "따오기"
reaction: "반엉" reaction: "반엉"
login: "로그인"
_actions: _actions:
reply: "답하기" reply: "답하기"
_deck: _deck:

View File

@ -8,6 +8,9 @@ search: "검색"
notifications: "알림" notifications: "알림"
username: "유저명" username: "유저명"
password: "비밀번호" password: "비밀번호"
initialPasswordForSetup: "초기 설정용 비밀번호"
initialPasswordIsIncorrect: "초기 설정용 비밀번호가 올바르지 않습니다."
initialPasswordForSetupDescription: "Misskey를 직접 설치하는 경우, 설정 파일에 입력해둔 비밀번호를 사용하세요.\nMisskey 설치를 도와주는 호스팅 서비스 등을 사용하는 경우, 서비스 제공자로부터 받은 비밀번호를 사용하세요.\n비밀번호를 따로 설정하지 않은 경우, 아무것도 입력하지 않아도 됩니다."
forgotPassword: "비밀번호 재설정" forgotPassword: "비밀번호 재설정"
fetchingAsApObject: "연합에서 찾아보는 중" fetchingAsApObject: "연합에서 찾아보는 중"
ok: "확인" ok: "확인"
@ -451,6 +454,7 @@ totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
moderator: "모더레이터" moderator: "모더레이터"
moderation: "조정" moderation: "조정"
moderationNote: "조정 기록" moderationNote: "조정 기록"
moderationNoteDescription: "모더레이터 역할을 가진 유저만 보이는 메모를 적을 수 있습니다."
addModerationNote: "조정 기록 추가하기" addModerationNote: "조정 기록 추가하기"
moderationLogs: "모더레이션 로그" moderationLogs: "모더레이션 로그"
nUsersMentioned: "{n}명이 언급함" nUsersMentioned: "{n}명이 언급함"
@ -716,10 +720,7 @@ abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
reporter: "신고자" reporter: "신고자"
reporteeOrigin: "피신고자" reporteeOrigin: "피신고자"
reporterOrigin: "신고자" reporterOrigin: "신고자"
forwardReport: "리모트 서버에도 신고 내용 보내기"
forwardReportIsAnonymous: "리모트 서버에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다."
send: "전송" send: "전송"
abuseMarkAsResolved: "해결됨으로 표시"
openInNewTab: "새 탭에서 열기" openInNewTab: "새 탭에서 열기"
openInSideView: "사이드뷰로 열기" openInSideView: "사이드뷰로 열기"
defaultNavigationBehaviour: "기본 탐색 동작" defaultNavigationBehaviour: "기본 탐색 동작"
@ -921,6 +922,7 @@ followersVisibility: "팔로워의 공개 범위"
continueThread: "글타래 더 보기" continueThread: "글타래 더 보기"
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? " deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
incorrectPassword: "비밀번호가 올바르지 않습니다." incorrectPassword: "비밀번호가 올바르지 않습니다."
incorrectTotp: "OTP 번호가 틀렸거나 유효기간이 만료되어 있을 수 있습니다."
voteConfirm: "\"{choice}\"에 투표하시겠습니까?" voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
hide: "숨기기" hide: "숨기기"
useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시" useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시"
@ -1120,7 +1122,7 @@ preservedUsernames: "예약한 사용자 이름"
preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다." preservedUsernamesDescription: "예약할 사용자명을 한 줄에 하나씩 입력합니다. 여기에서 지정한 사용자명으로는 계정을 생성할 수 없게 됩니다. 단, 관리자 권한으로 계정을 생성할 때에는 해당되지 않으며, 이미 존재하는 계정도 영향을 받지 않습니다."
createNoteFromTheFile: "이 파일로 노트를 작성" createNoteFromTheFile: "이 파일로 노트를 작성"
archive: "아카이브" archive: "아카이브"
archived: "보관됨" archived: "아카이브 됨"
unarchive: "보관 취소" unarchive: "보관 취소"
channelArchiveConfirmTitle: "{name} 채널을 보존하시겠습니까?" channelArchiveConfirmTitle: "{name} 채널을 보존하시겠습니까?"
channelArchiveConfirmDescription: "보존한 채널은 채널 목록과 검색 결과에 표시되지 않으며 새로운 노트도 작성할 수 없습니다." channelArchiveConfirmDescription: "보존한 채널은 채널 목록과 검색 결과에 표시되지 않으며 새로운 노트도 작성할 수 없습니다."
@ -1283,6 +1285,15 @@ signinWithPasskey: "패스키로 로그인"
unknownWebAuthnKey: "등록되지 않은 패스키입니다." unknownWebAuthnKey: "등록되지 않은 패스키입니다."
passkeyVerificationFailed: "패스키 검증을 실패했습니다." passkeyVerificationFailed: "패스키 검증을 실패했습니다."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다." passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
messageToFollower: "팔로워에 보낼 메시지"
target: "대상"
_abuseUserReport:
forward: "전달"
forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
resolve: "해결됨"
accept: "인용"
reject: "기각"
resolveTutorial: "적절한 신고 내용에 대응한 경우, \"인용\"을 선택하여 \"해결됨\"으로 기록합니다.\n적절하지 않은 신고를 받은 경우, \"기각\"을 선택하여 \"기각\"으로 기록합니다."
_delivery: _delivery:
status: "전송 상태" status: "전송 상태"
stop: "정지됨" stop: "정지됨"
@ -1989,7 +2000,7 @@ _sfx:
_soundSettings: _soundSettings:
driveFile: "드라이브에 있는 오디오를 사용" driveFile: "드라이브에 있는 오디오를 사용"
driveFileWarn: "드라이브에 있는 파일을 선택하세요." driveFileWarn: "드라이브에 있는 파일을 선택하세요."
driveFileTypeWarn: "이 파일은 지원되지 않습니다." driveFileTypeWarn: "이 파"
driveFileTypeWarnDescription: "오디오 파일을 선택하세요." driveFileTypeWarnDescription: "오디오 파일을 선택하세요."
driveFileDurationWarn: "오디오가 너무 깁니다" driveFileDurationWarn: "오디오가 너무 깁니다"
driveFileDurationWarnDescription: "긴 오디오로 설정할 경우 미스키 사용에 지장이 갈 수도 있습니다. 그래도 괜찮습니까?" driveFileDurationWarnDescription: "긴 오디오로 설정할 경우 미스키 사용에 지장이 갈 수도 있습니다. 그래도 괜찮습니까?"
@ -2392,6 +2403,7 @@ _notification:
followedBySomeUsers: "{n}명에게 팔로우됨" followedBySomeUsers: "{n}명에게 팔로우됨"
flushNotification: "알림 이력을 초기화" flushNotification: "알림 이력을 초기화"
exportOfXCompleted: "{x} 추출에 성공했습니다." exportOfXCompleted: "{x} 추출에 성공했습니다."
login: "로그인 알림이 있습니다"
_types: _types:
all: "전부" all: "전부"
note: "사용자의 새 글" note: "사용자의 새 글"
@ -2407,6 +2419,7 @@ _notification:
roleAssigned: "역할이 부여 됨" roleAssigned: "역할이 부여 됨"
achievementEarned: "도전 과제 획득" achievementEarned: "도전 과제 획득"
exportCompleted: "추출을 성공함" exportCompleted: "추출을 성공함"
login: "로그인"
test: "알림 테스트" test: "알림 테스트"
app: "연동된 앱을 통한 알림" app: "연동된 앱을 통한 알림"
_actions: _actions:
@ -2470,7 +2483,7 @@ _webhookSettings:
reaction: "누군가 내 노트에 리액션했을 때" reaction: "누군가 내 노트에 리액션했을 때"
mention: "누군가 나를 멘션했을 때" mention: "누군가 나를 멘션했을 때"
_systemEvents: _systemEvents:
abuseReport: "유저로부터 신고를 받았을 때" abuseReport: "유저"
abuseReportResolved: "받은 신고를 처리했을 때" abuseReportResolved: "받은 신고를 처리했을 때"
userCreated: "유저가 생성되었을 때" userCreated: "유저가 생성되었을 때"
deleteConfirm: "Webhook을 삭제할까요?" deleteConfirm: "Webhook을 삭제할까요?"
@ -2518,6 +2531,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "파일에 열람주의를 설정" markSensitiveDriveFile: "파일에 열람주의를 설정"
unmarkSensitiveDriveFile: "파일에 열람주의를 해제" unmarkSensitiveDriveFile: "파일에 열람주의를 해제"
resolveAbuseReport: "신고 처리" resolveAbuseReport: "신고 처리"
forwardAbuseReport: "신고 전달"
updateAbuseReportNote: "신고 조정 노트 갱신"
createInvitation: "초대 코드 생성" createInvitation: "초대 코드 생성"
createAd: "광고 생성" createAd: "광고 생성"
deleteAd: "광고 삭제" deleteAd: "광고 삭제"
@ -2657,7 +2672,7 @@ _urlPreviewSetting:
timeoutDescription: "미리보기를 로딩하는데 걸리는 시간이 정한 시간보다 오래 걸리는 경우, 미리보기를 생성하지 않습니다." timeoutDescription: "미리보기를 로딩하는데 걸리는 시간이 정한 시간보다 오래 걸리는 경우, 미리보기를 생성하지 않습니다."
maximumContentLength: "Content-Length의 최대치 (byte)" maximumContentLength: "Content-Length의 최대치 (byte)"
maximumContentLengthDescription: "Content-Length가 이 값을 넘어서면 미리보기를 생성하지 않습니다." maximumContentLengthDescription: "Content-Length가 이 값을 넘어서면 미리보기를 생성하지 않습니다."
requireContentLength: "Content-Length를 얻었을 때만 미리보기 만들기" requireContentLength: "Content-Length를 받아온 경우에만 "
requireContentLengthDescription: "상대 서버가 Content-Length를 되돌려주지 않는다면 미리보기를 만들지 않습니다." requireContentLengthDescription: "상대 서버가 Content-Length를 되돌려주지 않는다면 미리보기를 만들지 않습니다."
userAgent: "User-Agent" userAgent: "User-Agent"
userAgentDescription: "미리보기를 얻을 때 사용한 User-Agent를 설정합니다. 비어 있다면 기본값의 User-Agent를 사용합니다." userAgentDescription: "미리보기를 얻을 때 사용한 User-Agent를 설정합니다. 비어 있다면 기본값의 User-Agent를 사용합니다."

View File

@ -456,6 +456,7 @@ _notification:
renote: "Renote" renote: "Renote"
quote: "ອ້າງອີງ" quote: "ອ້າງອີງ"
reaction: "Reaction" reaction: "Reaction"
login: "ເຂົ້າ​ສູ່​ລະ​ບົບ"
_actions: _actions:
reply: "ຕອບ​ກັບ" reply: "ຕອບ​ກັບ"
renote: "Renote" renote: "Renote"

View File

@ -486,6 +486,7 @@ _notification:
renote: "Herdelen" renote: "Herdelen"
quote: "Quote" quote: "Quote"
reaction: "Reacties" reaction: "Reacties"
login: "Inloggen"
_actions: _actions:
reply: "Antwoord" reply: "Antwoord"
renote: "Herdelen" renote: "Herdelen"

View File

@ -701,6 +701,7 @@ _notification:
renote: "Renotes" renote: "Renotes"
quote: "Sitater" quote: "Sitater"
reaction: "Reaksjoner" reaction: "Reaksjoner"
login: "Logg inn"
_actions: _actions:
reply: "Svar" reply: "Svar"
renote: "Renote" renote: "Renote"

View File

@ -689,10 +689,7 @@ abuseReported: "Twoje zgłoszenie zostało wysłane. Dziękujemy."
reporter: "Zgłaszający" reporter: "Zgłaszający"
reporteeOrigin: "Pochodzenie zgłoszonego" reporteeOrigin: "Pochodzenie zgłoszonego"
reporterOrigin: "Pochodzenie zgłaszającego" reporterOrigin: "Pochodzenie zgłaszającego"
forwardReport: "Przekaż zgłoszenie do innej instancji"
forwardReportIsAnonymous: "Zamiast twojego konta, anonimowe konto systemowe będzie wyświetlone jako zgłaszający na instancji zdalnej."
send: "Wyślij" send: "Wyślij"
abuseMarkAsResolved: "Oznacz zgłoszenie jako rozwiązane"
openInNewTab: "Otwórz w nowej karcie" openInNewTab: "Otwórz w nowej karcie"
openInSideView: "Otwórz w bocznym widoku" openInSideView: "Otwórz w bocznym widoku"
defaultNavigationBehaviour: "Domyślne zachowanie nawigacji" defaultNavigationBehaviour: "Domyślne zachowanie nawigacji"
@ -1509,6 +1506,7 @@ _notification:
reaction: "Reakcja" reaction: "Reakcja"
receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji" receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji"
followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji" followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji"
login: "Zaloguj się"
app: "Powiadomienia z aplikacji" app: "Powiadomienia z aplikacji"
_actions: _actions:
followBack: "zaobserwował cię z powrotem" followBack: "zaobserwował cię z powrotem"

View File

@ -1,5 +1,5 @@
--- ---
_lang_: "日本語" _lang_: "Português"
headlineMisskey: "Uma rede ligada por notas" headlineMisskey: "Uma rede ligada por notas"
introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações às notas de outras pessoas usando a função \"Reações\" 👍\nVamos explorar um novo mundo 🚀" introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações às notas de outras pessoas usando a função \"Reações\" 👍\nVamos explorar um novo mundo 🚀"
poweredByMisskeyDescription: "{name} é uma instância da plataforma de código aberto <b>Misskey</b>." poweredByMisskeyDescription: "{name} é uma instância da plataforma de código aberto <b>Misskey</b>."
@ -707,10 +707,7 @@ abuseReported: "Denúncia enviada. Obrigado por sua ajuda."
reporter: "Denunciante" reporter: "Denunciante"
reporteeOrigin: "Origem da denúncia" reporteeOrigin: "Origem da denúncia"
reporterOrigin: "Origem do denunciante" reporterOrigin: "Origem do denunciante"
forwardReport: "Encaminhar a denúncia para o servidor remoto"
forwardReportIsAnonymous: "No servidor remoto, suas informações não serão visíveis, e você será apresentado como uma conta do sistema anônima."
send: "Enviar" send: "Enviar"
abuseMarkAsResolved: "Marcar denúncia como resolvida"
openInNewTab: "Abrir em nova aba" openInNewTab: "Abrir em nova aba"
openInSideView: "Abrir em visão lateral" openInSideView: "Abrir em visão lateral"
defaultNavigationBehaviour: "Navegação padrão" defaultNavigationBehaviour: "Navegação padrão"
@ -1061,7 +1058,7 @@ resetPasswordConfirm: "Deseja realmente mudar a sua senha?"
sensitiveWords: "Palavras sensíveis" sensitiveWords: "Palavras sensíveis"
sensitiveWordsDescription: "A visibilidade de todas as notas contendo as palavras configuradas será colocadas como \"Início\" automaticamente. Você pode listar várias delas separando-as por linha." sensitiveWordsDescription: "A visibilidade de todas as notas contendo as palavras configuradas será colocadas como \"Início\" automaticamente. Você pode listar várias delas separando-as por linha."
sensitiveWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)" sensitiveWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
prohibitedWords: "Palavras proibídas" prohibitedWords: "Palavras proibidas"
prohibitedWordsDescription: "Habilita um erro ao tentar publicar uma nota contendo as palavras escolhidas. Várias palavras podem ser escolhidas, separando-as por linha." prohibitedWordsDescription: "Habilita um erro ao tentar publicar uma nota contendo as palavras escolhidas. Várias palavras podem ser escolhidas, separando-as por linha."
prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)" prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
hiddenTags: "Hashtags escondidas" hiddenTags: "Hashtags escondidas"
@ -1419,7 +1416,7 @@ _achievements:
_types: _types:
_notes1: _notes1:
title: "Configurando o meu misskey" title: "Configurando o meu misskey"
description: "Post uma nota pela primeira vez" description: "Poste uma nota pela primeira vez"
flavor: "Divirta-se com o Misskey!" flavor: "Divirta-se com o Misskey!"
_notes10: _notes10:
title: "Algumas notas" title: "Algumas notas"
@ -2376,6 +2373,7 @@ _notification:
followRequestAccepted: "Aceitou pedidos de seguidor" followRequestAccepted: "Aceitou pedidos de seguidor"
roleAssigned: "Cargo dado" roleAssigned: "Cargo dado"
achievementEarned: "Conquista desbloqueada" achievementEarned: "Conquista desbloqueada"
login: "Iniciar sessão"
app: "Notificações de aplicativos conectados" app: "Notificações de aplicativos conectados"
_actions: _actions:
followBack: "te seguiu de volta" followBack: "te seguiu de volta"

View File

@ -625,10 +625,7 @@ abuseReported: "Raportul tău a fost trimis. Mulțumim."
reporter: "Raportorul" reporter: "Raportorul"
reporteeOrigin: "Originea raportatului" reporteeOrigin: "Originea raportatului"
reporterOrigin: "Originea raportorului" reporterOrigin: "Originea raportorului"
forwardReport: "Redirecționează raportul către instanța externă"
forwardReportIsAnonymous: "În locul contului tău, va fi afișat un cont anonim, de sistem, ca raportor către instanța externă."
send: "Trimite" send: "Trimite"
abuseMarkAsResolved: "Marchează raportul ca rezolvat"
openInNewTab: "Deschide în tab nou" openInNewTab: "Deschide în tab nou"
openInSideView: "Deschide în vedere laterală" openInSideView: "Deschide în vedere laterală"
defaultNavigationBehaviour: "Comportament de navigare implicit" defaultNavigationBehaviour: "Comportament de navigare implicit"
@ -714,6 +711,7 @@ _notification:
renote: "Re-notează" renote: "Re-notează"
quote: "Citează" quote: "Citează"
reaction: "Reacție" reaction: "Reacție"
login: "Autentifică-te"
_actions: _actions:
reply: "Răspunde" reply: "Răspunde"
renote: "Re-notează" renote: "Re-notează"

View File

@ -700,10 +700,7 @@ abuseReported: "Жалоба отправлена. Большое спасибо
reporter: "Сообщивший" reporter: "Сообщивший"
reporteeOrigin: "О ком сообщено" reporteeOrigin: "О ком сообщено"
reporterOrigin: "Кто сообщил" reporterOrigin: "Кто сообщил"
forwardReport: "Отправить жалобу на инстанс автора."
forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись."
send: "Отправить" send: "Отправить"
abuseMarkAsResolved: "Отметить жалобу как решённую"
openInNewTab: "Открыть в новой вкладке" openInNewTab: "Открыть в новой вкладке"
openInSideView: "Открывать в боковой колонке" openInSideView: "Открывать в боковой колонке"
defaultNavigationBehaviour: "Поведение навигации по умолчанию" defaultNavigationBehaviour: "Поведение навигации по умолчанию"
@ -2046,6 +2043,7 @@ _notification:
receiveFollowRequest: "Получен запрос на подписку" receiveFollowRequest: "Получен запрос на подписку"
followRequestAccepted: "Запрос на подписку одобрен" followRequestAccepted: "Запрос на подписку одобрен"
achievementEarned: "Получение достижений" achievementEarned: "Получение достижений"
login: "Войти"
app: "Уведомления из приложений" app: "Уведомления из приложений"
_actions: _actions:
followBack: "отвечает взаимной подпиской" followBack: "отвечает взаимной подпиской"

View File

@ -17,3 +17,6 @@ _sfx:
note: "නෝට්" note: "නෝට්"
_profile: _profile:
username: "පරිශීලක නාමය" username: "පරිශීලක නාමය"
_notification:
_types:
login: "පිවිසෙන්න"

View File

@ -631,10 +631,7 @@ abuseReported: "Vaše nahlásenie je odoslané. Veľmi pekne ďakujeme."
reporter: "Nahlásil" reporter: "Nahlásil"
reporteeOrigin: "Pôvod nahláseného" reporteeOrigin: "Pôvod nahláseného"
reporterOrigin: "Pôvod nahlasovača" reporterOrigin: "Pôvod nahlasovača"
forwardReport: "Preposlať nahlásenie na server"
forwardReportIsAnonymous: "Namiesto vášho účtu bude zobrazený anonymný systémový účet na vzdialenom serveri ako autor nahlásenia."
send: "Poslať" send: "Poslať"
abuseMarkAsResolved: "Označiť nahlásenia ako vyriešené"
openInNewTab: "Otvoriť v novom tabe" openInNewTab: "Otvoriť v novom tabe"
openInSideView: "Otvoriť v bočnom paneli" openInSideView: "Otvoriť v bočnom paneli"
defaultNavigationBehaviour: "Predvolené správanie navigácie" defaultNavigationBehaviour: "Predvolené správanie navigácie"
@ -1409,6 +1406,7 @@ _notification:
pollEnded: "Hlasovanie skončilo" pollEnded: "Hlasovanie skončilo"
receiveFollowRequest: "Doručené žiadosti o sledovanie" receiveFollowRequest: "Doručené žiadosti o sledovanie"
followRequestAccepted: "Schválené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie"
login: "Prihlásiť sa"
app: "Oznámenia z prepojených aplikácií" app: "Oznámenia z prepojených aplikácií"
_actions: _actions:
followBack: "Sledovať späť\n" followBack: "Sledovať späť\n"

View File

@ -562,6 +562,7 @@ _notification:
renote: "Omnotera" renote: "Omnotera"
quote: "Citat" quote: "Citat"
reaction: "Reaktioner" reaction: "Reaktioner"
login: "Logga in"
_actions: _actions:
reply: "Svara" reply: "Svara"
renote: "Omnotera" renote: "Omnotera"

View File

@ -707,10 +707,7 @@ abuseReported: "เราได้ส่งรายงานของคุณ
reporter: "ผู้รายงาน" reporter: "ผู้รายงาน"
reporteeOrigin: "ปลายทางรายงาน" reporteeOrigin: "ปลายทางรายงาน"
reporterOrigin: "แหล่งผู้รายงาน" reporterOrigin: "แหล่งผู้รายงาน"
forwardReport: "ส่งต่อรายงานไปยังเซิร์ฟเวอร์ระยะไกล"
forwardReportIsAnonymous: "ข้อมูลของคุณจะไม่ปรากฏบนเซิร์ฟเวอร์ระยะไกลและปรากฏเป็นบัญชีระบบที่ไม่ระบุชื่อ"
send: "ส่ง" send: "ส่ง"
abuseMarkAsResolved: "ทำเครื่องหมายรายงานว่าแก้ไขแล้ว"
openInNewTab: "เปิดในแท็บใหม่" openInNewTab: "เปิดในแท็บใหม่"
openInSideView: "เปิดในมุมมองด้านข้าง" openInSideView: "เปิดในมุมมองด้านข้าง"
defaultNavigationBehaviour: "พฤติกรรมการนำทางที่เป็นค่าเริ่มต้น" defaultNavigationBehaviour: "พฤติกรรมการนำทางที่เป็นค่าเริ่มต้น"
@ -2374,6 +2371,7 @@ _notification:
followRequestAccepted: "อนุมัติให้ติดตามแล้ว" followRequestAccepted: "อนุมัติให้ติดตามแล้ว"
roleAssigned: "ให้บทบาท" roleAssigned: "ให้บทบาท"
achievementEarned: "ปลดล็อกความสำเร็จแล้ว" achievementEarned: "ปลดล็อกความสำเร็จแล้ว"
login: "เข้าสู่ระบบ"
app: "การแจ้งเตือนจากแอปที่มีลิงก์" app: "การแจ้งเตือนจากแอปที่มีลิงก์"
_actions: _actions:
followBack: "ติดตามกลับด้วย" followBack: "ติดตามกลับด้วย"

View File

@ -446,6 +446,7 @@ _notification:
reaction: "Tepkiler" reaction: "Tepkiler"
receiveFollowRequest: "Takip isteği alındı" receiveFollowRequest: "Takip isteği alındı"
followRequestAccepted: "Takip isteği kabul edildi" followRequestAccepted: "Takip isteği kabul edildi"
login: "Giriş Yap "
_actions: _actions:
reply: "yanıt" reply: "yanıt"
renote: "vazgeçme" renote: "vazgeçme"

View File

@ -17,3 +17,6 @@ _2fa:
renewTOTPCancel: "ئۇنى توختىتىڭ" renewTOTPCancel: "ئۇنى توختىتىڭ"
_widgets: _widgets:
profile: "profile" profile: "profile"
_notification:
_types:
login: "كىرىش"

View File

@ -630,10 +630,7 @@ abuseReported: "Дякуємо, вашу скаргу було відправл
reporter: "Репортер" reporter: "Репортер"
reporteeOrigin: "Про кого повідомлено" reporteeOrigin: "Про кого повідомлено"
reporterOrigin: "Хто повідомив" reporterOrigin: "Хто повідомив"
forwardReport: "Переслати звіт на віддалений інстанс"
forwardReportIsAnonymous: "Замість вашого облікового запису анонімний системний обліковий запис буде відображатися як доповідач на віддаленому інстансі"
send: "Відправити" send: "Відправити"
abuseMarkAsResolved: "Позначити скаргу як вирішену"
openInNewTab: "Відкрити в новій вкладці" openInNewTab: "Відкрити в новій вкладці"
openInSideView: "Відкрити збоку" openInSideView: "Відкрити збоку"
defaultNavigationBehaviour: "Поведінка навігації за замовчуванням" defaultNavigationBehaviour: "Поведінка навігації за замовчуванням"
@ -1587,6 +1584,7 @@ _notification:
reaction: "Реакції" reaction: "Реакції"
receiveFollowRequest: "Запити на підписку" receiveFollowRequest: "Запити на підписку"
followRequestAccepted: "Прийняті підписки" followRequestAccepted: "Прийняті підписки"
login: "Увійти"
app: "Сповіщення від додатків" app: "Сповіщення від додатків"
_actions: _actions:
reply: "Відповісти" reply: "Відповісти"

View File

@ -629,10 +629,7 @@ abuseReported: "Shikoyatingiz yetkazildi. Ma'lumot uchun rahmat."
reporter: "Shikoyat qiluvchi" reporter: "Shikoyat qiluvchi"
reporteeOrigin: "Xabarning kelib chiqishi" reporteeOrigin: "Xabarning kelib chiqishi"
reporterOrigin: "Xabarchining joylashuvi" reporterOrigin: "Xabarchining joylashuvi"
forwardReport: "Xabarni masofadagi serverga yuborish"
forwardReportIsAnonymous: "Sizning yuborayotgan xabaringiz o'z akkountingiz emas balki anonim tarzda qoladi"
send: "Yuborish" send: "Yuborish"
abuseMarkAsResolved: "Yuborilgan xabarni hal qilingan deb belgilash"
openInNewTab: "Yangi tab da ochish" openInNewTab: "Yangi tab da ochish"
openInSideView: "Yon panelda ochish" openInSideView: "Yon panelda ochish"
defaultNavigationBehaviour: "Standart navigatsiya harakati" defaultNavigationBehaviour: "Standart navigatsiya harakati"
@ -1057,6 +1054,7 @@ _notification:
quote: "Iqtibos keltirish" quote: "Iqtibos keltirish"
reaction: "Reaktsiyalar" reaction: "Reaktsiyalar"
receiveFollowRequest: "Qabul qilingan kuzatuv so'rovlari" receiveFollowRequest: "Qabul qilingan kuzatuv so'rovlari"
login: "Kirish"
_actions: _actions:
reply: "Javob berish" reply: "Javob berish"
renote: "Qayta qayd qilish" renote: "Qayta qayd qilish"

View File

@ -675,10 +675,7 @@ abuseReported: "Báo cáo đã được gửi. Cảm ơn bạn nhiều."
reporter: "Người báo cáo" reporter: "Người báo cáo"
reporteeOrigin: "Bị báo cáo" reporteeOrigin: "Bị báo cáo"
reporterOrigin: "Máy chủ người báo cáo" reporterOrigin: "Máy chủ người báo cáo"
forwardReport: "Chuyển tiếp báo cáo cho máy chủ từ xa"
forwardReportIsAnonymous: "Thay vì tài khoản của bạn, một tài khoản hệ thống ẩn danh sẽ được hiển thị dưới dạng người báo cáo ở máy chủ từ xa."
send: "Gửi" send: "Gửi"
abuseMarkAsResolved: "Đánh dấu đã xử lý"
openInNewTab: "Mở trong tab mới" openInNewTab: "Mở trong tab mới"
openInSideView: "Mở trong thanh bên" openInSideView: "Mở trong thanh bên"
defaultNavigationBehaviour: "Thao tác điều hướng mặc định" defaultNavigationBehaviour: "Thao tác điều hướng mặc định"
@ -1878,6 +1875,7 @@ _notification:
receiveFollowRequest: "Yêu cầu theo dõi" receiveFollowRequest: "Yêu cầu theo dõi"
followRequestAccepted: "Yêu cầu theo dõi được chấp nhận" followRequestAccepted: "Yêu cầu theo dõi được chấp nhận"
achievementEarned: "Hoàn thành Achievement" achievementEarned: "Hoàn thành Achievement"
login: "Đăng nhập"
app: "Từ app liên kết" app: "Từ app liên kết"
_actions: _actions:
followBack: "đã theo dõi lại bạn" followBack: "đã theo dõi lại bạn"

View File

@ -8,6 +8,9 @@ search: "搜索"
notifications: "通知" notifications: "通知"
username: "用户名" username: "用户名"
password: "密码" password: "密码"
initialPasswordForSetup: "初始化密码"
initialPasswordIsIncorrect: "初始化密码不正确"
initialPasswordForSetupDescription: "如果是自己安装的 Misskey请输入配置文件里设好的密码。\n如果使用的是 Misskey 的托管服务等,请输入服务商提供的密码。\n如果没有设置密码请留空并继续。"
forgotPassword: "忘记密码" forgotPassword: "忘记密码"
fetchingAsApObject: "在联邦宇宙查询中..." fetchingAsApObject: "在联邦宇宙查询中..."
ok: "OK" ok: "OK"
@ -90,7 +93,7 @@ followsYou: "正在关注你"
createList: "创建列表" createList: "创建列表"
manageLists: "管理列表" manageLists: "管理列表"
error: "错误" error: "错误"
somethingHappened: "出现了一些问题!" somethingHappened: "出错了"
retry: "重试" retry: "重试"
pageLoadError: "页面加载失败。" pageLoadError: "页面加载失败。"
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。" pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
@ -167,7 +170,7 @@ emojiUrl: "emoji 地址"
addEmoji: "添加表情符号" addEmoji: "添加表情符号"
settingGuide: "推荐配置" settingGuide: "推荐配置"
cacheRemoteFiles: "缓存远程文件" cacheRemoteFiles: "缓存远程文件"
cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。" cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。"
youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。" youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。"
cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件" cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件"
cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。" cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。"
@ -236,6 +239,8 @@ silencedInstances: "被静音的服务器"
silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。" silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。"
mediaSilencedInstances: "已隐藏媒体文件的服务器" mediaSilencedInstances: "已隐藏媒体文件的服务器"
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。"
federationAllowedHosts: "允许联合的服务器"
federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
muteAndBlock: "静音/拉黑" muteAndBlock: "静音/拉黑"
mutedUsers: "已静音用户" mutedUsers: "已静音用户"
blockedUsers: "已拉黑的用户" blockedUsers: "已拉黑的用户"
@ -449,6 +454,7 @@ totpDescription: "使用验证器输入一次性密码"
moderator: "监察员" moderator: "监察员"
moderation: "管理" moderation: "管理"
moderationNote: "管理笔记" moderationNote: "管理笔记"
moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。"
addModerationNote: "添加管理笔记" addModerationNote: "添加管理笔记"
moderationLogs: "管理日志" moderationLogs: "管理日志"
nUsersMentioned: "{n} 被提到" nUsersMentioned: "{n} 被提到"
@ -512,6 +518,7 @@ emojiStyle: "表情符号的样式"
native: "原生" native: "原生"
menuStyle: "菜单样式" menuStyle: "菜单样式"
style: "样式" style: "样式"
drawer: "抽屉"
popup: "弹窗" popup: "弹窗"
showNoteActionsOnlyHover: "仅在悬停时显示帖子操作" showNoteActionsOnlyHover: "仅在悬停时显示帖子操作"
showReactionsCount: "显示帖子的回应数" showReactionsCount: "显示帖子的回应数"
@ -713,10 +720,7 @@ abuseReported: "内容已发送。感谢您提交信息。"
reporter: "举报者" reporter: "举报者"
reporteeOrigin: "举报来源" reporteeOrigin: "举报来源"
reporterOrigin: "举报者来源" reporterOrigin: "举报者来源"
forwardReport: "将该举报信息转发给远程服务器"
forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。"
send: "发送" send: "发送"
abuseMarkAsResolved: "处理完毕"
openInNewTab: "在新标签页中打开" openInNewTab: "在新标签页中打开"
openInSideView: "在侧边栏中打开" openInSideView: "在侧边栏中打开"
defaultNavigationBehaviour: "默认导航" defaultNavigationBehaviour: "默认导航"
@ -918,6 +922,7 @@ followersVisibility: "关注者的公开范围"
continueThread: "查看更多帖子" continueThread: "查看更多帖子"
deleteAccountConfirm: "将要删除账户。是否确认?" deleteAccountConfirm: "将要删除账户。是否确认?"
incorrectPassword: "密码错误" incorrectPassword: "密码错误"
incorrectTotp: "一次性密码不正确或已过期"
voteConfirm: "确定投给 “{choice}” " voteConfirm: "确定投给 “{choice}” "
hide: "隐藏" hide: "隐藏"
useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示" useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示"
@ -1194,10 +1199,10 @@ followingOrFollower: "关注中或关注者"
fileAttachedOnly: "仅限媒体" fileAttachedOnly: "仅限媒体"
showRepliesToOthersInTimeline: "在时间线中包含给别人的回复" showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复" hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
showRepliesToOthersInTimelineAll: "在时间线中包含现在关注的所有人的回复" showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复"
hideRepliesToOthersInTimelineAll: "在时间线中隐藏现在关注的所有人的回复" hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复"
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?" confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?"
confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?" confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏所有现在关注的人的回复吗?"
externalServices: "外部服务" externalServices: "外部服务"
sourceCode: "源代码" sourceCode: "源代码"
sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。" sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。"
@ -1273,10 +1278,22 @@ genEmbedCode: "生成嵌入代码"
noteOfThisUser: "此用户的帖子" noteOfThisUser: "此用户的帖子"
clipNoteLimitExceeded: "无法再往此便签内添加更多帖子" clipNoteLimitExceeded: "无法再往此便签内添加更多帖子"
performance: "性能" performance: "性能"
modified: "有变更"
discard: "取消"
thereAreNChanges: "有 {n} 处更改"
signinWithPasskey: "使用通行密钥登录" signinWithPasskey: "使用通行密钥登录"
unknownWebAuthnKey: "此通行密钥未注册。" unknownWebAuthnKey: "此通行密钥未注册。"
passkeyVerificationFailed: "验证通行密钥失败。" passkeyVerificationFailed: "验证通行密钥失败。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。"
messageToFollower: "给关注者的消息"
target: "对象"
_abuseUserReport:
forward: "转发"
forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。"
resolve: "解决"
accept: "确认"
reject: "拒绝"
resolveTutorial: "如果举报内容有理且已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果举报内容站不住脚选择「拒绝」将案件以否定的态度标记为已解决。"
_delivery: _delivery:
status: "投递状态" status: "投递状态"
stop: "停止投递" stop: "停止投递"
@ -1613,7 +1630,7 @@ _achievements:
_postedAt0min0sec: _postedAt0min0sec:
title: "报时" title: "报时"
description: "在 0 点发布一篇帖子" description: "在 0 点发布一篇帖子"
flavor: "报时信号最后一响,零点整" flavor: "嘟 · 嘟 · 嘟 · 哔——"
_selfQuote: _selfQuote:
title: "自我引用" title: "自我引用"
description: "引用了自己的帖子" description: "引用了自己的帖子"
@ -2244,6 +2261,9 @@ _profile:
changeBanner: "修改横幅" changeBanner: "修改横幅"
verifiedLinkDescription: "如果将内容设置为 URL当链接所指向的网页内包含自己的个人资料链接时可以显示一个已验证图标。" verifiedLinkDescription: "如果将内容设置为 URL当链接所指向的网页内包含自己的个人资料链接时可以显示一个已验证图标。"
avatarDecorationMax: "最多可添加 {max} 个挂件" avatarDecorationMax: "最多可添加 {max} 个挂件"
followedMessage: "被关注时显示的消息"
followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在被请求被批准后显示。"
_exportOrImport: _exportOrImport:
allNotes: "所有帖子" allNotes: "所有帖子"
favoritedNotes: "收藏的帖子" favoritedNotes: "收藏的帖子"
@ -2383,6 +2403,7 @@ _notification:
followedBySomeUsers: "被 {n} 人关注" followedBySomeUsers: "被 {n} 人关注"
flushNotification: "重置通知历史" flushNotification: "重置通知历史"
exportOfXCompleted: "已完成 {x} 个导出" exportOfXCompleted: "已完成 {x} 个导出"
login: "有新的登录"
_types: _types:
all: "全部" all: "全部"
note: "用户的新帖子" note: "用户的新帖子"
@ -2398,6 +2419,7 @@ _notification:
roleAssigned: "授予的角色" roleAssigned: "授予的角色"
achievementEarned: "取得的成就" achievementEarned: "取得的成就"
exportCompleted: "已完成导出" exportCompleted: "已完成导出"
login: "登录"
test: "测试通知" test: "测试通知"
app: "关联应用的通知" app: "关联应用的通知"
_actions: _actions:
@ -2509,6 +2531,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "标记网盘文件为敏感媒体" markSensitiveDriveFile: "标记网盘文件为敏感媒体"
unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体" unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"
resolveAbuseReport: "处理举报" resolveAbuseReport: "处理举报"
forwardAbuseReport: "转发举报"
updateAbuseReportNote: "更新举报用管理笔记"
createInvitation: "生成邀请码" createInvitation: "生成邀请码"
createAd: "创建了广告" createAd: "创建了广告"
deleteAd: "删除了广告" deleteAd: "删除了广告"

View File

@ -8,6 +8,9 @@ search: "搜尋"
notifications: "通知" notifications: "通知"
username: "使用者名稱" username: "使用者名稱"
password: "密碼" password: "密碼"
initialPasswordForSetup: "初始設定用的密碼"
initialPasswordIsIncorrect: "初始設定用的密碼錯誤。"
initialPasswordForSetupDescription: "如果您自己安裝了 Misskey請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼請將其留空並繼續。"
forgotPassword: "忘記密碼" forgotPassword: "忘記密碼"
fetchingAsApObject: "從聯邦宇宙取得中..." fetchingAsApObject: "從聯邦宇宙取得中..."
ok: "OK" ok: "OK"
@ -716,10 +719,7 @@ abuseReported: "檢舉完成。感謝您的報告。"
reporter: "檢舉者" reporter: "檢舉者"
reporteeOrigin: "檢舉來源" reporteeOrigin: "檢舉來源"
reporterOrigin: "檢舉者來源" reporterOrigin: "檢舉者來源"
forwardReport: "將報告轉送給遠端伺服器"
forwardReportIsAnonymous: "在遠端實例上看不到您的資訊,顯示的報告者是匿名的系统帳戶。"
send: "發送" send: "發送"
abuseMarkAsResolved: "處理完畢"
openInNewTab: "在新分頁中開啟" openInNewTab: "在新分頁中開啟"
openInSideView: "在側欄中開啟" openInSideView: "在側欄中開啟"
defaultNavigationBehaviour: "預設導航" defaultNavigationBehaviour: "預設導航"
@ -921,6 +921,7 @@ followersVisibility: "追隨者的可見性"
continueThread: "查看更多貼文" continueThread: "查看更多貼文"
deleteAccountConfirm: "將要刪除帳戶。是否確定?" deleteAccountConfirm: "將要刪除帳戶。是否確定?"
incorrectPassword: "密碼錯誤。" incorrectPassword: "密碼錯誤。"
incorrectTotp: "一次性密碼錯誤,或者已過期。"
voteConfirm: "確定投給「{choice}」?" voteConfirm: "確定投給「{choice}」?"
hide: "隱藏" hide: "隱藏"
useDrawerReactionPickerForMobile: "在移動設備上使用抽屜顯示" useDrawerReactionPickerForMobile: "在移動設備上使用抽屜顯示"
@ -1283,6 +1284,7 @@ signinWithPasskey: "使用密碼金鑰登入"
unknownWebAuthnKey: "未註冊的金鑰。" unknownWebAuthnKey: "未註冊的金鑰。"
passkeyVerificationFailed: "驗證金鑰失敗。" passkeyVerificationFailed: "驗證金鑰失敗。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。"
messageToFollower: "給追隨者的訊息"
_delivery: _delivery:
status: "傳送狀態" status: "傳送狀態"
stop: "停止發送" stop: "停止發送"
@ -2392,6 +2394,7 @@ _notification:
followedBySomeUsers: "被{n}人追隨了" followedBySomeUsers: "被{n}人追隨了"
flushNotification: "重置通知歷史紀錄" flushNotification: "重置通知歷史紀錄"
exportOfXCompleted: "{x} 的匯出已完成。" exportOfXCompleted: "{x} 的匯出已完成。"
login: "已登入"
_types: _types:
all: "全部 " all: "全部 "
note: "使用者的最新貼文" note: "使用者的最新貼文"
@ -2407,6 +2410,7 @@ _notification:
roleAssigned: "已授予角色" roleAssigned: "已授予角色"
achievementEarned: "獲得成就" achievementEarned: "獲得成就"
exportCompleted: "已完成匯出。" exportCompleted: "已完成匯出。"
login: "登入"
test: "通知測試" test: "通知測試"
app: "應用程式通知" app: "應用程式通知"
_actions: _actions:

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.9.0", "version": "2024.10.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -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"`);
}
}

View File

@ -71,20 +71,20 @@
"@bull-board/fastify": "6.0.0", "@bull-board/fastify": "6.0.0",
"@bull-board/ui": "6.0.0", "@bull-board/ui": "6.0.0",
"@discordapp/twemoji": "15.1.0", "@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.0", "@fastify/accepts": "5.0.1",
"@fastify/cookie": "10.0.0", "@fastify/cookie": "10.0.1",
"@fastify/cors": "10.0.0", "@fastify/cors": "10.0.1",
"@fastify/express": "4.0.0", "@fastify/express": "4.0.1",
"@fastify/http-proxy": "10.0.0", "@fastify/http-proxy": "10.0.0",
"@fastify/multipart": "9.0.0", "@fastify/multipart": "9.0.1",
"@fastify/static": "8.0.0", "@fastify/static": "8.0.1",
"@fastify/view": "10.0.0", "@fastify/view": "10.0.1",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0", "@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "0.1.56", "@napi-rs/canvas": "0.1.56",
"@nestjs/common": "10.4.3", "@nestjs/common": "10.4.4",
"@nestjs/core": "10.4.3", "@nestjs/core": "10.4.4",
"@nestjs/testing": "10.4.3", "@nestjs/testing": "10.4.4",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sentry/node": "8.20.0", "@sentry/node": "8.20.0",
"@sentry/profiling-node": "8.20.0", "@sentry/profiling-node": "8.20.0",
@ -101,7 +101,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"bullmq": "5.13.2", "bullmq": "5.15.0",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -149,7 +149,7 @@
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.3.2", "otpauth": "9.3.4",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.13.0", "pg": "8.13.0",
"pkce-challenge": "4.1.0", "pkce-challenge": "4.1.0",
@ -166,7 +166,7 @@
"rename": "1.0.4", "rename": "1.0.4",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"sanitize-html": "2.13.0", "sanitize-html": "2.13.1",
"secure-json-parse": "2.7.0", "secure-json-parse": "2.7.0",
"sharp": "0.33.5", "sharp": "0.33.5",
"slacc": "0.0.10", "slacc": "0.0.10",
@ -187,14 +187,14 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.3", "@nestjs/platform-express": "10.4.4",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36", "@swc/jest": "0.2.36",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
"@types/archiver": "6.0.2", "@types/archiver": "6.0.2",
"@types/bcryptjs": "2.4.6", "@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.5", "@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/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.26", "@types/fluent-ffmpeg": "2.1.26",
"@types/htmlescape": "1.1.3", "@types/htmlescape": "1.1.3",

View File

@ -63,6 +63,8 @@ type Source = {
publishTarballInsteadOfProvideRepositoryUrl?: boolean; publishTarballInsteadOfProvideRepositoryUrl?: boolean;
setupPassword?: string;
proxy?: string; proxy?: string;
proxySmtp?: string; proxySmtp?: string;
proxyBypassHosts?: string[]; proxyBypassHosts?: string[];
@ -152,6 +154,7 @@ export type Config = {
version: string; version: string;
publishTarballInsteadOfProvideRepositoryUrl: boolean; publishTarballInsteadOfProvideRepositoryUrl: boolean;
setupPassword: string | undefined;
host: string; host: string;
hostname: string; hostname: string;
scheme: string; scheme: string;
@ -232,6 +235,7 @@ export function loadConfig(): Config {
return { return {
version, version,
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
setupPassword: config.setupPassword,
url: url.origin, url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10), port: config.port ?? parseInt(process.env.PORT ?? '', 10),
socket: config.socket, socket: config.socket,

View File

@ -22,6 +22,7 @@ import { RoleService } from '@/core/RoleService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdService } from './IdService.js'; import { IdService } from './IdService.js';
@Injectable() @Injectable()
@ -42,6 +43,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
private emailService: EmailService, private emailService: EmailService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private userEntityService: UserEntityService,
) { ) {
this.redisForSub.on('message', this.onMessage); this.redisForSub.on('message', this.onMessage);
} }
@ -135,6 +137,26 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
return; 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() const recipientWebhookIds = await this.fetchWebhookRecipients()
.then(it => it .then(it => it
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook') .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
@ -142,7 +164,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.filter(x => x != null)); .filter(x => x != null));
for (const webhookId of recipientWebhookIds) { for (const webhookId of recipientWebhookIds) {
await Promise.all( await Promise.all(
abuseReports.map(it => { convertedReports.map(it => {
return this.systemWebhookService.enqueueSystemWebhook( return this.systemWebhookService.enqueueSystemWebhook(
webhookId, webhookId,
type, type,

View File

@ -20,8 +20,10 @@ export class AbuseReportService {
constructor( constructor(
@Inject(DI.abuseUserReportsRepository) @Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository, private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private idService: IdService, private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService, private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService, private queueService: QueueService,
@ -77,16 +79,16 @@ export class AbuseReportService {
* - SystemWebhook * - SystemWebhook
* *
* @param params . * @param params .
* @param operator * @param moderator
* @see AbuseReportNotificationService.notify * @see AbuseReportNotificationService.notify
*/ */
@bindThis @bindThis
public async resolve( public async resolve(
params: { params: {
reportId: string; reportId: string;
forward: boolean; resolvedAs: MiAbuseUserReport['resolvedAs'];
}[], }[],
operator: MiUser, moderator: MiUser,
) { ) {
const paramsMap = new Map(params.map(it => [it.reportId, it])); const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({ const reports = await this.abuseUserReportsRepository.findBy({
@ -99,25 +101,15 @@ export class AbuseReportService {
await this.abuseUserReportsRepository.update(report.id, { await this.abuseUserReportsRepository.update(report.id, {
resolved: true, resolved: true,
assigneeId: operator.id, assigneeId: moderator.id,
forwarded: ps.forward && report.targetUserHost !== null, 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 this.moderationLogService
.log(operator, 'resolveAbuseReport', { .log(moderator, 'resolveAbuseReport', {
reportId: report.id, reportId: report.id,
report: report, report: report,
forwarded: ps.forward && report.targetUserHost !== null, resolvedAs: ps.resolvedAs,
}) })
.then(); .then();
} }
@ -125,4 +117,62 @@ export class AbuseReportService {
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) }) return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved')); .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,
});
}
}
} }

View File

@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js'; import { UserSearchService } from '@/core/UserSearchService.js';
import { WebhookTestService } from '@/core/WebhookTestService.js'; import { WebhookTestService } from '@/core/WebhookTestService.js';
import { FlashService } from '@/core/FlashService.js';
import { AccountMoveService } from './AccountMoveService.js'; import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js'; import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js'; import { AiService } from './AiService.js';
@ -217,6 +218,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService }; const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@ -367,6 +369,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService, WebhookTestService,
UtilityService, UtilityService,
FileInfoService, FileInfoService,
FlashService,
SearchService, SearchService,
ClipService, ClipService,
FeaturedService, FeaturedService,
@ -513,6 +516,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookTestService, $WebhookTestService,
$UtilityService, $UtilityService,
$FileInfoService, $FileInfoService,
$FlashService,
$SearchService, $SearchService,
$ClipService, $ClipService,
$FeaturedService, $FeaturedService,
@ -660,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService, WebhookTestService,
UtilityService, UtilityService,
FileInfoService, FileInfoService,
FlashService,
SearchService, SearchService,
ClipService, ClipService,
FeaturedService, FeaturedService,

View File

@ -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();
}
}

View File

@ -218,7 +218,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private utilityService: UtilityService, private utilityService: UtilityService,
private userBlockingService: UserBlockingService, 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 @bindThis

View File

@ -15,8 +15,14 @@ import { QueueService } from '@/core/QueueService.js';
const oneDayMillis = 24 * 60 * 60 * 1000; const oneDayMillis = 24 * 60 * 60 * 1000;
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport { type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
return { targetUser: Packed<'UserLite'> | null,
reporter: Packed<'UserLite'> | null,
assignee: Packed<'UserLite'> | null,
};
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1', id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user', targetUserId: 'dummy-target-user',
targetUser: null, targetUser: null,
@ -29,8 +35,17 @@ function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUser
comment: 'This is a dummy report for testing purposes.', comment: 'This is a dummy report for testing purposes.',
targetUserHost: null, targetUserHost: null,
reporterHost: null, reporterHost: null,
resolvedAs: null,
moderationNote: 'foo',
...override, ...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>): MiUser { function generateDummyUser(override?: Partial<MiUser>): MiUser {
@ -268,7 +283,8 @@ const dummyUser3 = generateDummyUser({
@Injectable() @Injectable()
export class WebhookTestService { export class WebhookTestService {
public static NoSuchWebhookError = class extends Error {}; public static NoSuchWebhookError = class extends Error {
};
constructor( constructor(
private userWebhookService: UserWebhookService, private userWebhookService: UserWebhookService,

View File

@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}) : null, }) : null,
forwarded: report.forwarded, forwarded: report.forwarded,
resolvedAs: report.resolvedAs,
moderationNote: report.moderationNote,
}); });
} }

View File

@ -5,10 +5,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js'; import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { MiFlash } from '@/models/Flash.js'; import type { MiFlash } from '@/models/Flash.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -20,10 +18,8 @@ export class FlashEntityService {
constructor( constructor(
@Inject(DI.flashsRepository) @Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository, private flashsRepository: FlashsRepository,
@Inject(DI.flashLikesRepository) @Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository, private flashLikesRepository: FlashLikesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
) { ) {
@ -34,25 +30,36 @@ export class FlashEntityService {
src: MiFlash['id'] | MiFlash, src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hint?: { hint?: {
packedUser?: Packed<'UserLite'> packedUser?: Packed<'UserLite'>,
likedFlashIds?: MiFlash['id'][],
}, },
): Promise<Packed<'Flash'>> { ): Promise<Packed<'Flash'>> {
const meId = me ? me.id : null; const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src }); 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 = undefined;
if (meId) {
isLiked = hint?.likedFlashIds
? hint.likedFlashIds.includes(flash.id)
: await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
}
return {
id: flash.id, id: flash.id,
createdAt: this.idService.parse(flash.id).date.toISOString(), createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(), updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId, userId: flash.userId,
user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意 user: user,
title: flash.title, title: flash.title,
summary: flash.summary, summary: flash.summary,
script: flash.script, script: flash.script,
visibility: flash.visibility, visibility: flash.visibility,
likedCount: flash.likedCount, likedCount: flash.likedCount,
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined, isLiked: isLiked,
}); };
} }
@bindThis @bindThis
@ -63,7 +70,19 @@ export class FlashEntityService {
const _users = flashes.map(({ user, userId }) => user ?? userId); const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me) const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u]))); .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,
})),
);
} }
} }

View File

@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit {
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility, followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility, 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 => ({ roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id, id: role.id,
name: role.name, name: role.name,
@ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, 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 ? { ...(isDetailed && isMe ? {
avatarId: user.avatarId, avatarId: user.avatarId,
bannerId: user.bannerId, bannerId: user.bannerId,

View File

@ -50,6 +50,9 @@ export class MiAbuseUserReport {
}) })
public resolved: boolean; public resolved: boolean;
/**
*
*/
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })
@ -60,6 +63,21 @@ export class MiAbuseUserReport {
}) })
public comment: string; 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 //#region Denormalized fields
@Index() @Index()
@Column('varchar', { @Column('varchar', {

View File

@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
import { id } from './util/id.js'; import { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
export const flashVisibility = ['public', 'private'] as const;
export type FlashVisibility = typeof flashVisibility[number];
@Entity('flash') @Entity('flash')
export class MiFlash { export class MiFlash {
@PrimaryColumn(id()) @PrimaryColumn(id())
@ -63,5 +66,5 @@ export class MiFlash {
@Column('varchar', { @Column('varchar', {
length: 512, default: 'public', length: 512, default: 'public',
}) })
public visibility: 'public' | 'private'; public visibility: FlashVisibility;
} }

View File

@ -3,12 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { userExportableEntities } from '@/types.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
import { MiNote } from './Note.js'; import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js'; import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js'; import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js'; import { MiDriveFile } from './DriveFile.js';
import { userExportableEntities } from '@/types.js';
export type MiNotification = { export type MiNotification = {
type: 'note'; type: 'note';
@ -86,6 +86,10 @@ export type MiNotification = {
createdAt: string; createdAt: string;
exportedEntity: typeof userExportableEntities[number]; exportedEntity: typeof userExportableEntities[number];
fileId: MiDriveFile['id']; fileId: MiDriveFile['id'];
} | {
type: 'login';
id: string;
createdAt: string;
} | { } | {
type: 'app'; type: 'app';
id: string; id: string;

View File

@ -322,6 +322,16 @@ export const packedNotificationSchema = {
format: 'id', format: 'id',
}, },
}, },
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['login'],
},
},
}, { }, {
type: 'object', type: 'object',
properties: { properties: {

View File

@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false, nullable: false, optional: false,
enum: ['public', 'followers', 'private'], 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: { roles: {
type: 'array', type: 'array',
nullable: false, optional: false, nullable: false, optional: false,
@ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string', type: 'string',
nullable: false, optional: true, 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 //#region relations
isFollowing: { isFollowing: {
type: 'boolean', type: 'boolean',
@ -630,6 +627,21 @@ export const packedMeDetailedOnlySchema = {
nullable: false, optional: false, nullable: false, optional: false,
ref: 'RolePolicies', 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 //#region secrets
email: { email: {
type: 'string', type: 'string',

View File

@ -59,7 +59,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('inbox'); 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 @bindThis

View File

@ -118,25 +118,27 @@ export class ApiServerService {
'hcaptcha-response'?: string; 'hcaptcha-response'?: string;
'g-recaptcha-response'?: string; 'g-recaptcha-response'?: string;
'turnstile-response'?: string; 'turnstile-response'?: string;
'm-captcha-response'?: string;
} }
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); }>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
fastify.post<{ fastify.post<{
Body: { Body: {
username: string; username: string;
password: string; password?: string;
token?: string; token?: string;
signature?: string; credential?: AuthenticationResponseJSON;
authenticatorData?: string; 'hcaptcha-response'?: string;
clientDataJSON?: string; 'g-recaptcha-response'?: string;
credentialId?: string; 'turnstile-response'?: string;
challengeId?: string; 'm-captcha-response'?: string;
}; };
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply)); }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
fastify.post<{ fastify.post<{
Body: { Body: {
credential?: AuthenticationResponseJSON; credential?: AuthenticationResponseJSON;
context?: string;
}; };
}>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply)); }>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));

View File

@ -68,6 +68,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_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.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_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_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@ -453,6 +455,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_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_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_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_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_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 }; const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@ -842,6 +846,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove, $admin_relays_remove,
$admin_resetPassword, $admin_resetPassword,
$admin_resolveAbuseUserReport, $admin_resolveAbuseUserReport,
$admin_forwardAbuseUserReport,
$admin_updateAbuseUserReport,
$admin_sendEmail, $admin_sendEmail,
$admin_serverInfo, $admin_serverInfo,
$admin_showModerationLogs, $admin_showModerationLogs,
@ -1225,6 +1231,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove, $admin_relays_remove,
$admin_resetPassword, $admin_resetPassword,
$admin_resolveAbuseUserReport, $admin_resolveAbuseUserReport,
$admin_forwardAbuseUserReport,
$admin_updateAbuseUserReport,
$admin_sendEmail, $admin_sendEmail,
$admin_serverInfo, $admin_serverInfo,
$admin_showModerationLogs, $admin_showModerationLogs,

View File

@ -5,12 +5,14 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { import type {
MiMeta,
SigninsRepository, SigninsRepository,
UserProfilesRepository, UserProfilesRepository,
UserSecurityKeysRepository,
UsersRepository, UsersRepository,
} from '@/models/_.js'; } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -20,6 +22,8 @@ import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js'; import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.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 { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
@ -31,12 +35,18 @@ export class SigninApiService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.userSecurityKeysRepository)
private userSecurityKeysRepository: UserSecurityKeysRepository,
@Inject(DI.signinsRepository) @Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository, private signinsRepository: SigninsRepository,
@ -45,6 +55,7 @@ export class SigninApiService {
private signinService: SigninService, private signinService: SigninService,
private userAuthService: UserAuthService, private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService, private webAuthnService: WebAuthnService,
private captchaService: CaptchaService,
) { ) {
} }
@ -53,9 +64,13 @@ export class SigninApiService {
request: FastifyRequest<{ request: FastifyRequest<{
Body: { Body: {
username: string; username: string;
password: string; password?: string;
token?: string; token?: string;
credential?: AuthenticationResponseJSON; credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string;
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
}; };
}>, }>,
reply: FastifyReply, reply: FastifyReply,
@ -92,11 +107,6 @@ export class SigninApiService {
return; return;
} }
if (typeof password !== 'string') {
reply.code(400);
return;
}
if (token != null && typeof token !== 'string') { if (token != null && typeof token !== 'string') {
reply.code(400); reply.code(400);
return; return;
@ -121,11 +131,32 @@ export class SigninApiService {
} }
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); 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 // Compare password
const same = await bcrypt.compare(password, profile.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 // Append signin history
await this.signinsRepository.insert({ await this.signinsRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
@ -139,6 +170,32 @@ export class SigninApiService {
}; };
if (!profile.twoFactorEnabled) { 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) { if (same) {
return this.signinService.signin(request, reply, user); return this.signinService.signin(request, reply, user);
} else { } else {
@ -180,7 +237,7 @@ export class SigninApiService {
id: '93b86c4b-72f9-40eb-9815-798928603d1e', id: '93b86c4b-72f9-40eb-9815-798928603d1e',
}); });
} }
} else { } else if (securityKeysAvailable) {
if (!same && !profile.usePasswordLessLogin) { if (!same && !profile.usePasswordLessLogin) {
return await fail(403, { return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
@ -190,7 +247,23 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id); const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
reply.code(200); 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 // never get here
} }

View File

@ -4,13 +4,16 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.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 { IdService } from '@/core/IdService.js';
import type { MiLocalUser } from '@/models/User.js'; import type { MiLocalUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { EmailService } from '@/core/EmailService.js';
import { NotificationService } from '@/core/NotificationService.js';
import type { FastifyRequest, FastifyReply } from 'fastify'; import type { FastifyRequest, FastifyReply } from 'fastify';
@Injectable() @Injectable()
@ -19,7 +22,12 @@ export class SigninService {
@Inject(DI.signinsRepository) @Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository, private signinsRepository: SigninsRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private signinEntityService: SigninEntityService, private signinEntityService: SigninEntityService,
private emailService: EmailService,
private notificationService: NotificationService,
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
@ -28,7 +36,8 @@ export class SigninService {
@bindThis @bindThis
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) { public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
setImmediate(async () => { setImmediate(async () => {
// Append signin history this.notificationService.createNotification(user.id, 'login', {});
const record = await this.signinsRepository.insertOne({ const record = await this.signinsRepository.insertOne({
id: this.idService.gen(), id: this.idService.gen(),
userId: user.id, userId: user.id,
@ -37,15 +46,22 @@ export class SigninService {
success: true, success: true,
}); });
// Publish signin event
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); 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); reply.code(200);
return { return {
finished: true,
id: user.id, id: user.id,
i: user.token, i: user.token!,
}; } satisfies Misskey.entities.SigninFlowResponse;
} }
} }

View File

@ -74,6 +74,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_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.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_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_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@ -457,6 +459,8 @@ const eps = [
['admin/relays/remove', ep___admin_relays_remove], ['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword], ['admin/reset-password', ep___admin_resetPassword],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport], ['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/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo], ['admin/server-info', ep___admin_serverInfo],
['admin/show-moderation-logs', ep___admin_showModerationLogs], ['admin/show-moderation-logs', ep___admin_showModerationLogs],

View File

@ -71,9 +71,22 @@ export const meta = {
}, },
assignee: { assignee: {
type: 'object', type: 'object',
nullable: true, optional: true, nullable: true, optional: false,
ref: 'UserDetailedNotMe', ref: 'UserDetailedNotMe',
}, },
forwarded: {
type: 'boolean',
nullable: false, optional: false,
},
resolvedAs: {
type: 'string',
nullable: true, optional: false,
enum: ['accept', 'reject', null],
},
moderationNote: {
type: 'string',
nullable: false, optional: false,
},
}, },
}, },
}, },
@ -88,7 +101,6 @@ export const paramDef = {
state: { type: 'string', nullable: true, default: null }, state: { type: 'string', nullable: true, default: null },
reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
forwarded: { type: 'boolean', default: false },
}, },
required: [], required: [],
} as const; } as const;

View File

@ -12,11 +12,27 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.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'; import { Packed } from '@/misc/json-schema.js';
export const meta = { export const meta = {
tags: ['admin'], 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: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
@ -35,6 +51,7 @@ export const paramDef = {
properties: { properties: {
username: localUsernameSchema, username: localUsernameSchema,
password: passwordSchema, password: passwordSchema,
setupPassword: { type: 'string', nullable: true },
}, },
required: ['username', 'password'], required: ['username', 'password'],
} as const; } as const;
@ -42,6 +59,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -52,7 +72,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, _me, token) => { super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
const realUsers = await this.instanceActorService.realLocalUsersPresent(); 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({ const { account, secret } = await this.signupService.signup({
username: ps.username, username: ps.username,

View File

@ -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<typeof meta, typeof paramDef> { // 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);
});
}
}

View File

@ -32,7 +32,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
reportId: { type: 'string', format: 'misskey:id' }, reportId: { type: 'string', format: 'misskey:id' },
forward: { type: 'boolean', default: false }, resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
}, },
required: ['reportId'], required: ['reportId'],
} as const; } as const;
@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchAbuseReport); 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);
}); });
} }
} }

View File

@ -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<typeof meta, typeof paramDef> { // 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);
});
}
}

View File

@ -652,7 +652,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (Array.isArray(ps.federationHosts)) { if (Array.isArray(ps.federationHosts)) {
set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
} }
const before = await this.metaService.fetch(true); const before = await this.metaService.fetch(true);

View File

@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { FlashService } from '@/core/FlashService.js';
export const meta = { export const meta = {
tags: ['flash'], tags: ['flash'],
@ -27,26 +28,25 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: {}, properties: {
offset: { type: 'integer', minimum: 0, default: 0 },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [], required: [],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.flashsRepository) private flashService: FlashService,
private flashsRepository: FlashsRepository,
private flashEntityService: FlashEntityService, private flashEntityService: FlashEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.flashsRepository.createQueryBuilder('flash') const result = await this.flashService.featured({
.andWhere('flash.likedCount > 0') offset: ps.offset,
.orderBy('flash.likedCount', 'DESC'); limit: ps.limit,
});
const flashs = await query.limit(10).getMany(); return await this.flashEntityService.packMany(result, me);
return await this.flashEntityService.packMany(flashs, me);
}); });
} }
} }

View File

@ -17,6 +17,7 @@
* roleAssigned - * roleAssigned -
* achievementEarned - * achievementEarned -
* exportCompleted - * exportCompleted -
* login -
* app - * app -
* test - * test -
*/ */
@ -34,6 +35,7 @@ export const notificationTypes = [
'roleAssigned', 'roleAssigned',
'achievementEarned', 'achievementEarned',
'exportCompleted', 'exportCompleted',
'login',
'app', 'app',
'test', 'test',
] as const; ] as const;
@ -97,6 +99,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
'forwardAbuseReport',
'updateAbuseReportNote',
'createInvitation', 'createInvitation',
'createAd', 'createAd',
'updateAd', 'updateAd',
@ -265,7 +269,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: { resolveAbuseReport: {
reportId: string; reportId: string;
report: any; report: any;
forwarded: boolean; forwarded?: boolean;
resolvedAs?: string | null;
};
forwardAbuseReport: {
reportId: string;
report: any;
};
updateAbuseReportNote: {
reportId: string;
report: any;
before: string;
after: string;
}; };
createInvitation: { createInvitation: {
invitations: any[]; invitations: any[];

View File

@ -136,13 +136,7 @@ describe('2要素認証', () => {
keyName: string, keyName: string,
credentialId: Buffer, credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON, requestOptions: PublicKeyCredentialRequestOptionsJSON,
}): { }): misskey.entities.SigninFlowRequest => {
username: string,
password: string,
credential: AuthenticationResponseJSON,
'g-recaptcha-response'?: string | null,
'hcaptcha-response'?: string | null,
} => {
// AuthenticatorAssertionResponse.authenticatorData // AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const authenticatorData = Buffer.concat([ const authenticatorData = Buffer.concat([
@ -202,17 +196,21 @@ describe('2要素認証', () => {
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('users/show', { const signinWithoutTokenResponse = await api('signin-flow', {
username, ...signinParam(),
}, alice); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(signinWithoutTokenResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); assert.deepStrictEqual(signinWithoutTokenResponse.body, {
finished: false,
next: 'totp',
});
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
@ -253,27 +251,23 @@ describe('2要素認証', () => {
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName); assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('users/show', { const signinResponse = await api('signin-flow', {
username,
});
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined); assert.strictEqual(signinResponse.body.finished, false);
assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined); assert.strictEqual(signinResponse.body.next, 'passkey');
assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined); assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url')); 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, keyName,
credentialId, credentialId,
requestOptions: signinResponse.body, requestOptions: signinResponse.body.authRequest,
} as any)); }));
assert.strictEqual(signinResponse2.status, 200); assert.strictEqual(signinResponse2.status, 200);
assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け // 後片付け
@ -315,28 +309,30 @@ describe('2要素認証', () => {
}, alice); }, alice);
assert.strictEqual(passwordLessResponse.status, 204); assert.strictEqual(passwordLessResponse.status, 204);
const usersShowResponse = await api('users/show', { const iResponse = await api('i', {}, alice);
username, assert.strictEqual(iResponse.status, 200);
}); assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
password: '', password: '',
}); });
assert.strictEqual(signinResponse.status, 200); 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({ ...signinWithSecurityKeyParam({
keyName, keyName,
credentialId, credentialId,
requestOptions: signinResponse.body, requestOptions: signinResponse.body.authRequest,
} as any), } as any),
password: '', password: '',
}); });
assert.strictEqual(signinResponse2.status, 200); assert.strictEqual(signinResponse2.status, 200);
assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け // 後片付け
@ -424,11 +420,11 @@ describe('2要素認証', () => {
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す // テストの実行順によっては複数残ってるので全部消す
const iResponse = await api('i', { const beforeIResponse = await api('i', {
}, alice); }, alice);
assert.strictEqual(iResponse.status, 200); assert.strictEqual(beforeIResponse.status, 200);
assert.ok(iResponse.body.securityKeysList); assert.ok(beforeIResponse.body.securityKeysList);
for (const key of iResponse.body.securityKeysList) { for (const key of beforeIResponse.body.securityKeysList) {
const removeKeyResponse = await api('i/2fa/remove-key', { const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
@ -437,17 +433,16 @@ describe('2要素認証', () => {
assert.strictEqual(removeKeyResponse.status, 200); assert.strictEqual(removeKeyResponse.status, 200);
} }
const usersShowResponse = await api('users/show', { const afterIResponse = await api('i', {}, alice);
username, assert.strictEqual(afterIResponse.status, 200);
}); assert.strictEqual(afterIResponse.body.securityKeys, false);
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
@ -468,11 +463,9 @@ describe('2要素認証', () => {
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('users/show', { const iResponse = await api('i', {}, alice);
username, assert.strictEqual(iResponse.status, 200);
}); assert.strictEqual(iResponse.body.twoFactorEnabled, true);
assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
const unregisterResponse = await api('i/2fa/unregister', { const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
@ -480,10 +473,11 @@ describe('2要素認証', () => {
}, alice); }, alice);
assert.strictEqual(unregisterResponse.status, 204); assert.strictEqual(unregisterResponse.status, 204);
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け

View File

@ -66,9 +66,9 @@ describe('Endpoints', () => {
}); });
}); });
describe('signin', () => { describe('signin-flow', () => {
test('間違ったパスワードでサインインできない', async () => { test('間違ったパスワードでサインインできない', async () => {
const res = await api('signin', { const res = await api('signin-flow', {
username: 'test1', username: 'test1',
password: 'bar', password: 'bar',
}); });
@ -77,7 +77,7 @@ describe('Endpoints', () => {
}); });
test('クエリをインジェクションできない', async () => { test('クエリをインジェクションできない', async () => {
const res = await api('signin', { const res = await api('signin-flow', {
username: 'test1', username: 'test1',
// @ts-expect-error password must be string // @ts-expect-error password must be string
password: { password: {
@ -89,7 +89,7 @@ describe('Endpoints', () => {
}); });
test('正しい情報でサインインできる', async () => { test('正しい情報でサインインできる', async () => {
const res = await api('signin', { const res = await api('signin-flow', {
username: 'test1', username: 'test1',
password: 'test1', password: 'test1',
}); });

View File

@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: webhookBody1.body.id, reportId: webhookBody1.body.id,
forward: false,
}, admin); }, admin);
}); });
@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: abuseReportId, reportId: abuseReportId,
forward: false,
}, admin); }, admin);
}); });
@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: webhookBody1.body.id, reportId: webhookBody1.body.id,
forward: false,
}, admin); }, admin);
}).catch(e => e.message); }).catch(e => e.message);
@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: abuseReportId, reportId: abuseReportId,
forward: false,
}, admin); }, admin);
}).catch(e => e.message); }).catch(e => e.message);
@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: abuseReportId, reportId: abuseReportId,
forward: false,
}, admin); }, admin);
}).catch(e => e.message); }).catch(e => e.message);
@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: abuseReportId, reportId: abuseReportId,
forward: false,
}, admin); }, admin);
}).catch(e => e.message); }).catch(e => e.message);

View File

@ -83,9 +83,6 @@ describe('ユーザー', () => {
publicReactions: user.publicReactions, publicReactions: user.publicReactions,
followingVisibility: user.followingVisibility, followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility, followersVisibility: user.followersVisibility,
twoFactorEnabled: user.twoFactorEnabled,
usePasswordLessLogin: user.usePasswordLessLogin,
securityKeys: user.securityKeys,
roles: user.roles, roles: user.roles,
memo: user.memo, memo: user.memo,
}); });
@ -149,6 +146,9 @@ describe('ユーザー', () => {
achievements: user.achievements, achievements: user.achievements,
loggedInDays: user.loggedInDays, loggedInDays: user.loggedInDays,
policies: user.policies, policies: user.policies,
twoFactorEnabled: user.twoFactorEnabled,
usePasswordLessLogin: user.usePasswordLessLogin,
securityKeys: user.securityKeys,
...(security ? { ...(security ? {
email: user.email, email: user.email,
emailVerified: user.emailVerified, emailVerified: user.emailVerified,
@ -343,9 +343,6 @@ describe('ユーザー', () => {
assert.strictEqual(response.publicReactions, true); assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.followingVisibility, 'public'); assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, '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.deepStrictEqual(response.roles, []);
assert.strictEqual(response.memo, null); assert.strictEqual(response.memo, null);
@ -385,6 +382,9 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.achievements, []); assert.deepStrictEqual(response.achievements, []);
assert.deepStrictEqual(response.loggedInDays, 0); assert.deepStrictEqual(response.loggedInDays, 0);
assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); 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.notStrictEqual(response.email, undefined);
assert.strictEqual(response.emailVerified, false); assert.strictEqual(response.emailVerified, false);
assert.deepStrictEqual(response.securityKeysList, []); assert.deepStrictEqual(response.securityKeysList, []);
@ -618,6 +618,9 @@ describe('ユーザー', () => {
{ label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator }, { label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
// @ts-expect-error UserDetailedNotMe doesn't include isModerator // @ts-expect-error UserDetailedNotMe doesn't include isModerator
{ label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined }, { 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 }, { label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
// FIXME: 落ちる // FIXME: 落ちる
//{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended }, //{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },

View File

@ -5,6 +5,7 @@
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { randomString } from '../utils.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { import {
AbuseReportNotificationRecipientRepository, AbuseReportNotificationRecipientRepository,
@ -25,7 +26,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { randomString } from '../utils.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
@ -110,6 +111,9 @@ describe('AbuseReportNotificationService', () => {
{ {
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }), provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
}, },
{
provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
},
{ {
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
}, },

View File

@ -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<MiFlash>) {
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<MiUser> = {}) {
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]);
});
});
});

View File

@ -18,7 +18,7 @@
"@tabler/icons-webfont": "3.3.0", "@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.10", "@vue/compiler-sfc": "3.5.11",
"astring": "1.9.0", "astring": "1.9.0",
"buraha": "0.0.1", "buraha": "0.0.1",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
@ -27,8 +27,8 @@
"frontend-shared": "workspace:*", "frontend-shared": "workspace:*",
"punycode": "2.3.1", "punycode": "2.3.1",
"rollup": "4.22.5", "rollup": "4.22.5",
"sass": "1.79.3", "sass": "1.79.4",
"shiki": "1.12.0", "shiki": "1.21.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
@ -36,7 +36,7 @@
"uuid": "10.0.0", "uuid": "10.0.0",
"json5": "2.2.3", "json5": "2.2.3",
"vite": "5.4.8", "vite": "5.4.8",
"vue": "3.5.10" "vue": "3.5.11"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/summaly": "5.1.0", "@misskey-dev/summaly": "5.1.0",
@ -51,10 +51,10 @@
"@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0", "@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0", "@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.10", "@vue/runtime-core": "3.5.11",
"acorn": "8.12.1", "acorn": "8.12.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint-plugin-import": "2.30.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.28.0", "eslint-plugin-vue": "9.28.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"happy-dom": "10.0.3", "happy-dom": "10.0.3",

View File

@ -38,8 +38,6 @@ const props = defineProps<{
host?: string | null; host?: string | null;
url?: string; url?: string;
useOriginalSize?: boolean; useOriginalSize?: boolean;
menu?: boolean;
menuReaction?: boolean;
fallbackToImage?: boolean; fallbackToImage?: boolean;
}>(); }>();

View File

@ -6,6 +6,7 @@
import { VNode, h, SetupContext, provide } from 'vue'; import { VNode, h, SetupContext, provide } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import EmUrl from '@/components/EmUrl.vue'; import EmUrl from '@/components/EmUrl.vue';
import EmTime from '@/components/EmTime.vue'; import EmTime from '@/components/EmTime.vue';
import EmLink from '@/components/EmLink.vue'; import EmLink from '@/components/EmLink.vue';
@ -13,7 +14,6 @@ import EmMention from '@/components/EmMention.vue';
import EmEmoji from '@/components/EmEmoji.vue'; import EmEmoji from '@/components/EmEmoji.vue';
import EmCustomEmoji from '@/components/EmCustomEmoji.vue'; import EmCustomEmoji from '@/components/EmCustomEmoji.vue';
import EmA from '@/components/EmA.vue'; import EmA from '@/components/EmA.vue';
import { host } from '@@/js/config.js';
function safeParseFloat(str: unknown): number | null { function safeParseFloat(str: unknown): number | null {
if (typeof str !== 'string' || str === '') return null; if (typeof str !== 'string' || str === '') return null;
@ -41,9 +41,6 @@ type MfmProps = {
rootScale?: number; rootScale?: number;
nyaize?: boolean | 'respect'; nyaize?: boolean | 'respect';
parsedNodes?: mfm.MfmNode[] | null; parsedNodes?: mfm.MfmNode[] | null;
enableEmojiMenu?: boolean;
enableEmojiMenuReaction?: boolean;
linkNavigationBehavior?: string;
}; };
type MfmEvents = { type MfmEvents = {
@ -52,8 +49,6 @@ type MfmEvents = {
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) { export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
provide('linkNavigationBehavior', props.linkNavigationBehavior);
const isNote = props.isNote ?? true; const isNote = props.isNote ?? true;
const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
@ -397,8 +392,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
normal: props.plain, normal: props.plain,
host: null, host: null,
useOriginalSize: scale >= 2.5, useOriginalSize: scale >= 2.5,
menu: props.enableEmojiMenu,
menuReaction: props.enableEmojiMenuReaction,
fallbackToImage: false, fallbackToImage: false,
})]; })];
} else { } else {

View File

@ -91,6 +91,11 @@ export function getConfig(): UserConfig {
} }
}, },
}, },
preprocessorOptions: {
scss: {
api: 'modern-compiler',
},
},
}, },
define: { define: {

View File

@ -68,6 +68,7 @@ export const notificationTypes = [
'roleAssigned', 'roleAssigned',
'achievementEarned', 'achievementEarned',
'exportCompleted', 'exportCompleted',
'login',
'test', 'test',
'app', 'app',
] as const; ] as const;

View File

@ -397,7 +397,18 @@ function toStories(component: string): Promise<string> {
const globs = await Promise.all([ const globs = await Promise.all([
glob('src/components/global/Mk*.vue'), glob('src/components/global/Mk*.vue'),
glob('src/components/global/RouterView.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/MkFlashPreview.vue'),
glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkGalleryPostPreview.vue'),
glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkSignupServerRules.vue'),

View File

@ -28,7 +28,7 @@
"@tabler/icons-webfont": "3.3.0", "@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.10", "@vue/compiler-sfc": "3.5.11",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
"astring": "1.9.0", "astring": "1.9.0",
"broadcast-channel": "7.0.0", "broadcast-channel": "7.0.0",
@ -39,12 +39,13 @@
"chartjs-chart-matrix": "2.0.1", "chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.0.1",
"chromatic": "11.10.4", "chromatic": "11.11.0",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.2", "cropperjs": "2.0.0-rc.2",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"frontend-shared": "workspace:*",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
@ -54,13 +55,12 @@
"misskey-bubble-game": "workspace:*", "misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"frontend-shared": "workspace:*",
"photoswipe": "5.4.4", "photoswipe": "5.4.4",
"punycode": "2.3.1", "punycode": "2.3.1",
"rollup": "4.22.5", "rollup": "4.22.5",
"sanitize-html": "2.13.0", "sanitize-html": "2.13.1",
"sass": "1.79.3", "sass": "1.79.3",
"shiki": "1.12.0", "shiki": "1.21.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.169.0", "three": "0.169.0",
@ -72,30 +72,31 @@
"uuid": "10.0.0", "uuid": "10.0.0",
"v-code-diff": "1.13.1", "v-code-diff": "1.13.1",
"vite": "5.4.8", "vite": "5.4.8",
"vue": "3.5.10", "vue": "3.5.11",
"vuedraggable": "next" "vuedraggable": "next"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/summaly": "5.1.0", "@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.3.3", "@storybook/addon-actions": "8.3.4",
"@storybook/addon-essentials": "8.3.3", "@storybook/addon-essentials": "8.3.4",
"@storybook/addon-interactions": "8.3.3", "@storybook/addon-interactions": "8.3.4",
"@storybook/addon-links": "8.3.3", "@storybook/addon-links": "8.3.4",
"@storybook/addon-mdx-gfm": "8.3.3", "@storybook/addon-mdx-gfm": "8.3.4",
"@storybook/addon-storysource": "8.3.3", "@storybook/addon-storysource": "8.3.4",
"@storybook/blocks": "8.3.3", "@storybook/blocks": "8.3.4",
"@storybook/components": "8.3.3", "@storybook/components": "8.3.4",
"@storybook/core-events": "8.3.3", "@storybook/core-events": "8.3.4",
"@storybook/manager-api": "8.3.3", "@storybook/manager-api": "8.3.4",
"@storybook/preview-api": "8.3.3", "@storybook/preview-api": "8.3.4",
"@storybook/react": "8.3.3", "@storybook/react": "8.3.4",
"@storybook/react-vite": "8.3.3", "@storybook/react-vite": "8.3.4",
"@storybook/test": "8.3.3", "@storybook/test": "8.3.4",
"@storybook/theming": "8.3.3", "@storybook/theming": "8.3.4",
"@storybook/types": "8.3.3", "@storybook/types": "8.3.4",
"@storybook/vue3": "8.3.3", "@storybook/vue3": "8.3.4",
"@storybook/vue3-vite": "8.3.3", "@storybook/vue3-vite": "8.3.4",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "^1.6.4",
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@types/matter-js": "0.19.7", "@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
@ -110,11 +111,11 @@
"@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0", "@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0", "@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.10", "@vue/runtime-core": "3.5.11",
"acorn": "8.12.1", "acorn": "8.12.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.15.0", "cypress": "13.15.0",
"eslint-plugin-import": "2.30.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.28.0", "eslint-plugin-vue": "9.28.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"happy-dom": "10.0.3", "happy-dom": "10.0.3",
@ -128,7 +129,7 @@
"react-dom": "18.3.1", "react-dom": "18.3.1",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"start-server-and-test": "2.0.8", "start-server-and-test": "2.0.8",
"storybook": "8.3.3", "storybook": "8.3.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "1.6.0", "vitest": "1.6.0",

View File

@ -230,19 +230,25 @@ export async function mainBoot() {
claimAchievement('collectAchievements30'); claimAchievement('collectAchievements30');
} }
if (!claimedAchievements.includes('justPlainLucky')) {
window.setInterval(() => { window.setInterval(() => {
if (Math.floor(Math.random() * 20000) === 0) { if (Math.floor(Math.random() * 20000) === 0) {
claimAchievement('justPlainLucky'); claimAchievement('justPlainLucky');
} }
}, 1000 * 10); }, 1000 * 10);
}
if (!claimedAchievements.includes('client30min')) {
window.setTimeout(() => { window.setTimeout(() => {
claimAchievement('client30min'); claimAchievement('client30min');
}, 1000 * 60 * 30); }, 1000 * 60 * 30);
}
if (!claimedAchievements.includes('client60min')) {
window.setTimeout(() => { window.setTimeout(() => {
claimAchievement('client60min'); claimAchievement('client60min');
}, 1000 * 60 * 60); }, 1000 * 60 * 60);
}
// 邪魔 // 邪魔
//const lastUsed = miLocalStorage.getItem('lastUsed'); //const lastUsed = miLocalStorage.getItem('lastUsed');

View File

@ -4,112 +4,153 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div class="bcekxzvu _margin _panel"> <MkFolder>
<div class="target"> <template #icon>
<MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'"> <i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--success)"></i>
<MkAvatar class="avatar" :user="report.targetUser" indicator/> <i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--error)"></i>
<div class="names"> <i v-else-if="report.resolved" class="ti ti-slash"></i>
<MkUserName class="name" :user="report.targetUser"/> <i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i>
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/> </template>
<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
<template #caption>{{ report.comment }}</template>
<template #suffix><MkTime :time="report.createdAt"/></template>
<template #footer>
<div class="_buttons">
<template v-if="!report.resolved">
<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
<MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
</template>
<template v-if="report.targetUser.host != null">
<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
</template>
<button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div> </div>
</MkA> </template>
<MkKeyValue>
<template #key>{{ i18n.ts.registeredDate }}</template> <div :class="$style.root" class="_gaps_s">
<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template> <MkFolder :withSpacer="false">
</MkKeyValue> <template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
<template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
<div style="container-type: inline-size;">
<RouterView :router="targetRouter"/>
</div> </div>
<div class="detail"> </MkFolder>
<div>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-message-2"></i></template>
<template #label>{{ i18n.ts.details }}</template>
<div class="_gaps_s">
<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/> <Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
</div> </div>
<hr/> </MkFolder>
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>
<MkFolder :withSpacer="false">
<template #icon><MkAvatar :user="report.reporter" style="width: 18px; height: 18px;"/></template>
<template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template>
<template #suffix>#{{ report.reporterId.toUpperCase() }}</template>
<div style="container-type: inline-size;">
<RouterView :router="reporterRouter"/>
</div>
</MkFolder>
<MkFolder :defaultOpen="false">
<template #icon><i class="ti ti-message-2"></i></template>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template>
<div class="_gaps_s">
<MkTextarea v-model="moderationNote" manualSave>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
</div>
</MkFolder>
<div v-if="report.assignee"> <div v-if="report.assignee">
{{ i18n.ts.moderator }}: {{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/> <MkAcct :user="report.assignee"/>
</div> </div>
<div><MkTime :time="report.createdAt"/></div>
<div class="action">
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
{{ i18n.ts.forwardReport }}
<template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
</MkSwitch>
<MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
</div>
</div>
</div> </div>
</MkFolder>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { provide, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js'; import { dateString } from '@/filters/date.js';
import MkFolder from '@/components/MkFolder.vue';
import RouterView from '@/components/global/RouterView.vue';
import { useRouterFactory } from '@/router/supplier';
import MkTextarea from '@/components/MkTextarea.vue';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{ const props = defineProps<{
report: any; report: Misskey.entities.AdminAbuseUserReportsResponse[number];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'resolved', reportId: string): void; (ev: 'resolved', reportId: string): void;
}>(); }>();
const forward = ref(props.report.forwarded); const routerFactory = useRouterFactory();
const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`);
targetRouter.init();
const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
reporterRouter.init();
function resolve() { const moderationNote = ref(props.report.moderationNote ?? '');
os.apiWithDialog('admin/resolve-abuse-user-report', {
forward: forward.value, watch(moderationNote, async () => {
os.apiWithDialog('admin/update-abuse-user-report', {
reportId: props.report.id, reportId: props.report.id,
moderationNote: moderationNote.value,
}).then(() => {
});
});
function resolve(resolvedAs) {
os.apiWithDialog('admin/resolve-abuse-user-report', {
reportId: props.report.id,
resolvedAs,
}).then(() => { }).then(() => {
emit('resolved', props.report.id); emit('resolved', props.report.id);
}); });
} }
function forward() {
os.apiWithDialog('admin/forward-abuse-user-report', {
reportId: props.report.id,
}).then(() => {
});
}
function showMenu(ev: MouseEvent) {
os.popupMenu([{
icon: 'ti ti-id',
text: 'Copy ID',
action: () => {
copyToClipboard(props.report.id);
},
}, {
icon: 'ti ti-json',
text: 'Copy JSON',
action: () => {
copyToClipboard(JSON.stringify(props.report, null, '\t'));
},
}], ev.currentTarget ?? ev.target);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.bcekxzvu { .root {
display: flex;
> .target {
width: 35%;
box-sizing: border-box;
text-align: left;
padding: 24px;
border-right: solid 1px var(--divider);
> .info {
display: flex;
box-sizing: border-box;
align-items: center;
padding: 14px;
border-radius: 8px;
--c: rgb(255 196 0 / 15%);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
> .avatar {
width: 42px;
height: 42px;
}
> .names {
margin-left: 0.3em;
padding: 0 8px;
flex: 1;
> .name {
font-weight: bold;
}
}
}
}
> .detail {
flex: 1;
padding: 24px;
}
} }
</style> </style>

View File

@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { StoryObj } from '@storybook/vue3';
import MkExtensionInstaller from './MkExtensionInstaller.vue';
import lightTheme from '@@/themes/_light.json5';
export const Plugin = {
render(args) {
return {
components: {
MkExtensionInstaller,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkExtensionInstaller v-bind="props" />',
};
},
args: {
extension: {
type: 'plugin',
raw: '"do nothing"',
meta: {
name: 'do nothing plugin',
version: '1.0',
author: 'syuilo and misskey-project',
description: 'a plugin that does nothing',
permissions: ['read:account'],
config: {
'doNothing': true,
},
},
},
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkExtensionInstaller>;
export const Theme = {
render(args) {
return {
components: {
MkExtensionInstaller,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkExtensionInstaller v-bind="props" />',
};
},
args: {
extension: {
type: 'theme',
raw: JSON.stringify(lightTheme),
meta: lightTheme,
},
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkExtensionInstaller>;

View File

@ -0,0 +1,146 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps_m" :class="$style.extInstallerRoot">
<div :class="$style.extInstallerIconWrapper">
<i v-if="isPlugin" class="ti ti-plug"></i>
<i v-else-if="isTheme" class="ti ti-palette"></i>
<!-- 拡張用 -->
<i v-else class="ti ti-download"></i>
</div>
<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2>
<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
<MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
<FormSection>
<template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template>
<div class="_gaps_s">
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts.name }}</template>
<template #value>{{ extension.meta.name }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.author }}</template>
<template #value>{{ extension.meta.author }}</template>
</MkKeyValue>
</FormSplit>
<MkKeyValue v-if="isPlugin">
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
</MkKeyValue>
<MkKeyValue v-if="isPlugin">
<template #key>{{ i18n.ts.version }}</template>
<template #value>{{ extension.meta.version }}</template>
</MkKeyValue>
<MkKeyValue v-if="isPlugin">
<template #key>{{ i18n.ts.permission }}</template>
<template #value>
<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
</ul>
<template v-else>{{ i18n.ts.none }}</template>
</template>
</MkKeyValue>
<MkKeyValue v-if="isTheme">
<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
</MkKeyValue>
<MkFolder>
<template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
<MkCode :code="extension.raw"/>
</MkFolder>
</div>
</FormSection>
<slot name="additionalInfo"/>
<div class="_buttonsCenter">
<MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
</div>
</div>
</template>
<script lang="ts">
export type Extension = {
type: 'plugin';
raw: string;
meta: {
name: string;
version: string;
author: string;
description?: string;
permissions?: string[];
config?: Record<string, any>;
};
} | {
type: 'theme';
raw: string;
meta: {
name: string;
author: string;
base?: 'light' | 'dark';
};
};
</script>
<script lang="ts" setup>
import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import MkCode from '@/components/MkCode.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import { i18n } from '@/i18n.js';
const isPlugin = computed(() => props.extension.type === 'plugin');
const isTheme = computed(() => props.extension.type === 'theme');
const props = defineProps<{
extension: Extension;
}>();
const emits = defineEmits<{
(ev: 'confirm'): void;
}>();
</script>
<style lang="scss" module>
.extInstallerRoot {
border-radius: var(--radius);
background: var(--panel);
padding: 1.5rem;
}
.extInstallerIconWrapper {
width: 48px;
height: 48px;
font-size: 24px;
line-height: 48px;
text-align: center;
border-radius: 50%;
margin-left: auto;
margin-right: auto;
background-color: var(--accentedBg);
color: var(--accent);
}
.extInstallerTitle {
font-size: 1.2rem;
text-align: center;
margin: 0;
}
.extInstallerNormDesc {
text-align: center;
}
.extInstallerKVList {
margin-top: 0;
margin-bottom: 0;
}
</style>

View File

@ -38,9 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<KeepAlive> <KeepAlive>
<div v-show="opened"> <div v-show="opened">
<MkSpacer :marginMin="14" :marginMax="22"> <MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22">
<slot></slot> <slot></slot>
</MkSpacer> </MkSpacer>
<div v-else>
<slot></slot>
</div>
<div v-if="$slots.footer" :class="$style.footer"> <div v-if="$slots.footer" :class="$style.footer">
<slot name="footer"></slot> <slot name="footer"></slot>
</div> </div>
@ -59,9 +62,11 @@ import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
defaultOpen?: boolean; defaultOpen?: boolean;
maxHeight?: number | null; maxHeight?: number | null;
withSpacer?: boolean;
}>(), { }>(), {
defaultOpen: false, defaultOpen: false,
maxHeight: null, maxHeight: null,
withSpacer: true,
}); });
const getBgColor = (el: HTMLElement) => { const getBgColor = (el: HTMLElement) => {

View File

@ -0,0 +1,100 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
:class="[
$style.root,
tail === 'left' ? $style.left : $style.right,
negativeMargin === true && $style.negativeMargin,
shadow === true && $style.shadow,
]"
>
<div :class="$style.bg">
<svg v-if="tail !== 'none'" :class="$style.tail" version="1.1" viewBox="0 0 14.597 14.58" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-173.71 -87.184)">
<path d="m188.19 87.657c-1.469 2.3218-3.9315 3.8312-6.667 4.0865-2.2309-1.7379-4.9781-2.6816-7.8061-2.6815h-5.1e-4v12.702h12.702v-5.1e-4c2e-5 -1.9998-0.47213-3.9713-1.378-5.754 2.0709-1.6834 3.2732-4.2102 3.273-6.8791-6e-5 -0.49375-0.0413-0.98662-0.1235-1.4735z" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-width=".33225" style="paint-order:stroke fill markers"/>
</g>
</svg>
<div :class="$style.content">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
tail?: 'left' | 'right' | 'none';
negativeMargin?: boolean;
shadow?: boolean;
}>(), {
tail: 'right',
negativeMargin: false,
shadow: false,
});
</script>
<style module lang="scss">
.root {
--fukidashi-radius: var(--radius);
--fukidashi-bg: var(--panel);
position: relative;
display: inline-block;
min-height: calc(var(--fukidashi-radius) * 2);
padding-top: calc(var(--fukidashi-radius) * .13);
&.shadow {
filter: drop-shadow(0 4px 32px var(--shadow));
}
&.left {
padding-left: calc(var(--fukidashi-radius) * .13);
&.negativeMargin {
margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}
&.right {
padding-right: calc(var(--fukidashi-radius) * .13);
&.negativeMargin {
margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}
}
.bg {
width: 100%;
height: 100%;
background: var(--fukidashi-bg);
border-radius: var(--fukidashi-radius);
}
.content {
position: relative;
padding: 8px 12px;
}
.tail {
position: absolute;
top: 0;
display: block;
width: calc(var(--fukidashi-radius) * 1.13);
height: auto;
fill: var(--fukidashi-bg);
}
.left .tail {
left: 0;
transform: rotateY(180deg);
}
.right .tail {
right: 0;
}
</style>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior"> <MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :behavior="navigationBehavior">
<img :class="$style.icon" :src="avatarUrl" alt=""> <img :class="$style.icon" :src="avatarUrl" alt="">
<span> <span>
<span>@{{ username }}</span> <span>@{{ username }}</span>
@ -16,7 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import { computed } from 'vue'; import { computed } from 'vue';
import tinycolor from 'tinycolor2';
import { host as localHost } from '@@/js/config.js'; import { host as localHost } from '@@/js/config.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@ -37,11 +36,7 @@ const isMe = $i && (
`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase() `@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
); );
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention')); const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`) ? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
: `/avatar/@${props.username}@${props.host}`, : `/avatar/@${props.username}@${props.host}`,
); );
@ -53,9 +48,11 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
padding: 4px 8px 4px 4px; padding: 4px 8px 4px 4px;
border-radius: 999px; border-radius: 999px;
color: var(--mention); color: var(--mention);
background: color(from var(--mention) srgb r g b / 0.1);
&.isMe { &.isMe {
color: var(--mentionMe); color: var(--mentionMe);
background: color(from var(--mentionMe) srgb r g b / 0.1);
} }
} }

View File

@ -437,9 +437,11 @@ onBeforeUnmount(() => {
&.big:not(.asDrawer) { &.big:not(.asDrawer) {
> .menu { > .menu {
min-width: 230px;
> .item { > .item {
padding: 6px 20px; padding: 6px 20px;
font-size: 1em; font-size: 0.95em;
line-height: 24px; line-height: 24px;
} }
} }

View File

@ -5,8 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<header :class="$style.root"> <header :class="$style.root">
<component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;">
<div style="display: flex; white-space: nowrap; align-items: baseline;">
<div v-if="mock" :class="$style.name"> <div v-if="mock" :class="$style.name">
<MkUserName :user="note.user"/> <MkUserName :user="note.user"/>
</div> </div>
@ -15,8 +13,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA> </MkA>
<div v-if="note.user.isBot" :class="$style.isBot">bot</div> <div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div> <div :class="$style.username"><MkAcct :user="note.user"/></div>
</div>
</component>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> <div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/> <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
</div> </div>

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