2024.11.0-yumechinokuni.4 #18

Merged
yume merged 31 commits from develop into master 2024-11-15 18:52:01 -06:00
75 changed files with 3340 additions and 3732 deletions

View file

@ -168,8 +168,8 @@ id: 'aidx'
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# ──────────────┐
#──┘ Web Security └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
@ -180,6 +180,24 @@ id: 'aidx'
# - https://hstspreload.org/
#hstsPreload: false
# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks.
# browserSandboxing:
# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true.
# strictOriginReferrer: true
# csp:
# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution.
# disable: false
# # Merge additional directives into the CSP header. The default is an empty object.
# # You may want to list your CDN or other trusted domains here.
# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added.
# appendDirectives:
# 'script-src':
# - "'unsafe-eval'" # do not use this ... just an example
# - 'https://example.com'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Number of worker processes
#clusterLimit: 1

View file

@ -162,8 +162,8 @@ id: 'aidx'
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# ──────────────┐
#──┘ Web Security └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
@ -174,6 +174,24 @@ id: 'aidx'
# - https://hstspreload.org/
#hstsPreload: false
# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks.
# browserSandboxing:
# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true.
# strictOriginReferrer: true
# csp:
# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution.
# disable: false
# # Merge additional directives into the CSP header. The default is an empty object.
# # You may want to list your CDN or other trusted domains here.
# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added.
# appendDirectives:
# 'script-src':
# - "'unsafe-eval'" # do not use this ... just an example
# - 'https://example.com'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Number of worker processes
#clusterLimit: 1

View file

@ -244,8 +244,8 @@ id: 'aidx'
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# ──────────────┐
#──┘ Web Security └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
@ -256,6 +256,24 @@ id: 'aidx'
# - https://hstspreload.org/
#hstsPreload: false
# Enable additional security headers that reduce the risk of XSS attacks or privacy leaks.
# browserSandboxing:
# # Do not send the Referrer header to other domains. The default when browserSandboxing is missing is true.
# strictOriginReferrer: true
# csp:
# # Do not send a CSP header. The default is a strict CSP header that prevents any form of external fetching or execution.
# disable: false
# # Merge additional directives into the CSP header. The default is an empty object.
# # You may want to list your CDN or other trusted domains here.
# # Media proxies are automatically added to the CSP header. This is an exception, things like Sentry will not be automatically added.
# appendDirectives:
# 'script-src':
# - "'unsafe-eval'" # do not use this ... just an example
# - 'https://example.com'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Number of worker processes
#clusterLimit: 1

View file

@ -5,7 +5,7 @@
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20.16.0"
"version": "22.11.0"
},
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
},

View file

