Merge branch 'develop' into renovate/major-backend-update-dependencies

This commit is contained in:
かっこかり 2025-05-29 13:19:35 +09:00 committed by GitHub
commit 66f7e5acd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 609 additions and 143 deletions

View File

@ -18,6 +18,7 @@
- デフォルトは**テキスト、JSON、画像、動画、音声ファイル**になっています。zipなど、その他の種別のファイルは含まれていないため、必要に応じて設定を変更してください。
- 場合によってはファイル種別を正しく検出できないことがあります(特にテキストフォーマット)。その場合、ファイル種別は application/octet-stream と見做されます。
- したがって、それらの種別不明ファイルを許可したい場合は application/octet-stream を指定に追加してください。
- Feat: プレビュー先がリダイレクトを伴う場合、リダイレクト先のコンテンツを取得しに行くか否かを設定できるように(#16043)
- Enhance: UIのアイコンデータの読み込みを軽量化
- 依存関係の更新
@ -56,8 +57,10 @@
- ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。
- Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正
- Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように
- Fix: Twitchのクリップがプレイヤーで再生できない問題を修正
### Server
- Enhance: リストやフォローをエクスポートする際にリプライを含むかどうかの情報を含むように
- Enhance: チャットルームの最大メンバー数を30人から50人に調整
- Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加
- Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加
@ -65,6 +68,7 @@
- Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正
- Fix: ユーザ除外アンテナをインポートできない問題を修正
- Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正
- Fix: ミュート対象ユーザーが引用されているートがRNされたときにミュートを貫通してしまう問題を修正 #16009
- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように
- Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正

View File

@ -1001,6 +1001,7 @@ failedToUpload: "Ha fallat la pujada"
cannotUploadBecauseInappropriate: "Aquest fitxer no es pot pujar perquè s'ha trobat que algunes parts són inapropiades."
cannotUploadBecauseNoFreeSpace: "Ha fallat la pujada del fitxer perquè no hi ha capacitat al Disc."
cannotUploadBecauseExceedsFileSizeLimit: "Aquest fitxer no es pot pujar perquè supera la mida permesa."
cannotUploadBecauseUnallowedFileType: "Impossible pujar l'arxiu no és un tipus de fitxer autoritzat."
beta: "Proves"
enableAutoSensitive: "Marcar com a sensible automàticament "
enableAutoSensitiveDescription: "Permet la detecció i el marcat automàtic dels mitjans sensibles fent servir aprenentatge automàtic quan sigui possible. Si aquesta opció es troba desactivada potser que estigui activada per a tota la instància. "
@ -1359,6 +1360,9 @@ emojiUnmute: "Deixar de silenciar emojis"
muteX: "Silenciar {x}"
unmuteX: "Deixar de silenciar {x}"
abort: "Cancel·lar"
tip: "Trucs i consells"
redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
hideAllTips: "Amagar tots els trucs i consells"
_chat:
noMessagesYet: "Encara no tens missatges "
newMessage: "Missatge nou"
@ -1981,6 +1985,9 @@ _role:
canImportMuting: "Autoritza la importació de silenciats"
canImportUserLists: "Autoritza la importació de llistes d'usuaris "
chatAvailability: "Es permet xatejar"
uploadableFileTypes: "Tipus de fitxers que en podeu pujar"
uploadableFileTypes_caption: "Especifica el tipus MIME. Es poden especificar diferents tipus MIME separats amb una nova línia, i es poden especificar comodins amb asteriscs (*). (Per exemple: image/*)"
uploadableFileTypes_caption2: "Pot que no sigui possible determinar el tipus MIME d'alguns arxius. Per permetre aquests tipus d'arxius afegeix {x} a les especificacions."
_condition:
roleAssignedTo: "Assignat a rols manuals"
isLocal: "Usuari local"
@ -2896,6 +2903,8 @@ _offlineScreen:
_urlPreviewSetting:
title: "Configuració per a la previsualització de l'URL"
enable: "Activa la previsualització de l'URL"
allowRedirect: "Permet la redirecció de la visualització prèvia "
allowRedirectDescription: "Estableix si es mostra o no la redirecció a la vista prèvia quan l'adreça URL introduïda té una redirecció. Si es desactiva s'estalvien recursos del servidor, però no es mostrarà el contingut de la redirecció."
timeout: "Temps màxim per carregar la previsualització de l'URL (ms)"
timeoutDescription: "Si l'obtenció de la previsualització triga més que el temps establert, no es generarà la vista prèvia."
maximumContentLength: "Longitud màxima del contingut (bytes)"
@ -3092,6 +3101,8 @@ _uploader:
abortConfirm: "Hi ha un arxiu que no s'ha pujat, vols cancel·lar?"
doneConfirm: "Hi han fitxers no pujats, vols completar-los?"
maxFileSizeIsX: "La mida màxima d'arxiu que es pot pujar és {x}."
allowedTypes: "Tipus de fitxers que en podeu pujar"
tip: "L'arxiu encara no s'ha carregat. En aquest quadre de diàleg, pots comprovar, canviar el nom, comprimir i retallar l'arxiu abans de pujar-lo. Quan estigui llest pots iniciar la càrrega polsant el boto \"Pujar\""
_clientPerformanceIssueTip:
title: "Si creus que el consum de bateria és molt alt"
makeSureDisabledAdBlocker: "Desactiva els bloquejadors de publicitat"
@ -3100,3 +3111,7 @@ _clientPerformanceIssueTip:
makeSureDisabledCustomCss_description: "L'anul·lació dels estils pot afectar el rendiment. Comprova que el CSS personalitzat o les extensions que reescriuen estils no estiguin activats."
makeSureDisabledAddons: "Desactiva extensions"
makeSureDisabledAddons_description: "Algunes extensions poden interferir en el comportament del client i afectar el rendiment. Desactiva les extensions del navegador i comprovar-ho."
_clip:
tip: "Clip és una funció que permet organitzar les teves notes."
_userLists:
tip: "Es poden crear llistes amb qualsevol usuari. La llista creada es pot mostrar com una línia de temps."

View File

@ -579,6 +579,7 @@ newNoteRecived: "There are new notes"
newNote: "New Note"
sounds: "Sounds"
sound: "Sounds"
notificationSoundSettings: "Notification sound settings"
listen: "Listen"
none: "None"
showInPage: "Show in page"
@ -1000,6 +1001,7 @@ failedToUpload: "Upload failed"
cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially inappropriate."
cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity."
cannotUploadBecauseExceedsFileSizeLimit: "This file cannot be uploaded as it exceeds the file size limit."
cannotUploadBecauseUnallowedFileType: "Unable to upload due to unauthorized file type."
beta: "Beta"
enableAutoSensitive: "Automatic marking as sensitive"
enableAutoSensitiveDescription: "Allows automatic detection and marking of sensitive media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
@ -1357,6 +1359,10 @@ emojiMute: "Mute emoji"
emojiUnmute: "Unmute emoji"
muteX: "Mute {x}"
unmuteX: "Unmute {x}"
abort: "Abort"
tip: "Tips & Tricks"
redisplayAllTips: "Show all “Tips & Tricks” again"
hideAllTips: "Hide all \"Tips & Tricks\""
_chat:
noMessagesYet: "No messages yet"
newMessage: "New message"
@ -1979,6 +1985,9 @@ _role:
canImportMuting: "Allow importing muting"
canImportUserLists: "Allow importing lists"
chatAvailability: "Allow Chat"
uploadableFileTypes: "Uploadable file types"
uploadableFileTypes_caption: "Specifies the allowed MIME/file types. Multiple MIME types can be specified by separating them with a new line, and wildcards can be specified with an asterisk (*). (e.g., image/*)"
uploadableFileTypes_caption2: "Some files types might fail to be detected. To allow such files, add {x} to the specification."
_condition:
roleAssignedTo: "Assigned to manual roles"
isLocal: "Local user"
@ -2894,6 +2903,8 @@ _offlineScreen:
_urlPreviewSetting:
title: "URL preview settings"
enable: "Enable URL preview"
allowRedirect: "Allow URL preview redirection"
allowRedirectDescription: "If a URL has a redirection set, you can enable this feature to follow the redirection and display a preview of the redirected content. Disabling this will save server resources, but redirected content will not be displayed."
timeout: "Time out when getting preview (ms)"
timeoutDescription: "If it takes longer than this value to get the preview, the preview wont be generated."
maximumContentLength: "Maximum Content-Length (bytes)"
@ -3090,6 +3101,8 @@ _uploader:
abortConfirm: "Some files have not been uploaded, do you want to abort?"
doneConfirm: "Some files have not been uploaded, do you want to continue anyway?"
maxFileSizeIsX: "The maximum file size that can be uploaded is {x}"
allowedTypes: "Uploadable file types"
tip: "The file has not yet been uploaded so this dialog allows you to confirm, rename, compress, and crop the file before uploading. When ready, you can start uploading by pressing the “Upload” button."
_clientPerformanceIssueTip:
title: "Performance tips"
makeSureDisabledAdBlocker: "Disable your adblocker"
@ -3098,3 +3111,7 @@ _clientPerformanceIssueTip:
makeSureDisabledCustomCss_description: "Overriding styles can affect performance. Please make sure that custom CSS or extensions that override styles are not enabled."
makeSureDisabledAddons: "Disable extensions"
makeSureDisabledAddons_description: "Some extensions may interfere with client behavior and affect performance. Please disable your browser extensions and see if this improves the situation."
_clip:
tip: "Clip is a feature that allows you to organize your notes."
_userLists:
tip: "Lists can contain any user you specify when creating, the created list can then be displayed as a timeline showing only the specified users."

10
locales/index.d.ts vendored
View File

@ -9707,7 +9707,7 @@ export interface Locale extends ILocale {
*/
"excludeInactiveUsers": string;
/**
* TLに含むようにする
* TLに含むかの情報がファイルにない場合にTLに含むようにする
*/
"withReplies": string;
};
@ -11232,6 +11232,14 @@ export interface Locale extends ILocale {
* URLプレビューを有効にする
*/
"enable": string;
/**
*
*/
"allowRedirect": string;
/**
* URLがリダイレクトされる場合に
*/
"allowRedirectDescription": string;
/**
* (ms)
*/

View File

@ -220,6 +220,7 @@ silenceThisInstance: "Silenziare l'istanza"
mediaSilenceThisInstance: "Silenzia i media dell'istanza"
operations: "Operazioni"
software: "Software"
softwareName: "Nome del software"
version: "Versione"
metadata: "Metadato"
withNFiles: "{n} file in allegato"
@ -297,6 +298,7 @@ uploadFromUrl: "Incolla URL immagine"
uploadFromUrlDescription: "URL del file che vuoi caricare"
uploadFromUrlRequested: "Caricamento richiesto"
uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo."
uploadNFiles: "Caricare {n} file singolarmente"
explore: "Esplora"
messageRead: "Visualizzato"
noMoreHistory: "Non c'è più cronologia da visualizzare"
@ -574,8 +576,10 @@ showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timel
showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline"
withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita"
newNoteRecived: "Nuove Note da leggere"
newNote: "Nuova Nota"
sounds: "Impostazioni suoni"
sound: "Suono"
notificationSoundSettings: "Preferenze di notifica"
listen: "Ascolta"
none: "Nessuno"
showInPage: "Visualizza in pagina"
@ -790,6 +794,7 @@ wide: "Largo"
narrow: "Stretto"
reloadToApplySetting: "Le tue preferenze verranno impostate dopo il ricaricamento della pagina. Vuoi ricaricare adesso?"
needReloadToApply: "È necessario riavviare per rendere effettive le modifiche."
needToRestartServerToApply: "Per attivare le modifiche, occorre riavviare il server."
showTitlebar: "Visualizza la barra del titolo"
clearCache: "Svuota la cache"
onlineUsersCount: "{n} persone attive adesso"
@ -996,6 +1001,7 @@ failedToUpload: "errore di caricamento"
cannotUploadBecauseInappropriate: "Non è possibile caricarlo perché è stato stabilito che potrebbe contenere contenuti inappropriati."
cannotUploadBecauseNoFreeSpace: "Impossibile caricare a causa della mancanza di spazio libero sul drive."
cannotUploadBecauseExceedsFileSizeLimit: "Il file non può essere caricato perché eccede le dimensioni consentite."
cannotUploadBecauseUnallowedFileType: "Impossibile caricare a causa di un tipo file non autorizzato."
beta: "Versione beta"
enableAutoSensitive: "Determinazione automatica del NSFW"
enableAutoSensitiveDescription: "Se disponibile, il flag NSFW viene impostato automaticamente sui media utilizzando l'apprendimento automatico. Anche se questa funzione è disattivata, in alcuni casi può essere impostata automaticamente."
@ -1342,6 +1348,20 @@ embed: "Incorporare"
settingsMigrating: "Migrazione delle impostazioni. Attendere prego ... (Puoi anche migrare manualmente in un secondo momento, nel menu: Impostazioni → Altro → Migrazione delle impostazioni)"
readonly: "Sola lettura"
goToDeck: "Torna al Deck"
federationJobs: "Coda di federazione"
scrollToClose: "Scorri per chiudere"
advice: "Consiglio"
realtimeMode: "Modalità in tempo reale"
turnItOn: "Attivare"
turnItOff: "Disattivare"
emojiMute: "Silenzia emoji"
emojiUnmute: "De silenzia emoji"
muteX: "Silenzia {x}"
unmuteX: "De silenzia {x}"
abort: "Annulla"
tip: "Suggerimento"
redisplayAllTips: "Mostra tutti i suggerimenti"
hideAllTips: "Nascondi tutti i suggerimenti"
_chat:
noMessagesYet: "Ancora nessun messaggio"
newMessage: "Nuovo messaggio"
@ -1375,6 +1395,8 @@ _chat:
chatNotAvailableInOtherAccount: "La chat non è disponibile nel profilo dell'altra persona."
cannotChatWithTheUser: "Impossibile chattare con questa persona"
cannotChatWithTheUser_description: "La chat potrebbe non essere disponibile, oppure l'altra persona potrebbe non esserlo."
youAreNotAMemberOfThisRoomButInvited: "Non partecipi a questa stanza di chat, ma hai ricevuto un invito. Per partecipare, accetta l'invito."
doYouAcceptInvitation: "Intendi accettare l'invito?"
chatWithThisUser: "Chatta con questa persona"
thisUserAllowsChatOnlyFromFollowers: "Questa persona permette di chattare soltanto i propri Follower."
thisUserAllowsChatOnlyFromFollowing: "Questa persona permette di chattare soltanto ai suoi Follow."
@ -1414,10 +1436,19 @@ _settings:
makeEveryTextElementsSelectable: "Imposta ogni elemento come selezionabile"
makeEveryTextElementsSelectable_description: "Potrebbe ridurre l'usabilità in alcune situazioni."
useStickyIcons: "Fissa le icone durante lo scorrimento"
enableHighQualityImagePlaceholders: "Mostra un segnaposto per immagini in alta qualità"
uiAnimations: "Animazione dell'interfaccia"
showNavbarSubButtons: "Mostra i pulsanti secondari nella barra di navigazione"
ifOn: "Quando attivato"
ifOff: "Quando disattivato"
enableSyncThemesBetweenDevices: "Sincronizzare il tema tra i dispositivi"
enablePullToRefresh: "Scorri e aggiorna"
enablePullToRefresh_description: "Clicca col mouse e gira la rotella."
realtimeMode_description: "Connette al server e aggiorna il contenuto in tempo reale. Potrebbe aumentare l'uso dei dati e il consumo della batteria."
contentsUpdateFrequency: "Frequenza di ricezione contenuti"
contentsUpdateFrequency_description: "Se l'impostazione è alta, verranno aggiornati più frequentemente, consumando più dati e più batteria."
contentsUpdateFrequency_description2: "Quando la modalità è in tempo reale, arriveranno a prescindere."
showUrlPreview: "Mostra anteprima dell'URL"
_chat:
showSenderName: "Mostra il nome del mittente"
sendOnEnter: "Invio spedisce"
@ -1425,6 +1456,7 @@ _preferencesProfile:
profileName: "Nome del profilo"
profileNameDescription: "Impostare il nome che indentifica questo dispositivo."
profileNameDescription2: "Es: \"PC principale\" o \"Cellulare\""
manageProfiles: "Gestione profili"
_preferencesBackup:
autoBackup: "Backup automatico"
restoreFromBackup: "Ripristinare da backup"
@ -1463,6 +1495,7 @@ _delivery:
manuallySuspended: "Sospesa manualmente"
goneSuspended: "Sospensione server a causa dell'eliminazione"
autoSuspendedForNotResponding: "Sospensione del server a causa di mancata risposta"
softwareSuspended: "Attualmente non disponibile perché il software non è più distribuito"
_bubbleGame:
howToPlay: "Come giocare"
hold: "Tieni"
@ -1594,6 +1627,23 @@ _serverSettings:
openRegistration: "Registrazioni aperte"
openRegistrationWarning: "Lapertura della registrazione comporta dei rischi. Ti consigliamo di attivarla solo se hai predisposto il monitoraggio continuo del tuo server e puoi rispondere immediatamente se si verifica un problema."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo."
deliverSuspendedSoftware: "Software fuori produzione"
deliverSuspendedSoftwareDescription: "A causa di vulnerabilità o altri motivi, puoi interrompere la distribuzione di un software da un server specificandone il nome e la versione. Le informazioni sono fornite dall'altro server e l'autenticità non è garantita. Puoi indicare un intervallo di versione semantica, ma specificando >= 2024.3.1 non verranno incluse le versioni personalizzate come ad esempio 2024.3.1-custom.0, pertanto ti consigliamo di specificare una versione come >= 2024.3.1-0."
singleUserMode: "Modalità utente singolo"
singleUserMode_description: "Se sei l'unica persona a utilizzare questo server, l'abilitazione di questa modalità ottimizzerà le prestazioni."
signToActivityPubGet: "Firma delle richieste GET"
signToActivityPubGet_description: "Normalmente questa opzione dovrebbe essere abilitata. Se si verificano problemi con la comunicazione federata, disabilitarla potrebbe migliorare la situazione, ma d'altro canto potrebbe rendere impossibile la comunicazione, a seconda del server."
proxyRemoteFiles: "Proxy di file remoti"
proxyRemoteFiles_description: "Se abilitato, i file remoti verranno serviti tramite proxy. Utile per generare miniature delle immagini e proteggere la privacy degli utenti."
allowExternalApRedirect: "Consenti reindirizzamenti per le query tramite ActivityPub"
allowExternalApRedirect_description: "Se abilitata, consente ad altri server di interrogare contenuti di terze parti tramite il tuo server, con conseguente potenziale falsificazione dei contenuti."
userGeneratedContentsVisibilityForVisitor: "Visibilità dei contenuti generati dagli utenti ai non utenti"
userGeneratedContentsVisibilityForVisitor_description: "Questa funzionalità è utile per impedire che contenuti remoti inappropriati e difficili da moderare vengano inavvertitamente resi pubblici su Internet tramite il proprio server."
userGeneratedContentsVisibilityForVisitor_description2: "Esistono dei rischi nell'esporre incondizionatamente su internet tutto il contenuto del tuo server, incluso il contenuto remoto ricevuto da altri server. In particolare, occorre prestare attenzione, perché le persone non consapevoli della federazione potrebbero erroneamente credere che il contenuto remoto sia stato invece creato all'interno del proprio server."
_userGeneratedContentsVisibilityForVisitor:
all: "Tutto pubblico"
localOnly: "Pubblica solo contenuti locali, mantieni privati i contenuti remoti"
none: "Tutto privato"
_accountMigration:
moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto"
@ -1911,6 +1961,7 @@ _role:
canManageCustomEmojis: "Gestire le emoji personalizzate"
canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo"
driveCapacity: "Capienza del Drive"
maxFileSize: "Dimensione massima del file caricabile"
alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
canUpdateBioMedia: "Può aggiornare foto profilo e di testata"
pinMax: "Quantità massima di Note in primo piano"
@ -1933,6 +1984,9 @@ _role:
canImportMuting: "Può importare Silenziati"
canImportUserLists: "Può importare liste di Profili"
chatAvailability: "Chat consentita"
uploadableFileTypes: "Tipi di file caricabili"
uploadableFileTypes_caption: "Specifica il tipo MIME. Puoi specificare più valori separandoli andando a capo, oppure indicare caratteri jolly con un asterisco (*). Ad esempio: image/*"
uploadableFileTypes_caption2: "A seconda del file, il tipo potrebbe non essere determinato. Se si desidera consentire tali file, aggiungere {x} alla specifica."
_condition:
roleAssignedTo: "Assegnato a ruoli manualmente"
isLocal: "Profilo locale"
@ -2785,6 +2839,12 @@ _dataSaver:
_avatar:
title: "Immagine del profilo"
description: "Impedire l'animazione per l'immagine del profilo. Le immagini animate possono avere dimensioni file maggiori rispetto a quelle normali, puoi ridurre ulteriormente l'utilizzo dei dati."
_urlPreviewThumbnail:
title: "Nascondi le miniature nell'anteprima URL"
description: "Le immagini in miniatura nell'anteprima URL non verranno più caricate."
_disableUrlPreview:
title: "Disabilita l'anteprima URL"
description: "Disabilita la funzione di anteprima URL. A differenza di una semplice immagine in miniatura, questo riduce il tempo necessario per caricare le informazioni collegate."
_code:
title: "Codice evidenziato"
description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato."
@ -2990,3 +3050,65 @@ _search:
pleaseEnterServerHost: "Inserire il nome host"
pleaseSelectUser: "Per favore, seleziona un profilo"
serverHostPlaceholder: "Es: misskey.example.com"
_serverSetupWizard:
installCompleted: "L'installazione di Misskey è completata!"
firstCreateAccount: "Per prima cosa, crea un account amministratore."
accountCreated: "Il tuo account amministratore è stato creato!"
serverSetting: "Configurazione del server"
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Questa procedura guidata ti aiuterà a configurare facilmente il tuo server in modo ottimale."
settingsYouMakeHereCanBeChangedLater: "Potrai anche modificare le impostazioni in seguito."
howWillYouUseMisskey: "Come si usa Misskey?"
_use:
single: "Modalità utenza singola"
single_description: "Se intendi usarlo come tuo server personale"
single_youCanCreateMultipleAccounts: "Anche se lo utilizzi come server per una sola persona, puoi creare più account in base alle tue esigenze."
group: "Modalità multi utentza"
group_description: "Invita altre persone fidate ad usare il server insieme a te"
open: "Server aperto"
open_description: "Per ospitare un numero imprecisato di persone"
openServerAdvice: "Ospitare un numero imprecisato di persone comporta dei rischi. Ti consigliamo di adottare un solido sistema di moderazione, in modo da poter gestire eventuali problemi che potrebbero presentarsi pubblicando contenuti proposti da altre persone, che potrebbero essere sconosciute."
openServerAntiSpamAdvice: "Presta molta attenzione alla sicurezza, ad esempio attivando funzionalità anti-bot (iscrizioni automatiche) come reCAPTCHA. Questo può evitare che il tuo server diventi un trampolino di lancio per lo spam di altri."
howManyUsersDoYouExpect: "Quante persone pensi che parteciperanno?"
_scale:
small: "100 persone o meno (piccolo)"
medium: "Da 100 a 1000 persone (medio)"
large: "Oltre 1000 persone (grande)"
largeScaleServerAdvice: "Configurare grandi server potrebbe richiedere conoscenze infrastrutturali avanzate, ad esempio, il bilanciamento del carico e la replicazione del database."
doYouConnectToFediverse: "Vuoi connetterti al Fediverso?"
doYouConnectToFediverse_description1: "Collegandosi a una rete di server distribuiti, denominata Fediverso, potrai scambiare contenuti con altri server, tramite il protocollo di comunicazione ActivityPub."
doYouConnectToFediverse_description2: "Connettersi al Fediverso è anche detto \"federazione\"."
youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi."
adminInfo: "Informazioni sull'amministratore"
adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste."
adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione."
followingSettingsAreRecommended: "Si consigliano le seguenti impostazioni:"
applyTheseSettings: "Applica questa impostazione"
skipSettings: "Salta l'installazione"
settingsCompleted: "Installazione completata!"
settingsCompleted_description: "Grazie per il tuo impegno. Adesso che hai completato la configurazione, puoi iniziare a utilizzare il tuo server."
settingsCompleted_description2: "Le impostazioni dettagliate del server possono essere effettuate tramite il Pannello di controllo."
donationRequest: "Per favore Fai una donazione"
_donationRequest:
text1: "Misskey è un software libero sviluppato da volontari."
text2: "Se puoi, ti preghiamo di prendere in considerazione l'idea di fare una donazione, così potremo continuare a sviluppare."
text3: "Sono previsti anche dei vantaggi speciali per i sostenitori!"
_uploader:
compressedToX: "Compresso in {x}"
savedXPercent: "{x}% risparmiati"
abortConfirm: "Alcuni file non sono stati caricati. Vuoi annullare l'operazione?"
doneConfirm: "Alcuni file non sono stati caricati. Vuoi completarli?"
maxFileSizeIsX: "La dimensione massima del file che puoi caricare è {x}."
allowedTypes: "Tipi di file caricabili"
tip: "Il file non è ancora stato caricato. Puoi controllare, rinominare, comprimere, ritagliare, prima del caricamento. Quando hai finito, premi il bottone \"Carica\" per completare."
_clientPerformanceIssueTip:
title: "Se ritieni che la batteria si stia scaricando troppo"
makeSureDisabledAdBlocker: "Disattiva il tuo AdBlocker"
makeSureDisabledAdBlocker_description: "Gli AdBlocker possono influire sulle prestazioni. Controlla se nel tuo sistema operativo, nel browser o nei componenti aggiuntivi è abilitato un AdBlocker."
makeSureDisabledCustomCss: "Disabilita CSS personalizzato"
makeSureDisabledCustomCss_description: "La riscrittura degli stili CSS può influire sulle prestazioni. Assicurati di non avere CSS personalizzati o estensioni abilitate che sovrascrivano i tuoi stili."
makeSureDisabledAddons: "Disabilitare le estensioni"
makeSureDisabledAddons_description: "Alcune estensioni potrebbero interferire con il funzionamento del client e comprometterne le prestazioni. Prova a disattivare le estensioni del browser e vedi se il problema persiste."
_clip:
tip: "Le clip sono una funzionalità che consente di raggruppare le Note."
_userLists:
tip: "Puoi creare un elenco di Note create da qualsiasi profilo. L'elenco è visualizzato come una sequenza temporale."

View File

@ -2556,7 +2556,7 @@ _exportOrImport:
userLists: "リスト"
excludeMutingUsers: "ミュートしているユーザーを除外"
excludeInactiveUsers: "使われていないアカウントを除外"
withReplies: "インポートした人による返信をTLに含むようにする"
withReplies: "返信をTLに含むかの情報がファイルにない場合に、インポートした人による返信をTLに含むようにする"
_charts:
federation: "連合"
@ -2987,6 +2987,8 @@ _offlineScreen:
_urlPreviewSetting:
title: "URLプレビューの設定"
enable: "URLプレビューを有効にする"
allowRedirect: "プレビュー先のリダイレクトを許可"
allowRedirectDescription: "入力されたURLがリダイレクトされる場合に、そのリダイレクト先をたどってプレビューを表示するかどうかを設定します。無効にするとサーバーリソースの節約になりますが、リダイレクト先の内容は表示されなくなります。"
timeout: "プレビュー取得時のタイムアウト(ms)"
timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。"
maximumContentLength: "Content-Lengthの最大値(byte)"

View File

@ -297,6 +297,7 @@ uploadFromUrl: "Tải lên bằng một URL"
uploadFromUrlDescription: "URL của tập tin bạn muốn tải lên"
uploadFromUrlRequested: "Đã yêu cầu tải lên"
uploadFromUrlMayTakeTime: "Sẽ mất một khoảng thời gian để tải lên xong."
uploadNFiles: "Tải lên {n} tập tin"
explore: "Khám phá"
messageRead: "Đã đọc"
noMoreHistory: "Không còn gì để đọc"
@ -576,6 +577,7 @@ withRepliesByDefaultForNewlyFollowed: "Mặc định hiển thị trả lời t
newNoteRecived: "Đã nhận tút mới"
sounds: "Âm thanh"
sound: "Âm thanh"
notificationSoundSettings: "Cài đặt âm thanh thông báo"
listen: "Nghe"
none: "Không"
showInPage: "Hiện trong trang"
@ -1174,9 +1176,12 @@ mutualFollow: "Theo dõi lẫn nhau"
followingOrFollower: "Đang theo dõi hoặc người theo dõi"
externalServices: "Các dịch vụ bên ngoài"
sourceCode: "Mã nguồn"
sourceCodeIsNotYetProvided: "Mã nguồn hiện chưa có sẵn, vui lòng liên hệ với quản trị viên để khắc phục sự cố này."
repositoryUrlDescription: "Nếu bạn có kho lưu trữ mã nguồn có thể truy cập công khai, hãy nhập URL. Nếu bạn đang sử dụng Misskey theo mặc định (không thực hiện bất kỳ thay đổi nào đối với mã nguồn), hãy nhập https://github.com/misskey-dev/misskey."
feedback: "Phản hồi"
feedbackUrl: "URL phản hồi"
impressum: "Thông tin nhà điều hành"
impressumUrl: "URL thông tin nhà điều hành"
privacyPolicy: "Chính sách bảo mật"
privacyPolicyUrl: "URL Chính sách bảo mật"
tosAndPrivacyPolicy: "Điều khoản sử dụng và Chính sách bảo mật"
@ -1190,6 +1195,7 @@ showAvatarDecorations: "Hiển thị trang trí ảnh đại diện"
releaseToRefresh: "Thả để làm mới"
refreshing: "Đang làm mới"
pullDownToRefresh: "Kéo xuống để làm mới"
signupPendingError: "Đã xảy ra sự cố khi xác minh địa chỉ email của bạn. Liên kết có thể đã hết hạn."
cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải có chú thích."
decorate: "Trang trí"
lastNDays: "{n} ngày trước"
@ -1200,11 +1206,17 @@ passkeyVerificationFailed: "Xác minh mật khẩu không thành công."
messageToFollower: "Tin nhắn cho người theo dõi"
yourNameContainsProhibitedWords: "Tên bạn đang cố gắng đổi có chứa chuỗi ký tự bị cấm."
yourNameContainsProhibitedWordsDescription: "Tên có chứa chuỗi ký tự bị cấm. Nếu bạn muốn sử dụng tên này, hãy liên hệ với quản trị viên máy chủ của bạn."
pleaseSelectAccount: "Chọn tài khoản của bạn"
federationDisabled: "Liên kết bị vô hiệu hóa trên máy chủ này. Bạn không thể tương tác với người dùng trên các máy chủ khác."
reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?"
preferences: "Thiết lập môi trường"
accessibility: "Khả năng tiếp cận"
paste: "dán"
postForm: "Mẫu đăng"
information: "Giới thiệu"
chat: "Trò chuyện"
migrateOldSettings: "Di chuyển cài đặt cũ"
migrateOldSettings_description: "Thông thường, quá trình này diễn ra tự động, nhưng nếu vì lý do nào đó mà quá trình di chuyển không thành công, bạn có thể kích hoạt thủ công quy trình di chuyển, quá trình này sẽ ghi đè lên thông tin cấu hình hiện tại của bạn."
_chat:
invitations: "Mời"
noHistory: "Không có dữ liệu"
@ -1797,6 +1809,7 @@ _widgets:
_userList:
chooseList: "Chọn danh sách"
clicker: "clicker"
chat: "Trò chuyện"
_cw:
hide: "Ẩn"
show: "Tải thêm"
@ -2022,6 +2035,7 @@ _deck:
channel: "Kênh"
mentions: "Lượt nhắc"
direct: "Nhắn riêng"
chat: "Trò chuyện"
_dialog:
charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}"
charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}"

