1
0
mirror of https://github.com/hotomoe/hotomoe synced 2024-11-27 06:18:11 +09:00

Merge upstream

This commit is contained in:
オスカー、 2024-10-27 20:30:51 +09:00
commit c9587d51c1
Signed by: SWREI
GPG Key ID: 139D6573F92DA9F7
96 changed files with 6413 additions and 4550 deletions

View File

@ -57,7 +57,7 @@ jobs:
- name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@ -117,7 +117,7 @@ jobs:
with:
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@ -20,7 +20,7 @@ jobs:
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@ -29,7 +29,7 @@ jobs:
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4.0.4
- uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -55,7 +55,7 @@ jobs:
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4.0.4
- uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@ -80,7 +80,7 @@ jobs:
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4.0.4
- uses: actions/setup-node@v4.1.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@ -37,7 +37,7 @@ jobs:
with:
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@ -30,7 +30,7 @@ jobs:
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@ -27,7 +27,7 @@ jobs:
with:
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@ -28,7 +28,7 @@ jobs:
with:
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View File

@ -1234,7 +1234,7 @@ _announcement:
forExistingUsers: "Anunci per usuaris registrats"
forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació."
needConfirmationToRead: "Es necessita confirmació de lectura de la notificació "
needConfirmationToReadDescription: "Si s'activa es mostrarà un diàleg per confirmar la lectura d'aquesta notificació. A més aquesta notificació serà exclosa de qualsevol funcionalitat com \"Marcar tot com a llegit\"."
needConfirmationToReadDescription: "Si s'activa es mostrarà un diàleg per confirmar la lectura d'aquesta notificació."
end: "Final de la notificació "
tooManyActiveAnnouncementDescription: "Tenir massa notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els anuncis que siguin antics."
readConfirmTitle: "Marcar com llegida?"
@ -2003,8 +2003,8 @@ _permissions:
"read:admin:show-user": "Veure informació privada de l'usuari "
"read:admin:show-users": "Veure informació privada de l'usuari "
"write:admin:suspend-user": "Suspendre usuari"
"write:admin:unset-user-avatar": "Esborrar avatar d'usuari "
"write:admin:unset-user-banner": "Esborrar bàner de l'usuari "
"write:admin:user-avatar": "Esborrar avatar d'usuari "
"write:admin:user-banner": "Esborrar bàner de l'usuari "
"write:admin:unsuspend-user": "Treure la suspensió d'un usuari"
"write:admin:meta": "Gestionar les metadades de la instància"
"write:admin:user-note": "Gestionar les notes de moderació "

View File

@ -1189,7 +1189,7 @@ _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."
needConfirmationToRead: "Separate Lesebestätigung erfordern"
needConfirmationToReadDescription: "Ist dies aktiviert, so wird beim Markieren dieser Ankündigung als gelesen ein separates Bestätigungsfenster angezeigt. Auch wird sie von der \"Alle als gelesen markieren\"-Funktion ausgenommen."
needConfirmationToReadDescription: "Ist dies aktiviert, so wird beim Markieren dieser Ankündigung als gelesen ein separates Bestätigungsfenster angezeigt."
end: "Ankündigung archivieren"
tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren."
readConfirmTitle: "Als gelesen markieren?"

View File

@ -138,6 +138,9 @@ mute: "Mute"
unmute: "Unmute"
renoteMute: "Mute Renotes"
renoteUnmute: "Unmute Renotes"
mutedReactions: "Mute reactions"
muteThisReaction: "Mute this reaction"
unmuteThisReaction: "Unmute this reaction"
block: "Block"
unblock: "Unblock"
suspend: "Suspend"
@ -693,6 +696,7 @@ useGlobalSetting: "Use global settings"
useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made."
other: "Other"
regenerateLoginToken: "Regenerate login token"
regenerateLoginTokenConfirm: "Are you sure you want to regenerate the login token? All devices will be logged out."
regenerateLoginTokenDescription: "Regenerates the token used internally during login. Normally this action is not necessary. If regenerated, all devices will be logged out."
theKeywordWhenSearchingForCustomEmoji: "This is the keyword when searching for custom emojis."
setMultipleBySeparatingWithSpace: "Separate multiple entries with spaces."
@ -1062,7 +1066,7 @@ likeOnlyForRemote: "All (Only likes for remote instances)"
nonSensitiveOnly: "Non-sensitive only"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)"
rolesAssignedToMe: "Roles assigned to me"
resetPasswordConfirm: "Really reset your password?"
resetPasswordConfirm: "Are you sure you want to reset password?"
sensitiveWords: "Sensitive words"
sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks."
sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression."
@ -1277,6 +1281,7 @@ sensitiveDoubleClickRequired: "Require double-click to open sensitive media"
mutualLink: "Mutual Link"
saveThisFile: "Save this file to Drive"
CheckedByHIBP: "In addition to ensuring your passwords are secure, HIBP scans for password leaks."
changeUserName: "Change name"
_bubbleGame:
howToPlay: "How to play"
hold: "Hold"
@ -1326,7 +1331,9 @@ _announcement:
forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
needConfirmationToRead: "Require separate read confirmation"
needConfirmationToReadDescription: "A separate prompt to confirm marking this announcement as read will be displayed if enabled. This announcement will also be excluded from any \"Mark all as read\" functionality."
needConfirmationToReadDescription: "A separate prompt to confirm marking this announcement as read will be displayed if enabled."
needEnrollmentTutorialToRead: "Require tutorial completion to read"
needEnrollmentTutorialToReadDescription: "Users must complete the tutorial to read this announcement if enabled."
end: "Archive announcement"
tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete."
readConfirmTitle: "Mark as read?"
@ -2122,6 +2129,7 @@ _permissions:
"read:admin:user-ips": "View user IP addresses"
"read:admin:meta": "View instance metadata"
"write:admin:reset-password": "Reset user password"
"write:admin:regenerate-user-token": "Regenerate user login token"
"write:admin:resolve-abuse-user-report": "Resolve user report"
"write:admin:send-email": "Send email"
"read:admin:server-info": "View server info"
@ -2129,11 +2137,12 @@ _permissions:
"read:admin:show-user": "View private user info"
"read:admin:show-users": "View private user info"
"write:admin:suspend-user": "Suspend user"
"write:admin:unset-user-avatar": "Remove user avatar"
"write:admin:unset-user-banner": "Remove user banner"
"write:admin:unset-user-mutual-link": "Remove user mutual link"
"write:admin:user-avatar": "Remove user avatar"
"write:admin:user-banner": "Remove user banner"
"write:admin:user-mutual-link": "Remove user mutual link"
"write:admin:unsuspend-user": "Unsuspend user"
"write:admin:meta": "Manage instance metadata"
"write:admin:user-name": "Change user name"
"write:admin:user-note": "Manage moderation note"
"write:admin:roles": "Manage roles"
"read:admin:roles": "View roles"

View File

@ -1252,7 +1252,7 @@ _announcement:
forExistingUsers: "Solo para usuarios registrados"
forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán."
needConfirmationToRead: "Requerir confirmación de lectura aparte"
needConfirmationToReadDescription: "Si se habilita esta opción, se pedirá una confirmación de lectura aparte. Además, este anuncio será excluido de cualquier funcionalidad de \"Marcar todos como leídos\"."
needConfirmationToReadDescription: "Si se habilita esta opción, se pedirá una confirmación de lectura aparte."
end: "Anuncios archivados"
tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos."
readConfirmTitle: "¿Marcar como leído?"
@ -2031,8 +2031,8 @@ _permissions:
"read:admin:show-user": "Ver información privada de usuario"
"read:admin:show-users": "Ver información privada de usuario"
"write:admin:suspend-user": "Suspender cuentas de usuario"
"write:admin:unset-user-avatar": "Quitar avatares de usuario"
"write:admin:unset-user-banner": "Quitar banner de usuarios"
"write:admin:user-avatar": "Quitar avatares de usuario"
"write:admin:user-banner": "Quitar banner de usuarios"
"write:admin:unsuspend-user": "Quitar suspensión de cuentas de usuario"
"write:admin:meta": "Edición de metadatos de la instancia"
"write:admin:user-note": "Moderación de notas"

View File

@ -1237,7 +1237,7 @@ _bubbleGame:
_announcement:
forExistingUsers: "Pour les utilisateurs existants seulement"
needConfirmationToRead: "Exiger la confirmation de la lecture"
needConfirmationToReadDescription: "Si activé, afficher un dialogue de confirmation quand l'annonce est marquée comme lue. Aussi, elle sera exclue de « marquer tout comme lu » ."
needConfirmationToReadDescription: "Si activé, afficher un dialogue de confirmation quand l'annonce est marquée comme lue."
end: "Archiver l'annonce"
tooManyActiveAnnouncementDescription: "Un grand nombre d'annonces actives peut baisser l'expérience utilisateur. Considérez d'archiver les annonces obsolètes."
readConfirmTitle: "Marquer comme lu ?"

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "Hanya pengguna yang telah ada"
forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya."
needConfirmationToRead: "Membutuhkan konfirmasi terpisah bahwa telah dibaca"
needConfirmationToReadDescription: "Permintaan terpisah untuk mengonfirmasi menandai pengumuman ini telah dibaca akan ditampilkan apabila fitur ini dinyalakan. Pengumuman ini juga akan dikecualikan dari fungsi \"Tandai semua telah dibaca\"."
needConfirmationToReadDescription: "Permintaan terpisah untuk mengonfirmasi menandai pengumuman ini telah dibaca akan ditampilkan apabila fitur ini dinyalakan."
end: "Arsipkan pengumuman"
tooManyActiveAnnouncementDescription: "Terlalu banyak pengumuman dapat memperburuk pengalaman pengguna. Mohon pertimbangkan untuk mengarsipkan pengumuman yang sudah usang/tidak relevan."
readConfirmTitle: "Tandai telah dibaca?"
@ -2034,8 +2034,8 @@ _permissions:
"read:admin:show-user": "Lihat informasi pengguna privat"
"read:admin:show-users": "Lihat informasi pengguna privat"
"write:admin:suspend-user": "Tangguhkan pengguna"
"write:admin:unset-user-avatar": "Hapus avatar pengguna"
"write:admin:unset-user-banner": "Hapus banner pengguna"
"write:admin:user-avatar": "Hapus avatar pengguna"
"write:admin:user-banner": "Hapus banner pengguna"
"write:admin:unsuspend-user": "Batalkan penangguhan pengguna"
"write:admin:meta": "Kelola metadata instansi"
"write:admin:user-note": "Kelola moderasi catatan"

77
locales/index.d.ts vendored
View File