@ -1,8 +1,10 @@
.autogen
.github
.forgejo
.travis
.vscode
.config
Dockerfile
build/
built/
@ -28,3 +30,5 @@ fluent-emojis/
.idea/
packages/*/.vscode/
packages/backend/test/compose.yml
/yume-mods

View file

@ -24,7 +24,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
services:
postgres:
@ -64,7 +64,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
services:
postgres:

View file

@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.1.1

View file

@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
api-json-name: [api-base.json, api-head.json]
include:
- api-json-name: api-base.json

View file

@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.1.1

View file

@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
services:
postgres:
@ -61,7 +61,7 @@ jobs:
- name: Test
run: pnpm --filter backend test-and-coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json
@ -71,7 +71,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
services:
postgres:
@ -108,7 +108,7 @@ jobs:
- name: Test
run: pnpm --filter backend test-and-coverage:e2e
- name: Upload to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4
with:

View file

@ -26,7 +26,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.1.1
@ -50,7 +50,7 @@ jobs:
- name: Test
run: pnpm --filter frontend test-and-coverage
- name: Upload Coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/frontend/coverage/coverage-final.json
@ -61,7 +61,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
browser: [chrome]
services:

View file

@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@ -51,7 +51,7 @@ jobs:
CI: true
- name: Upload Coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/misskey-js/coverage/coverage-final.json

View file

@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.1.1

View file

@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
node-version: [20.16.0]
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.1.1

View file

@ -1 +1 @@
20.16.0
22.11.0

View file

@ -1,3 +1,10 @@
## 2024.11.0-yumechinokuni.4
- Upstream: 2024.11.0-alpha.1 タッグをマージする
- DevOps: 管理者アクセストークンがユーザー登録できるようになる (write:admin:create-account)
- Frontend: Stream再接続ロジックdata raceを修正
- Security: CSPにCDNなどの外部ホストはホワイトリストできるように
## 2024.11.0-yumechinokuni.3
- Security: CSPの設定を強化
@ -5,6 +12,10 @@
## 2024.11.0
### Note
- Node.js 20.xは非推奨になりました。Node.js 22.x (LTS)の利用を推奨します。
- DockerのNode.jsが22.11.0に更新されました
### General
- Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
@ -24,6 +35,9 @@
- Enhance: Self-XSS防止用の警告を追加
- Enhance: カタルーニャ語 (ca-ES) に対応
- Enhance: 個別お知らせページではMetaタグを出力するように
- Enhance: ノート詳細画面にロールのバッジを表示
- Enhance: 過去に送信したフォローリクエストを確認できるように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
@ -33,8 +47,11 @@
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/815)
### Server
- Enhance: DockerのNode.jsを22.11.0に更新
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
@ -50,6 +67,7 @@
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
- Fix: User Webhookテスト機能のMock Payloadを修正
### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
ARG NODE_VERSION=20.16.0-bullseye
ARG NODE_VERSION=22.11.0-bullseye
# build assets & compile TypeScript

View file

@ -64,6 +64,7 @@ services:
restart: always
image: l1drm/postgres-pgroonga:alpine-15-znver4
user: "${MISSKEY_UID}:${MISSKEY_GID}"
shm_size: 2gb
networks:
- internal_network
env_file:

View file

@ -8,37 +8,37 @@ search: "Cercar"
notifications: "Notificacions"
username: "Nom d'usuari"
password: "Contrasenya"
initialPasswordForSetup: "Contrasenya inicial per la configuració inicial"
initialPasswordForSetup: "Contrasenya inicial per fer la primera configuració "
initialPasswordIsIncorrect: "La contrasenya no és correcta."
initialPasswordForSetupDescription: "Fes servir la contrasenya que has fet servir al fitxer de configuració, si tu mateix has instal·lat Misskey.\nSi fas servir una empresa d'allotjament de Misskey, fes servir la contrasenya que t'han donat.\nSi no has posat cap contrasenya deixar l'espai en blanc."
forgotPassword: "Contrasenya oblidada"
fetchingAsApObject: "Cercant en el Fediverse..."
forgotPassword: "Restableix la contrasenya "
fetchingAsApObject: "Cercant al Fediverse..."
ok: "OK"
gotIt: "Ho he entès!"
gotIt: "D'acord "
cancel: "Cancel·lar"
noThankYou: "No, gràcies"
enterUsername: "Introdueix el teu nom d'usuari"
renotedBy: "Impulsat per {user}"
noNotes: "Cap nota"
noNotifications: "Cap notificació"
instance: "Servidor"
instance: "Instància "
settings: "Preferències"
notificationSettings: "Paràmetres de notificacions"
notificationSettings: "Configurar les notificacions"
basicSettings: "Configuració bàsica"
otherSettings: "Configuració avançada"
openInWindow: "Obrir en una nova finestra"
otherSettings: "Altres configuracions"
openInWindow: "Obrir en una finestra nova"
profile: "Perfil"
timeline: "Línia de temps"
noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia."
login: "Iniciar sessió"
loggingIn: "Identificant-se"
loggingIn: "Iniciar la sessió "
logout: "Tancar la sessió"
signup: "Registrar-se"
uploading: "Pujant..."
save: "Desa"
users: "Usuaris"
addUser: "Afegir un usuari"
favorite: "Afegir a preferits"
favorite: "Afegeix als preferits"
favorites: "Favorits"
unfavorite: "Eliminar dels preferits"
favorited: "Afegit als preferits."
@ -50,26 +50,26 @@ copyContent: "Copiar el contingut"
copyLink: "Copiar l'enllaç"
copyLinkRenote: "Copiar l'enllaç de la renota"
delete: "Elimina"
deleteAndEdit: "Elimina i edita"
deleteAndEdit: "Eliminar i editar"
deleteAndEditConfirm: "Segur que vols eliminar aquesta publicació i editar-la? Perdràs totes les reaccions, impulsos i respostes."
addToList: "Afegir a una llista"
addToAntenna: "Afegir a l'antena"
addToAntenna: "Afegir a una antena"
sendMessage: "Enviar un missatge"
copyRSS: "Copiar RSS"
copyUsername: "Copiar nom d'usuari"
copyUserId: "Copiar ID d'usuari"
copyNoteId: "Copiar ID de nota"
copyFileId: "Copiar ID d'arxiu"
copyFolderId: "Copiar ID de carpeta"
copyProfileUrl: "Copiar URL del perfil"
copyNoteId: "Copiar ID de la nota"
copyFileId: "Copiar ID de l'arxiu"
copyFolderId: "Copiar ID de la carpeta"
copyProfileUrl: "Copiar adreça URL del perfil"
searchUser: "Cercar un usuari"
searchThisUsersNotes: "Cerca les publicacions de l'usuari"
reply: "Respondre"
searchThisUsersNotes: "Cercar les publicacions de l'usuari"
reply: "Respon"
loadMore: "Carregar més"
showMore: "Veure més"
showLess: "Mostra menys"
showLess: "Mostrar menys"
youGotNewFollower: "t'ha seguit"
receiveFollowRequest: "Sol·licitud de seguiment rebuda"
receiveFollowRequest: "Has rebut una sol·licitud de seguiment"
followRequestAccepted: "Sol·licitud de seguiment acceptada"
mention: "Menció"
mentions: "Mencions"
@ -78,25 +78,25 @@ importAndExport: "Importar / Exportar"
import: "Importar"
export: "Exporta"
files: "Fitxers"
download: "Baixar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer adjunt també se suprimiran."
unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?"
exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà a la teva unitat un cop completat."
importRequested: "Has sol·licitat una importació. Això pot trigar una estona."
download: "Descarregar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer també seran esborrades."
unfollowConfirm: "Segur que vols deixar de seguir a {name}?"
exportRequested: "Has sol·licitat una exportació de dades. Això pot trigar una estona. S'afegirà a la teva unitat de disc un cop estigui completada."
importRequested: "Has sol·licitat una importació de dades. Això pot trigar una estona."
lists: "Llistes"
noLists: "No tens cap llista"
note: "Nota"
notes: "Notes"
following: "Seguint"
following: "Segueixes "
followers: "Seguidors"
followsYou: "Et segueix"
createList: "Crear llista"
manageLists: "Gestionar les llistes"
error: "Error"
somethingHappened: "S'ha produït un error"
retry: "Torna-ho a intentar"
retry: "Torna-ho a provar"
pageLoadError: "S'ha produït un error en carregar la pàgina"
pageLoadErrorDescription: "Això normalment es deu a errors de xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar una estona."
pageLoadErrorDescription: "Això normalment és a causa d'errors a la xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar un temps."
serverIsDead: "Aquest servidor no respon. Espera una estona i torna-ho a provar."
youShouldUpgradeClient: "Per veure aquesta pàgina, actualitzeu-la per actualitzar el vostre client."
enterListName: "Introdueix un nom per a la llista"
@ -104,52 +104,52 @@ privacy: "Privadesa"
makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació"
defaultNoteVisibility: "Visibilitat per defecte"
follow: "Seguint"
followRequest: "Enviar la sol·licitud de seguiment"
followRequest: "Enviar sol·licitud de seguiment"
followRequests: "Sol·licituds de seguiment"
unfollow: "Deixar de seguir"
followRequestPending: "Sol·licituds de seguiment pendents"
enterEmoji: "Introduir un emoji"
renote: "Impulsa"
renote: "Impulsar "
unrenote: "Anul·la l'impuls"
renoted: "S'ha impulsat"
renotedToX: "Impulsat per {name}."
cantRenote: "No es pot impulsar aquesta publicació"
cantReRenote: "No es pot impulsar l'impuls."
cantReRenote: "No es pot impulsar un impuls."
quote: "Cita"
inChannelRenote: "Renotar només al Canal"
inChannelQuote: "Citar només al Canal"
renoteToChannel: "Impulsa a un canal"
renoteToOtherChannel: "Impulsa a un altre canal"
inChannelRenote: "Impulsar només a un canal"
inChannelQuote: "Citar només a un canal"
renoteToChannel: "Impulsar a un canal"
renoteToOtherChannel: "Impulsar a un altre canal"
pinnedNote: "Nota fixada"
pinned: "Fixar al perfil"
you: "Tu"
clickToShow: "Fes clic per mostrar"
sensitive: "NSFW"
sensitive: "Sensible"
add: "Afegir"
reaction: "Reaccions"
reaction: "Reacció "
reactions: "Reaccions"
emojiPicker: "Selecció d'emojis"
pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
pinnedEmojisSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
emojiPickerDisplay: "Visualitza el selector d'emojis"
emojiPicker: "Selector d'emojis"
pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb qui vols reaccionar"
pinnedEmojisSettingDescription: "Selecciona quins emojis vols deixar fixats i es mostrin en obrir el selector d'emojis"
emojiPickerDisplay: "Mostrar el selector d'emojis"
overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció"
overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats"
overwriteFromPinnedEmojis: "Sobreescriu els emojis fixats al panel de reaccions"
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
attachCancel: "Eliminar el fitxer adjunt"
deleteFile: "Esborrar l'arxiu "
markAsSensitive: "Marcar com a NSFW"
markAsSensitive: "Marcar com a sensible"
unmarkAsSensitive: "Deixar de marcar com a sensible"
enterFileName: "Defineix nom del fitxer"
mute: "Silencia"
unmute: "Deixa de silenciar"
renoteMute: "Silenciar Renotes"
renoteUnmute: "Treure el silenci de les renotes"
renoteMute: "Silenciar impulsos"
renoteUnmute: "Treure el silenci dels impulsos"
block: "Bloqueja"
unblock: "Desbloqueja"
suspend: "Suspèn"
unsuspend: "Deixa de suspendre"
blockConfirm: "Vols bloquejar?"
blockConfirm: "Vols bloquejar-lo?"
unblockConfirm: "Vols desbloquejar-lo?"
suspendConfirm: "Estàs segur que vols suspendre aquest compte?"
unsuspendConfirm: "Estàs segur que vols treure la suspensió d'aquest compte?"
@ -175,11 +175,11 @@ youCanCleanRemoteFilesCache: "Pots netejar la memòria cau fent clic al botó de
cacheRemoteSensitiveFiles: "Posar a la memòria cau arxius remots sensibles"
cacheRemoteSensitiveFilesDescription: "Quan aquesta opció és desactiva, els arxius remots sensibles es carregant directament del servidor d'origen sense que es guardin a la memòria cau."
flagAsBot: "Marca aquest compte com a bot"
flagAsBotDescription: "Marca aquest compte com a bot"
flagAsBotDescription: "Activa aquesta opció si el compte el controla un programa. Si s'activa, actuarà com un senyal per altres desenvolupadors per prevenir cadenes d'interacció sense fi i ajustar els paràmetres interns de Misskey pe tractar el compte com un bot."
flagAsCat: "Marca aquest compte com a gat"
flagAsCatDescription: "Activeu aquesta opció per marcar aquest compte com a gat."
flagShowTimelineReplies: "Mostra les respostes a la línia de temps"
flagShowTimelineRepliesDescription: "Mostra les respostes a la línia de temps"
flagShowTimelineRepliesDescription: "Mostra les respostes dels usuaris a les notes d'altres usuaris a la línia de temps."
autoAcceptFollowed: "Aprova automàticament les sol·licituds de seguiment dels usuaris que segueixes"
addAccount: "Afegeix un compte"
reloadAccountsList: "Recarregar la llista de contactes"
@ -204,7 +204,7 @@ selectUser: "Selecciona usuari/a"
recipient: "Destinatari"
annotation: "Comentaris"
federation: "Federació"
instances: "Servidors"
instances: "Instàncies "
registeredAt: "Registrat a"
latestRequestReceivedAt: "Última petició rebuda"
latestStatus: "Últim estat"
@ -213,7 +213,7 @@ charts: "Gràfics"
perHour: "Per hora"
perDay: "Per dia"
stopActivityDelivery: "Deixa d'enviar activitats"
blockThisInstance: "Deixa d'enviar activitats"
blockThisInstance: "Bloca aquesta instància "
silenceThisInstance: "Silencia aquesta instància "
mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instància "
operations: "Accions"
@ -228,7 +228,7 @@ network: "Xarxa"
disk: "Disc"
instanceInfo: "Informació del fitxer d'instal·lació"
statistics: "Estadístiques"
clearQueue: "Esborrar la cua"
clearQueue: "Esborra la cua de feina"
clearQueueConfirmTitle: "Esteu segur que voleu esborrar la cua?"
clearQueueConfirmText: "Les notes no lliurades que quedin a la cua no es federaran. Normalment aquesta operació no és necessària."
clearCachedFiles: "Esborra la memòria cau"
@ -254,7 +254,7 @@ processing: "S'està processant..."
preview: "Vista prèvia"
default: "Per defecte"
defaultValueIs: "Per defecte: {value}"
noCustomEmojis: "Cap emoji personalitzat"
noCustomEmojis: "No hi ha emojis personalitzats"
noJobs: "No hi ha feines"
federating: "Federant"
blocked: "Bloquejat"
@ -268,11 +268,11 @@ instanceFollowers: "Seguidors del servidor"
instanceUsers: "Usuaris del servidor"
changePassword: "Canvia la contrasenya"
security: "Seguretat"
retypedNotMatch: "L'entrada no coincideix"
retypedNotMatch: "Les entrades no coincideix"
currentPassword: "Contrasenya actual"
newPassword: "Contrasenya nova"
newPasswordRetype: "Contrasenya nou (repeteix-la)"
attachFile: "Adjunta fitxers"
newPasswordRetype: "Contrasenya nova (repeteix-la)"
attachFile: "Afegeix un arxiu"
more: "Més"
featured: "Destacat"
usernameOrUserId: "Nom o ID d'usuari"
@ -282,25 +282,25 @@ announcements: "Anuncis"
imageUrl: "URL de la imatge"
remove: "Eliminar"
removed: "Eliminat"
removeAreYouSure: "Segur que voleu retirar «{x}»?"
deleteAreYouSure: "Segur que voleu retirar «{x}»?"
resetAreYouSure: "Segur que voleu restablir-ho?"
areYouSure: "Està segur?"
removeAreYouSure: "Segur que vols esborrar «{x}»?"
deleteAreYouSure: "Segur que vols esborrar «{x}»?"
resetAreYouSure: "Segur que vols restablir-ho?"
areYouSure: "Estàs segur?"
saved: "S'ha desat"
messaging: "Xat"
upload: "Puja"
keepOriginalUploading: "Guarda la imatge original"
keepOriginalUploadingDescription: "Guarda la imatge pujada com hi és. Si està apagat, una versió per a la visualització a la xarxa serà generada quan sigui pujada."
fromDrive: "Des de la unitat"
keepOriginalUploadingDescription: "Guarda la imatge pujada sense modificar. Si està desactivat, es generarà una versió per visualitzar a la web en pujar la imatge."
fromDrive: "Des del Disc"
fromUrl: "Des d'un enllaç"
uploadFromUrl: "Carrega des d'un enllaç"
uploadFromUrlDescription: "Enllaç del fitxer que vols carregar"
uploadFromUrlRequested: "Càrrega sol·licitada"
uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot prendre un temps"
uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot trigar un temps"
explore: "Explora"
messageRead: "Vist"
noMoreHistory: "No hi resta més per veure"
startMessaging: "Començar a xatejar"
noMoreHistory: "No hi ha res més per veure"
startMessaging: "Comença a xatejar"
nUsersRead: "Vist per {n}"
agreeTo: "Accepto que {0}"
agree: "Hi estic d'acord"
@ -312,7 +312,7 @@ home: "Inici"
remoteUserCaution: "Ja que aquest usuari resideix a una instància remota, la informació mostrada es podria trobar incompleta."
activity: "Activitat"
images: "Imatges"
image: "Imatges"
image: "Imatge"
birthday: "Aniversari"
yearsOld: "{age} anys"
registeredDate: "Data de registre"
@ -327,10 +327,10 @@ darkThemes: "Temes foscos"
syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu"
drive: "Unitat"
fileName: "Nom del Fitxer"
selectFile: "Selecciona fitxers"
selectFile: "Selecciona un fitxer"
selectFiles: "Selecciona fitxers"
selectFolder: "Selecció de carpeta"
selectFolders: "Selecció de carpeta"
selectFolders: "Selecció de carpetes"
fileNotSelected: "Cap fitxer seleccionat"
renameFile: "Canvia el nom del fitxer"
folderName: "Nom de la carpeta"
@ -359,9 +359,9 @@ reload: "Actualitza"
doNothing: "Ignora"
reloadConfirm: "Vols recarregar?"
watch: "Veure"
unwatch: "Deixar de veure"
unwatch: "Deixa de veure"
accept: "Acceptar"
reject: "Denegar"
reject: "Denega"
normal: "Normal"
instanceName: "Nom del servidor"
instanceDescription: "Descripció del servidor"
@ -382,7 +382,7 @@ enableLocalTimeline: "Activa la línia de temps local"
enableGlobalTimeline: "Activa la línia de temps global"
disablingTimelinesInfo: "Fins i tot si aquestes línies de temps són desactivades, els administradors i els moderadors poden continuar visualitzant per conveniència."
registration: "Registre"
enableRegistration: "Permet els registres d'usuaris"
enableRegistration: "Permet el registre de nous usuaris"
invite: "Convida"
driveCapacityPerLocalAccount: "Capacitat del disc per usuaris locals"
driveCapacityPerRemoteAccount: "Capacitat del disc per usuaris remots"
@ -393,20 +393,20 @@ basicInfo: "Informació bàsica"
pinnedUsers: "Usuaris fixats"
pinnedUsersDescription: "Llista d'usuaris, separats per salts de línia, que seran fixats a la pestanya \"Explorar\"."
pinnedPages: "Pàgines fixades"
pinnedPagesDescription: "Escriu els camins de les pàgines que vols fixar a la pàgina d'inici d'aquesta instància. Separades per salts de línia."
pinnedPagesDescription: "Escriu les adreces de les pàgines que vols fixar a la pàgina d'inici d'aquesta instància. Separades per salts de línia."
pinnedClipId: "ID del retall fixat"
pinnedNotes: "Nota fixada"
hcaptcha: "hCaptcha"
enableHcaptcha: "Activar hCaptcha"
enableHcaptcha: "Activa hCaptcha"
hcaptchaSiteKey: "Clau del lloc"
hcaptchaSecretKey: "Clau secreta"
mcaptcha: "mCaptcha"
enableMcaptcha: "Activar mCaptcha"
enableMcaptcha: "Activa mCaptcha"
mcaptchaSiteKey: "Clau del lloc"
mcaptchaSecretKey: "Clau secreta"
mcaptchaInstanceUrl: "Adreça URL del servidor mCaptcha"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Activar reCAPTCHA"
enableRecaptcha: "Activa reCAPTCHA"
recaptchaSiteKey: "Clau del lloc"
recaptchaSecretKey: "Clau secreta"
turnstile: "Turnstile"
@ -448,14 +448,14 @@ aboutMisskey: "Quant a Misskey"
administrator: "Administrador/a"
token: "Codi de verificació"
2fa: "Autenticació de doble factor"
setupOf2fa: "Configurar l'autenticació de doble factor"
setupOf2fa: "Configura l'autenticació de doble factor"
totp: "Aplicació d'autenticació"
totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'autenticació"
moderator: "Moderador/a"
moderation: "Moderació"
moderationNote: "Nota de moderació "
moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors."
addModerationNote: "Afegir una nota de moderació "
addModerationNote: "Afegeix una nota de moderació "
moderationLogs: "Registre de moderació "
nUsersMentioned: "{n} usuaris mencionats"
securityKeyAndPasskey: "Clau de seguretat / Clau de pas"
@ -471,13 +471,13 @@ reduceUiAnimation: "Redueix les animacions de la interfície"
share: "Comparteix"
notFound: "No s'ha trobat"
notFoundDescription: "No es troba cap pàgina que correspongui a aquesta adreça"
uploadFolder: "Carpeta per defecte per pujades"
uploadFolder: "Carpeta per defecte on desar els arxius pujats"
markAsReadAllNotifications: "Marca totes les notificacions com a llegides"
markAsReadAllUnreadNotes: "Marca-ho tot com a llegit"
markAsReadAllTalkMessages: "Marcar tots els missatges com llegits"
help: "Ajuda"
inputMessageHere: "Escriu aquí el teu missatge "
close: "Tancar"
close: "Tanca"
invites: "Convida"
members: "Membres"
transfer: "Transferir"
@ -508,7 +508,7 @@ normalPassword: "Bona contrasenya"
strongPassword: "Contrasenya segura"
passwordMatched: "Correcte!"
passwordNotMatched: "No coincideix"
signinWith: "Inicia sessió amb amb {x}"
signinWith: "Inicia sessió amb {x}"
signinFailed: "Autenticació sense èxit. Intenta-ho un altre cop utilitzant la contrasenya i el nom correctes."
or: "O"
language: "Idioma"
@ -591,7 +591,7 @@ chooseEmoji: "Tria un emoji"
unableToProcess: "L'operació no pot ser completada "
recentUsed: "Utilitzat recentment"
install: "Instal·lació "
uninstall: "Desinstal·lar "
uninstall: "Desinstal·la"
installedApps: "Aplicacions autoritzades "
nothing: "No hi ha res per veure aquí "
installedDate: "Data d'instal·lació"
@ -608,13 +608,13 @@ output: "Sortida"
script: "Script"
disablePagesScript: "Desactivar AiScript a les pàgines "
updateRemoteUser: "Actualitzar la informació de l'usuari remot"
unsetUserAvatar: "Desactivar l'avatar "
unsetUserAvatar: "Desactiva l'avatar "
unsetUserAvatarConfirm: "Segur que vols desactivar l'avatar?"
unsetUserBanner: "Desactivar el bàner "
unsetUserBanner: "Desactiva el bàner "
unsetUserBannerConfirm: "Segur que vols desactivar el bàner?"
deleteAllFiles: "Esborrar tots els arxius"
deleteAllFiles: "Esborra tots els arxius"
deleteAllFilesConfirm: "Segur que vols esborrar tots els arxius?"
removeAllFollowing: "Deixar de seguir tots els usuaris seguits"
removeAllFollowing: "Deixa de seguir tots els usuaris seguits"
removeAllFollowingDescription: "El fet d'executar això, et farà deixar de seguir a tots els usuaris de {host}. Si us plau, executa això si l'amfitrió, per exemple, ja no existeix."
userSuspended: "Aquest usuari ha sigut suspès"
userSilenced: "Aquest usuari està sent silenciat"
@ -1183,8 +1183,8 @@ currentAnnouncements: "Informes actuals"
pastAnnouncements: "Informes passats"
youHaveUnreadAnnouncements: "Tens informes per llegir."
useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
replies: "Respondre"
renotes: "Impulsa"
replies: "Respon"
renotes: "Impulsar "
loadReplies: "Mostrar les respostes"
loadConversation: "Mostrar la conversació "
pinnedList: "Llista fixada"
@ -1299,6 +1299,7 @@ yourNameContainsProhibitedWordsDescription: "Si de veritat vols fer servir aques
thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autor requereix l'inici de sessió per poder veure"
lockdown: "Bloquejat"
pleaseSelectAccount: "Seleccionar un compte"
availableRoles: "Roles disponibles "
_accountSettings:
requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut"
requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació."

View file

@ -8,6 +8,8 @@ search: "Suchen"
notifications: "Benachrichtigungen"
username: "Benutzername"
password: "Passwort"
initialPasswordForSetup: "Initiales Passwort für die Einrichtung"
initialPasswordIsIncorrect: "Das initiale Passwort für die Einrichtung ist falsch"
forgotPassword: "Passwort vergessen"
fetchingAsApObject: "Wird aus dem Fediverse angefragt …"
ok: "OK"
@ -60,6 +62,7 @@ copyFileId: "Datei-ID kopieren"
copyFolderId: "Ordner-ID kopieren"
copyProfileUrl: "Profil-URL kopieren"
searchUser: "Nach einem Benutzer suchen"
searchThisUsersNotes: "Notizen dieses Benutzers suchen"
reply: "Antworten"
loadMore: "Mehr laden"
showMore: "Mehr anzeigen"
@ -150,6 +153,7 @@ editList: "Liste bearbeiten"
selectChannel: "Kanal auswählen"
selectAntenna: "Antenne auswählen"
editAntenna: "Antenne bearbeiten"
createAntenna: "Erstelle eine Antenne"
selectWidget: "Widget auswählen"
editWidgets: "Widgets bearbeiten"
editWidgetsExit: "Fertig"
@ -186,6 +190,7 @@ followConfirm: "Möchtest du {name} wirklich folgen?"
proxyAccount: "Proxy-Benutzerkonto"
proxyAccountDescription: "Ein Proxy-Konto ist ein Benutzerkonto, das unter bestimmten Bedingungen als Follower für Benutzer fremder Instanzen fungiert. Wenn zum Beispiel ein Benutzer einen Benutzer einer fremden Instanz zu einer Liste hinzufügt, werden die Aktivitäten des entfernten Benutzers nicht an die Instanz übermittelt, wenn kein lokaler Benutzer diesem Benutzer folgt; stattdessen folgt das Proxy-Konto."
host: "Hostname"
selectSelf: "Mich auswählen"
selectUser: "Benutzer auswählen"
recipient: "Empfänger"
annotation: "Anmerkung"
@ -312,6 +317,7 @@ selectFile: "Datei auswählen"
selectFiles: "Dateien auswählen"
selectFolder: "Ordner auswählen"
selectFolders: "Ordner auswählen"
fileNotSelected: "Keine Datei ausgewählt"
renameFile: "Datei umbenennen"
folderName: "Ordnername"
createFolder: "Ordner erstellen"
@ -399,6 +405,7 @@ name: "Name"
antennaSource: "Antennenquelle"
antennaKeywords: "Zu beobachtende Schlüsselwörter"
antennaExcludeKeywords: "Zu ignorierende Schlüsselwörter"
antennaExcludeBots: "Bot-Accounts ausschließen"
antennaKeywordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen"
notifyAntenna: "Über neue Notizen benachrichtigen"
withFileAntenna: "Nur Notizen mit Dateien"
@ -885,6 +892,7 @@ unmuteThread: "Threadstummschaltung aufheben"
continueThread: "Weiteren Threadverlauf anzeigen"
deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzdem fortfahren?"
incorrectPassword: "Falsches Passwort."
incorrectTotp: "Das Einmalpasswort ist falsch oder abgelaufen."
voteConfirm: "Wirklich für „{choice}“ abstimmen?"
hide: "Inhalt verbergen"
useDrawerReactionPickerForMobile: "Auf mobilen Geräten ausfahrbare Reaktionsauswahl anzeigen"
@ -909,6 +917,9 @@ oneHour: "Eine Stunde"
oneDay: "Einen Tag"
oneWeek: "Eine Woche"
oneMonth: "1 Monat"
threeMonths: "3 Monate"
oneYear: "1 Jahr"
threeDays: "3 Tage"
reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt."
failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden"
rateLimitExceeded: "Versuchsanzahl überschritten"
@ -982,6 +993,7 @@ neverShow: "Nicht wieder anzeigen"
remindMeLater: "Vielleicht später"
didYouLikeMisskey: "Gefällt dir Misskey?"
pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!"
correspondingSourceIsAvailable: "Der entsprechende Quellcode ist verfügbar unter {anchor}"
roles: "Rollen"
role: "Rolle"
noRole: "Rolle nicht gefunden"
@ -1032,6 +1044,7 @@ resetPasswordConfirm: "Wirklich Passwort zurücksetzen?"
sensitiveWords: "Sensible Wörter"
sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden."
sensitiveWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden."
prohibitedWords: "Verbotene Wörter"
prohibitedWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden."
hiddenTags: "Ausgeblendete Hashtags"
hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden."
@ -1078,6 +1091,8 @@ preservedUsernames: "Reservierte Benutzernamen"
preservedUsernamesDescription: "Gib zu reservierende Benutzernamen durch Zeilenumbrüche getrennt an. Diese werden für die Registrierung gesperrt, können aber von Administratoren zur manuellen Erstellung von Konten verwendet werden. Existierende Konten, die diese Namen bereits verwenden, werden nicht beeinträchtigt."
createNoteFromTheFile: "Notiz für diese Datei schreiben"
archive: "Archivieren"
archived: "Archiviert"
unarchive: "Dearchivieren"
channelArchiveConfirmTitle: "{name} wirklich archivieren?"
channelArchiveConfirmDescription: "Ein archivierter Kanal taucht nicht mehr in der Kanalliste oder in Suchergebnissen auf. Zudem können ihm keine Beiträge mehr hinzugefügt werden."
thisChannelArchived: "Dieser Kanal wurde archiviert."
@ -1176,15 +1191,35 @@ signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen.
cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
doReaction: "Reagieren"
code: "Code"
remainingN: "Verbleibend: {n}"
decorate: "Dekorieren"
addMfmFunction: "MFM hinzufügen"
sfx: "Soundeffekte"
showReplay: "Wiederholung anzeigen"
lastNDays: "Letzten {n} Tage"
surrender: "Abbrechen"
keepOriginalFilename: "Ursprünglichen Dateinamen beibehalten"
tryAgain: "Bitte später erneut versuchen"
confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen"
createdLists: "Erstellte Listen"
createdAntennas: "Erstellte Antennen"
genEmbedCode: "Einbettungscode generieren"
noteOfThisUser: "Notizen dieses Benutzers"
clipNoteLimitExceeded: "Zu diesem Clip können keine weiteren Notizen hinzugefügt werden."
discard: "Verwerfen"
signinWithPasskey: "Mit Passkey anmelden"
passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert."
pleaseSelectAccount: "Bitte Konto auswählen"
availableRoles: "Verfügbare Rollen"
_abuseUserReport:
forward: "Weiterleiten"
_delivery:
stop: "Gesperrt"
_type:
none: "Wird veröffentlicht"
_bubbleGame:
howToPlay: "Wie man spielt"
_announcement:
forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
@ -1515,6 +1550,10 @@ _achievements:
description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne"
_tutorialCompleted:
description: "Tutorial abgeschlossen"
_bubbleGameExplodingHead:
title: "🤯"
_bubbleGameDoubleExplodingHead:
title: "Doppel🤯"
_role:
new: "Rolle erstellen"
edit: "Rolle bearbeiten"
@ -2115,6 +2154,7 @@ _notification:
pollEnded: "Umfrageergebnisse sind verfügbar"
newNote: "Neue Notiz"
unreadAntennaNote: "Antenne {name}"
roleAssigned: "Rolle zugewiesen"
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
achievementEarned: "Errungenschaft freigeschaltet"
testNotification: "Testbenachrichtigung"

View file

@ -331,6 +331,7 @@ selectFile: "Select a file"
selectFiles: "Select files"
selectFolder: "Select a folder"
selectFolders: "Select folders"
fileNotSelected: "No file selected"
renameFile: "Rename file"
folderName: "Folder name"
createFolder: "Create a folder"
@ -1298,6 +1299,7 @@ yourNameContainsProhibitedWordsDescription: "If you wish to use this name, pleas
thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view"
lockdown: "Lockdown"
pleaseSelectAccount: "Select an account"
availableRoles: "Available roles"
_accountSettings:
requireSigninToViewContents: "Require sign-in to view contents"
requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information."
@ -2119,6 +2121,7 @@ _permissions:
"read:flash-likes": "View list of liked Plays"
"write:flash-likes": "Edit list of liked Plays"
"read:admin:abuse-user-reports": "View user reports"
"write:admin:create-account": "Create user account"
"write:admin:delete-account": "Delete user account"
"write:admin:delete-all-files-of-a-user": "Delete all files of a user"
"read:admin:index-stats": "View database index stats"
@ -2223,7 +2226,7 @@ _widgets:
_userList:
chooseList: "Select a list"
clicker: "Clicker"
birthdayFollowings: "Users who celebrate their birthday today"
birthdayFollowings: "Today's Birthdays"
_cw:
hide: "Hide"
show: "Show content"
@ -2730,3 +2733,9 @@ _embedCodeGen:
generateCode: "Generate embed code"
codeGenerated: "The code has been generated"
codeGeneratedDescription: "Paste the generated code into your website to embed the content."
_selfXssPrevention:
warning: "WARNING"
title: "\"Paste something on this screen\" is all a scam."
description1: "If you paste something here, a malicious user could hijack your account or steal your personal information."
description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window."
description3: "For more information, please refer to this. {link}"

14
locales/index.d.ts vendored
View file

@ -8238,6 +8238,10 @@ export interface Locale extends ILocale {
*
*/
"read:admin:abuse-user-reports": string;
/**
*
*/
"write:admin:create-account": string;
/**
*
*/
@ -10579,6 +10583,16 @@ export interface Locale extends ILocale {
*/
"description3": ParameterizedString<"link">;
};
"_followRequest": {
/**
*
*/
"recieved": string;
/**
*
*/
"sent": string;
};
}
declare const locales: {
[lang: string]: Locale;

View file

@ -2163,6 +2163,7 @@ _permissions:
"read:flash-likes": "Playのいいねを見る"
"write:flash-likes": "Playのいいねを操作する"
"read:admin:abuse-user-reports": "ユーザーからの通報を見る"
"write:admin:create-account": "ユーザーアカウントを作成する"
"write:admin:delete-account": "ユーザーアカウントを削除する"
"write:admin:delete-all-files-of-a-user": "ユーザーのすべてのファイルを削除する"
"read:admin:index-stats": "データベースインデックスに関する情報を見る"
@ -2819,3 +2820,7 @@ _selfXssPrevention:
description1: "ここに何かを貼り付けると、悪意のあるユーザーにアカウントを乗っ取られたり、個人情報を盗まれたりする可能性があります。"
description2: "貼り付けようとしているものが何なのかを正確に理解していない場合は、%c今すぐ作業を中止してこのウィンドウを閉じてください。"
description3: "詳しくはこちらをご確認ください。 {link}"
_followRequest:
recieved: "受け取った申請"
sent: "送った申請"

View file

@ -42,7 +42,7 @@ favorite: "즐겨찾기"
favorites: "즐겨찾기"
unfavorite: "즐겨찾기에서 제거"
favorited: "즐겨찾기에 등록했습니다."
alreadyFavorited: "이미 즐겨찾기에 등록습니다."
alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다."
cantFavorite: "즐겨찾기에 등록하지 못했습니다."
pin: "프로필에 고정"
unpin: "프로필에서 고정 해제"
@ -947,6 +947,9 @@ oneHour: "1시간"
oneDay: "1일"
oneWeek: "일주일"
oneMonth: "1개월"
threeMonths: "3개월"
oneYear: "1년"
threeDays: "3일"
reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다."
failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다"
rateLimitExceeded: "요청 제한 횟수를 초과하였습니다"
@ -1286,13 +1289,29 @@ signinWithPasskey: "패스키로 로그인"
unknownWebAuthnKey: "등록되지 않은 패스키입니다."
passkeyVerificationFailed: "패스키 검증을 실패했습니다."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
messageToFollower: "팔로워에 보낼 메시지"
messageToFollower: "팔로워에 보낼 메시지"
target: "대상"
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해야 볼 수 있도록 설정되어 있습니다."
lockdown: "잠금"
pleaseSelectAccount: "계정을 선택해주세요."
availableRoles: "사용 가능한 역할"
_accountSettings:
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페이지에 삽입, 노트 인용을 지원하지 않는 서버에서 볼 수 없게 됩니다."
requireSigninToViewContentsDescription3: "원격 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다."
makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 볼 수 있도록 설정하기"
makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다.비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
makeNotesHiddenBefore: "과거 노트 비공개로 전환하기"
makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
mayNotEffectForFederatedNotes: "원격 서버에 연합된 노트에는 효과가 없을 수도 있습니다."
notesHavePassedSpecifiedPeriod: "지정한 시간이 경과된 노트"
notesOlderThanSpecifiedDateAndTime: "지정된 날짜 및 시간 이전의 노트"
_abuseUserReport:
forward: "전달"
forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
@ -2157,8 +2176,11 @@ _auth:
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
pleaseGoBack: "앱으로 돌아가서 시도해 주세요"
callback: "앱으로 돌아갑니다"
accepted: "접근 권한이 부여되었습니다."
denied: "접근이 거부되었습니다"
scopeUser: "다음 사용자로 활동하고 있습니다."
pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오."
byClickingYouWillBeRedirectedToThisUrl: "접근을 허용하면 자동으로 다음 URL로 이동합니다."
_antennaSources:
all: "모든 노트"
homeTimeline: "팔로우중인 유저의 노트"
@ -2710,3 +2732,9 @@ _embedCodeGen:
generateCode: "임베디드 코드를 만들기"
codeGenerated: "코드를 만들었습니다."
codeGeneratedDescription: "만들어진 코드를 웹 사이트에 붙여서 사용하세요."
_selfXssPrevention:
warning: "경고"
title: "“이 화면에 뭔가를 붙여넣어라\"는 것은 모두 사기입니다."
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
description3: "자세한 내용은 여기를 확인해 주세요. {link}"

View file

@ -385,6 +385,7 @@ passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en
resetPassword: "Återställ Lösenord"
newPasswordIs: "Det nya lösenordet är \"{password}\""
share: "Dela"
markAsReadAllTalkMessages: "Markera alla meddelanden som lästa"
help: "Hjälp"
close: "Stäng"
invites: "Inbjudan"
@ -393,12 +394,15 @@ transfer: "Överför"
text: "Text"
enable: "Aktivera"
next: "Nästa"
retype: "Ange igen"
noMessagesYet: "Inga meddelanden än"
invitations: "Inbjudan"
invitationCode: "Inbjudningskod"
available: "Tillgängligt"
weakPassword: "Svagt Lösenord"
normalPassword: "Medel Lösenord"
strongPassword: "Starkt Lösenord"
signinWith: "Logga in med {x}"
signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt."
or: "eller"
language: "Språk"
@ -410,70 +414,122 @@ existingAccount: "Existerande konto"
regenerate: "Regenerera"
fontSize: "Textstorlek"
openImageInNewTab: "Öppna bild i ny flik"
appearance: "Utseende"
clientSettings: "Klientinställningar"
accountSettings: "Kontoinställningar"
numberOfDays: "Antal dagar"
objectStorageUseSSL: "Använd SSL"
serverLogs: "Serverloggar"
deleteAll: "Radera alla"
sounds: "Ljud"
sound: "Ljud"
listen: "Lyssna"
none: "Ingen"
volume: "Volym"
notUseSound: "Inaktivera ljud"
chooseEmoji: "Välj en emoji"
recentUsed: "Senast använd"
install: "Installera"
uninstall: "Avinstallera"
deleteAllFiles: "Radera alla filer"
deleteAllFilesConfirm: "Är du säker på att du vill radera alla filer?"
menu: "Meny"
addItem: "Lägg till objekt"
serviceworkerInfo: "Måste vara aktiverad för pushnotiser."
enableInfiniteScroll: "Ladda mer automatiskt"
enablePlayer: "Öppna videospelare"
description: "Beskrivning"
permission: "Behörigheter"
enableAll: "Aktivera alla"
disableAll: "Inaktivera alla"
edit: "Ändra"
enableEmail: "Aktivera epost-utskick"
email: "E-post"
emailAddress: "E-postadress"
smtpHost: "Värd"
smtpUser: "Användarnamn"
smtpPass: "Lösenord"
emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering"
makeActive: "Aktivera"
copy: "Kopiera"
overview: "Översikt"
logs: "Logg"
database: "Databas"
channel: "kanal"
create: "Skapa"
other: "Mer"
abuseReports: "Rapporter"
reportAbuse: "Rapporter"
reportAbuseOf: "Rapportera {name}"
abuseReported: "Din rapport har skickats. Tack så mycket."
send: "Skicka"
openInNewTab: "Öppna i ny flik"
createNew: "Skapa ny"
private: "Privat"
i18nInfo: "Misskey översätts till många olika språk av volontärer. Du kan hjälpa till med översättningen på {link}."
accountInfo: "Kontoinformation"
followersCount: "Antal följare"
yes: "Ja"
no: "Nej"
clips: "Klipp"
duplicate: "Duplicera"
reloadToApplySetting: "Inställningen tillämpas efter sidan laddas om. Vill du göra det nu?"
clearCache: "Rensa cache"
onlineUsersCount: "{n} användare är online"
nUsers: "{n} användare"
nNotes: "{n} Noter"
backgroundColor: "Bakgrundsbild"
textColor: "Text"
saveAs: "Spara som..."
youAreRunningUpToDateClient: "Klienten du använder är uppdaterat."
newVersionOfClientAvailable: "Ny version av klienten är tillgänglig."
editCode: "Redigera kod"
publish: "Publicera"
typingUsers: "{users} skriver"
goBack: "Tillbaka"
addDescription: "Lägg till beskrivning"
info: "Om"
online: "Online"
active: "Aktiv"
offline: "Offline"
enabled: "Aktiverad"
quickAction: "Snabbåtgärder"
user: "Användare"
gallery: "Galleri"
popularPosts: "Populära inlägg"
customCssWarn: "Den här inställningen borde bara ändrats av en som har rätta kunskaper. Om du ställer in det här fel så kan klienten sluta fungera rätt."
global: "Global"
squareAvatars: "Visa fyrkantiga profilbilder"
sent: "Skicka"
searchResult: "Sökresultat"
learnMore: "Läs mer"
misskeyUpdated: "Misskey har uppdaterats!"
translate: "Översätt"
controlPanel: "Kontrollpanel"
manageAccounts: "Hantera konton"
incorrectPassword: "Fel lösenord."
hide: "Dölj"
welcomeBackWithName: "Välkommen tillbaka, {name}"
clickToFinishEmailVerification: "Tryck på [{ok}] för att slutföra bekräftelsen på e-postadressen."
size: "Storlek"
searchByGoogle: "Sök"
indefinitely: "Aldrig"
tenMinutes: "10 minuter"
oneHour: "En timme"
oneDay: "En dag"
oneWeek: "En vecka"
oneMonth: "En månad"
threeMonths: "3 månader"
oneYear: "1 år"
threeDays: "3 dagar"
file: "Filer"
label: "Etikett"
cannotUploadBecauseNoFreeSpace: "Kan inte ladda upp filen för att det finns inget lagringsutrymme kvar."
cannotUploadBecauseExceedsFileSizeLimit: "Kan inte ladda upp filen för att den är större än filstorleksgränsen."
beta: "Beta"
enableAutoSensitive: "Automatisk NSFW markering"
enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen."
move: "Flytta"
pushNotification: "Pushnotiser"
subscribePushNotification: "Aktivera pushnotiser"
unsubscribePushNotification: "Avaktivera pushnotiser"
@ -482,16 +538,38 @@ pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för
windowMaximize: "Maximera"
windowMinimize: "Minimera"
windowRestore: "Återställ"
tools: "Verktyg"
like: "Gilla"
pleaseDonate: "Misskey är en gratis programvara som används på {host}. Donera gärna för att göra utvecklingen ständigt, tack!"
roles: "Roll"
role: "Roll"
color: "Färg"
resetPasswordConfirm: "Återställ verkligen ditt lösenord?"
dataSaver: "Databesparing"
icon: "Profilbild"
forYou: "För dig"
replies: "Svara"
renotes: "Omnotera"
loadReplies: "Visa svar"
loadConversation: "Visa konversation"
authentication: "Autentisering"
sourceCode: "Källkod"
doReaction: "Lägg till reaktion"
code: "Kod"
gameRetry: "Försök igen"
inquiry: "Kontakt"
tryAgain: "Försök igen senare"
signinWithPasskey: "Logga in med nyckel"
unknownWebAuthnKey: "Okänd nyckel"
_delivery:
stop: "Suspenderad"
_type:
none: "Publiceras"
_initialAccountSetting:
profileSetting: "Profilinställningar"
_initialTutorial:
_reaction:
title: "Vad är reaktioner?"
_achievements:
_types:
_open3windows:
@ -499,13 +577,25 @@ _achievements:
description: "Ha minst 3 fönster öppna samtidigt"
_ffVisibility:
public: "Publicera"
private: "Privat"
_ad:
back: "Tillbaka"
_gallery:
like: "Gilla"
_email:
_follow:
title: "följde dig"
_aboutMisskey:
source: "Källkod"
_channel:
setBanner: "Välj banner"
removeBanner: "Ta bort banner"
nameAndDescription: "Namn och beskrivning"
_menuDisplay:
hide: "Dölj"
_theme:
description: "Beskrivning"
color: "Färg"
keys:
mention: "Nämn"
renote: "Omnotera"
@ -530,13 +620,19 @@ _widgets:
_userList:
chooseList: "Välj lista"
_cw:
hide: "Dölj"
show: "Ladda mer"
chars: "{count} tecken"
files: "{count} fil(er)"
_poll:
infinite: "Aldrig"
_visibility:
home: "Hem"
followers: "Följare"
_profile:
name: "Namn"
username: "Användarnamn"
metadataLabel: "Etikett"
changeAvatar: "Ändra profilbild"
changeBanner: "Ändra banner"
_exportOrImport:
@ -547,9 +643,12 @@ _exportOrImport:
userLists: "Listor"
_charts:
federation: "Federation"
activeUsers: "Aktiva användare"
_timelines:
home: "Hem"
global: "Global"
_play:
summary: "Beskrivning"
_pages:
blocks:
image: "Bilder"
@ -584,3 +683,10 @@ _abuseReport:
_moderationLogTypes:
suspend: "Suspendera"
resetPassword: "Återställ Lösenord"
_reversi:
blackOrWhite: "Svart/Vit"
rules: "Regler"
black: "Svart"
white: "Vit"
_selfXssPrevention:
warning: "VARNING"

View file

@ -8,6 +8,9 @@ search: "Tìm kiếm"
notifications: "Thông báo"
username: "Tên người dùng"
password: "Mật khẩu"
initialPasswordForSetup: "Mật khẩu ban đầu để thiết lập"
initialPasswordIsIncorrect: "Mật khẩu ban đầu đã nhập sai"
initialPasswordForSetupDescription: "Nếu bạn tự cài đặt Misskey, hãy sử dụng mật khẩu ban đầu của bạn đã nhập trong tệp cấu hình.\nNếu bạn đang sử dụng dịch vụ nào đó giống như dịch vụ lưu trữ của Misskey, hãy sử dụng mật khẩu ban đầu được cung cấp.\nNếu bạn chưa đặt mật khẩu ban đầu, vui lòng để trống và tiếp tục."
forgotPassword: "Quên mật khẩu"
fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..."
ok: "Đồng ý"

View file

@ -107,7 +107,7 @@ follow: "关注"
followRequest: "关注申请"
followRequests: "关注申请"
unfollow: "取消关注"
followRequestPending: "关注请求批准"
followRequestPending: "关注请求批准"
enterEmoji: "输入表情符号"
renote: "转发"
unrenote: "取消转发"
@ -136,7 +136,7 @@ overwriteFromPinnedEmojisForReaction: "从「置顶(回应)」设置覆盖"
overwriteFromPinnedEmojis: "从全局设置覆盖"
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
rememberNoteVisibility: "保存上次设置的可见性"
attachCancel: "删除附件"
attachCancel: "取消添加附件"
deleteFile: "删除文件"
markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容"
@ -603,7 +603,7 @@ descendingOrder: "降序"
scratchpad: "AiScript 控制台"
scratchpadDescription: "AiScript 控制台为 AiScript 提供了实验环境。您可以编写代码与 Misskey 交互,运行并查看结果。"
uiInspector: "UI 检查器"
uiInspectorDescription: "查看所有内存中由 UI 组件生成出的实例。UI 组件由 UI:C 系列函数所生成。"
uiInspectorDescription: "查看内存中所有由 UI 组件生成出的实例。UI 组件由 UI:C 系列函数所生成。"
output: "输出"
script: "脚本"
disablePagesScript: "禁用页面脚本"
@ -856,9 +856,9 @@ user: "用户"
administration: "管理"
accounts: "账户"
switch: "切换"
noMaintainerInformationWarning: "管理员信息未设置。"
noMaintainerInformationWarning: "尚未设置管理员信息。"
noInquiryUrlWarning: "尚未设置联络地址。"
noBotProtectionWarning: "Bot 防御未设置。"
noBotProtectionWarning: "尚未设置 Bot 防御。"
configure: "设置"
postToGallery: "发送到图库"
postToHashtag: "投稿到这个标签"
@ -874,11 +874,11 @@ priority: "优先级"
high: "高"
middle: "中"
low: "低"
emailNotConfiguredWarning: "电子邮件地址未设置。"
emailNotConfiguredWarning: "尚未设置电子邮件地址。"
ratio: "比率"
previewNoteText: "预览文本"
customCss: "自定义 CSS"
customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用"
customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用"
global: "全局"
squareAvatars: "显示方形头像图标"
sent: "发送"
@ -1057,7 +1057,7 @@ internalServerErrorDescription: "内部服务器发生了预期外的错误"
copyErrorInfo: "复制错误信息"
joinThisServer: "在本服务器上注册"
exploreOtherServers: "探索其他服务器"
letsLookAtTimeline: "时间线"
letsLookAtTimeline: "看看时间线"
disableFederationConfirm: "确定要禁用联合?"
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
disableFederationOk: "联合禁用"
@ -1076,7 +1076,7 @@ sensitiveWords: "敏感词"
sensitiveWordsDescription: "包含这些词的帖子将只在首页可见。可用换行来设定多个词。"
sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
prohibitedWords: "禁用词"
prohibitedWordsDescription: "发布包含设定词汇的帖子时将出错。可用换行设定多个关键字"
prohibitedWordsDescription: "发布包含设定词汇的帖子时将出错。可用换行设定多个关键字"
prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。"
hiddenTags: "隐藏标签"
hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。"
@ -1119,7 +1119,7 @@ vertical: "纵向"
horizontal: "横向"
position: "位置"
serverRules: "服务器规则"
pleaseConfirmBelowBeforeSignup: "在这个服务器上注册账号前,请确认以下信息。"
pleaseConfirmBelowBeforeSignup: "如果要在此服务器上注册,需要确认并同意以下内容。"
pleaseAgreeAllToContinue: "必须全部勾选「同意」才能够继续。"
continue: "继续"
preservedUsernames: "保留的用户名"
@ -1159,10 +1159,10 @@ turnOffToImprovePerformance: "关闭该选项可以提高性能。"
createInviteCode: "生成邀请码"
createWithOptions: "使用选项来创建"
createCount: "发行数"
inviteCodeCreated: "已创建邀请码"
inviteLimitExceeded: "可供发行的邀请码已达上限。"
createLimitRemaining: "可供发行的邀请码:剩余{limit}个"
inviteLimitResetCycle: "可以在{time}内发行最多{limit}个邀请码。"
inviteCodeCreated: "已生成邀请码"
inviteLimitExceeded: "可供生成的邀请码已达上限。"
createLimitRemaining: "可供生成的邀请码:剩余 {limit} 个"
inviteLimitResetCycle: "可以在 {time} 内生成最多 {limit} 个邀请码。"
expirationDate: "有效日期"
noExpirationDate: "不设置有效日期"
inviteCodeUsedAt: "邀请码被使用的日期和时间"
@ -1299,6 +1299,7 @@ yourNameContainsProhibitedWordsDescription: "用户名内含有违禁词。若
thisContentsAreMarkedAsSigninRequiredByAuthor: "根据发帖者的设定,需要登录才能显示"
lockdown: "锁定"
pleaseSelectAccount: "请选择帐户"
availableRoles: "可用角色"
_accountSettings:
requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
@ -1426,8 +1427,8 @@ _initialTutorial:
description: "对于服务器方针所要求要求的,又或者不适合直接展示的附件,请添加「敏感」标记。\n"
tryThisFile: "试试看,将附加到此窗口的图像标注为敏感!"
_exampleNote:
note: "拆纳豆包装时出错了…"
method: "要标注附件为敏感内容,请单击该文件以打开菜单,然后单击“标记为敏感内容”。"
note: "拆纳豆包装时失手了…"
method: "要标注附件为敏感内容,请单击该文件以打开菜单,然后单击「标记为敏感内容」。"
sensitiveSucceeded: "附加文件时,请遵循服务器的条款来设置正确敏感设定。\n"
doItToContinue: "将图像标记为敏感后才能够继续"
_done:

View file

@ -8,8 +8,8 @@ search: "搜尋"
notifications: "通知"
username: "使用者名稱"
password: "密碼"
initialPasswordForSetup: "初始設定的密碼"
initialPasswordIsIncorrect: "初始設定的密碼錯誤。"
initialPasswordForSetup: "啟動初始設定的密碼"
initialPasswordIsIncorrect: "啟動初始設定的密碼錯誤。"
initialPasswordForSetupDescription: "如果您自己安裝了 Misskey請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼請將其留空並繼續。"
forgotPassword: "忘記密碼"
fetchingAsApObject: "從聯邦宇宙取得中..."
@ -1299,6 +1299,7 @@ yourNameContainsProhibitedWordsDescription: "名稱中包含禁止使用的字
thisContentsAreMarkedAsSigninRequiredByAuthor: "作者將其設定為需要登入才能顯示。"
lockdown: "鎖定"
pleaseSelectAccount: "請選擇帳戶"
availableRoles: "可用角色"
_accountSettings:
requireSigninToViewContents: "須登入以顯示內容"
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"

View file

@ -1,10 +1,10 @@
{
"name": "misskey",
"version": "2024.11.0-yumechinokuni.3p2",
"version": "2024.11.0-yumechinokuni.4",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
"url": "https://forge.yumechi.jp/yume.yumechi-no-kuni.git"
},
"packageManager": "pnpm@9.6.0",
"workspaces": [
@ -52,26 +52,26 @@
},
"dependencies": {
"cssnano": "6.1.2",
"execa": "8.0.1",
"execa": "9.5.1",
"fast-glob": "3.3.2",
"ignore-walk": "6.0.5",
"js-yaml": "4.1.0",
"postcss": "8.4.47",
"postcss": "8.4.49",
"tar": "6.2.1",
"terser": "5.33.0",
"typescript": "5.6.2",
"esbuild": "0.23.1",
"terser": "5.36.0",
"typescript": "5.6.3",
"esbuild": "0.24.0",
"glob": "11.0.0"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.0.3",
"@types/node": "20.14.12",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"cross-env": "7.0.3",
"cypress": "13.14.2",
"eslint": "9.8.0",
"globals": "15.9.0",
"cypress": "13.15.2",
"eslint": "9.14.0",
"globals": "15.12.0",
"ncp": "2.0.0",
"start-server-and-test": "2.0.8"
},

View file

@ -69,32 +69,32 @@
"dependencies": {
"@aws-sdk/client-s3": "3.620.0",
"@aws-sdk/lib-storage": "3.620.0",
"@bull-board/api": "6.0.0",
"@bull-board/fastify": "6.0.0",
"@bull-board/ui": "6.0.0",
"@bull-board/api": "6.5.0",
"@bull-board/fastify": "6.5.0",
"@bull-board/ui": "6.5.0",
"@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.1",
"@fastify/cookie": "10.0.1",
"@fastify/cookie": "11.0.1",
"@fastify/cors": "10.0.1",
"@fastify/express": "4.0.1",
"@fastify/http-proxy": "10.0.0",
"@fastify/http-proxy": "10.0.1",
"@fastify/multipart": "9.0.1",
"@fastify/static": "8.0.1",
"@fastify/static": "8.0.2",
"@fastify/view": "10.0.1",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "0.1.56",
"@nestjs/common": "10.4.4",
"@nestjs/core": "10.4.4",
"@nestjs/testing": "10.4.4",
"@nestjs/common": "10.4.7",
"@nestjs/core": "10.4.7",
"@nestjs/testing": "10.4.7",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "8.20.0",
"@sentry/profiling-node": "8.20.0",
"@sentry/node": "8.38.0",
"@sentry/profiling-node": "8.38.0",
"@simplewebauthn/server": "10.0.1",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12",
"@swc/core": "1.6.6",
"@swc/core": "1.9.2",
"@twemoji/parser": "15.1.1",
"accepts": "1.3.8",
"ajv": "8.17.1",
@ -103,7 +103,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.15.0",
"bullmq": "5.26.1",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@ -117,11 +117,11 @@
"fastify": "5.0.0",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.5.0",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.4.2",
"happy-dom": "15.7.4",
"form-data": "4.0.1",
"got": "14.4.4",
"happy-dom": "15.11.4",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
@ -134,7 +134,7 @@
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
"meilisearch": "0.42.0",
"meilisearch": "0.45.0",
"juice": "11.0.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
@ -142,18 +142,18 @@
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1",
"nanoid": "5.0.7",
"nanoid": "5.0.8",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "6.9.15",
"nodemailer": "6.9.16",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.3.4",
"parse5": "7.1.2",
"pg": "8.13.0",
"parse5": "7.2.1",
"pg": "8.13.1",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@ -180,7 +180,7 @@
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typescript": "5.6.2",
"typescript": "5.6.3",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
@ -189,28 +189,28 @@
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.4",
"@nestjs/platform-express": "10.4.7",
"@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36",
"@swc/jest": "0.2.37",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.2",
"@types/archiver": "6.0.3",
"@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.5",
"@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.26",
"@types/fluent-ffmpeg": "2.1.27",
"@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.7",
"@types/jest": "29.5.13",
"@types/jest": "29.5.14",
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.14.12",
"@types/node": "22.9.0",
"@types/nodemailer": "6.4.16",
"@types/oauth": "0.9.5",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.10",
@ -227,14 +227,14 @@
"@types/tinycolor2": "1.4.6",
"@types/tmp": "0.2.6",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/ws": "8.5.12",
"@types/web-push": "3.6.4",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.30.0",
"execa": "9.4.0",
"execa": "9.5.1",
"fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",

View file

@ -20,6 +20,18 @@ type RedisOptionsSource = Partial<RedisOptions> & {
prefix?: string;
};
type BrowserSandboxing = {
// send Referrer-Policy: strict-origin
strictOriginReferrer?: boolean;
csp?: {
disable?: boolean;
appendDirectives?: {
[directive: string]: string | string[];
}
};
};
/**
*
*/
@ -65,6 +77,8 @@ type Source = {
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
browserSandboxing?: BrowserSandboxing;
setupPassword?: string;
proxy?: string;
@ -155,7 +169,9 @@ export type Config = {
proxyRemoteFiles: boolean | undefined;
signToActivityPubGet: boolean | undefined;
cspPrerenderedContent: Map<string, CSPHashed>,
browserSandboxing: BrowserSandboxing;
cspPrerenderedContent: Map<string, CSPHashed>;
version: string;
gitDescribe: string;
@ -252,6 +268,7 @@ export function loadConfig(): Config {
version,
gitCommit,
gitDescribe,
browserSandboxing: config.browserSandboxing ?? { strictOriginReferrer: true },
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
setupPassword: config.setupPassword,
url: url.origin,

View file

@ -87,9 +87,9 @@ type UploadFromUrlArgs = {
@Injectable()
export class DriveService {
public static NoSuchFolderError = class extends Error {};
public static InvalidFileNameError = class extends Error {};
public static CannotUnmarkSensitiveError = class extends Error {};
public static NoSuchFolderError = class extends Error { };
public static InvalidFileNameError = class extends Error { };
public static CannotUnmarkSensitiveError = class extends Error { };
private registerLogger: Logger;
private downloaderLogger: Logger;
private deleteLogger: Logger;
@ -170,11 +170,11 @@ export class DriveService {
}
const baseUrl = this.meta.objectStorageBaseUrl
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
?? `${this.meta.objectStorageUseSSL ? 'https' : 'http'}://${this.meta.objectStorageEndpoint}${this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : ''}/${this.meta.objectStorageBucket}`;
// for original
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`;
const url = `${baseUrl}/${key}`;
// for alts
let webpublicKey: string | null = null;
@ -191,7 +191,7 @@ export class DriveService {
if (alts.webpublic) {
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
webpublicUrl = `${baseUrl}/${webpublicKey}`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, alts.webpublic.ext, name));
@ -199,7 +199,7 @@ export class DriveService {
if (alts.thumbnail) {
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
thumbnailUrl = `${baseUrl}/${thumbnailKey}`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`));

View file

@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
import type { MiWebhook, WebhookEventTypes, webhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
@ -36,6 +36,7 @@ import type {
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
import type { Packed } from '@/misc/json-schema.js';
import { type UserWebhookPayload } from './UserWebhookService.js';
@Injectable()
export class QueueService {
@ -469,10 +470,10 @@ export class QueueService {
* @see UserWebhookDeliverProcessorService
*/
@bindThis
public userWebhookDeliver(
public userWebhookDeliver<T extends WebhookEventTypes>(
webhook: MiWebhook,
type: typeof webhookEventTypes[number],
content: Packed<'UserWebhookBody'>,
type: T,
content: UserWebhookPayload<T>,
opts?: { attempts?: number },
) {
const data: UserWebhookDeliverJobData = {

View file

@ -30,7 +30,7 @@ import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
import { UserWebhookService } from './UserWebhookService.js';
import { UserWebhookPayload, UserWebhookService } from './UserWebhookService.js';
import { QueueService } from './QueueService.js';
import { Packed } from '@/misc/json-schema.js';
@ -273,7 +273,7 @@ export class ReactionService {
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
const userObj = await this.userEntityService.pack(user.id, null, { schema: 'UserLite' });
const payload: Packed<'UserWebhookReactionBody'> = {
const payload: UserWebhookPayload<'reaction'> = {
note: noteObj,
reaction: {
id: record.id,

View file

@ -6,11 +6,31 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { type WebhooksRepository } from '@/models/_.js';
import { MiWebhook } from '@/models/Webhook.js';
import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
export type UserWebhookPayload<T extends WebhookEventTypes> =
T extends 'reaction' ? {
reaction: {
id: string,
user: Packed<'UserLite'>,
reaction: string,
}
note: Packed<'Note'>,
}:
T extends 'note' | 'reply' | 'renote' |'mention' ? {
note: Packed<'Note'>,
} :
T extends 'follow' | 'unfollow' ? {
user: Packed<'UserDetailedNotMe'>,
} :
T extends 'followed' ? {
user: Packed<'UserLite'>,
} : never;
@Injectable()
export class UserWebhookService implements OnApplicationShutdown {

View file

@ -10,7 +10,7 @@ import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWeb
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
@ -259,7 +259,7 @@ function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailed
};
}
function generateDummyReactionPayload(note_override?: Partial<MiNote>): Packed<'UserWebhookReactionBody'> {
function generateDummyReactionPayload(note_override?: Partial<MiNote>): UserWebhookPayload<'reaction'> {
const dummyNote = generateDummyNote(note_override);
const dummyReaction = {
id: 'dummy-reaction-1',
@ -324,10 +324,10 @@ export class WebhookTestService {
* - on
*/
@bindThis
public async testUserWebhook(
public async testUserWebhook<T extends WebhookEventTypes>(
params: {
webhookId: MiWebhook['id'],
type: WebhookEventTypes,
type: T,
override?: Partial<Omit<MiWebhook, 'id'>>,
},
sender: MiUser | null,
@ -339,11 +339,7 @@ export class WebhookTestService {
}
const webhook = webhooks[0];
const send = (contents:
Packed<'UserWebhookNoteBody'> |
Packed<'UserWebhookUserBody'> |
Packed<'UserWebhookReactionBody'>,
) => {
const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
const merged = {
...webhook,
...params.override,
@ -351,7 +347,7 @@ export class WebhookTestService {
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図.
// また、Jobの試行回数も1回だけ.
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
};
const dummyNote1 = generateDummyNote({
@ -383,37 +379,35 @@ export class WebhookTestService {
switch (params.type) {
case 'note': {
send(wrapBodyEnum('note', toPackedNote(dummyNote1)));
send('note', { note: toPackedNote(dummyNote1) });
break;
}
case 'reply': {
send(wrapBodyEnum('note', toPackedNote(dummyReply1)));
send('reply', { note: toPackedNote(dummyReply1) });
break;
}
case 'renote': {
send(wrapBodyEnum('note', toPackedNote(dummyRenote1)));
send('renote', { note: toPackedNote(dummyRenote1) });
break;
}
case 'mention': {
send(wrapBodyEnum('note', toPackedNote(dummyMention1)));
send('mention', { note: toPackedNote(dummyMention1) });
break;
}
case 'follow': {
send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser1)));
send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
break;
}
case 'followed': {
send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser2)));
send('followed', { user: toPackedUserLite(dummyUser2) });
break;
}
case 'unfollow': {
send(wrapBodyEnum('user', toPackedUserDetailedNotMe(dummyUser3)));
break;
}
case 'reaction': {
send(generateDummyReactionPayload());
send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
break;
}
// まだ実装されていない (#9485)
case 'reaction': return;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;

View file

@ -59,7 +59,6 @@ import {
} from '@/models/json-schema/meta.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
import { packedUserWebhookBodySchema, packedUserWebhookNoteBodySchema, packedUserWebhookReactionBodySchema, packedUserWebhookUserBodySchema } from '@/models/json-schema/user-webhook.js';
export const refs = {
UserLite: packedUserLiteSchema,
@ -69,10 +68,6 @@ export const refs = {
MeDetailed: packedMeDetailedSchema,
UserDetailed: packedUserDetailedSchema,
User: packedUserSchema,
UserWebhookBody: packedUserWebhookBodySchema,
UserWebhookNoteBody: packedUserWebhookNoteBodySchema,
UserWebhookUserBody: packedUserWebhookUserBodySchema,
UserWebhookReactionBody: packedUserWebhookReactionBodySchema,
UserList: packedUserListSchema,
Ad: packedAdSchema,

View file

@ -1,80 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedUserWebhookNoteBodySchema = {
type: 'object',
properties: {
note: {
type: 'object',
ref: 'Note',
optional: false,
nullable: false,
},
},
nullable: false,
optional: false,
} as const;
export const packedUserWebhookUserBodySchema = {
type: 'object',
properties: {
user: {
type: 'object',
ref: 'UserLite',
optional: false,
nullable: false,
},
},
nullable: false,
optional: false,
} as const;
export const packedUserWebhookReactionBodySchema = {
type: 'object',
properties: {
note: {
type: 'object',
ref: 'Note',
optional: false,
nullable: false,
},
reaction: {
type: 'object',
properties: {
id: {
type: 'string',
optional: false,
nullable: false,
},
user: {
type: 'object',
ref: 'UserLite',
optional: false,
nullable: false,
},
reaction: {
type: 'string',
optional: false,
nullable: false,
},
},
optional: false,
nullable: false,
},
},
nullable: false,
optional: false,
} as const;
export const packedUserWebhookBodySchema = {
type: 'object',
oneOf: [
packedUserWebhookNoteBodySchema,
packedUserWebhookUserBodySchema,
packedUserWebhookReactionBodySchema,
],
nullable: false,
optional: false,
} as const;

View file

@ -89,14 +89,28 @@ export class ServerService implements OnApplicationShutdown {
fastify.addHook('onRequest', makeHstsHook(host, preload));
}
// Other Security/Privacy Headers
fastify.addHook('onRequest', (_, reply, done) => {
reply.header('x-content-type-options', 'nosniff');
reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC
if (this.config.browserSandboxing.strictOriginReferrer) {
reply.header('referrer-policy', 'strict-origin');
}
done();
});
// CSP
if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV === 'production' && !this.config.browserSandboxing.csp?.disable) {
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
const generatedCSP = generateCSP(this.config.cspPrerenderedContent, {
mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined,
script_src: [
`https://${new URL(this.config.url).host}/embed_vite/`,
`https://${new URL(this.config.url).host}/vite/`,
],
});
fastify.addHook('onRequest', (_, reply, done) => {
reply.header('Content-Security-Policy', generatedCSP);
reply.header('content-security-policy', generatedCSP);
done();
});
}

View file

@ -187,6 +187,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js'
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
@ -574,6 +575,7 @@ const $following_invalidate: Provider = { provide: 'ep:following/invalidate', us
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default };
const $following_requests_sent: Provider = { provide: 'ep:following/requests/sent', useClass: ep___following_requests_sent.default };
const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default };
const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default };
const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default };
@ -965,6 +967,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$following_requests_accept,
$following_requests_cancel,
$following_requests_list,
$following_requests_sent,
$following_requests_reject,
$gallery_featured,
$gallery_popular,

View file

@ -193,6 +193,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js'
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
@ -578,6 +579,7 @@ const eps = [
['following/requests/accept', ep___following_requests_accept],
['following/requests/cancel', ep___following_requests_cancel],
['following/requests/list', ep___following_requests_list],
['following/requests/sent', ep___following_requests_sent],
['following/requests/reject', ep___following_requests_reject],
['gallery/featured', ep___gallery_featured],
['gallery/popular', ep___gallery_popular],

View file

@ -15,18 +15,21 @@ 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 { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin'],
errors: {
accessDenied: {
httpStatusCode: 403,
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
},
wrongInitialPassword: {
httpStatusCode: 401,
message: 'Initial password is incorrect.',
code: 'INCORRECT_INITIAL_PASSWORD',
id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62',
@ -65,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private roleService: RoleService,
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
@ -85,8 +89,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// 初期パスワードが設定されていないのに初期パスワードが入力された場合
throw new ApiError(meta.errors.wrongInitialPassword);
}
} else if ((realUsers && !me?.isRoot) || token !== null) {
// 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合
} else if (!(me?.isRoot) && !await this.roleService.isAdministrator(me)) {
// 管理者でない場合
throw new ApiError(meta.errors.accessDenied);
} else if (token && !token?.permission.includes('write:admin:create-account')) {
// access token を使うときは write:admin:create-account 権限が必要
throw new ApiError(meta.errors.accessDenied);
}

View file

@ -0,0 +1,77 @@
/*
* 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 { QueryService } from '@/core/QueryService.js';
import type { FollowRequestsRepository } from '@/models/_.js';
import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['following', 'account'],
requireCredential: true,
kind: 'read:following',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
follower: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
followee: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private followRequestEntityService: FollowRequestEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId)
.andWhere('request.followerId = :meId', { meId: me.id });
const requests = await query
.limit(ps.limit)
.getMany();
return await this.followRequestEntityService.packMany(requests, me);
});
}
}

View file

@ -13,6 +13,8 @@ export type CSPHashed = {
export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
mediaProxy?: string,
script_src?: string[],
append?: { [key: string]: string | string[] },
}) {
const keys = Array.from(hashedMap.keys());
const scripts = keys
@ -22,7 +24,7 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
.filter(name => name.endsWith('.css'))
.map(name => `'${hashedMap.get(name)!.integrity}'`);
return ([
const cpolicy = [
['default-src', ['\'self\'']],
['img-src',
[
@ -42,7 +44,11 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
//
// ref: https://github.com/shikijs/shiki/issues/671
['style-src-attr', ['\'self\'', '\'unsafe-inline\'']],
['script-src', ['\'self\'', '\'wasm-unsafe-eval\'', ...scripts]],
['script-src', [
...(options.script_src ? options.script_src : ['\'self\'']),
'\'wasm-unsafe-eval\'',
...scripts
]],
['object-src', ['\'none\'']],
['base-uri', ['\'self\'']],
['form-action', ['\'self\'']],
@ -52,7 +58,23 @@ export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
[
['upgrade-insecure-requests', []],
] : []),
] as [string, string[]][])
] as [string, string[]][];
if (options.append) {
for (const [name, values] of Object.entries(options.append)) {
if (!values) {
continue;
}
const found = cpolicy.find(([n]) => n === name);
if (found) {
found[1].push(...(Array.isArray(values) ? values : [values]));
} else {
cpolicy.push([name, Array.isArray(values) ? values : [values]]);
}
}
}
return cpolicy
.map(([name, values]) => {
return `${name} ${values.join(' ')}`;
}).join('; ');

View file

@ -248,14 +248,28 @@ export class ClientServerService {
fastify.addHook('onRequest', makeHstsHook(host, preload));
}
// Other Security/Privacy Headers
fastify.addHook('onRequest', (_, reply, done) => {
reply.header('x-content-type-options', 'nosniff');
reply.header('permissions-policy', 'interest-cohort=()'); // Disable FLoC
if (this.config.browserSandboxing.strictOriginReferrer ?? true) {
reply.header('referrer-policy', 'strict-origin');
}
done();
});
// CSP
if (process.env.NODE_ENV === 'production') {
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
const generatedCSP = generateCSP(this.config.cspPrerenderedContent, {
mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined,
script_src: [
`https://${new URL(this.config.url).host}/embed_vite/`,
`https://${new URL(this.config.url).host}/vite/`,
],
});
fastify.addHook('onRequest', (_, reply, done) => {
reply.header('Content-Security-Policy', generatedCSP);
reply.header('content-security-policy', generatedCSP);
done();
});
}

View file

@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import type * as misskey from 'misskey-js';
import { api, role, signup } from '../utils.js';
describe('Admin Create User', () => {
let admin: misskey.entities.SignupResponse;
let user: misskey.entities.SignupResponse;
let formerAdmin: misskey.entities.SignupResponse;
let adminRole : misskey.entities.Role;
let formerAdminRole : misskey.entities.Role;
beforeAll(async () => {
admin = await signup({ username: 'admin' });
formerAdmin = await signup({ username: 'former_admin' });
user = await signup({ username: 'user' });
adminRole = await role(admin, {
name: 'admin',
isAdministrator: true
});
formerAdminRole = await role(admin, {
name: 'former_admin',
isAdministrator: true
});
const addAdminRole = await api('admin/roles/assign', {
userId: admin.id,
roleId: adminRole.id
}, admin);
assert.strictEqual(addAdminRole.status, 204);
const addFormerAdminRole = await api('admin/roles/assign', {
userId: formerAdmin.id,
roleId: formerAdminRole.id
}, admin);
assert.strictEqual(addFormerAdminRole.status, 204);
}, 1000 * 60 * 2);
test('Create User', async () => {
const newUser1 = await api('admin/accounts/create', {
username: 'new_user1',
password: 'password',
}, admin);
assert.strictEqual(newUser1.status, 200);
const newUser2 = await api('admin/accounts/create', {
username: 'new_user2',
password: 'password',
}, formerAdmin);
assert.strictEqual(newUser2.status, 200);
const newUser3 = await api('admin/accounts/create', {
username: 'new_user3',
password: 'password',
}, user);
assert.strictEqual(newUser3.status, 403);
});
test('Revoking Admin Role', async () => {
const res = await api('admin/roles/delete', {roleId: formerAdminRole.id}, admin);
assert.strictEqual(res.status, 204);
const res2 = await api('admin/roles/delete', {roleId: adminRole.id}, formerAdmin);
assert.strictEqual(res2.status, 403);
});
test('Revoked User Should Not Create User', async () => {
const newUser4 = await api('admin/accounts/create', {
username: 'new_user4',
password: 'password',
}, formerAdmin);
assert.strictEqual(newUser4.status, 403);
const newUser5 = await api('admin/accounts/create', {
username: 'new_user5',
password: 'password',
}, admin);
assert.strictEqual(newUser5.status, 200);
});
})

View file

@ -7,7 +7,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { beforeAll, describe, jest } from '@jest/globals';
import { WebhookTestService } from '@/core/WebhookTestService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@ -123,7 +123,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('note');
expect((calls[2] as Packed<'UserWebhookNoteBody'>).note.id).toBe('dummy-note-1');
expect((calls[2] as UserWebhookPayload<'note'>).note.id).toBe('dummy-note-1');
});
test('reply', async () => {
@ -132,7 +132,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('reply');
expect((calls[2] as Packed<'UserWebhookNoteBody'>).note.id).toBe('dummy-reply-1');
expect((calls[2] as UserWebhookPayload<'reply'>).note.id).toBe('dummy-reply-1');
});
test('renote', async () => {
@ -141,7 +141,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('renote');
expect((calls[2] as Packed<'UserWebhookNoteBody'>).note.id).toBe('dummy-renote-1');
expect((calls[2] as UserWebhookPayload<'renote'>).note.id).toBe('dummy-renote-1');
});
test('mention', async () => {
@ -150,7 +150,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('mention');
expect((calls[2] as Packed<'UserWebhookNoteBody'>).note.id).toBe('dummy-mention-1');
expect((calls[2] as UserWebhookPayload<'mention'>).note.id).toBe('dummy-mention-1');
});
test('follow', async () => {
@ -159,7 +159,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('follow');
expect((calls[2] as Packed<'UserWebhookUserBody'>).user.id).toBe('dummy-user-1');
expect((calls[2] as UserWebhookPayload<'follow'>).user.id).toBe('dummy-user-1');
});
test('followed', async () => {
@ -168,7 +168,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('followed');
expect((calls[2] as Packed<'UserWebhookUserBody'>).user.id).toBe('dummy-user-2');
expect((calls[2] as UserWebhookPayload<'followed'>).user.id).toBe('dummy-user-2');
});
test('unfollow', async () => {
@ -177,16 +177,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('unfollow');
expect((calls[2] as Packed<'UserWebhookUserBody'>).user.id).toBe('dummy-user-3');
});
test('reaction', async () => {
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reaction' }, alice);
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('reaction');
expect((calls[2] as Packed<'UserWebhookReactionBody'>).reaction.id).toBe('dummy-reaction-1');
expect((calls[2] as UserWebhookPayload<'unfollow'>).user.id).toBe('dummy-user-3');
});
describe('NoSuchWebhookError', () => {

View file

@ -14,11 +14,11 @@
"@discordapp/twemoji": "15.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.2",
"@rollup/pluginutils": "5.1.3",
"@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.11",
"@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.12",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@ -26,47 +26,47 @@
"misskey-js": "workspace:*",
"frontend-shared": "workspace:*",
"punycode": "2.3.1",
"rollup": "4.22.5",
"rollup": "4.26.0",
"sass": "1.79.4",
"shiki": "1.21.0",
"shiki": "1.22.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typescript": "5.6.2",
"typescript": "5.6.3",
"uuid": "10.0.0",
"json5": "2.2.3",
"vite": "5.4.8",
"vue": "3.5.11"
"vite": "5.4.11",
"vue": "3.5.12"
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.6",
"@types/micromatch": "4.0.9",
"@types/node": "20.14.12",
"@types/node": "22.9.0",
"@types/punycode": "2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.12",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.11",
"acorn": "8.12.1",
"@vue/runtime-core": "3.5.12",
"acorn": "8.14.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.28.0",
"eslint-plugin-vue": "9.31.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.3.4",
"msw": "2.6.4",
"nodemon": "3.1.7",
"prettier": "3.3.3",
"start-server-and-test": "2.0.8",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "2.1.6",
"vue-component-type-helpers": "2.1.10",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.1.6"
"vue-tsc": "2.1.10"
}
}

View file

@ -21,12 +21,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "20.14.12",
"nodemon": "3.1.7",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"esbuild": "0.23.0",
"eslint-plugin-vue": "9.27.0",
"typescript": "5.5.4",
"esbuild": "0.24.0",
"eslint-plugin-vue": "9.31.0",
"typescript": "5.6.3",
"vue-eslint-parser": "9.4.3"
},
"files": [
@ -34,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.4.37"
"vue": "3.5.12"
}
}

View file

@ -23,23 +23,23 @@
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.2",
"@rollup/pluginutils": "5.1.3",
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.11",
"@vitejs/plugin-vue": "5.2.0",
"@vue/compiler-sfc": "3.5.12",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
"astring": "1.9.0",
"broadcast-channel": "7.0.0",
"buraha": "0.0.1",
"canvas-confetti": "1.9.3",
"chart.js": "4.4.4",
"chart.js": "4.4.6",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
"chromatic": "11.11.0",
"chromatic": "11.18.1",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.2",
"date-fns": "2.30.0",
@ -57,10 +57,10 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode": "2.3.1",
"rollup": "4.22.5",
"rollup": "4.26.0",
"sanitize-html": "2.13.1",
"sass": "1.79.3",
"shiki": "1.21.0",
"shiki": "1.22.2",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.169.0",
@ -68,74 +68,74 @@
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typescript": "5.6.2",
"typescript": "5.6.3",
"uuid": "10.0.0",
"v-code-diff": "1.13.1",
"vite": "5.4.8",
"vue": "3.5.11",
"vite": "5.4.11",
"vue": "3.5.12",
"vuedraggable": "next"
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.3.4",
"@storybook/addon-essentials": "8.3.4",
"@storybook/addon-interactions": "8.3.4",
"@storybook/addon-links": "8.3.4",
"@storybook/addon-mdx-gfm": "8.3.4",
"@storybook/addon-storysource": "8.3.4",
"@storybook/blocks": "8.3.4",
"@storybook/components": "8.3.4",
"@storybook/core-events": "8.3.4",
"@storybook/manager-api": "8.3.4",
"@storybook/preview-api": "8.3.4",
"@storybook/react": "8.3.4",
"@storybook/react-vite": "8.3.4",
"@storybook/test": "8.3.4",
"@storybook/theming": "8.3.4",
"@storybook/types": "8.3.4",
"@storybook/vue3": "8.3.4",
"@storybook/vue3-vite": "8.3.4",
"@storybook/addon-actions": "8.4.4",
"@storybook/addon-essentials": "8.4.4",
"@storybook/addon-interactions": "8.4.4",
"@storybook/addon-links": "8.4.4",
"@storybook/addon-mdx-gfm": "8.4.4",
"@storybook/addon-storysource": "8.4.4",
"@storybook/blocks": "8.4.4",
"@storybook/components": "8.4.4",
"@storybook/core-events": "8.4.4",
"@storybook/manager-api": "8.4.4",
"@storybook/preview-api": "8.4.4",
"@storybook/react": "8.4.4",
"@storybook/react-vite": "8.4.4",
"@storybook/test": "8.4.4",
"@storybook/theming": "8.4.4",
"@storybook/types": "8.4.4",
"@storybook/vue3": "8.4.4",
"@storybook/vue3-vite": "8.4.4",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "^1.6.4",
"@types/estree": "1.0.6",
"@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9",
"@types/node": "20.14.12",
"@types/node": "22.9.0",
"@types/punycode": "2.1.4",
"@types/sanitize-html": "2.13.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.12",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.5.11",
"acorn": "8.12.1",
"@vue/runtime-core": "3.5.12",
"acorn": "8.14.0",
"cross-env": "7.0.3",
"cypress": "13.15.0",
"cypress": "13.15.2",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.28.0",
"eslint-plugin-vue": "9.31.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.4.9",
"msw-storybook-addon": "2.0.3",
"msw": "2.6.4",
"msw-storybook-addon": "2.0.4",
"nodemon": "3.1.7",
"prettier": "3.3.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.8",
"storybook": "8.3.4",
"storybook": "8.4.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "1.6.0",
"vitest-fetch-mock": "0.2.2",
"vue-component-type-helpers": "2.1.6",
"vue-component-type-helpers": "2.1.10",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.1.6"
"vue-tsc": "2.1.10"
}
}

View file

@ -91,7 +91,10 @@ async function onClick() {
text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }),
});
if (canceled) return;
if (canceled) {
wait.value = false;
return;
}
await misskeyApi('following/delete', {
userId: props.user.id,
@ -125,7 +128,10 @@ async function onClick() {
});
hasPendingFollowRequestFromYou.value = true;
if ($i == null) return;
if ($i == null) {
wait.value = false;
return;
}
claimAchievement('following1');

View file

@ -62,7 +62,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
</div>
</div>
<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
<div :class="$style.noteHeaderUsernameAndBadgeRoles">
<div :class="$style.noteHeaderUsername">
<MkAcct :user="appearNote.user"/>
</div>
<div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles">
<img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
</div>
</div>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
</div>
</header>
@ -679,12 +686,30 @@ function loadConversation() {
float: right;
}
.noteHeaderUsernameAndBadgeRoles {
display: flex;
}
.noteHeaderUsername {
margin-bottom: 2px;
margin-right: 0.5em;
line-height: 1.3;
word-wrap: anywhere;
}
.noteHeaderBadgeRoles {
margin: 0 .5em 0 0;
}
.noteHeaderBadgeRole {
height: 1.3em;
vertical-align: -20%;
& + .noteHeaderBadgeRole {
margin-left: 0.2em;
}
}
.noteContent {
container-type: inline-size;
overflow-wrap: break-word;

View file

@ -27,7 +27,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
function whatIsNew() {
modal.value?.close();
window.open(`https://misskey-hub.net/docs/releases/#_${version.replace(/\./g, '')}`, '_blank');
window.open(`https://forge.yumechi.jp/yume/yumechi-no-kuni/commits/tag/${version.replace(/^v/g, '')}`, '_blank');
}
onMounted(() => {

View file

@ -40,7 +40,6 @@ export const navbarItemDef = reactive({
followRequests: {
title: i18n.ts.followRequests,
icon: 'ti ti-user-plus',
show: computed(() => $i != null && $i.isLocked),
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
to: '/my/follow-requests',
},

View file

@ -275,6 +275,9 @@ const patronsWithIcon = [{
}, {
name: 'Yatoigawa',
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
}, {
name: '秋瀬カヲル',
icon: 'https://assets.misskey-hub.net/patrons/0f22aeb866484f4fa51db6721e3f9847.jpg',
}].map(patron => ({
...patron,
icon: getProxiedImageUrl(patron.icon, 'avatar'),
@ -386,6 +389,7 @@ const patrons = [
'ケモナーのケシン',
'こまつぶり',
'まゆつな空高',
'asata',
];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

View file

@ -627,6 +627,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.ip {
display: flex;
word-break: break-all;
> :global(.date) {
opacity: 0.7;

View file

@ -5,8 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div :key="tab" class="_gaps">
<MkPagination ref="paginationComponent" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
@ -17,57 +19,90 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{items}">
<div class="mk-follow-requests">
<div v-for="req in items" :key="req.id" class="user _panel">
<MkAvatar class="avatar" :user="req.follower" indicator link preview/>
<MkAvatar class="avatar" :user="displayUser(req)" indicator link preview/>
<div class="body">
<div class="name">
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
<p class="acct">@{{ acct(req.follower) }}</p>
<MkA v-user-preview="displayUser(req).id" class="name" :to="userPage(displayUser(req))"><MkUserName :user="displayUser(req)"/></MkA>
<p class="acct">@{{ acct(displayUser(req)) }}</p>
</div>
<div class="commands">
<MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
<MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
<div v-if="tab === 'list'" class="commands">
<MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
<MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
</div>
<div v-else class="commands">
<MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.cancel }}</MkButton>
</div>
</div>
</div>
</div>
</template>
</MkPagination>
</div>
</MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { shallowRef, computed } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import * as Misskey from 'misskey-js';
import { shallowRef, computed, ref } from 'vue';
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import { userPage, acct } from '@/filters/user.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { infoImageUrl } from '@/instance.js';
import { $i } from '@/account.js';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
const pagination = {
endpoint: 'following/requests/list' as const,
const pagination = computed<Paging>(() => tab.value === 'list' ? {
endpoint: 'following/requests/list',
limit: 10,
};
} : {
endpoint: 'following/requests/sent',
limit: 10,
});
function accept(user) {
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
function accept(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => {
paginationComponent.value?.reload();
});
}
function reject(user) {
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
function reject(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
paginationComponent.value?.reload();
});
}
function cancel(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
paginationComponent.value?.reload();
});
}
function displayUser(req) {
return tab.value === 'list' ? req.follower : req.followee;
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
const headerTabs = computed(() => [
{
key: 'list',
title: i18n.ts._followRequest.recieved,
icon: 'ti ti-mail',
}, {
key: 'sent',
title: i18n.ts._followRequest.sent,
icon: 'ti ti-send',
},
]);
const tab = ref($i?.isLocked ? 'list' : 'sent');
definePageMetadata(() => ({
title: i18n.ts.followRequests,

View file

@ -38,11 +38,9 @@ function createStream(): Misskey.IStream {
console.log('Stream disconnected, attempting to reconnect...');
if (reconnectAttempts < RECONNECT_MAX_ATTEMPTS) {
const delay = getReconnectDelay();
reconnectTimeout = window.setTimeout(() => {
reconnectAttempts++;
stream = null;
useStream();
}, delay);
reconnectTimeout = window.setTimeout(useStream, delay);
} else {
console.error('Max reconnection attempts reached');
}

View file

@ -497,20 +497,17 @@ html[data-color-scheme=dark] ._woodenFrame {
10%,
20% {
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
transform: scale3d(0.91, 0.91, 0.91) rotate3d(0, 0, 1, -2deg);
}
30%,
50%,
70%,
90% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
70% {
transform: scale3d(1.09, 1.09, 1.09) rotate3d(0, 0, 1, 2deg);
}
40%,
60%,
80% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
50%,
90% {
transform: scale3d(1.09, 1.09, 1.09) rotate3d(0, 0, 1, -2deg);
}
to {

View file

@ -22,16 +22,16 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/matter-js": "0.19.6",
"@types/matter-js": "0.19.7",
"@types/seedrandom": "3.0.8",
"@types/node": "20.11.5",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "7.1.0",
"nodemon": "3.0.2",
"execa": "8.0.1",
"typescript": "5.3.3",
"esbuild": "0.19.11",
"glob": "10.3.10"
"nodemon": "3.1.7",
"execa": "9.5.1",
"typescript": "5.6.3",
"esbuild": "0.24.0",
"glob": "11.0.0"
},
"files": [
"built"

View file

@ -1504,6 +1504,8 @@ declare namespace entities {
FollowingRequestsCancelResponse,
FollowingRequestsListRequest,
FollowingRequestsListResponse,
FollowingRequestsSentRequest,
FollowingRequestsSentResponse,
FollowingRequestsRejectRequest,
GalleryFeaturedRequest,
GalleryFeaturedResponse,
@ -2022,6 +2024,12 @@ type FollowingRequestsListResponse = operations['following___requests___list']['
// @public (undocumented)
type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json'];
// @public (undocumented)
type FollowingRequestsSentRequest = operations['following___requests___sent']['requestBody']['content']['application/json'];
// @public (undocumented)
type FollowingRequestsSentResponse = operations['following___requests___sent']['responses']['200']['content']['application/json'];
// @public (undocumented)
type FollowingUpdateAllRequest = operations['following___update-all']['requestBody']['content']['application/json'];
@ -2876,7 +2884,7 @@ type PartialRolePolicyOverride = Partial<{
}>;
// @public (undocumented)
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:create-account", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
// @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];

View file

@ -8,14 +8,14 @@
},
"devDependencies": {
"@readme/openapi-parser": "2.6.0",
"@types/node": "20.9.1",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"openapi-types": "12.1.3",
"openapi-typescript": "6.7.3",
"ts-case-convert": "2.0.7",
"ts-case-convert": "2.1.0",
"tsx": "4.4.0",
"typescript": "5.6.2"
"typescript": "5.6.3"
},
"files": [
"built"

View file

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.11.0-yumechinokuni.0",
"version": "2024.11.0-yumechinokuni.3",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@ -35,10 +35,10 @@
"directory": "packages/misskey-js"
},
"devDependencies": {
"@microsoft/api-extractor": "7.47.9",
"@swc/jest": "0.2.36",
"@types/jest": "29.5.13",
"@types/node": "20.14.12",
"@microsoft/api-extractor": "7.47.11",
"@swc/jest": "0.2.37",
"@types/jest": "29.5.14",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"jest": "29.7.0",
@ -47,17 +47,17 @@
"mock-socket": "9.3.1",
"ncp": "2.0.0",
"nodemon": "3.1.7",
"execa": "9.4.0",
"execa": "9.5.1",
"tsd": "0.31.2",
"typescript": "5.6.2",
"esbuild": "0.23.1",
"typescript": "5.6.3",
"esbuild": "0.24.0",
"glob": "11.0.0"
},
"files": [
"built"
],
"dependencies": {
"@simplewebauthn/types": "10.0.0",
"@simplewebauthn/types": "11.0.0",
"eventemitter3": "5.0.1",
"reconnecting-websocket": "4.4.0"
}

View file

@ -2008,6 +2008,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:following*
*/
request<E extends 'following/requests/sent', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View file

@ -279,6 +279,8 @@ import type {
FollowingRequestsCancelResponse,
FollowingRequestsListRequest,
FollowingRequestsListResponse,
FollowingRequestsSentRequest,
FollowingRequestsSentResponse,
FollowingRequestsRejectRequest,
GalleryFeaturedRequest,
GalleryFeaturedResponse,
@ -761,6 +763,7 @@ export type Endpoints = {
'following/requests/accept': { req: FollowingRequestsAcceptRequest; res: EmptyResponse };
'following/requests/cancel': { req: FollowingRequestsCancelRequest; res: FollowingRequestsCancelResponse };
'following/requests/list': { req: FollowingRequestsListRequest; res: FollowingRequestsListResponse };
'following/requests/sent': { req: FollowingRequestsSentRequest; res: FollowingRequestsSentResponse };
'following/requests/reject': { req: FollowingRequestsRejectRequest; res: EmptyResponse };
'gallery/featured': { req: GalleryFeaturedRequest; res: GalleryFeaturedResponse };
'gallery/popular': { req: EmptyRequest; res: GalleryPopularResponse };

View file

@ -282,6 +282,8 @@ export type FollowingRequestsCancelRequest = operations['following___requests___
export type FollowingRequestsCancelResponse = operations['following___requests___cancel']['responses']['200']['content']['application/json'];
export type FollowingRequestsListRequest = operations['following___requests___list']['requestBody']['content']['application/json'];
export type FollowingRequestsListResponse = operations['following___requests___list']['responses']['200']['content']['application/json'];
export type FollowingRequestsSentRequest = operations['following___requests___sent']['requestBody']['content']['application/json'];
export type FollowingRequestsSentResponse = operations['following___requests___sent']['responses']['200']['content']['application/json'];
export type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json'];
export type GalleryFeaturedRequest = operations['gallery___featured']['requestBody']['content']['application/json'];
export type GalleryFeaturedResponse = operations['gallery___featured']['responses']['200']['content']['application/json'];

View file

@ -1753,6 +1753,15 @@ export type paths = {
*/
post: operations['following___requests___list'];
};
'/following/requests/sent': {
/**
* following/requests/sent
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:following*
*/
post: operations['following___requests___sent'];
};
'/following/requests/reject': {
/**
* following/requests/reject
@ -16066,6 +16075,69 @@ export type operations = {
};
};
};
/**
* following/requests/sent
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:following*
*/
following___requests___sent: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
/** @default 10 */
limit?: number;
};
};
};
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': {
/** Format: id */
id: string;
follower: components['schemas']['UserLite'];
followee: components['schemas']['UserLite'];
}[];
};
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* following/requests/reject
* @description No description provided.

View file

@ -64,6 +64,7 @@ export const permissions = [
'read:flash-likes',
'write:flash-likes',
'read:admin:abuse-user-reports',
'write:admin:create-account',
'write:admin:delete-account',
'write:admin:delete-all-files-of-a-user',
'read:admin:index-stats',

View file

@ -22,14 +22,14 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "20.11.5",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "7.1.0",
"execa": "8.0.1",
"nodemon": "3.0.2",
"typescript": "5.3.3",
"esbuild": "0.19.11",
"glob": "10.3.10"
"execa": "9.5.1",
"nodemon": "3.1.7",
"typescript": "5.6.3",
"esbuild": "0.24.0",
"glob": "11.0.0"
},
"files": [
"built"

View file

@ -9,7 +9,7 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"esbuild": "0.23.1",
"esbuild": "0.24.0",
"idb-keyval": "6.2.1",
"misskey-js": "workspace:*"
},
@ -18,7 +18,7 @@
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
"eslint-plugin-import": "2.30.0",
"nodemon": "3.1.7",
"typescript": "5.6.2"
"typescript": "5.6.3"
},
"type": "module"
}

File diff suppressed because it is too large Load diff

View file

@ -9,15 +9,15 @@
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@types/mdast": "4.0.3",
"@types/node": "20.10.7",
"@types/mdast": "4.0.4",
"@types/node": "22.9.0",
"@vitest/coverage-v8": "1.1.3",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.3.3",
"unified": "11.0.4",
"vite": "5.4.6",
"typescript": "5.6.3",
"unified": "11.0.5",
"vite": "5.4.11",
"vite-node": "1.1.3",
"vitest": "1.1.3"
}

1
yume-mods/admin-scripts/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/secrets

View file

@ -0,0 +1,79 @@
#!/usr/bin/env fish
argparse --name batch_register 'n/number=' 'h/host=' 'm/mount=' -- $argv
set -l dir_name (dirname (realpath (status -f)))
echo "Running in $dir_name"
if [ -f $dir_name/.env ]
source $dir_name/.env
end
if [ -f $dir_name/secrets/.env ]
source $dir_name/secrets/.env
end
if ! set -q MISSKEY_TOKEN
echo "Please set MISSKEY_TOKEN in .env file."
exit 1
end
if ! set -q VAULT_MOUNT
echo "Please set VAULT_MOUNT in .env file."
exit 1
end
if set -q _flag_host
set HOST $_flag_host
else
set HOST "https://test0.mi.yumechi.jp"
end
if set -q _flag_number
set NUMBER $_flag_number
else
set NUMBER 1
end
if ! set -q ACCOUNT_PREFIX
set ACCOUNT_PREFIX test_automated_
end
for i in (seq 1 $NUMBER)
set -l USERNAME "$ACCOUNT_PREFIX"(openssl rand 8 | base64 | tr -d /+=)
set -l PASSWORD (openssl rand 16 | base64)
echo "[$i/$NUMBER]" \
"Registering $USERNAME"
echo "$PASSWORD" | vault kv put -mount=$VAULT_MOUNT $USERNAME username=$USERNAME password=-
or begin
echo "Failed to save password for $USERNAME"
exit 3
end
set -l start (date +%s%N)
jq -nr --arg username $USERNAME --arg password $PASSWORD '{
username: $username,
password: $password
}' | curl -f --tlsv1.2 -d@- \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MISSKEY_TOKEN" \
"$HOST/api/admin/accounts/create" | \
jq --arg password $PASSWORD \
'{"id": .id, "username": .username, "password": $password, "success": true, "token": .token}' | \
tee /dev/stderr | \
vault kv put -mount=$VAULT_MOUNT $USERNAME @/dev/stdin
or begin
echo "Failed to register $USERNAME"
vault kv destroy -mount=$VAULT_MOUNT -versions=1 $USERNAME
exit 2
end
echo "[$i/$NUMBER]" \
"Registered $USERNAME" (math \((date +%s%N) - $start\) / 1000000) ms
end
echo "Done."