View File

@ -1001,6 +1001,7 @@ failedToUpload: "上传失败"
cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法上传。"
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
cannotUploadBecauseExceedsFileSizeLimit: "无法上传文件,超过文件大小限制。"
cannotUploadBecauseUnallowedFileType: "因文件类型被禁止而无法上传。"
beta: "测试"
enableAutoSensitive: "自动 NSFW 识别"
enableAutoSensitiveDescription: "使用机器学习在可用时自动使用 NSFW 标记来标记媒体。即使您关闭此功能,根据服务器的不同,它仍然可能会自动设置。"
@ -1981,6 +1982,9 @@ _role:
canImportMuting: "允许导入隐藏列表"
canImportUserLists: "允许导入用户列表"
chatAvailability: "允许聊天"
uploadableFileTypes: "可上传的文件类型"
uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*"
uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。"
_condition:
roleAssignedTo: "已分配给手动角色"
isLocal: "是本地用户"
@ -3092,6 +3096,7 @@ _uploader:
abortConfirm: "还有未上传的文件,要中止吗?"
doneConfirm: "还有未上传的文件,要完成吗?"
maxFileSizeIsX: "可上传最大 {x} 的文件。"
allowedTypes: "可上传的文件类型"
_clientPerformanceIssueTip:
title: "如果觉得电池耗电过高"
makeSureDisabledAdBlocker: "请关闭广告拦截器"