@ -580,6 +580,18 @@ export interface Locale extends ILocale {
*
*/
"renoteUnmute": string;
/**
*
*/
"mutedReactions": string;
/**
*
*/
"muteThisReaction": string;
/**
*
*/
"unmuteThisReaction": string;
/**
*
*/
@ -1796,6 +1808,22 @@ export interface Locale extends ILocale {
*
*/
"moderationLogs": string;
/**
* 使
*/
"userAccountMoveLogs": string;
/**
* {from} {to}
*/
"userAccountMoveLogsTitle": ParameterizedString<"from" | "to">;
/**
* ID
*/
"movedToId": string;
/**
* ID
*/
"moveFromId": string;
/**
* {n}稿
*/
@ -2780,6 +2808,10 @@ export interface Locale extends ILocale {
*
*/
"regenerateLoginToken": string;
/**
*
*/
"regenerateLoginTokenConfirm": string;
/**
* 使
*/
@ -3664,6 +3696,13 @@ export interface Locale extends ILocale {
*
*/
"deleteAccountConfirm": string;
/**
*
*
*
*
*/
"deleteAccountConfirmAndWarn": string;
/**
*
*/
@ -4261,7 +4300,7 @@ export interface Locale extends ILocale {
*/
"rolesAssignedToMe": string;
/**
*
*
*/
"resetPasswordConfirm": string;
/**
@ -4388,6 +4427,10 @@ export interface Locale extends ILocale {
*
*/
"accountMoved": string;
/**
*
*/
"accountMovedFrom": string;
/**
*
*/
@ -5207,6 +5250,10 @@ export interface Locale extends ILocale {
* HIBPを通じてパスワードの漏洩を検査します
*/
"checkedByHIBP": string;
/**
*
*/
"changeUserName": string;
"_bubbleGame": {
/**
*
@ -5374,9 +5421,17 @@ export interface Locale extends ILocale {
*/
"needConfirmationToRead": string;
/**
*
*
*/
"needConfirmationToReadDescription": string;
/**
*
*/
"needEnrollmentTutorialToRead": string;
/**
*
*/
"needEnrollmentTutorialToReadDescription": string;
/**
*
*/
@ -7249,6 +7304,10 @@ export interface Locale extends ILocale {
*
*/
"youCantUseThisTime": string;
/**
*
*/
"dontLogin": string;
};
"_ad": {
/**
@ -8372,6 +8431,10 @@ export interface Locale extends ILocale {
*
*/
"write:admin:reset-password": string;
/**
*
*/
"write:admin:regenerate-user-token": string;
/**
*
*/
@ -8403,15 +8466,15 @@ export interface Locale extends ILocale {
/**
*
*/
"write:admin:unset-user-avatar": string;
"write:admin:user-avatar": string;
/**
*
*/
"write:admin:unset-user-banner": string;
"write:admin:user-banner": string;
/**
*
*/
"write:admin:unset-user-mutual-link": string;
"write:admin:user-mutual-link": string;
/**
*
*/
@ -8420,6 +8483,10 @@ export interface Locale extends ILocale {
*
*/
"write:admin:meta": string;
/**
*
*/
"write:admin:user-name": string;
/**
*
*/

View File

@ -52,7 +52,8 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
export function build() {
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
const metaUrl = import.meta.url;
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => {

View File

@ -1252,7 +1252,7 @@ _announcement:
forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
needConfirmationToRead: "Richiede la conferma di lettura"
needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura. Inoltre, non è soggetto a conferme di lettura massicce."
needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura."
end: "Archivia l'annuncio"
tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi."
readConfirmTitle: "Segnare come già letto?"
@ -2027,8 +2027,8 @@ _permissions:
"read:admin:show-user": "Vedere le informazioni private degli account utente"
"read:admin:show-users": "Vedere le informazioni private degli account utente"
"write:admin:suspend-user": "Sospendere i profili"
"write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili"
"write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili"
"write:admin:user-avatar": "Rimuovere la foto profilo dai profili"
"write:admin:user-banner": "Rimuovere l'immagine testata dai profili"
"write:admin:unsuspend-user": "Togliere la sospensione ai profili"
"write:admin:meta": "Modificare i metadati dell'istanza"
"write:admin:user-note": "Scrivere annotazioni di moderazione"

View File

@ -141,6 +141,9 @@ mute: "ミュート"
unmute: "ミュート解除"
renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除"
mutedReactions: "リアクションのミュート"
muteThisReaction: "このリアクションをミュートする"
unmuteThisReaction: "このリアクションのミュートを解除する"
block: "ブロック"
unblock: "ブロック解除"
suspend: "凍結"
@ -695,6 +698,7 @@ useGlobalSetting: "グローバル設定を使う"
useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
other: "その他"
regenerateLoginToken: "ログイントークンを再生成"
regenerateLoginTokenConfirm: "ログイントークンを再生成しますか?"
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
@ -1066,7 +1070,7 @@ likeOnlyForRemote: "全て (リモートはいいねのみ)"
nonSensitiveOnly: "非センシティブのみ"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "非センシティブのみ (リモートはいいねのみ)"
rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワードリセットしますか?"
resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
@ -1303,6 +1307,7 @@ hideCounters: "すべてのカウンターを隠す"
hideCountersDescription: "ユーザーページのノート、フォロー、フォロワー数、およびこれらの統計をすべて非表示にします。"
youAreOnVacation: "現在、休暇モードを使用しています。 このメッセージを閉じるにはここをクリックしてください。"
checkedByHIBP: "パスワードの安全性に加え、HIBPを通じてパスワードの漏洩を検査します。"
changeUserName: "名前を変更"
_bubbleGame:
howToPlay: "遊び方"
@ -1350,7 +1355,9 @@ _announcement:
forExistingUsers: "既存ユーザーのみ"
forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
needConfirmationToRead: "既読にするのに確認が必要"
needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象になりません。"
needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。"
needEnrollmentTutorialToRead: "チュートリアルの受講が必要"
needEnrollmentTutorialToReadDescription: "有効にすると、このお知らせを既読にするためにはチュートリアルの受講が必要です。"
end: "お知らせを終了"
tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。"
readConfirmTitle: "既読にしますか?"
@ -2200,6 +2207,7 @@ _permissions:
"read:admin:user-ips": "ユーザーのIPアドレスを見る"
"read:admin:meta": "インスタンスのメタデータを見る"
"write:admin:reset-password": "ユーザーのパスワードをリセットする"
"write:admin:regenerate-user-token": "ユーザーのログイントークンを再生成する"
"write:admin:resolve-abuse-user-report": "ユーザーからの通報を解決する"
"write:admin:send-email": "メールを送る"
"read:admin:server-info": "サーバーの情報を見る"
@ -2207,11 +2215,12 @@ _permissions:
"read:admin:show-user": "ユーザーのプライベートな情報を見る"
"read:admin:show-users": "ユーザーのプライベートな情報を見る"
"write:admin:suspend-user": "ユーザーを凍結する"
"write:admin:unset-user-avatar": "ユーザーのアバターを削除する"
"write:admin:unset-user-banner": "ユーザーのバーナーを削除する"
"write:admin:unset-user-mutual-link": "ユーザーの相互リンクを削除する"
"write:admin:user-avatar": "ユーザーのアバターを削除する"
"write:admin:user-banner": "ユーザーのバーナーを削除する"
"write:admin:user-mutual-link": "ユーザーの相互リンクを削除する"
"write:admin:unsuspend-user": "ユーザーの凍結を解除する"
"write:admin:meta": "インスタンスのメタデータを操作する"
"write:admin:user-name": "ユーザーの名前を変更する"
"write:admin:user-note": "モデレーションノートを操作する"
"write:admin:roles": "ロールを操作する"
"read:admin:roles": "ロールを見る"

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "もうおるユーザーのみ"
forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。"
needConfirmationToRead: "既読にするんやったら確認してや"
needConfirmationToReadDescription: "オンにしたら、このお知らせを既読にする時に確認するで。ついでに、一括既読しても既読扱いにならへんで。"
needConfirmationToReadDescription: "オンにしたら、このお知らせを既読にする時に確認するで。"
end: "お知らせやめる"
tooManyActiveAnnouncementDescription: "お知らせが多すぎてUXが落ちそうや。終わったお知らせはアーカイブに突っ込んだほうがええかも。"
readConfirmTitle: "既読にしてええんやな?"
@ -2034,8 +2034,8 @@ _permissions:
"read:admin:show-user": "ユーザーのプライベートな情報見る"
"read:admin:show-users": "ユーザーのプライベートな情報見る"
"write:admin:suspend-user": "ユーザーを凍結"
"write:admin:unset-user-avatar": "ユーザーのアバターを削除"
"write:admin:unset-user-banner": "ユーザーのバナーを削除"
"write:admin:user-avatar": "ユーザーのアバターを削除"
"write:admin:user-banner": "ユーザーのバナーを削除"
"write:admin:unsuspend-user": "ユーザーの凍結解除"
"write:admin:meta": "インスタンスのメタデータいじる"
"write:admin:user-note": "モデレーションノートいじる"

View File

@ -138,6 +138,9 @@ mute: "뮤트"
unmute: "뮤트 해제"
renoteMute: "리노트 뮤트하기"
renoteUnmute: "리노트 뮤트 해제"
mutedReactions: "리액션 뮤트"
muteThisReaction: "이 리액션을 뮤트하기"
unmuteThisReaction: "이 리액션의 뮤트를 해제하기"
block: "차단"
unblock: "차단 해제"
suspend: "정지"
@ -692,6 +695,7 @@ useGlobalSetting: "글로벌 설정을 사용하기"
useGlobalSettingDesc: "활성화하면 계정의 알림 설정이 적용됩니다. 비활성화하면 개별적으로 설정할 수 있게 됩니다."
other: "기타"
regenerateLoginToken: "로그인 토큰을 재생성"
regenerateLoginTokenConfirm: "정말 로그인 토큰을 재생성하시겠습니까? 이 작업을 실행하면 이 계정으로 로그인한 모든 기기에서 로그아웃됩니다."
regenerateLoginTokenDescription: "로그인할 때 사용되는 내부 토큰을 재생성합니다. 일반적으로 이 작업을 실행할 필요는 없습니다. 이 기능을 사용하면 이 계정으로 로그인한 모든 기기에서 로그아웃됩니다."
theKeywordWhenSearchingForCustomEmoji: "맞춤 이모티콘을 검색할 때 키워드가 됩니다."
setMultipleBySeparatingWithSpace: "공백으로 구분하여 여러 개 설정할 수 있습니다."
@ -1293,6 +1297,7 @@ reportComplete: "신고 완료"
blockThisUser: "이 사용자 차단하기"
muteThisUser: "이 사용자 뮤트하기"
checkedByHIBP: "비밀번호의 안전성과 더불어, HIBP를 통해 비밀번호 유출을 검사합니다."
changeUserName: "이름 변경"
_bubbleGame:
howToPlay: "설명"
hold: "홀드"
@ -1337,7 +1342,9 @@ _announcement:
forExistingUsers: "기존 유저에게만 알림"
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
needConfirmationToRead: "읽음으로 표시하기 전에 확인하기"
needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 띄웁니다. '모두 읽음'의 대상에서도 제외됩니다."
needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 띄웁니다."
needEnrollmentTutorialToRead: "읽음으로 표시하기 전에 튜토리얼 진행하기"
needEnrollmentTutorialToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 사용자에게 튜토리얼을 진행하도록 요구합니다."
end: "공지에서 내리기"
tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 사용자 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다."
readConfirmTitle: "읽음으로 표시합니까?"
@ -2149,9 +2156,9 @@ _permissions:
"read:admin:show-user": "사용자 개인정보 보기"
"read:admin:show-users": "사용자 개인정보 보기"
"write:admin:suspend-user": "사용자 정지하기"
"write:admin:unset-user-avatar": "사용자 아바타 삭제하기"
"write:admin:unset-user-banner": "사용자 배너 삭제하기"
"write:admin:unset-user-mutual-link": "사용자의 서로링크 삭제하기"
"write:admin:user-avatar": "사용자 아바타 삭제하기"
"write:admin:user-banner": "사용자 배너 삭제하기"
"write:admin:user-mutual-link": "사용자의 서로링크 삭제하기"
"write:admin:unsuspend-user": "사용자 정지 해제하기"
"write:admin:meta": "인스턴스 메타데이터 수정하기"
"write:admin:user-note": "조정 기록 수정하기"

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
needConfirmationToRead: "จำเป็นต้องยืนยันว่าอ่านแล้ว"
needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว นอกจากนี้ยังทำให้ประกาศนี้ยังไม่ถูกอ่านเมื่อใช้ฟังก์ชั่น “ทำเครื่องหมายฯ ทั้งหมดว่าอ่านแล้ว”"
needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว"
end: "เก็บประกาศ"
tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ"
readConfirmTitle: "ทำเครื่องหมายว่าอ่านแล้วเลยไหม?"
@ -2034,8 +2034,8 @@ _permissions:
"read:admin:show-user": "ดูข้อมูลส่วนตัวของผู้ใช้"
"read:admin:show-users": "ดูข้อมูลส่วนตัวของผู้ใช้"
"write:admin:suspend-user": "ระงับผู้ใช้"
"write:admin:unset-user-avatar": "ลบอวตารผู้ใช้"
"write:admin:unset-user-banner": "ลบแบนเนอร์ผู้ใช้"
"write:admin:user-avatar": "ลบอวตารผู้ใช้"
"write:admin:user-banner": "ลบแบนเนอร์ผู้ใช้"
"write:admin:unsuspend-user": "ยกเลิกการระงับผู้ใช้"
"write:admin:meta": "จัดการข้อมูลเมตาของอินสแตนซ์"
"write:admin:user-note": "จัดการโน้ตการกลั่นกรอง"

View File

@ -1275,7 +1275,7 @@ _announcement:
forExistingUsers: "仅限现有用户"
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
needConfirmationToRead: "需要确认才能标记为已读"
needConfirmationToReadDescription: "若启用,则会在标记已读时会显示确认对话框。此外,它也会不受批量已读操作的影响。"
needConfirmationToReadDescription: "若启用,则会在标记已读时会显示确认对话框。"
end: "结束公告"
tooManyActiveAnnouncementDescription: "若有大量活动公告,可能会造成用户体验下降。请考虑归档已完成的公告。"
readConfirmTitle: "标记为已读?"
@ -2052,8 +2052,8 @@ _permissions:
"read:admin:show-user": "查看用户的非公开信息"
"read:admin:show-users": "查看用户的非公开信息"
"write:admin:suspend-user": "冻结用户"
"write:admin:unset-user-avatar": "删除用户头像"
"write:admin:unset-user-banner": "删除用户横幅"
"write:admin:user-avatar": "删除用户头像"
"write:admin:user-banner": "删除用户横幅"
"write:admin:unsuspend-user": "解除用户冻结"
"write:admin:meta": "编辑实例元数据"
"write:admin:user-note": "编辑管理笔记"

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
needConfirmationToRead: "必須確認才能標記為已讀"
needConfirmationToReadDescription: "啟用代表此公告將顯示對話方塊以確認是否標記為已讀,同時不會受「標記所有公告為已讀」功能影響。"
needConfirmationToReadDescription: "啟用代表此公告將顯示對話方塊以確認是否標記為已讀。"
end: "結束公告"
tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。"
readConfirmTitle: "標記為已讀嗎?"
@ -2034,8 +2034,8 @@ _permissions:
"read:admin:show-user": "查看使用者的私密資訊"
"read:admin:show-users": "查看使用者的私密資訊"
"write:admin:suspend-user": "凍結使用者"
"write:admin:unset-user-avatar": "刪除使用者的頭像"
"write:admin:unset-user-banner": "刪除使用者的橫幅"
"write:admin:user-avatar": "刪除使用者的頭像"
"write:admin:user-banner": "刪除使用者的橫幅"
"write:admin:unsuspend-user": "解除凍結使用者"
"write:admin:meta": "編輯實例的詮釋資料"
"write:admin:user-note": "編輯審查筆記"

View File

@ -0,0 +1,11 @@
export class AnnouncementNeedEnrollmentTutorialToRead1729270181417 {
name = 'AnnouncementNeedEnrollmentTutorialToRead1729270181417'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" ADD "needEnrollmentTutorialToRead" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "needEnrollmentTutorialToRead"`);
}
}

View File

@ -34,20 +34,18 @@
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.5.7",
"@swc/core-darwin-x64": "1.5.7",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.5.7",
"@swc/core-linux-arm64-gnu": "1.5.7",
"@swc/core-linux-arm64-musl": "1.5.7",
"@swc/core-linux-x64-gnu": "1.5.7",
"@swc/core-linux-x64-musl": "1.5.7",
"@swc/core-win32-arm64-msvc": "1.5.7",
"@swc/core-win32-ia32-msvc": "1.5.7",
"@swc/core-win32-x64-msvc": "1.5.7",
"@tensorflow/tfjs": "4.19.0",
"@tensorflow/tfjs-node": "4.19.0",
"@swc/core-darwin-arm64": "1.7.39",
"@swc/core-darwin-x64": "1.7.39",
"@swc/core-linux-arm-gnueabihf": "1.7.39",
"@swc/core-linux-arm64-gnu": "1.7.39",
"@swc/core-linux-arm64-musl": "1.7.39",
"@swc/core-linux-x64-gnu": "1.7.39",
"@swc/core-linux-x64-musl": "1.7.39",
"@swc/core-win32-arm64-msvc": "1.7.39",
"@swc/core-win32-ia32-msvc": "1.7.39",
"@swc/core-win32-x64-msvc": "1.7.39",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.8",
"slacc-android-arm-eabi": "0.0.10",
"slacc-android-arm64": "0.0.10",
@ -66,34 +64,34 @@
},
"dependencies": {
"@authenio/samlify-node-xmllint": "2.0.0",
"@aws-sdk/client-s3": "3.627.0",
"@aws-sdk/lib-storage": "3.627.0",
"@bull-board/api": "5.21.3",
"@bull-board/fastify": "5.21.3",
"@bull-board/ui": "5.21.3",
"@discordapp/twemoji": "15.0.3",
"@elastic/elasticsearch": "^8.14.0",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1",
"@fastify/cors": "9.0.1",
"@fastify/express": "3.0.0",
"@fastify/formbody": "7.4.0",
"@fastify/http-proxy": "9.5.0",
"@fastify/multipart": "8.3.0",
"@fastify/static": "7.0.4",
"@fastify/view": "9.1.0",
"@aws-sdk/client-s3": "3.676.0",
"@aws-sdk/lib-storage": "3.676.0",
"@bull-board/api": "6.2.4",
"@bull-board/fastify": "6.2.4",
"@bull-board/ui": "6.2.4",
"@discordapp/twemoji": "15.1.0",
"@elastic/elasticsearch": "8.15.1",
"@fastify/accepts": "5.0.1",
"@fastify/cookie": "11.0.1",
"@fastify/cors": "10.0.1",
"@fastify/express": "4.0.1",
"@fastify/formbody": "8.0.1",
"@fastify/http-proxy": "10.0.1",
"@fastify/multipart": "9.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.53",
"@nestjs/common": "10.3.10",
"@nestjs/core": "10.3.10",
"@nestjs/testing": "10.3.10",
"@napi-rs/canvas": "0.1.58",
"@nestjs/common": "10.4.5",
"@nestjs/core": "10.4.5",
"@nestjs/testing": "10.4.5",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "10.0.1",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "3.1.4",
"@swc/cli": "0.3.12",
"@swc/core": "1.5.7",
"@simplewebauthn/server": "11.0.0",
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "3.2.5",
"@swc/cli": "0.4.0",
"@swc/core": "1.7.39",
"@twemoji/parser": "15.1.1",
"accepts": "1.3.8",
"ajv": "8.17.1",
@ -101,41 +99,41 @@
"async-mutex": "0.5.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "5.12.3",
"body-parser": "1.20.3",
"bullmq": "5.21.2",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
"chokidar": "3.6.0",
"chokidar": "4.0.1",
"cli-highlight": "2.1.11",
"color-convert": "2.0.1",
"content-disposition": "0.5.4",
"date-fns": "3.6.0",
"date-fns": "4.1.0",
"deep-email-validator": "0.1.21",
"fastify": "4.28.1",
"fastify-http-errors-enhanced": "5.0.4",
"fastify-raw-body": "4.3.0",
"fastify": "5.0.0",
"fastify-http-errors-enhanced": "6.0.0",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.4.0",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.4.2",
"happy-dom": "14.12.3",
"form-data": "4.0.1",
"got": "14.4.3",
"happy-dom": "15.7.4",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
"ioredis": "5.4.1",
"ip-cidr": "3.1.0",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0",
"is-svg": "5.1.0",
"jose": "5.6.3",
"jose": "5.9.6",
"js-yaml": "4.1.0",
"jsdom": "24.1.1",
"jsdom": "25.0.1",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
"meilisearch": "0.41.0",
"meilisearch": "0.44.1",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
@ -146,17 +144,17 @@
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"node-forge": "1.3.1",
"nodemailer": "6.9.14",
"nodemailer": "6.9.15",
"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.1",
"parse5": "7.1.2",
"pg": "8.12.0",
"pino": "9.3.2",
"pino-pretty": "11.2.2",
"otpauth": "9.3.4",
"parse5": "7.2.0",
"pg": "8.13.0",
"pino": "9.5.0",
"pino-pretty": "11.3.0",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@ -165,26 +163,26 @@
"qrcode": "1.5.4",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.21.3",
"re2": "1.21.4",
"redis-lock": "0.1.4",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",
"samlify": "2.8.11",
"sanitize-html": "2.13.0",
"sanitize-html": "2.13.1",
"secure-json-parse": "2.7.0",
"sharp": "0.33.4",
"sharp": "0.33.5",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.23.4",
"systeminformation": "5.23.5",
"tinycolor2": "1.6.0",
"tmp": "0.2.3",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typescript": "5.5.4",
"typescript": "5.6.3",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
@ -195,58 +193,58 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0",
"@nestjs/platform-express": "10.3.10",
"@simplewebauthn/types": "10.0.0",
"@nestjs/platform-express": "10.4.5",
"@simplewebauthn/types": "11.0.0",
"@swc/jest": "0.2.36",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.2",
"@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.5",
"@types/color-convert": "2.0.3",
"@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.25",
"@types/fluent-ffmpeg": "2.1.26",
"@types/htmlescape": "^1.1.3",
"@types/http-link-header": "1.0.7",
"@types/jest": "29.5.12",
"@types/jest": "29.5.13",
"@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": "22.2.0",
"@types/node": "22.7.8",
"@types/node-forge": "1.3.11",
"@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.5",
"@types/nodemailer": "6.4.16",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.6",
"@types/pg": "8.11.10",
"@types/pug": "2.0.10",
"@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7",
"@types/sanitize-html": "2.11.0",
"@types/sanitize-html": "2.13.0",
"@types/semver": "7.5.8",
"@types/simple-oauth2": "5.0.7",
"@types/sinonjs__fake-timers": "8.1.5",
"@types/tinycolor2": "1.4.6",
"@types/tmp": "0.2.6",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.5.12",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"aws-sdk-client-mock": "4.0.1",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1",
"execa": "9.3.0",
"eslint": "8.57.1",
"eslint-plugin-import": "2.31.0",
"execa": "9.4.1",
"fkill": "^9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.1.4",
"nodemon": "3.1.7",
"pid-port": "1.0.0",
"simple-oauth2": "5.1.0"
}

View File

@ -50,10 +50,10 @@ type Source = {
redisForJobQueue?: RedisOptionsSource;
redisForSystemQueue?: RedisOptionsSource;
redisForEndedPollNotificationQueue?: RedisOptionsSource;
redisForDeliverQueue?: RedisOptionsSource;
redisForInboxQueue?: RedisOptionsSource;
redisForDeliverQueues?: Array<RedisOptionsSource>;
redisForInboxQueues?: Array<RedisOptionsSource>;
redisForDbQueue?: RedisOptionsSource;
redisForRelationshipQueue?: RedisOptionsSource;
redisForRelationshipQueues?: Array<RedisOptionsSource>;
redisForObjectStorageQueue?: RedisOptionsSource;
redisForWebhookDeliverQueue?: RedisOptionsSource;
redisForTimelines?: RedisOptionsSource;
@ -220,10 +220,10 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource;
redisForSystemQueue: RedisOptions & RedisOptionsSource;
redisForEndedPollNotificationQueue: RedisOptions & RedisOptionsSource;
redisForDeliverQueue: RedisOptions & RedisOptionsSource;
redisForInboxQueue: RedisOptions & RedisOptionsSource;
redisForDeliverQueues: Array<RedisOptions & RedisOptionsSource>;
redisForInboxQueues: Array<RedisOptions & RedisOptionsSource>;
redisForDbQueue: RedisOptions & RedisOptionsSource;
redisForRelationshipQueue: RedisOptions & RedisOptionsSource;
redisForRelationshipQueues: Array<RedisOptions & RedisOptionsSource>;
redisForObjectStorageQueue: RedisOptions & RedisOptionsSource;
redisForWebhookDeliverQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
@ -296,10 +296,10 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForSystemQueue: config.redisForSystemQueue ? convertRedisOptions(config.redisForSystemQueue, host) : redisForJobQueue,
redisForEndedPollNotificationQueue: config.redisForEndedPollNotificationQueue ? convertRedisOptions(config.redisForEndedPollNotificationQueue, host) : redisForJobQueue,
redisForDeliverQueue: config.redisForDeliverQueue ? convertRedisOptions(config.redisForDeliverQueue, host) : redisForJobQueue,
redisForInboxQueue: config.redisForInboxQueue ? convertRedisOptions(config.redisForInboxQueue, host) : redisForJobQueue,
redisForDeliverQueues: config.redisForDeliverQueues ? config.redisForDeliverQueues.map(config => convertRedisOptions(config, host)) : [redisForJobQueue],
redisForInboxQueues: config.redisForInboxQueues ? config.redisForInboxQueues.map(config => convertRedisOptions(config, host)) : [redisForJobQueue],
redisForDbQueue: config.redisForDbQueue ? convertRedisOptions(config.redisForDbQueue, host) : redisForJobQueue,
redisForRelationshipQueue: config.redisForRelationshipQueue ? convertRedisOptions(config.redisForRelationshipQueue, host) : redisForJobQueue,
redisForRelationshipQueues: config.redisForRelationshipQueues ? config.redisForRelationshipQueues.map(config => convertRedisOptions(config, host)) : [redisForJobQueue],
redisForObjectStorageQueue: config.redisForObjectStorageQueue ? convertRedisOptions(config.redisForObjectStorageQueue, host) : redisForJobQueue,
redisForWebhookDeliverQueue: config.redisForWebhookDeliverQueue ? convertRedisOptions(config.redisForWebhookDeliverQueue, host) : redisForJobQueue,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,

View File

@ -91,6 +91,7 @@ export class AnnouncementService {
forExistingUsers: values.forExistingUsers,
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
needEnrollmentTutorialToRead: values.needEnrollmentTutorialToRead,
closeDuration: values.closeDuration,
displayOrder: values.displayOrder,
userId: values.userId,
@ -200,6 +201,7 @@ export class AnnouncementService {
icon: values.icon,
forExistingUsers: values.forExistingUsers,
needConfirmationToRead: values.needConfirmationToRead,
needEnrollmentTutorialToRead: values.needEnrollmentTutorialToRead,
closeDuration: values.closeDuration,
displayOrder: values.displayOrder,
silence: values.silence,

View File

@ -266,10 +266,7 @@ export class FileInfoService {
}
private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
const watcher = new FSWatcher({
cwd,
disableGlobbing: true,
});
const watcher = new FSWatcher({ cwd });
let finished = false;
command.once('end', () => {
finished = true;

View File

@ -9,15 +9,16 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
import { Queues } from '@/misc/queues.js';
import type { Provider } from '@nestjs/common';
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>;
export type DeliverQueue = Queues<DeliverJobData>;
export type InboxQueue = Queues<InboxJobData>;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type RelationshipQueue = Queues<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
@ -35,13 +36,13 @@ const $endedPollNotification: Provider = {
const $deliver: Provider = {
provide: 'queue:deliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config.redisForDeliverQueue, config.bullmqQueueOptions, QUEUE.DELIVER)),
useFactory: (config: Config) => new Queues(config.redisForDeliverQueues.map(queueConfig => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(queueConfig, config.bullmqQueueOptions, QUEUE.DELIVER)))),
inject: [DI.config],
};
const $inbox: Provider = {
provide: 'queue:inbox',
useFactory: (config: Config) => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config.redisForInboxQueue, config.bullmqQueueOptions, QUEUE.INBOX)),
useFactory: (config: Config) => new Queues(config.redisForInboxQueues.map(queueConfig => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(queueConfig, config.bullmqQueueOptions, QUEUE.INBOX)))),
inject: [DI.config],
};
@ -53,7 +54,7 @@ const $db: Provider = {
const $relationship: Provider = {
provide: 'queue:relationship',
useFactory: (config: Config) => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config.redisForRelationshipQueue, config.bullmqQueueOptions, QUEUE.RELATIONSHIP)),
useFactory: (config: Config) => new Queues(config.redisForRelationshipQueues.map(queueConfig => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(queueConfig, config.bullmqQueueOptions, QUEUE.RELATIONSHIP)))),
inject: [DI.config],
};

View File

@ -96,8 +96,8 @@ export class WebAuthnService {
@bindThis
public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{
credentialID: string;
credentialPublicKey: Uint8Array;
id: string;
publicKey: Uint8Array;
attestationObject: Uint8Array;
fmt: AttestationFormat;
counter: number;
@ -139,15 +139,15 @@ export class WebAuthnService {
const { registrationInfo } = verification;
return {
credentialID: registrationInfo.credentialID,
credentialPublicKey: registrationInfo.credentialPublicKey,
id: registrationInfo.credential.id,
publicKey: registrationInfo.credential.publicKey,
attestationObject: registrationInfo.attestationObject,
fmt: registrationInfo.fmt,
counter: registrationInfo.counter,
counter: registrationInfo.credential.counter,
userVerified: registrationInfo.userVerified,
credentialDeviceType: registrationInfo.credentialDeviceType,
credentialBackedUp: registrationInfo.credentialBackedUp,
transports: response.response.transports,
transports: registrationInfo.credential.transports,
};
}
@ -228,9 +228,9 @@ export class WebAuthnService {
expectedChallenge: challenge,
expectedOrigin: relyingParty.origin,
expectedRPID: relyingParty.rpId,
authenticator: {
credentialID: key.id,
credentialPublicKey: Buffer.from(key.publicKey, 'base64url'),
credential: {
id: key.id,
publicKey: Buffer.from(key.publicKey, 'base64url'),
counter: key.counter,
transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined,
},

View File

@ -54,6 +54,7 @@ export class AnnouncementEntityService {
display: announcement.display,
forYou: announcement.userId === me?.id,
needConfirmationToRead: announcement.needConfirmationToRead,
needEnrollmentTutorialToRead: announcement.needEnrollmentTutorialToRead,
closeDuration: announcement.closeDuration,
displayOrder: announcement.displayOrder,
silence: announcement.silence,

View File

@ -8,7 +8,7 @@ import type { onRequestHookHandler } from 'fastify';
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
const index = request.url.indexOf('?');
if (~index) {
reply.redirect(301, request.url.slice(0, index));
reply.redirect(request.url.slice(0, index), 301);
}
done();
};

View File

@ -0,0 +1,69 @@
import { EventEmitter } from 'node:events';
import * as Bull from 'bullmq';
export class Queues<DataType = any, ResultType = any, NameType extends string = string> {
public readonly queues: ReadonlyArray<Bull.Queue<DataType, ResultType, NameType>>;
constructor(queues: Bull.Queue<DataType, ResultType, NameType>[]) {
if (queues.length === 0) {
throw new Error('queues cannot be empty.');
}
this.queues = queues;
}
getRandomQueue(): Bull.Queue<DataType, ResultType, NameType> {
return this.queues[Math.floor(Math.random() * this.queues.length)];
}
add(name: NameType, data: DataType, opts?: Bull.JobsOptions): Promise<Bull.Job<DataType, ResultType, NameType>> {
return this.getRandomQueue().add(name, data, opts);
}
async addBulk(jobs: { name: NameType; data: DataType; opts?: Bull.BulkJobOptions }[]): Promise<Bull.Job<DataType, ResultType, NameType>[]> {
return (await Promise.allSettled(jobs.map(job => this.add(job.name, job.data, job.opts))))
.filter((value): value is PromiseFulfilledResult<Bull.Job<DataType, ResultType, NameType>> => value.status === 'fulfilled')
.flatMap(value => value.value);
}
async close(): Promise<void> {
await Promise.allSettled(this.queues.map(queue => queue.close()));
}
async getDelayed(start?: number, end?: number): Promise<Bull.Job<DataType, ResultType, NameType>[]> {
return (await Promise.allSettled(this.queues.map(queue => queue.getDelayed(start, end))))
.filter((value): value is PromiseFulfilledResult<Bull.Job<DataType, ResultType, NameType>[]> => value.status === 'fulfilled')
.flatMap(value => value.value);
}
async getJobCounts(...types: Bull.JobType[]): Promise<{ [p: string]: number }> {
return (await Promise.allSettled(this.queues.map(queue => queue.getJobCounts(...types))))
.filter((value): value is PromiseFulfilledResult<Record<string, number>> => value.status === 'fulfilled')
.reduce((previousValue, currentValue) => {
for (const key in currentValue.value) {
previousValue[key] = (previousValue[key] || 0) + currentValue.value[key];
}
return previousValue;
}, {} as Record<string, number>);
}
once<U extends keyof Bull.QueueListener<DataType, ResultType, NameType>>(event: U, listener: Bull.QueueListener<DataType, ResultType, NameType>[U]): void {
const e = new EventEmitter();
e.once(event, listener);
const listener1 = (...args: any[]) => e.emit(event, ...args);
this.queues.forEach(queue => queue.once(event, listener1));
e.once(event, () => this.queues.forEach(queue => queue.off(event, listener1)));
}
async clean(grace: number, limit: number, type?: 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed'): Promise<NameType[]> {
return (await Promise.allSettled(this.queues.map(queue => queue.clean(grace, limit, type))))
.filter((value): value is PromiseFulfilledResult<NameType[]> => value.status === 'fulfilled')
.flatMap(value => value.value);
}
async getJobs(types?: Bull.JobType[] | Bull.JobType, start?: number, end?: number, asc?: boolean): Promise<Bull.Job<DataType, ResultType, NameType>[]> {
return (await Promise.allSettled(this.queues.map(queue => queue.getJobs(types, start, end, asc))))
.filter((value): value is PromiseFulfilledResult<Bull.Job<DataType, ResultType, NameType>[]> => value.status === 'fulfilled')
.flatMap(value => value.value);
}
}

View File

@ -61,6 +61,11 @@ export class MiAnnouncement {
})
public needConfirmationToRead: boolean;
@Column('boolean', {
default: false,
})
public needEnrollmentTutorialToRead: boolean;
@Column('integer', {
nullable: false,
default: 0,

View File

@ -48,6 +48,10 @@ export const packedAnnouncementSchema = {
type: 'boolean',
optional: false, nullable: false,
},
needEnrollmentTutorialToRead: {
type: 'boolean',
optional: false, nullable: false,
},
forYou: {
type: 'boolean',
optional: false, nullable: false,

View File

@ -76,10 +76,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
private logger: Logger;
private systemQueueWorker: Bull.Worker;
private dbQueueWorker: Bull.Worker;
private deliverQueueWorker: Bull.Worker;
private inboxQueueWorker: Bull.Worker;
private deliverQueueWorkers: Bull.Worker[];
private inboxQueueWorkers: Bull.Worker[];
private webhookDeliverQueueWorker: Bull.Worker;
private relationshipQueueWorker: Bull.Worker;
private relationshipQueueWorkers: Bull.Worker[];
private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker;
@ -209,51 +209,59 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#endregion
//#region deliver
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
...baseWorkerOptions(this.config.redisForDeliverQueue, this.config.bullmqWorkerOptions, QUEUE.DELIVER),
autorun: false,
concurrency: this.config.deliverJobConcurrency ?? 128,
limiter: {
max: this.config.deliverJobPerSec ?? 128,
duration: 1000,
},
settings: {
backoffStrategy: httpRelatedBackoff,
},
this.deliverQueueWorkers = this.config.redisForDeliverQueues
.filter((_, index) => process.env.QUEUE_WORKER_INDEX == null || index === Number.parseInt(process.env.QUEUE_WORKER_INDEX, 10))
.map(config => new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), {
...baseWorkerOptions(config, this.config.bullmqWorkerOptions, QUEUE.DELIVER),
autorun: false,
concurrency: this.config.deliverJobConcurrency ?? 128,
limiter: {
max: this.config.deliverJobPerSec ?? 128,
duration: 1000,
},
settings: {
backoffStrategy: httpRelatedBackoff,
},
}));
this.deliverQueueWorkers.forEach((worker, index) => {
const deliverLogger = this.logger.createSubLogger(`deliver-${index}`);
worker
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
});
const deliverLogger = this.logger.createSubLogger('deliver');
this.deliverQueueWorker
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
//#endregion
//#region inbox
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
...baseWorkerOptions(this.config.redisForInboxQueue, this.config.bullmqWorkerOptions, QUEUE.INBOX),
autorun: false,
concurrency: this.config.inboxJobConcurrency ?? 16,
limiter: {
max: this.config.inboxJobPerSec ?? 32,
duration: 1000,
},
settings: {
backoffStrategy: httpRelatedBackoff,
},
this.inboxQueueWorkers = this.config.redisForInboxQueues
.filter((_, index) => process.env.QUEUE_WORKER_INDEX == null || index === Number.parseInt(process.env.QUEUE_WORKER_INDEX, 10))
.map(config => new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
...baseWorkerOptions(config, this.config.bullmqWorkerOptions, QUEUE.INBOX),
autorun: false,
concurrency: this.config.inboxJobConcurrency ?? 16,
limiter: {
max: this.config.inboxJobPerSec ?? 32,
duration: 1000,
},
settings: {
backoffStrategy: httpRelatedBackoff,
},
}));
this.inboxQueueWorkers.forEach((worker, index) => {
const inboxLogger = this.logger.createSubLogger(`inbox-${index}`);
worker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, error: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
});
const inboxLogger = this.logger.createSubLogger('inbox');
this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, error: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
//#endregion
//#region webhook deliver
@ -281,32 +289,36 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#endregion
//#region relationship
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
switch (job.name) {
case 'follow': return this.relationshipProcessorService.processFollow(job);
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
case 'block': return this.relationshipProcessorService.processBlock(job);
case 'unblock': return this.relationshipProcessorService.processUnblock(job);
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
}
}, {
...baseWorkerOptions(this.config.redisForRelationshipQueue, this.config.bullmqWorkerOptions, QUEUE.RELATIONSHIP),
autorun: false,
concurrency: this.config.relationshipJobConcurrency ?? 16,
limiter: {
max: this.config.relationshipJobPerSec ?? 64,
duration: 1000,
},
this.relationshipQueueWorkers = this.config.redisForRelationshipQueues
.filter((_, index) => process.env.QUEUE_WORKER_INDEX == null || index === Number.parseInt(process.env.QUEUE_WORKER_INDEX, 10))
.map(config => new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
switch (job.name) {
case 'follow': return this.relationshipProcessorService.processFollow(job);
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
case 'block': return this.relationshipProcessorService.processBlock(job);
case 'unblock': return this.relationshipProcessorService.processUnblock(job);
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
}
}, {
...baseWorkerOptions(config, this.config.bullmqWorkerOptions, QUEUE.RELATIONSHIP),
autorun: false,
concurrency: this.config.relationshipJobConcurrency ?? 16,
limiter: {
max: this.config.relationshipJobPerSec ?? 64,
duration: 1000,
},
}));
this.relationshipQueueWorkers.forEach((worker, index) => {
const relationshipLogger = this.logger.createSubLogger(`relationship-${index}`);
worker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, error: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
});
const relationshipLogger = this.logger.createSubLogger('relationship');
this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, error: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
//#endregion
//#region object storage
@ -345,10 +357,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
await Promise.all([
this.systemQueueWorker.run(),
this.dbQueueWorker.run(),
this.deliverQueueWorker.run(),
this.inboxQueueWorker.run(),
...this.deliverQueueWorkers.map(worker => worker.run()),
this.inboxQueueWorkers.map(worker => worker.run()),
this.webhookDeliverQueueWorker.run(),
this.relationshipQueueWorker.run(),
this.relationshipQueueWorkers.map(worker => worker.run()),
this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(),
]);
@ -359,10 +371,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
await Promise.all([
this.systemQueueWorker.close(),
this.dbQueueWorker.close(),
this.deliverQueueWorker.close(),
this.inboxQueueWorker.close(),
...this.deliverQueueWorkers.map(worker => worker.close()),
this.inboxQueueWorkers.map(worker => worker.close()),
this.webhookDeliverQueueWorker.close(),
this.relationshipQueueWorker.close(),
this.relationshipQueueWorkers.map(worker => worker.close()),
this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(),
]);

View File

@ -82,7 +82,7 @@ export class FileServerService {
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`);
return reply.redirect(`${this.config.url}/files/${request.params.key}`, 301);
});
done();
});
@ -147,12 +147,12 @@ export class FileServerService {
url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(301, url.toString());
return await reply.redirect(url.toString(), 301);
} else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) {
file.cleanup();
return await reply.redirect(301, externalThumbnail);
return await reply.redirect(externalThumbnail, 301);
}
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
@ -167,7 +167,7 @@ export class FileServerService {
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(301, url.toString());
return await reply.redirect(url.toString(), 301);
}
}
@ -313,12 +313,18 @@ export class FileServerService {
url.searchParams.append(key, value);
}
return await reply.redirect(
301,
return reply.redirect(
url.toString(),
301,
);
}
if (!request.headers['user-agent']) {
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
}
// Create temp file
const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') {

View File

@ -155,7 +155,7 @@ export class ServerService implements OnApplicationShutdown {
if (emoji == null) {
if ('fallback' in request.query) {
return await reply.redirect('/static-assets/emoji-unknown.png');
return reply.redirect('/static-assets/emoji-unknown.png');
} else {
reply.code(404);
return;
@ -176,9 +176,9 @@ export class ServerService implements OnApplicationShutdown {
if ('static' in request.query) url.searchParams.set('static', '1');
}
return await reply.redirect(
301,
return reply.redirect(
url.toString(),
301,
);
});

View File

@ -75,6 +75,7 @@ import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_regenerateUserToken from './endpoints/admin/regenerate-user-token.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
@ -85,6 +86,7 @@ import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js';
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
@ -469,6 +471,7 @@ const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass:
const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default };
const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
const $admin_regenerateUserToken: Provider = { provide: 'ep:admin/regenerate-user-token', useClass: ep___admin_regenerateUserToken.default };
const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
@ -479,6 +482,7 @@ const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: e
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
const $admin_updateUserName: Provider = { provide: 'ep:admin/update-user-name', useClass: ep___admin_updateUserName.default };
const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default };
const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default };
const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default };
@ -867,6 +871,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_list,
$admin_relays_remove,
$admin_resetPassword,
$admin_regenerateUserToken,
$admin_resolveAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
@ -877,6 +882,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_suspendUser,
$admin_unsuspendUser,
$admin_updateMeta,
$admin_updateUserName,
$admin_updateUserNote,
$admin_roles_create,
$admin_roles_delete,
@ -1259,6 +1265,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_list,
$admin_relays_remove,
$admin_resetPassword,
$admin_regenerateUserToken,
$admin_resolveAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
@ -1269,6 +1276,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_suspendUser,
$admin_unsuspendUser,
$admin_updateMeta,
$admin_updateUserName,
$admin_updateUserNote,
$admin_roles_create,
$admin_roles_delete,

View File

@ -75,6 +75,7 @@ import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_regenerateUserToken from './endpoints/admin/regenerate-user-token.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
@ -85,6 +86,7 @@ import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_updateUserName from './endpoints/admin/update-user-name.js';
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
@ -467,6 +469,7 @@ const eps = [
['admin/relays/list', ep___admin_relays_list],
['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword],
['admin/regenerate-user-token', ep___admin_regenerateUserToken],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
['admin/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo],
@ -477,6 +480,7 @@ const eps = [
['admin/suspend-user', ep___admin_suspendUser],
['admin/unsuspend-user', ep___admin_unsuspendUser],
['admin/update-meta', ep___admin_updateMeta],
['admin/update-user-name', ep___admin_updateUserName],
['admin/update-user-note', ep___admin_updateUserNote],
['admin/roles/create', ep___admin_roles_create],
['admin/roles/delete', ep___admin_roles_delete],

View File

@ -62,6 +62,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
needEnrollmentTutorialToRead: {
type: 'boolean',
optional: false, nullable: false,
},
closeDuration: {
type: 'number',
optional: false, nullable: false,
@ -92,6 +96,7 @@ export const paramDef = {
display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' },
forExistingUsers: { type: 'boolean', default: false },
needConfirmationToRead: { type: 'boolean', default: false },
needEnrollmentTutorialToRead: { type: 'boolean', default: false },
closeDuration: { type: 'number', default: 0 },
displayOrder: { type: 'number', default: 0 },
silence: { type: 'boolean', default: false },
@ -115,6 +120,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
display: ps.display,
forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead,
needEnrollmentTutorialToRead: ps.needEnrollmentTutorialToRead,
closeDuration: ps.closeDuration,
displayOrder: ps.displayOrder,
silence: ps.silence,

View File

@ -72,6 +72,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
needEnrollmentTutorialToRead: {
type: 'boolean',
optional: false, nullable: false,
},
closeDuration: {
type: 'number',
optional: false, nullable: false,
@ -139,6 +143,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isActive: announcement.isActive,
forExistingUsers: announcement.forExistingUsers,
needConfirmationToRead: announcement.needConfirmationToRead,
needEnrollmentTutorialToRead: announcement.needEnrollmentTutorialToRead,
closeDuration: announcement.closeDuration,
displayOrder: announcement.displayOrder,
silence: announcement.silence,

View File

@ -37,6 +37,7 @@ export const paramDef = {
display: { type: 'string', enum: ['normal', 'banner', 'dialog'] },
forExistingUsers: { type: 'boolean' },
needConfirmationToRead: { type: 'boolean' },
needEnrollmentTutorialToRead: { type: 'boolean' },
closeDuration: { type: 'number', default: 0 },
displayOrder: { type: 'number', default: 0 },
silence: { type: 'boolean' },
@ -68,6 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
icon: ps.icon,
forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead,
needEnrollmentTutorialToRead: ps.needEnrollmentTutorialToRead,
closeDuration: ps.closeDuration,
displayOrder: ps.displayOrder,
silence: ps.silence,

View File

@ -56,7 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const jobs = await this.deliverQueue.getJobs(['delayed']);
const res = [] as [string, number][];
const res = new Map<string, number>();
for (const job of jobs) {
let host: string;
@ -68,17 +68,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
continue;
}
const found = res.find(x => x[0] === host);
const found = res.get(host);
if (found) {
found[1]++;
res.set(host, found + 1);
} else {
res.push([host, 1]);
res.set(host, 1);
}
}
res.sort((a, b) => b[1] - a[1]);
return res;
return Array.from(res.entries()).sort((a, b) => b[1] - a[1]);
});
}
}

View File

@ -56,7 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const jobs = await this.inboxQueue.getJobs(['delayed']);
const res = [] as [string, number][];
const res = new Map<string, number>();
for (const job of jobs) {
let host: string;
@ -68,17 +68,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
continue;
}
const found = res.find(x => x[0] === host);
const found = res.get(host);
if (found) {
found[1]++;
res.set(host, found + 1);
} else {
res.push([host, 1]);
res.set(host, 1);
}
}
res.sort((a, b) => b[1] - a[1]);
return res;
return Array.from(res.entries()).sort((a, b) => b[1] - a[1]);
});
}
}

View File

@ -0,0 +1,61 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:regenerate-user-token',
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
const oldToken = user.token;
if (oldToken == null) return;
const newToken = generateUserToken();
await this.usersRepository.update(user.id, {
token: newToken,
});
// Publish event
this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: user.id, oldToken, newToken });
this.globalEventService.publishMainStream(user.id, 'myTokenRegenerated');
this.moderationLogService.log(me, 'regenerateUserToken', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
});
}
}

View File

@ -14,7 +14,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:unset-user-avatar',
kind: 'write:admin:user-avatar',
} as const;
export const paramDef = {

View File

@ -14,7 +14,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:unset-user-banner',
kind: 'write:admin:user-banner',
} as const;
export const paramDef = {

View File

@ -12,7 +12,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:unset-user-mutual-link',
kind: 'write:admin:user-mutual-link',
} as const;
export const paramDef = {

View File

@ -0,0 +1,53 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:user-name',
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
name: { type: 'string' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
await this.usersRepository.update(user.id, {
name: ps.name ?? null,
});
this.moderationLogService.log(me, 'updateUserName', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
before: user.name,
after: ps.name ?? null,
});
});
}
}

View File

@ -96,13 +96,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential);
const keyId = keyInfo.credentialID;
const keyId = keyInfo.id;
await this.userSecurityKeysRepository.insert({
id: keyId,
userId: me.id,
name: ps.name,
publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'),
publicKey: Buffer.from(keyInfo.publicKey).toString('base64url'),
counter: keyInfo.counter,
credentialDeviceType: keyInfo.credentialDeviceType,
credentialBackedUp: keyInfo.credentialBackedUp,

View File

@ -211,7 +211,10 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? {
get: info,
get: {
...info,
operationId: info.operationId + '_get',
},
} : {}),
post: info,
};

View File

@ -64,11 +64,12 @@ export class JWTIdentifyProviderService {
fastify.all<{
Params: { serviceId: string };
Querystring?: { serviceurl?: string, return_to?: string };
Body?: { serviceurl?: string, return_to?: string };
Querystring?: { serviceurl?: string, return_to?: string, prompt?: string };
Body?: { serviceurl?: string, return_to?: string, prompt?: string };
}>('/:serviceId', async (request, reply) => {
const serviceId = request.params.serviceId;
const returnTo = request.query?.return_to ?? request.query?.serviceurl ?? request.body?.return_to ?? request.body?.serviceurl;
const prompt = request.query?.prompt ?? request.body?.prompt ?? 'consent';
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'jwt' });
if (!ssoServiceProvider) {
@ -101,6 +102,7 @@ export class JWTIdentifyProviderService {
transactionId: transactionId,
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
kind: 'jwt',
prompt: prompt,
});
});
@ -203,7 +205,7 @@ export class JWTIdentifyProviderService {
.setIssuer(ssoServiceProvider.issuer)
.setAudience(ssoServiceProvider.audience)
.setIssuedAt()
.setExpirationTime('10m')
.setExpirationTime('2w')
.setJti(randomUUID())
.setSubject(user.id)
.encrypt(key);
@ -220,7 +222,7 @@ export class JWTIdentifyProviderService {
.setIssuer(ssoServiceProvider.issuer)
.setAudience(ssoServiceProvider.audience)
.setIssuedAt()
.setExpirationTime('10m')
.setExpirationTime('2w')
.setJti(randomUUID())
.setSubject(user.id)
.sign(key);

View File

@ -201,13 +201,14 @@ export class SAMLIdentifyProviderService {
fastify.all<{
Params: { serviceId: string };
Querystring?: { SAMLRequest?: string; RelayState?: string };
Body?: { SAMLRequest?: string; RelayState?: string };
Querystring?: { SAMLRequest?: string; RelayState?: string, prompt?: string };
Body?: { SAMLRequest?: string; RelayState?: string, prompt?: string };
}>('/:serviceId', async (request, reply) => {
const serviceId = request.params.serviceId;
const binding = request.query?.SAMLRequest ? 'redirect' : 'post';
const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest;
const relayState = request.query?.RelayState ?? request.body?.RelayState;
const prompt = request.query?.prompt ?? request.body?.prompt ?? 'consent';
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'saml', privateKey: Not(IsNull()) });
if (!ssoServiceProvider) {
@ -268,6 +269,7 @@ export class SAMLIdentifyProviderService {
transactionId: transactionId,
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
kind: 'saml',
prompt: prompt,
});
} catch (err) {
this.#logger.error('Failed to parse SAML request', { error: err });

View File

@ -25,7 +25,16 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import { MetaService } from '@/core/MetaService.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
SystemQueue,
WebhookDeliverQueue,
} from '@/core/QueueModule.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
@ -110,6 +119,7 @@ export class ClientServerService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {
@ -235,12 +245,13 @@ export class ClientServerService {
queues: [
this.systemQueue,
this.endedPollNotificationQueue,
this.deliverQueue,
this.inboxQueue,
this.dbQueue,
this.objectStorageQueue,
this.webhookDeliverQueue,
].map(q => new BullMQAdapter(q)),
].map(q => new BullMQAdapter(q))
.concat(this.deliverQueue.queues.map((q, index) => new BullMQAdapter(q, { prefix: `${index}-` })))
.concat(this.inboxQueue.queues.map((q, index) => new BullMQAdapter(q, { prefix: `${index}-` })))
.concat(this.relationshipQueue.queues.map((q, index) => new BullMQAdapter(q, { prefix: `${index}-` }))),
serverAdapter,
});

View File

@ -4,3 +4,4 @@ block meta
meta(name='misskey:sso:transaction-id' content=transactionId)
meta(name='misskey:sso:service-name' content=serviceName)
meta(name='misskey:sso:kind' content=kind)
meta(name='misskey:sso:prompt' content=prompt)

View File

@ -55,6 +55,7 @@ export const moderationLogTypes = [
'updateServerSettings',
'suspend',
'unsuspend',
'updateUserName',
'updateUserNote',
'addCustomEmoji',
'updateCustomEmoji',
@ -75,6 +76,7 @@ export const moderationLogTypes = [
'deleteGlobalAnnouncement',
'deleteUserAnnouncement',
'resetPassword',
'regenerateUserToken',
'suspendRemoteInstance',
'unsuspendRemoteInstance',
'updateRemoteInstanceNote',
@ -114,6 +116,13 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
updateUserName: {
userId: string;
userUsername: string;
userHost: string | null;
before: string | null;
after: string | null;
};
updateUserNote: {
userId: string;
userUsername: string;
@ -217,6 +226,11 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
regenerateUserToken: {
userId: string;
userUsername: string;
userHost: string | null;
};
suspendRemoteInstance: {
id: string;
host: string;

View File

@ -17,34 +17,34 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"@discordapp/twemoji": "15.0.3",
"@discordapp/twemoji": "15.1.0",
"@github/webauthn-json": "2.1.1",
"@isaacs/ttlcache": "1.4.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.7",
"@rollup/plugin-typescript": "11.1.6",
"@rollup/pluginutils": "5.1.0",
"@syuilo/aiscript": "0.18.0",
"@tabler/icons-webfont": "3.12.0",
"@rollup/plugin-replace": "6.0.1",
"@rollup/plugin-typescript": "12.1.1",
"@rollup/pluginutils": "5.1.2",
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.19.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.2",
"@vue/compiler-sfc": "3.4.15",
"@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.12",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9",
"astring": "1.8.6",
"astring": "1.9.0",
"broadcast-channel": "7.0.0",
"buraha": "0.0.1",
"canvas-confetti": "1.9.3",
"chart.js": "4.4.3",
"chart.js": "4.4.5",
"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.7.0",
"chromatic": "11.14.0",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.0",
"date-fns": "3.6.0",
"date-fns": "4.1.0",
"escape-regexp": "0.0.1",
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
@ -60,86 +60,86 @@
"moment-timezone": "^0.5.45",
"photoswipe": "5.4.4",
"punycode": "2.3.1",
"rollup": "4.20.0",
"sanitize-html": "2.13.0",
"sass": "1.77.8",
"shiki": "1.12.1",
"rollup": "4.24.0",
"sanitize-html": "2.13.1",
"sass": "1.80.3",
"shiki": "1.22.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.167.1",
"three": "0.169.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typescript": "5.5.4",
"typescript": "5.6.3",
"uuid": "10.0.0",
"v-code-diff": "1.12.1",
"vite": "5.4.0",
"vue": "3.4.15",
"v-code-diff": "1.13.1",
"vite": "5.4.9",
"vue": "3.5.12",
"vuedraggable": "next"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.2.8",
"@storybook/addon-essentials": "8.2.8",
"@storybook/addon-interactions": "8.2.8",
"@storybook/addon-links": "8.2.8",
"@storybook/addon-mdx-gfm": "8.2.8",
"@storybook/addon-storysource": "8.2.8",
"@storybook/blocks": "8.2.8",
"@storybook/components": "8.2.8",
"@storybook/core-events": "8.2.8",
"@storybook/manager-api": "8.2.8",
"@storybook/preview-api": "8.2.8",
"@storybook/react": "8.2.8",
"@storybook/react-vite": "8.2.8",
"@storybook/test": "8.2.8",
"@storybook/theming": "8.2.8",
"@storybook/types": "8.2.8",
"@storybook/vue3": "8.2.8",
"@storybook/vue3-vite": "8.2.8",
"@storybook/addon-actions": "8.3.6",
"@storybook/addon-essentials": "8.3.6",
"@storybook/addon-interactions": "8.3.6",
"@storybook/addon-links": "8.3.6",
"@storybook/addon-mdx-gfm": "8.3.6",
"@storybook/addon-storysource": "8.3.6",
"@storybook/blocks": "8.3.6",
"@storybook/components": "8.3.6",
"@storybook/core-events": "8.3.6",
"@storybook/manager-api": "8.3.6",
"@storybook/preview-api": "8.3.6",
"@storybook/react": "8.3.6",
"@storybook/react-vite": "8.3.6",
"@storybook/test": "8.3.6",
"@storybook/theming": "8.3.6",
"@storybook/types": "8.3.6",
"@storybook/vue3": "8.3.6",
"@storybook/vue3-vite": "8.3.6",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "^1.6.4",
"@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5",
"@types/estree": "1.0.6",
"@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9",
"@types/node": "22.2.0",
"@types/node": "22.7.8",
"@types/punycode": "2.1.4",
"@types/sanitize-html": "2.11.0",
"@types/sanitize-html": "2.13.0",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.12",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"@vitest/coverage-v8": "0.34.6",
"@vue/runtime-core": "3.4.15",
"acorn": "8.12.1",
"@vitest/coverage-v8": "2.1.3",
"@vue/runtime-core": "3.5.12",
"acorn": "8.13.0",
"cross-env": "7.0.3",
"cypress": "13.13.2",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-vue": "9.27.0",
"cypress": "13.15.0",
"eslint": "8.57.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.29.1",
"fast-glob": "3.3.2",
"happy-dom": "14.12.3",
"happy-dom": "15.7.4",
"intersection-observer": "0.12.2",
"micromatch": "4.0.7",
"msw": "2.3.5",
"micromatch": "4.0.8",
"msw": "2.5.0",
"msw-storybook-addon": "2.0.3",
"nodemon": "3.1.4",
"nodemon": "3.1.7",
"prettier": "3.3.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"start-server-and-test": "2.0.5",
"storybook": "8.2.8",
"start-server-and-test": "2.0.8",
"storybook": "8.3.6",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.6",
"vitest-fetch-mock": "0.2.2",
"vue-component-type-helpers": "2.0.29",
"vitest": "2.1.3",
"vitest-fetch-mock": "0.3.0",
"vue-component-type-helpers": "2.1.6",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.0.29"
"vue-tsc": "2.1.6"
}
}

View File

@ -11,7 +11,6 @@ import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js';
import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -97,7 +96,7 @@ export async function mainBoot() {
}).render();
}
}
}
}
} catch (error) {
// console.error(error);
console.error('Failed to initialise the seasonal screen effect canvas context:', error);
@ -237,10 +236,10 @@ export async function mainBoot() {
}
}
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
}
// const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
// if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
// popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
// }
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト

View File

@ -46,6 +46,7 @@ export const Default = {
imageUrl: null,
display: 'dialog',
needConfirmationToRead: false,
needEnrollmentTutorialToRead: false,
silence: false,
forYou: true,
},

View File

@ -19,13 +19,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</div>
<MkButton :class="$style.gotIt" primary full :disabled="gotItDisabled" @click="gotIt">{{ i18n.ts.gotIt }}<span v-if="secVisible"> ({{ sec }})</span></MkButton>
<MkButton :class="$style.gotIt" primary gradate full :disabled="gotItDisabled" @click="gotIt">
{{ !announcement.needEnrollmentTutorialToRead ? i18n.ts.gotIt : i18n.ts._initialAccountSetting.startTutorial }}
<span v-if="secVisible"> ({{ sec }})</span>
</MkButton>
</div>
</MkModal>
</template>
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { defineAsyncComponent, onMounted, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@ -46,6 +49,18 @@ const secVisible = ref(true);
const sec = ref(props.announcement.closeDuration);
async function gotIt(): Promise<void> {
if (props.announcement.needEnrollmentTutorialToRead) {
modal.value?.close();
const tutorialCompleted = await (new Promise<boolean>(resolve => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
done: () => {
resolve(true);
},
}, 'closed');
}));
if (!tutorialCompleted) return;
}
if (props.announcement.needConfirmationToRead) {
const confirm = await os.confirm({
type: 'question',

View File

@ -100,8 +100,8 @@ async function requestRender() {
sitekey: props.sitekey,
theme: defaultStore.state.darkMode ? 'dark' : 'light',
callback: callback,
'expired-callback': callback,
'error-callback': callback,
'expired-callback': () => callback(undefined),
'error-callback': () => callback(undefined),
});
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
const { default: Widget } = await import('@mcaptcha/vanilla-glue');

View File

@ -174,6 +174,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
</button>
</div>
<MkButton v-if="reactionTabType" :class="$style.reactionMuteButton" @click="reactionMuteToggle(reactionTabTypeTrimLocal)">
<i :class="!mutedReactions.includes(reactionTabTypeTrimLocal) ? 'ti ti-mood-off' : 'ti ti-mood-happy'"/>
{{ !mutedReactions.includes(reactionTabTypeTrimLocal) ? i18n.ts.muteThisReaction : i18n.ts.unmuteThisReaction }}
</MkButton>
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
<template #default="{ items }">
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
@ -297,6 +301,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
const mutedReactions = ref<string[]>(defaultStore.state.mutedReactions);
const keymap = {
'r': () => reply(true),
@ -316,6 +321,7 @@ provide('react', (reaction: string) => {
const tab = ref(props.initialTab);
const reactionTabType = ref<string | null>(null);
const reactionTabTypeTrimLocal = computed(() => reactionTabType.value?.replace('@.', '') ?? null);
const renotesPagination = computed<Paging>(() => ({
endpoint: 'notes/renotes',
@ -497,6 +503,18 @@ async function translate(): Promise<void> {
translation.value = res;
}
async function reactionMuteToggle(reactionName: string | null) {
if (reactionName == null) return;
if (!mutedReactions.value.includes(reactionName)) {
mutedReactions.value.push(reactionName);
defaultStore.set('mutedReactions', mutedReactions.value);
} else {
mutedReactions.value = mutedReactions.value.filter(x => x !== reactionName);
defaultStore.set('mutedReactions', mutedReactions.value);
}
}
function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
pleaseLogin();
@ -782,6 +800,10 @@ function loadConversation() {
margin-bottom: 8px;
}
.reactionMuteButton {
margin-bottom: 8px;
}
.reactionTab {
padding: 4px 6px;
border: solid 1px var(--divider);

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkPullToRefresh :refresher="() => reload()">
<MkPagination ref="pagingComponent" :pagination="pagination">
<MkPagination ref="pagingComponent" :pagination="pagination" :filter="filterMutedNotification">
<template #empty>
<div class="_fullinfo">
<img :src="infoImageUrl" class="_ghost"/>
@ -34,6 +34,7 @@ import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js';
import { defaultStore } from '@/store.js';
import { filterMutedNotification } from '@/scripts/filter-muted-notification.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import * as Misskey from 'misskey-js';
@ -63,7 +64,7 @@ function onNotification(notification) {
useStream().send('readNotification');
}
if (!isMuted) {
if (!isMuted && filterMutedNotification(notification)) {
pagingComponent.value?.prepend(notification);
}
}

View File

@ -84,6 +84,7 @@ const props = withDefaults(defineProps<{
pagination: Paging;
disableAutoLoad?: boolean;
displayLimit?: number;
filter?: (item: MisskeyEntity) => boolean;
}>(), {
displayLimit: 20,
});
@ -178,6 +179,8 @@ async function init(): Promise<void> {
limit: props.pagination.limit ?? 10,
allowPartial: true,
}).then(res => {
res = res.filter(item => !props.filter || props.filter(item));
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (i === 3) item._shouldInsertAd_ = true;
@ -219,6 +222,8 @@ const fetchMore = async (): Promise<void> => {
untilId: items.value[items.value.length - 1].id,
}),
}).then(res => {
res = res.filter(item => !props.filter || props.filter(item));
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (i === 10) item._shouldInsertAd_ = true;
@ -283,6 +288,8 @@ const fetchMoreAhead = async (): Promise<void> => {
sinceId: items.value[0].id,
}),
}).then(res => {
res = res.filter(item => !props.filter || props.filter(item));
if (res.length === 0) {
items.value = res.concat(items.value);
more.value = false;

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/>
<MkCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl" :fallbackToImage="true"/>
<MkEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/>
</template>

View File

@ -22,6 +22,7 @@ import * as Misskey from 'misskey-js';
import { inject, watch, ref } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@ -45,6 +46,13 @@ if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.m
reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
}
function shouldDisplayReaction([reaction]: [string, number]): boolean {
if (!$i) return true; //
if (reaction === props.note.myReaction) return true; //
if (!defaultStore.state.mutedReactions.includes(reaction.replace('@.', ''))) return true; // @. suffix
return false;
}
function onMockToggleReaction(emoji: string, count: number) {
if (!mock) return;
@ -80,7 +88,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
}
reactions.value = newReactions;
reactions.value = newReactions.filter(shouldDisplayReaction);
}, { immediate: true, deep: true });
</script>
@ -104,6 +112,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
flex-wrap: wrap;
align-items: center;
margin: 4px -2px 0 -2px;
max-width: 100%;
&:empty {
display: none;

View File

@ -39,6 +39,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(ev: 'done'): void;
(ev: 'closed'): void;
}>();
@ -49,6 +50,10 @@ const page = ref(props.initialPage ?? 0);
function handlePageChange(to: number) {
page.value = to;
if (to === 9) {
claimAchievement('tutorialCompleted');
emit('done');
}
}
async function close(skip?: boolean) {

View File

@ -42,6 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._announcement.needConfirmationToRead }}
<template #caption>{{ i18n.ts._announcement.needConfirmationToReadDescription }}</template>
</MkSwitch>
<MkSwitch v-model="needEnrollmentTutorialToRead">
{{ i18n.ts._announcement.needEnrollmentTutorialToRead }}
<template #caption>{{ i18n.ts._announcement.needEnrollmentTutorialToReadDescription }}</template>
</MkSwitch>
<MkInput v-model="closeDuration" type="number">
<template #label>{{ i18n.ts.dialogCloseDuration }}</template>
<template #suffix>{{ i18n.ts._time.second }}</template>
@ -90,6 +94,7 @@ const text = ref<string>(props.announcement ? props.announcement.text : '');
const icon = ref<string>(props.announcement ? props.announcement.icon : 'info');
const display = ref<string>(props.announcement ? props.announcement.display : 'dialog');
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
const needEnrollmentTutorialToRead = ref(props.announcement ? props.announcement.needEnrollmentTutorialToRead : false);
const closeDuration = ref<number>(props.announcement ? props.announcement.closeDuration : 0);
const displayOrder = ref<number>(props.announcement ? props.announcement.displayOrder : 0);
const silence = ref<boolean>(props.announcement ? props.announcement.silence : false);
@ -115,6 +120,7 @@ async function done(): Promise<void> {
imageUrl: null,
display: display.value,
needConfirmationToRead: needConfirmationToRead.value,
needEnrollmentTutorialToRead: needEnrollmentTutorialToRead.value,
closeDuration: closeDuration.value,
displayOrder: displayOrder.value,
silence: silence.value,

View File

@ -4,7 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<span v-if="errored">:{{ customEmojiName }}:</span>
<span v-if="errored && !fallbackToImage">:{{ customEmojiName }}:</span>
<img v-else-if="errored" src="/client-assets/dummy.png" :alt="alt" :title="alt" decoding="async" :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"/>
<img
v-else
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
@ -19,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, inject, ref } from 'vue';
import { computed, inject, ref, watch } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js';
@ -39,6 +40,7 @@ const props = defineProps<{
useOriginalSize?: boolean;
menu?: boolean;
menuReaction?: boolean;
fallbackToImage?: boolean;
}>();
const react = inject<((name: string) => void) | null>('react', null);
@ -73,6 +75,10 @@ const url = computed(() => {
: proxied;
});
watch(url, (newValue) => {
errored.value = (newValue === undefined);
});
const alt = computed(() => `:${customEmojiName.value}:`);
const errored = ref(url.value == null);

View File

@ -63,7 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.moderation }}</template>
<div class="_gaps">
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
<MkButton v-if="user?.host == null" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<div v-if="user?.host == null" class="_buttons">
<MkButton @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<MkButton danger @click="regenerateLoginToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
</div>
<MkButton inline danger @click="updateUserName"><i class="ti ti-user-edit"></i> {{ i18n.ts.changeUserName }}</MkButton>
<MkButton inline danger @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
<MkButton inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
<MkFolder v-if="user?.mutualLinkSections && user?.mutualLinkSections.reduce((acc, section) => acc + section.mutualLinks.length, 0) > 0">
@ -339,6 +343,18 @@ async function resetPassword() {
}
}
async function regenerateLoginToken() {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.regenerateLoginTokenConfirm,
});
if (confirm.canceled) return;
await os.apiWithDialog('admin/regenerate-user-token', {
userId: user.value.id,
}).then(refreshUser);
}
async function toggleSuspend(v) {
const confirm = await os.confirm({
type: 'warning',
@ -353,6 +369,20 @@ async function toggleSuspend(v) {
}
}
async function updateUserName() {
const { canceled, result: name } = await os.inputText({
type: 'text',
title: i18n.ts.enterUsername,
default: '',
});
if (canceled) return;
await os.apiWithDialog('admin/update-user-name', {
userId: user.value.id,
name: name || undefined,
}).then(refreshUser);
}
async function unsetUserAvatar() {
const confirm = await os.confirm({
type: 'warning',

View File

@ -66,6 +66,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
{{ i18n.ts._announcement.needConfirmationToRead }}
</MkSwitch>
<MkSwitch v-model="announcement.needEnrollmentTutorialToRead" :helpText="i18n.ts._announcement.needEnrollmentTutorialToReadDescription">
{{ i18n.ts._announcement.needEnrollmentTutorialToRead }}
</MkSwitch>
<MkInput v-model="announcement.closeDuration" type="number">
<template #label>{{ i18n.ts.dialogCloseDuration }}</template>
<template #suffix>{{ i18n.ts._time.second }}</template>

View File

@ -9,14 +9,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<b
:class="{
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword', 'regenerateUserToken', 'updateUserName', 'unsetUserAvatar', 'unsetUserBanner', 'unsetUserMutualLink'].includes(log.type),
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
}"
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'updateUserName'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unsetUserAvatar'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unsetUserBanner'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unsetUserMutualLink'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'regenerateUserToken'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-equal-not"></i> {{ log.info.roleName }}</span>
<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
@ -59,6 +64,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div>
</template>
<template v-else-if="log.type === 'updateUserName'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div>
</template>
<template v-else-if="log.type === 'updateUserNote'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div :class="$style.diff">

View File

@ -34,7 +34,10 @@
</MkA>
</div>
<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<MkButton primary gradate @click="read(announcement)">
<i :class="!announcement.needEnrollmentTutorialToRead ? 'ti ti-check' : 'ti ti-presentation'"/>
{{ !announcement.needEnrollmentTutorialToRead ? i18n.ts.gotIt : i18n.ts._initialAccountSetting.startTutorial }}
</MkButton>
</div>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
@ -45,7 +48,7 @@
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@ -75,6 +78,17 @@ function fetch() {
}
async function read(announcement): Promise<void> {
if (announcement.needEnrollmentTutorialToRead) {
const tutorialCompleted = await (new Promise<boolean>(resolve => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
done: () => {
resolve(true);
},
}, 'closed');
}));
if (!tutorialCompleted) return;
}
if (announcement.needConfirmationToRead) {
const confirm = await os.confirm({
type: 'question',

View File

@ -36,7 +36,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</div>
<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<MkButton primary gradate @click="read(announcement)">
<i :class="!announcement.needEnrollmentTutorialToRead ? 'ti ti-check' : 'ti ti-presentation'"/>
{{ !announcement.needEnrollmentTutorialToRead ? i18n.ts.gotIt : i18n.ts._initialAccountSetting.startTutorial }}
</MkButton>
</div>
</section>
</MkPagination>
@ -47,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, defineAsyncComponent } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@ -81,6 +84,17 @@ const paginationEl = ref<InstanceType<typeof MkPagination>>();
const tab = ref('current');
async function read(announcement): Promise<void> {
if (announcement.needEnrollmentTutorialToRead) {
const tutorialCompleted = await (new Promise<boolean>(resolve => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
done: () => {
resolve(true);
},
}, 'closed');
}));
if (!tutorialCompleted) return;
}
if (announcement.needConfirmationToRead) {
const confirm = await os.confirm({
type: 'question',

View File

@ -12,6 +12,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.mutedReactions }}</template>
<div class="_gaps">
<div v-panel style="border-radius: var(--radius); padding: var(--margin);">
<button v-for="emoji in mutedReactions" class="_button" :class="$style.emojisItem" @click="removeReaction(emoji, $event)">
<MkCustomEmoji v-if="emoji && emoji[0] === ':'" :name="emoji"/>
<MkEmoji v-else :emoji="emoji ? emoji : 'null'"/>
</button>
<button class="_button" @click="chooseReaction">
<i class="ti ti-plus"></i>
</button>
</div>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-planet-off"></i></template>
<template #label>{{ i18n.ts.instanceMute }}</template>
@ -119,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, watch, Ref } from 'vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
import MkPagination from '@/components/MkPagination.vue';
@ -132,6 +149,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { infoImageUrl } from '@/instance.js';
import { signinRequired } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import { defaultStore } from '@/store.js';
const $i = signinRequired();
@ -154,6 +174,37 @@ const expandedRenoteMuteItems = ref([]);
const expandedMuteItems = ref([]);
const expandedBlockItems = ref([]);
const mutedReactions = ref<string[]>(defaultStore.state.mutedReactions);
watch(mutedReactions, () => {
defaultStore.set('mutedReactions', mutedReactions.value);
}, {
deep: true,
});
const chooseReaction = (ev: MouseEvent) => pickEmoji(mutedReactions, ev);
const removeReaction = (reaction: string, ev: MouseEvent) => remove(mutedReactions, reaction, ev);
function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.remove,
action: () => {
itemsRef.value = itemsRef.value.filter(x => x !== reaction);
},
}], ev.currentTarget ?? ev.target);
}
async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
os.pickEmoji(ev.currentTarget ?? ev.target, {
showPinned: false,
}).then(it => {
const emoji = it;
if (!itemsRef.value.includes(emoji)) {
itemsRef.value.push(emoji);
}
});
}
async function unrenoteMute(user, ev) {
os.popupMenu([{
text: i18n.ts.renoteUnmute,
@ -263,4 +314,9 @@ definePageMetadata(() => ({
transform: rotateX(180deg);
}
}
.emojisItem{
display: inline-block;
padding: 8px;
}
</style>

View File

@ -120,6 +120,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'sound_notification',
'sound_antenna',
'sound_channel',
'mutedReactions',
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme',

View File

@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue';
import { ref, nextTick, onMounted } from 'vue';
import MkSignin from '@/components/MkSignin.vue';
import MkButton from '@/components/MkButton.vue';
import { $i, login } from '@/account.js';
@ -47,6 +47,7 @@ if (transactionIdMeta) {
}
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:service-name"]')?.content;
const kind = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:kind"]')?.content;
const prompt = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:prompt"]')?.content;
const loading = ref(false);
const postBindingForm = ref<HTMLFormElement | null>(null);
@ -90,6 +91,12 @@ async function authorize(): Promise<void> {
}
}
onMounted(() => {
if ($i && prompt === 'none') {
onAccept();
}
});
definePageMetadata(() => ({
title: 'Single Sign-On',
icon: 'ti ti-apps',

View File

@ -0,0 +1,16 @@
import * as Misskey from 'misskey-js';
import { defaultStore } from '@/store.js';
export function filterMutedNotification(notification: Misskey.entities.Notification): boolean {
switch (notification.type) {
case 'reaction':
if (defaultStore.state.mutedReactions.includes(notification.reaction.replace('@.', ''))) return false;
break;
case 'reaction:grouped':
notification.reactions = notification.reactions.filter(reaction => !defaultStore.state.mutedReactions.includes(reaction.reaction.replace('@.', '')));
if (notification.reactions.length === 0) return false;
break;
}
return true;
}

View File

@ -543,6 +543,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
},
mutedReactions: {
where: 'account',
default: [] as string[],
},
}));
// TODO: 他のタブと永続化されたstateを同期

View File

@ -51,6 +51,7 @@ import { swInject } from './sw-inject.js';
import XNotification from './notification.vue';
import { popups } from '@/os.js';
import { pendingApiRequestsCount } from '@/scripts/misskey-api.js';
import { filterMutedNotification } from '@/scripts/filter-muted-notification.js';
import { uploads } from '@/scripts/upload.js';
import * as sound from '@/scripts/sound.js';
import { $i } from '@/account.js';
@ -73,6 +74,8 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
useStream().send('readNotification');
}
if (!filterMutedNotification(notification)) return;
notifications.value.unshift(notification);
window.setTimeout(() => {
if (notifications.value.length > 3) notifications.value.pop();

View File

@ -53,6 +53,7 @@ const devConfig = {
'/cli': httpUrl,
'/inbox': httpUrl,
'/emoji/': httpUrl,
'/queue': httpUrl,
'/notes': {
target: httpUrl,
headers: {

View File

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

View File

@ -26,19 +26,19 @@
"devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@types/matter-js": "0.19.7",
"@types/node": "22.2.0",
"@types/node": "22.7.8",
"@types/seedrandom": "3.0.8",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.0",
"nodemon": "3.1.4",
"typescript": "5.5.4"
"eslint": "8.57.1",
"nodemon": "3.1.7",
"typescript": "5.6.3"
},
"files": [
"built"
],
"dependencies": {
"esbuild": "0.23.0",
"esbuild": "0.24.0",
"eventemitter3": "5.0.1",
"glob": "11.0.0",
"matter-js": "0.20.0",

View File

@ -268,6 +268,9 @@ type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBo
// @public (undocumented)
type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminRegenerateUserTokenRequest = operations['admin___regenerate-user-token']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
@ -391,6 +394,9 @@ type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBo
// @public (undocumented)
type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminUpdateUserNameRequest = operations['admin___update-user-name']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminUpdateUserNoteRequest = operations['admin___update-user-note']['requestBody']['content']['application/json'];
@ -1293,6 +1299,7 @@ declare namespace entities {
AdminRelaysRemoveRequest,
AdminResetPasswordRequest,
AdminResetPasswordResponse,
AdminRegenerateUserTokenRequest,
AdminResolveAbuseUserReportRequest,
AdminSendEmailRequest,
AdminServerInfoResponse,
@ -1307,6 +1314,7 @@ declare namespace entities {
AdminSuspendUserRequest,
AdminUnsuspendUserRequest,
AdminUpdateMetaRequest,
AdminUpdateUserNameRequest,
AdminUpdateUserNoteRequest,
AdminRolesCreateRequest,
AdminRolesCreateResponse,
@ -2398,6 +2406,9 @@ type ModerationLog = {
} | {
type: 'unsuspend';
info: ModerationLogPayloads['unsuspend'];
} | {
type: 'updateUserName';
info: ModerationLogPayloads['updateUserName'];
} | {
type: 'updateUserNote';
info: ModerationLogPayloads['updateUserNote'];
@ -2458,6 +2469,9 @@ type ModerationLog = {
} | {
type: 'resetPassword';
info: ModerationLogPayloads['resetPassword'];
} | {
type: 'regenerateUserToken';
info: ModerationLogPayloads['regenerateUserToken'];
} | {
type: 'suspendRemoteInstance';
info: ModerationLogPayloads['suspendRemoteInstance'];
@ -2515,7 +2529,7 @@ type ModerationLog = {
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createSSOServiceProvider", "updateSSOServiceProvider", "deleteSSOServiceProvider", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "unsetUserMutualBanner"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserName", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "regenerateUserToken", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createSSOServiceProvider", "updateSSOServiceProvider", "deleteSSOServiceProvider", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "unsetUserMutualBanner"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@ -2767,7 +2781,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
function parse(acct: string): Acct;
// @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", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "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-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-link", "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:indie-auth", "read:admin:indie-auth", "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:sso", "read:admin:sso", "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", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "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:indie-auth", "read:admin:indie-auth", "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:sso", "read:admin:sso", "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

@ -9,15 +9,15 @@
"devDependencies": {
"@misskey-dev/eslint-plugin": "^1.0.0",
"@readme/openapi-parser": "2.6.0",
"@types/node": "22.2.0",
"@types/node": "22.7.8",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.0",
"eslint": "8.57.1",
"openapi-types": "12.1.3",
"openapi-typescript": "6.7.6",
"ts-case-convert": "2.0.7",
"tsx": "4.17.0",
"typescript": "5.5.4"
"ts-case-convert": "2.1.0",
"tsx": "4.19.1",
"typescript": "5.6.3"
},
"files": [
"built"

View File

@ -58,7 +58,7 @@ export class APIClient {
this.fetch(`${this.origin}/api/${endpoint}`, {
method: 'POST',
body: JSON.stringify({
...params,
...(params ?? {}),
i: credential !== undefined ? credential : this.credential,
}),
headers: {

View File

@ -259,7 +259,7 @@ declare module '../api.js' {
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar*
* **Credential required**: *Yes* / **Permission**: *write:admin:user-avatar*
*/
request<E extends 'admin/unset-user-avatar', P extends Endpoints[E]['req']>(
endpoint: E,
@ -270,7 +270,7 @@ declare module '../api.js' {
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner*
* **Credential required**: *Yes* / **Permission**: *write:admin:user-banner*
*/
request<E extends 'admin/unset-user-banner', P extends Endpoints[E]['req']>(
endpoint: E,
@ -293,7 +293,7 @@ declare module '../api.js' {
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-link*
* **Credential required**: *Yes* / **Permission**: *write:admin:user-mutual-link*
*/
request<E extends 'admin/unset-user-mutual-link', P extends Endpoints[E]['req']>(
endpoint: E,
@ -767,6 +767,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:regenerate-user-token*
*/
request<E extends 'admin/regenerate-user-token', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
@ -877,6 +888,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:user-name*
*/
request<E extends 'admin/update-user-name', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View File

@ -89,6 +89,7 @@ import type {
AdminRelaysRemoveRequest,
AdminResetPasswordRequest,
AdminResetPasswordResponse,
AdminRegenerateUserTokenRequest,
AdminResolveAbuseUserReportRequest,
AdminSendEmailRequest,
AdminServerInfoResponse,
@ -103,6 +104,7 @@ import type {
AdminSuspendUserRequest,
AdminUnsuspendUserRequest,
AdminUpdateMetaRequest,
AdminUpdateUserNameRequest,
AdminUpdateUserNoteRequest,
AdminRolesCreateRequest,
AdminRolesCreateResponse,
@ -655,6 +657,7 @@ export type Endpoints = {
'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse };
'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse };
'admin/regenerate-user-token': { req: AdminRegenerateUserTokenRequest; res: EmptyResponse };
'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse };
'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
@ -665,6 +668,7 @@ export type Endpoints = {
'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse };
'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse };
'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse };
'admin/update-user-name': { req: AdminUpdateUserNameRequest; res: EmptyResponse };
'admin/update-user-note': { req: AdminUpdateUserNoteRequest; res: EmptyResponse };
'admin/roles/create': { req: AdminRolesCreateRequest; res: AdminRolesCreateResponse };
'admin/roles/delete': { req: AdminRolesDeleteRequest; res: EmptyResponse };

View File

@ -92,6 +92,7 @@ export type AdminRelaysListResponse = operations['admin___relays___list']['respo
export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['requestBody']['content']['application/json'];
export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json'];
export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json'];
export type AdminRegenerateUserTokenRequest = operations['admin___regenerate-user-token']['requestBody']['content']['application/json'];
export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json'];
export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
@ -106,6 +107,7 @@ export type AdminShowUsersResponse = operations['admin___show-users']['responses
export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json'];
export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
export type AdminUpdateUserNameRequest = operations['admin___update-user-name']['requestBody']['content']['application/json'];
export type AdminUpdateUserNoteRequest = operations['admin___update-user-note']['requestBody']['content']['application/json'];
export type AdminRolesCreateRequest = operations['admin___roles___create']['requestBody']['content']['application/json'];
export type AdminRolesCreateResponse = operations['admin___roles___create']['responses']['200']['content']['application/json'];

File diff suppressed because it is too large Load Diff

View File

@ -53,6 +53,7 @@ export const permissions = [
'read:admin:user-ips',
'read:admin:meta',
'write:admin:reset-password',
'write:admin:regenerate-user-token',
'write:admin:resolve-abuse-user-report',
'write:admin:send-email',
'read:admin:server-info',
@ -61,12 +62,13 @@ export const permissions = [
'read:admin:show-user',
'read:admin:show-users',
'write:admin:suspend-user',
'write:admin:unset-user-avatar',
'write:admin:unset-user-banner',
'write:admin:unset-user-mutual-link',
'write:admin:unsuspend-user',
'write:admin:meta',
'write:admin:user-name',
'write:admin:user-note',
'write:admin:user-avatar',
'write:admin:user-banner',
'write:admin:user-mutual-link',
'write:admin:roles',
'read:admin:roles',
'write:admin:relays',
@ -105,6 +107,7 @@ export const moderationLogTypes = [
'updateServerSettings',
'suspend',
'unsuspend',
'updateUserName',
'updateUserNote',
'addCustomEmoji',
'updateCustomEmoji',
@ -125,6 +128,7 @@ export const moderationLogTypes = [
'deleteGlobalAnnouncement',
'deleteUserAnnouncement',
'resetPassword',
'regenerateUserToken',
'suspendRemoteInstance',
'unsuspendRemoteInstance',
'updateRemoteInstanceNote',
@ -164,6 +168,13 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
updateUserName: {
userId: string;
userUsername: string;
userHost: string | null;
before: string | null;
after: string | null;
}
updateUserNote: {
userId: string;
userUsername: string;
@ -267,6 +278,11 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
regenerateUserToken: {
userId: string;
userUsername: string;
userHost: string | null;
};
suspendRemoteInstance: {
id: string;
host: string;

View File

@ -29,6 +29,9 @@ export type ModerationLog = {
} | {
type: 'unsuspend';
info: ModerationLogPayloads['unsuspend'];
} | {
type: 'updateUserName';
info: ModerationLogPayloads['updateUserName'];
} | {
type: 'updateUserNote';
info: ModerationLogPayloads['updateUserNote'];
@ -89,6 +92,9 @@ export type ModerationLog = {
} | {
type: 'resetPassword';
info: ModerationLogPayloads['resetPassword'];
} | {
type: 'regenerateUserToken';
info: ModerationLogPayloads['regenerateUserToken'];
} | {
type: 'suspendRemoteInstance';
info: ModerationLogPayloads['suspendRemoteInstance'];

View File

@ -25,16 +25,16 @@
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@types/node": "22.2.0",
"@types/node": "22.7.8",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.0",
"nodemon": "3.1.4",
"typescript": "5.5.4"
"eslint": "8.57.1",
"nodemon": "3.1.7",
"typescript": "5.6.3"
},
"dependencies": {
"crc-32": "1.2.2",
"esbuild": "0.23.0",
"esbuild": "0.24.0",
"glob": "11.0.0"
},
"files": [

View File

@ -9,18 +9,18 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"esbuild": "0.23.0",
"esbuild": "0.24.0",
"idb-keyval": "6.2.1",
"misskey-js": "workspace:*"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@types/serviceworker": "0.0.92",
"@types/serviceworker": "0.0.101",
"@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1",
"nodemon": "3.1.4",
"typescript": "5.5.4"
"eslint": "8.57.1",
"eslint-plugin-import": "2.31.0",
"nodemon": "3.1.7",
"typescript": "5.6.3"
},
"type": "module"
}

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.3.3",
"typescript": "5.6.3",
"unified": "11.0.4",
"vite": "5.0.12",
"vite-node": "1.1.3",