View File

@ -1001,6 +1001,7 @@ failedToUpload: "上傳失敗"
cannotUploadBecauseInappropriate: "由於判定可能包含不適當的內容,因此無法上傳。"
cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此無法上傳。"
cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。"
cannotUploadBecauseUnallowedFileType: "由於檔案類型不被允許,無法上傳。\n"
beta: "測試版"
enableAutoSensitive: "自動 NSFW 判定"
enableAutoSensitiveDescription: "如果可行,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依伺服器規則而自動啟用。"
@ -1359,6 +1360,9 @@ emojiUnmute: "表情符號解除靜音"
muteX: "將 {x} 靜音"
unmuteX: "將 {x} 解除靜音"
abort: "取消"
tip: "提示與技巧"
redisplayAllTips: "重新顯示所有「提示與技巧」"
hideAllTips: "隱藏所有「提示與技巧」"
_chat:
noMessagesYet: "尚無訊息"
newMessage: "新訊息"
@ -1981,6 +1985,9 @@ _role:
canImportMuting: "允許匯入靜音名單"
canImportUserLists: "允許匯入清單"
chatAvailability: "允許聊天"
uploadableFileTypes: "可上傳的檔案類型"
uploadableFileTypes_caption: "請指定 MIME 類型。可以用換行區隔多個類型,也可以使用星號(*作為萬用字元進行指定。例如image/*\n"
uploadableFileTypes_caption2: "有些檔案可能無法判斷其類型。若要允許這類檔案,請在指定中加入 {x}。"
_condition:
roleAssignedTo: "手動指派角色完成"
isLocal: "本地使用者"
@ -2896,6 +2903,8 @@ _offlineScreen:
_urlPreviewSetting:
title: "URL 預覽設定"
enable: "啟用 URL 預覽"
allowRedirect: "允許預覽目標的重新導向"
allowRedirectDescription: "設定當輸入的 URL 發生重新導向時,是否追蹤該重新導向並顯示預覽。若停用此功能,雖可節省伺服器資源,但將無法顯示重新導向後的內容。\n"
timeout: "取得預覽的逾時時間 (ms)"
timeoutDescription: "若取得預覽所需的時間超過這個值,則不會產生預覽。"
maximumContentLength: "Content-Length 的最大値 (byte)"
@ -3092,6 +3101,8 @@ _uploader:
abortConfirm: "有些檔案尚未上傳,您要中止嗎?"
doneConfirm: "有些檔案尚未上傳,是否要完成上傳?"
maxFileSizeIsX: "可上傳的最大檔案大小為 {x}。"
allowedTypes: "可上傳的檔案類型"
tip: "檔案尚未上傳。您可以在此對話框中進行上傳前的確認、重新命名、壓縮、裁切等操作。準備完成後,請點選「上傳」按鈕開始上傳。\n"
_clientPerformanceIssueTip:
title: "如果覺得電池消耗過快的話"
makeSureDisabledAdBlocker: "請將廣告阻擋器停用"
@ -3100,3 +3111,7 @@ _clientPerformanceIssueTip:
makeSureDisabledCustomCss_description: "覆蓋樣式可能會影響效能。請確認是否啟用了自訂 CSS 或其他會覆蓋樣式的擴充功能。\n"
makeSureDisabledAddons: "請停用擴充功能"
makeSureDisabledAddons_description: "部分擴充功能可能會干擾用戶端的運作並影響效能。請嘗試停用瀏覽器的擴充功能,以確認是否能改善情況"
_clip:
tip: "摘錄是一項可以用來整理貼文的功能。"
_userLists:
tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.5.1-beta.2",
"version": "2025.5.1-beta.3",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AddUrlPreviewAllowRedirect1748310233000 {
name = 'AddUrlPreviewAllowRedirect1748310233000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewAllowRedirect" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewAllowRedirect"`);
}
}

View File

@ -528,7 +528,7 @@ export class DriveService {
return info.type.mime === mimeType;
});
if (!isAllowed) {
throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', 'Unallowed file type.');
throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', `Unallowed file type: ${info.type.mime}`);
}
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;

View File

@ -120,6 +120,8 @@ export class FanoutTimelineEndpointService {
filter = (note) => {
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
if (isUserRelated(note.renote, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
if (isUserRelated(note.renote, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
if (isInstanceMuted(note, userMutedInstances)) return false;

View File

@ -77,9 +77,51 @@ export class QueryService {
return q;
}
/**
* 使
*
*
*
* Notes for future maintainers:
* 1) FanoutTimelineEndpointService
* FanoutTimelineEndpointService
* 2) queryService
*
* - packages/backend/src/server/api/endpoints/clips/notes.ts
*/
@bindThis
public generateBaseNoteFilteringQuery(
query: SelectQueryBuilder<any>,
me: { id: MiUser['id'] } | null,
{
excludeUserFromMute,
excludeAuthor,
}: {
excludeUserFromMute?: MiUser['id'],
excludeAuthor?: boolean,
} = {},
): void {
this.generateBlockedHostQueryForNote(query, excludeAuthor);
this.generateSuspendedUserQueryForNote(query, excludeAuthor);
if (me) {
this.generateMutedUserQueryForNotes(query, me, { excludeUserFromMute });
this.generateBlockedUserQueryForNotes(query, me);
this.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote', excludeUserFromMute });
this.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
}
}
// ここでいうBlockedは被Blockedの意
@bindThis
public generateBlockedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
public generateBlockedUserQueryForNotes(
q: SelectQueryBuilder<any>,
me: { id: MiUser['id'] },
{
noteColumn = 'note',
}: {
noteColumn?: string,
} = {},
): void {
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
.select('blocking.blockerId')
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
@ -88,16 +130,20 @@ export class QueryService {
// 投稿の返信先の作者にブロックされていない かつ
// 投稿の引用元の作者にブロックされていない
q
.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
.where(`${noteColumn}.userId IS NULL`)
.orWhere(`${noteColumn}.userId NOT IN (${ blockingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => {
qb
.where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
.where(`${noteColumn}.replyUserId IS NULL`)
.orWhere(`${noteColumn}.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.renoteUserId IS NULL`)
.orWhere(`${noteColumn}.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
}));
q.setParameters(blockingQuery.getParameters());
@ -137,13 +183,23 @@ export class QueryService {
}
@bindThis
public generateMutedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
public generateMutedUserQueryForNotes(
q: SelectQueryBuilder<any>,
me: { id: MiUser['id'] },
{
excludeUserFromMute,
noteColumn = 'note',
}: {
excludeUserFromMute?: MiUser['id'],
noteColumn?: string,
} = {},
): void {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
if (exclude) {
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
if (excludeUserFromMute) {
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: excludeUserFromMute });
}
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
@ -154,32 +210,36 @@ export class QueryService {
// 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない
q
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
.where(`${noteColumn}.userId IS NULL`)
.orWhere(`${noteColumn}.userId NOT IN (${ mutingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => {
qb
.where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
.where(`${noteColumn}.replyUserId IS NULL`)
.orWhere(`${noteColumn}.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.renoteUserId IS NULL`)
.orWhere(`${noteColumn}.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
// mute instances
.andWhere(new Brackets(qb => {
qb
.andWhere('note.userHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
.andWhere(`${noteColumn}.userHost IS NULL`)
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.userHost)`);
}))
.andWhere(new Brackets(qb => {
qb
.where('note.replyUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
.where(`${noteColumn}.replyUserHost IS NULL`)
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.replyUserHost)`);
}))
.andWhere(new Brackets(qb => {
qb
.where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
.where(`${noteColumn}.renoteUserHost IS NULL`)
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.renoteUserHost)`);
}));
q.setParameters(mutingQuery.getParameters());

View File

@ -234,10 +234,7 @@ export class SearchService {
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
return query.limit(pagination.limit).getMany();
}

View File

@ -91,7 +91,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
public async addMember(target: MiUser, list: MiUserList, me: MiUser, options: { withReplies?: boolean } = {}) {
const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id,
});
@ -104,6 +104,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
userId: target.id,
userListId: list.id,
userListUserId: list.userId,
withReplies: options.withReplies ?? false,
} as MiUserListMembership);
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });

View File

@ -3,7 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
import type { MiUser } from '@/models/_.js';
interface NoteLike {
userId: MiUser['id'];
reply?: NoteLike | null;
renote?: NoteLike | null;
replyUserId?: MiUser['id'] | null;
renoteUserId?: MiUser['id'] | null;
}
export function isUserRelated(note: NoteLike | null | undefined, userIds: Set<string>, ignoreAuthor = false): boolean {
if (!note) {
return false;
}
@ -12,13 +22,16 @@ export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = fa
return true;
}
if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) {
const replyUserId = note.replyUserId ?? note.reply?.userId;
if (replyUserId != null && replyUserId !== note.userId && userIds.has(replyUserId)) {
return true;
}
if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) {
const renoteUserId = note.renoteUserId ?? note.renote?.userId;
if (renoteUserId != null && renoteUserId !== note.userId && userIds.has(renoteUserId)) {
return true;
}
return false;
}

View File

@ -619,6 +619,11 @@ export class MiMeta {
})
public urlPreviewEnabled: boolean;
@Column('boolean', {
default: true,
})
public urlPreviewAllowRedirect: boolean;
@Column('integer', {
default: 10000,
})

View File

@ -94,7 +94,8 @@ export class ExportFollowingProcessorService {
continue;
}
const content = this.utilityService.getFullApAccount(u.username, u.host);
const userAcct = this.utilityService.getFullApAccount(u.username, u.host);
const content = `${userAcct},withReplies=${following.withReplies}`;
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {

View File

@ -67,10 +67,12 @@ export class ExportUserListsProcessorService {
const users = await this.usersRepository.findBy({
id: In(memberships.map(j => j.userId)),
});
const usersWithReplies = new Set(memberships.filter(m => m.withReplies).map(m => m.userId));
for (const u of users) {
const acct = this.utilityService.getFullApAccount(u.username, u.host);
const content = `${list.name},${acct}`;
// 3rd column and later will be key=value pairs
const content = `${list.name},${acct},withReplies=${usersWithReplies.has(u.id)}`;
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {

View File

@ -67,8 +67,19 @@ export class ImportFollowingProcessorService {
const user = job.data.user;
try {
const acct = line.split(',')[0].trim();
const parts = line.split(',');
const acct = parts[0].trim();
const { username, host } = Acct.parse(acct);
let withReplies: boolean | null = null;
for (const keyValue of parts.slice(2)) {
const [key, value] = keyValue.split('=');
switch (key) {
case 'withReplies':
withReplies = value === 'true';
break;
}
}
if (!host) return;
@ -95,7 +106,7 @@ export class ImportFollowingProcessorService {
this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`);
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]);
await this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: withReplies ?? job.data.withReplies }]);
} catch (e) {
this.logger.warn(`Error: ${e}`);
}

View File

@ -70,8 +70,19 @@ export class ImportUserListsProcessorService {
linenum++;
try {
const listName = line.split(',')[0].trim();
const { username, host } = Acct.parse(line.split(',')[1].trim());
const parts = line.split(',');
const listName = parts[0].trim();
const { username, host } = Acct.parse(parts[1].trim());
let withReplies = false;
for (const keyValue of parts.slice(2)) {
const [key, value] = keyValue.split('=');
switch (key) {
case 'withReplies':
withReplies = value === 'true';
break;
}
}
let list = await this.userListsRepository.findOneBy({
userId: user.id,
@ -100,7 +111,9 @@ export class ImportUserListsProcessorService {
if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
this.userListService.addMember(target, list!, user);
await this.userListService.addMember(target, list, user, {
withReplies: withReplies,
});
} catch (e) {
this.logger.warn(`Error in line:${linenum} ${e}`);
}

View File

@ -495,6 +495,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
urlPreviewAllowRedirect: {
type: 'boolean',
optional: false, nullable: false,
},
urlPreviewTimeout: {
type: 'number',
optional: false, nullable: false,
@ -704,6 +708,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
notesPerOneAd: instance.notesPerOneAd,
summalyProxy: instance.urlPreviewSummaryProxyUrl,
urlPreviewEnabled: instance.urlPreviewEnabled,
urlPreviewAllowRedirect: instance.urlPreviewAllowRedirect,
urlPreviewTimeout: instance.urlPreviewTimeout,
urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength,
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,

View File

@ -170,6 +170,7 @@ export const paramDef = {
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
},
urlPreviewEnabled: { type: 'boolean' },
urlPreviewAllowRedirect: { type: 'boolean' },
urlPreviewTimeout: { type: 'integer' },
urlPreviewMaximumContentLength: { type: 'integer' },
urlPreviewRequireContentLength: { type: 'boolean' },
@ -664,6 +665,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.urlPreviewEnabled = ps.urlPreviewEnabled;
}
if (ps.urlPreviewAllowRedirect !== undefined) {
set.urlPreviewAllowRedirect = ps.urlPreviewAllowRedirect;
}
if (ps.urlPreviewTimeout !== undefined) {
set.urlPreviewTimeout = ps.urlPreviewTimeout;
}

View File

@ -111,11 +111,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
const notes = await query.getMany();
if (sinceId != null && untilId == null) {

View File

@ -121,12 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
this.queryService.generateBaseNoteFilteringQuery(query, me);
//#endregion
return await query.limit(ps.limit).getMany();

View File

@ -91,6 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote' });
this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
}
const notes = await query

View File

@ -70,12 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
this.queryService.generateBaseNoteFilteringQuery(query, me);
const notes = await query.limit(ps.limit).getMany();

View File

@ -78,11 +78,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
this.queryService.generateBaseNoteFilteringQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');

View File

@ -243,10 +243,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -156,10 +156,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {

View File

@ -72,11 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedNoteThreadQuery(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
if (ps.visibility) {
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });

View File

@ -72,10 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
const renotes = await query.limit(ps.limit).getMany();

View File

@ -56,10 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
const timeline = await query.limit(ps.limit).getMany();

View File

@ -96,10 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
try {
if ('tag' in ps) {

View File

@ -199,10 +199,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -184,10 +184,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -102,10 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);

View File

@ -186,12 +186,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query, true);
this.queryService.generateSuspendedUserQueryForNote(query, true);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
this.queryService.generateBaseNoteFilteringQuery(query, me, {
excludeAuthor: true,
excludeUserFromMute: ps.userId,
});
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');

View File

@ -64,6 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateMutedUserQueryForUsers(query, me);
this.queryService.generateBlockQueryForUsers(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')

View File

@ -122,7 +122,7 @@ export class UrlPreviewService {
: undefined;
return summaly(url, {
followRedirects: false,
followRedirects: this.meta.urlPreviewAllowRedirect,
lang: lang ?? 'ja-JP',
agent: agent,
userAgent: meta.urlPreviewUserAgent ?? undefined,
@ -137,6 +137,7 @@ export class UrlPreviewService {
const queryStr = query({
url: url,
lang: lang ?? 'ja-JP',
followRedirects: this.meta.urlPreviewAllowRedirect,
userAgent: meta.urlPreviewUserAgent ?? undefined,
operationTimeout: meta.urlPreviewTimeout,
contentLengthLimit: meta.urlPreviewMaximumContentLength,

View File

@ -9,9 +9,9 @@ import * as assert from 'assert';
// node-fetch only supports it's own Blob yet
// https://github.com/node-fetch/node-fetch/pull/1664
import { Blob } from 'node-fetch';
import { MiUser } from '@/models/_.js';
import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
import { MiUser } from '@/models/_.js';
describe('Endpoints', () => {
let alice: misskey.entities.SignupResponse;
@ -572,19 +572,10 @@ describe('Endpoints', () => {
describe('drive', () => {
test('ドライブ情報を取得できる', async () => {
await uploadFile(alice, {
blob: new Blob([new Uint8Array(256)]),
});
await uploadFile(alice, {
blob: new Blob([new Uint8Array(512)]),
});
await uploadFile(alice, {
blob: new Blob([new Uint8Array(1024)]),
});
const res = await api('drive', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).toHaveProperty('usage', 1792);
expect(res.body).toHaveProperty('usage', 0);
});
});

View File

@ -345,6 +345,44 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、フォローしているユーザーによるリノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、フォローしているユーザーによるリノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
@ -687,6 +725,42 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
@ -1383,6 +1457,39 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
@ -1391,6 +1498,8 @@ describe('Timelines', () => {
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id });
const bobNote4 = await post(bob, { renoteId: bobNote2.id });
const bobNote5 = await post(bob, { renoteId: bobNote3.id });
await waitForPushToTl();
@ -1399,6 +1508,8 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote4.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote5.id), true);
});
test.concurrent('自身の visibility: specified なノートが含まれる', async () => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -6,9 +6,15 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Test } from '@nestjs/testing';
import { jest } from '@jest/globals';
import { MockResolver } from '../misc/mock-resolver.js';
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
import type { MiRemoteUser } from '@/models/User.js';
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@ -19,14 +25,14 @@ import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DownloadService } from '@/core/DownloadService.js';
import type { MiRemoteUser } from '@/models/User.js';
import { genAidx } from '@/misc/id/aidx.js';
import { MockResolver } from '../misc/mock-resolver.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const host = 'https://host1.test';
@ -120,7 +126,13 @@ describe('ActivityPub', () => {
imports: [GlobalModule, CoreModule],
})
.overrideProvider(DownloadService).useValue({
async downloadUrl(): Promise<{ filename: string }> {
async downloadUrl(url: string, path: string): Promise<{ filename: string }> {
if (url.endsWith('.png')) {
fs.copyFileSync(
_dirname + '/../resources/hw.png',
path,
);
}
return {
filename: 'dummy.tmp',
};
@ -440,7 +452,7 @@ describe('ActivityPub', () => {
});
});
describe('JSON-LD', () =>{
describe('JSON-LD', () => {
test('Compaction', async () => {
const jsonLd = jsonLdService.use();

View File

@ -17,6 +17,8 @@ import { ServerModule } from '@/server/ServerModule.js';
import { ServerService } from '@/server/ServerService.js';
import { IdService } from '@/core/IdService.js';
// TODO: uploadableFileTypes で許可されていないファイルが弾かれるかのテスト
describe('/drive/files/create', () => {
let module: TestingModule;
let server: FastifyInstance;
@ -25,6 +27,8 @@ describe('/drive/files/create', () => {
let root: MiUser;
let role_tinyAttachment: MiRole;
let role_imageOnly: MiRole;
let role_allowAllTypes: MiRole;
let folder: MiDriveFolder;
@ -64,10 +68,34 @@ describe('/drive/files/create', () => {
});
roleService = module.get<RoleService>(RoleService);
role_tinyAttachment = await roleService.create({
role_imageOnly = await roleService.create({
name: 'test-role001',
description: 'Test role001 description',
target: 'manual',
policies: {
uploadableFileTypes: {
useDefault: false,
priority: 1,
value: ['image/png'],
},
},
});
role_allowAllTypes = await roleService.create({
name: 'test-role002',
description: 'Test role002 description',
target: 'manual',
policies: {
uploadableFileTypes: {
useDefault: false,
priority: 1,
value: ['*/*'],
},
},
});
role_tinyAttachment = await roleService.create({
name: 'test-role003',
description: 'Test role003 description',
target: 'manual',
policies: {
maxFileSizeMb: {
useDefault: false,
@ -82,6 +110,10 @@ describe('/drive/files/create', () => {
beforeEach(async () => {
await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => {
});
await roleService.unassign(root.id, role_imageOnly.id).catch(() => {
});
await roleService.unassign(root.id, role_allowAllTypes.id).catch(() => {
});
});
afterAll(async () => {
@ -110,7 +142,9 @@ describe('/drive/files/create', () => {
.field('i', root.token ?? '');
}
test('200 ok', async () => {
test('200 ok (all types allowed)', async () => {
await roleService.assign(root.id, role_allowAllTypes.id);
const name = randomString();
const comment = randomString();
const result = await postFile({
@ -127,7 +161,24 @@ describe('/drive/files/create', () => {
expect(result.body.folderId).toBe(folder.id);
});
test('200 ok(with role)', async () => {
test('400 when not allowed type', async () => {
await roleService.assign(root.id, role_imageOnly.id);
const name = randomString();
const comment = randomString();
const result = await postFile({
name: name,
comment: comment,
isSensitive: true,
force: true,
fileContent: Buffer.from('a'.repeat(10)),
});
expect(result.statusCode).toBe(400);
expect(result.body.error.code).toBe('UNALLOWED_FILE_TYPE');
});
test('200 ok (with size limited role)', async () => {
await roleService.assign(root.id, role_allowAllTypes.id);
await roleService.assign(root.id, role_tinyAttachment.id);
const name = randomString();
@ -147,6 +198,7 @@ describe('/drive/files/create', () => {
});
test('413 too large', async () => {
await roleService.assign(root.id, role_allowAllTypes.id);
await roleService.assign(root.id, role_tinyAttachment.id);
const name = randomString();

View File

@ -66,9 +66,15 @@ export function getConfig(): UserConfig {
return {
base: '/embed_vite/',
// The console is shared with backend, so clearing the console will also clear the backend log.
clearScreen: false,
server: {
host,
// The backend allows access from any addresses, so vite also allows access from any addresses.
host: '0.0.0.0',
allowedHosts: host ? [host] : undefined,
port: 5174,
strictPort: true,
hmr: {
// バックエンド経由での起動時、Viteは5174経由でアセットを参照していると思い込んでいるが実際は3000から配信される
// そのため、バックエンドのWSサーバーにHMRのWSリクエストが吸収されてしまい、正しくHMRが機能しない

View File

@ -146,6 +146,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
<template v-if="urlPreviewForm.state.urlPreviewEnabled">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewAllowRedirect">
<template #label>{{ i18n.ts._urlPreviewSetting.allowRedirect }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.allowRedirectDescription }}</template>
</MkSwitch>
<MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
@ -288,7 +293,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed, reactive } from 'vue';
import { computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@ -301,7 +306,6 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import { useForm } from '@/composables/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkRadios from '@/components/MkRadios.vue';
@ -370,6 +374,7 @@ const adForm = useForm({
const urlPreviewForm = useForm({
urlPreviewEnabled: meta.urlPreviewEnabled,
urlPreviewAllowRedirect: meta.urlPreviewAllowRedirect,
urlPreviewTimeout: meta.urlPreviewTimeout,
urlPreviewMaximumContentLength: meta.urlPreviewMaximumContentLength,
urlPreviewRequireContentLength: meta.urlPreviewRequireContentLength,
@ -378,6 +383,7 @@ const urlPreviewForm = useForm({
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
urlPreviewEnabled: state.urlPreviewEnabled,
urlPreviewAllowRedirect: state.urlPreviewAllowRedirect,
urlPreviewTimeout: state.urlPreviewTimeout,
urlPreviewMaximumContentLength: state.urlPreviewMaximumContentLength,
urlPreviewRequireContentLength: state.urlPreviewRequireContentLength,

View File

@ -9,24 +9,12 @@ import { apiUrl } from '@@/js/config.js';
import { $i } from '@/i.js';
export const pendingApiRequestsCount = ref(0);
export type Endpoint = keyof Misskey.Endpoints;
export type Request<E extends Endpoint> = Misskey.Endpoints[E]['req'];
export type AnyRequest<E extends Endpoint | (string & unknown)> =
(E extends Endpoint ? Request<E> : never) | object;
export type Response<E extends Endpoint | (string & unknown), P extends AnyRequest<E>> =
E extends Endpoint
? P extends Request<E> ? Misskey.api.SwitchCaseResponseType<E, P> : never
: object;
// Implements Misskey.api.ApiClient.request
export function misskeyApi<
ResT = void,
E extends Endpoint | NonNullable<string> = Endpoint,
P extends AnyRequest<E> = E extends Endpoint ? Request<E> : never,
_ResT = ResT extends void ? Response<E, P> : ResT,
E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
>(
endpoint: E,
data: P & { i?: string | null; } = {} as any,

View File

@ -15,7 +15,7 @@ export function transformPlayerUrl(url: string): string {
const urlParams = new URLSearchParams(urlObj.search);
if (urlObj.hostname === 'player.twitch.tv') {
if (urlObj.hostname === 'player.twitch.tv' || urlObj.hostname === 'clips.twitch.tv') {
// TwitchはCSPの制約あり
// https://dev.twitch.tv/docs/embed/video-and-clips/
urlParams.set('parent', hostname);

View File

@ -81,9 +81,15 @@ export function getConfig(): UserConfig {
return {
base: '/vite/',
// The console is shared with backend, so clearing the console will also clear the backend log.
clearScreen: false,
server: {
host,
// The backend allows access from any addresses, so vite also allows access from any addresses.
host: '0.0.0.0',
allowedHosts: host ? [host] : undefined,
port: 5173,
strictPort: true,
hmr: {
// バックエンド経由での起動時、Viteは5173経由でアセットを参照していると思い込んでいるが実際は3000から配信される
// そのため、バックエンドのWSサーバーにHMRのWSリクエストが吸収されてしまい、正しくHMRが機能しない

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.5.1-beta.2",
"version": "2025.5.1-beta.3",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -8809,6 +8809,7 @@ export type operations = {
uri: string;
version: string;
urlPreviewEnabled: boolean;
urlPreviewAllowRedirect: boolean;
urlPreviewTimeout: number;
urlPreviewMaximumContentLength: number;
urlPreviewRequireContentLength: boolean;
@ -11536,6 +11537,7 @@ export type operations = {
/** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */
summalyProxy?: string | null;
urlPreviewEnabled?: boolean;
urlPreviewAllowRedirect?: boolean;
urlPreviewTimeout?: number;
urlPreviewMaximumContentLength?: number;
urlPreviewRequireContentLength?: boolean;