diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 940b095fe2..2921746295 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -95,6 +95,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForTimelines: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── diff --git a/.config/example.yml b/.config/example.yml index 03864a3299..0e4f2f5a15 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -105,6 +105,16 @@ redis: # # You can specify more ioredis options... # #username: example-username +#redisForTimelines: +# host: localhost +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 +# # You can specify more ioredis options... +# #username: example-username + # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 861b0008a0..a78d91900b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,9 @@ "service": "app", "workspaceFolder": "/workspace", "features": { - "ghcr.io/devcontainers-contrib/features/pnpm:2": {}, + "ghcr.io/devcontainers-contrib/features/pnpm:2": { + "version": "8.8.0" + }, "ghcr.io/devcontainers/features/node:1": { "version": "20.5.1" } diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 5dcd41599a..3d57d1245d 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -95,6 +95,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForTimelines: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── diff --git a/CHANGELOG.md b/CHANGELOG.md index ee2f11b156..f9ccaa9f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,37 @@ --> +## 2023.10.0 +### NOTE +- 2023.9.2で導入されたノート編集機能はクオリティの高い実装が困難であることが判明したため撤回されました +- アップデート後、アップデートより前の時点にTLを遡ることはできません + - アップデート後であっても、今後のアップデートで2023.10.0以前のTLに遡れるようになる可能性はあります + +### Changes +- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました +- API: notes/global-timeline は現在常に `[]` を返します +- API: notes/featured でページネーションは他APIと同様 untilId を使って行うようになりました + +### General +- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました +- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました +- Feat: ユーザーごとのハイライト +- Enhance: ソフトワードミュートとハードワードミュートは統合されました +- Enhance: モデレーションログ機能の強化 +- Enhance: ローカリゼーションの更新 +- Enhance: 依存関係の更新 +- Fix: ダイレクト投稿をリノートできてしまう問題を修正 +- Fix: ユーザーリストTLにチャンネル投稿が含まれる問題を修正 + +### Client +- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に +- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正 + +### Server +- Enhance: タイムライン取得時のパフォーマンスを大幅に向上 +- Enhance: ハイライト取得時のパフォーマンスを大幅に向上 +- Enhance: 不要なPostgreSQLのインデックスを削除しパフォーマンスを向上 + ## 2023.9.3 ### General - Enhance: ノートの翻訳機能の利用可否をロールで設定可能に diff --git a/chart/files/default.yml b/chart/files/default.yml index 90b574b99f..87b2f677eb 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -116,6 +116,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForTimelines: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 55b7cbb88c..e835c4aeee 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1184,11 +1184,6 @@ _wordMute: muteWords: "الكلمات المحظورة" muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"." muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية" - softDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني." - hardDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني.بالإضافة إلى أن هذه الملاحظات ستبقى مخفية حتى وإن تغيرت الشروط." - soft: "لينة" - hard: "قاسية" - mutedNotes: "الملاحظات المكتومة" _instanceMute: instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب." instanceMuteDescription2: "مدخلة لكل سطر" @@ -1248,8 +1243,6 @@ _sfx: note: "الملاحظات" noteMy: "ملاحظتي" notification: "الإشعارات" - chat: "المحادثة" - chatBg: "المحادثة (الخلفية)" antenna: "الهوائيات" channel: "إشعارات القنات" _ago: diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 64b32d176b..6d6771ac0f 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -932,11 +932,6 @@ _wordMute: muteWords: "নিঃশব্দ করা শব্দগুলি" muteWordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।" muteWordsDescription2: "রেগুলার এক্সপ্রেশন ব্যবহার করতে স্ল্যাশ দিয়ে কীওয়ার্ডকে ঘিরে রাখুন।" - softDescription: "টাইমলাইন থেকে নির্দিষ্ট শর্তানুযায়ী নোট লুকিয়ে রাখে।" - hardDescription: "নির্দিষ্ট শর্তানুযায়ী নোটগুলিকে টাইমলাইন থেকে বাদ দেয়। আপনি শর্ত পরিবর্তন করলেও যে নোটগুলি যোগ করা হয়নি সেগুলি বাদ দেওয়া হবে।" - soft: "নমনীয়" - hard: "কঠোর" - mutedNotes: "মিউট করা নোটগুলি" _instanceMute: instanceMuteDescription: "কনফিগার করা ইন্সট্যান্সের সব নোট এবং রিনোট মিউট করুন, মিউট করা ইন্সট্যান্সের ব্যবহারকারীদের উত্তর সহ।" instanceMuteDescription2: "প্রতিটিকে আলাদা লাইনে লিখুন" @@ -1020,8 +1015,6 @@ _sfx: note: "নোটগুলি" noteMy: "নোট (আপনার)" notification: "বিজ্ঞপ্তি" - chat: "চ্যাট" - chatBg: "চ্যাট (ব্যাকগ্রাউন্ড)" antenna: "অ্যান্টেনাগুলি" channel: "চ্যানেলের বিজ্ঞপ্তি" _ago: diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index d1fd73b666..915388006f 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -398,7 +398,6 @@ _theme: _sfx: note: "Notes" notification: "Notificacions" - chat: "Xat" antenna: "Antenes" _2fa: renewTOTPCancel: "No, gràcies" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 762f033b13..db9105a13a 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1559,11 +1559,6 @@ _wordMute: muteWords: "Ztlumená slova" muteWordsDescription: "Podmínku AND oddělujte mezerami, podmínku OR oddělujte řádkovými zlomy." muteWordsDescription2: "Chcete-li použít regulární výrazy, obklopte klíčová slova lomítky." - softDescription: "Skrýt poznámky, které splňují nastavené podmínky, z časové osy." - hardDescription: "Zabrání přidání poznámek splňujících nastavené podmínky na časovou osu. Kromě toho nebudou tyto poznámky přidány na časovou osu, ani když se podmínky změní." - soft: "Měkký" - hard: "Tvrdý" - mutedNotes: "Ztlumené poznámky" _instanceMute: instanceMuteDescription: "Tímhle se ztlumí všechny poznámky/poznámky z uvedených instancí, včetně poznámek uživatelů, kteří odpovídají uživateli ze ztlumené instance." instanceMuteDescription2: "Oddělte novými řádky" @@ -1647,8 +1642,6 @@ _sfx: note: "Poznámky" noteMy: "Moje poznámka" notification: "Oznámení" - chat: "Zprávy" - chatBg: "Chat (Pozadí)" antenna: "Antény" channel: "Oznámení kanálu" _ago: diff --git a/locales/de-DE.yml b/locales/de-DE.yml index e7d435a2e6..e8a1da4821 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1126,6 +1126,9 @@ edited: "Bearbeitet" notificationRecieveConfig: "Benachrichtigungseinstellungen" mutualFollow: "Gegenseitig gefolgt" fileAttachedOnly: "Nur Notizen mit Dateien" +showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen" +hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen" +externalServices: "Externe Dienste" _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." @@ -1456,7 +1459,6 @@ _role: gtlAvailable: "Kann auf die globale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen" canPublicNote: "Kann öffentliche Notizen erstellen" - canEditNote: "Notizbearbeitung" canInvite: "Erstellung von Einladungscodes für diese Instanz" inviteLimit: "Maximalanzahl an Einladungen" inviteLimitCycle: "Zyklus des Einladungslimits" @@ -1476,6 +1478,7 @@ _role: descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver." canHideAds: "Kann Werbung ausblenden" canSearchNotes: "Nutzung der Notizsuchfunktion" + canUseTranslator: "Verwendung des Übersetzers" _condition: isLocal: "Lokaler Benutzer" isRemote: "Benutzer fremder Instanz" @@ -1609,11 +1612,6 @@ _wordMute: muteWords: "Stummgeschaltete Wörter" muteWordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen." muteWordsDescription2: "Umgib Schlüsselworter mit Schrägstrichen, um Reguläre Ausdrücke zu verwenden." - softDescription: "Notizen, die die angegebenen Konditionen erfüllen, in der Chronik ausblenden." - hardDescription: "Verhindern, dass Notizen, die die angegebenen Konditionen erfüllen, der Chronik hinzugefügt werden. Zudem werden diese Notizen auch nicht der Chronik hinzugefügt, falls die Konditionen geändert werden." - soft: "Leicht" - hard: "Schwer" - mutedNotes: "Stummgeschaltete Notizen" _instanceMute: instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz." instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben" @@ -1697,8 +1695,6 @@ _sfx: note: "Notizen" noteMy: "Meine Notizen" notification: "Benachrichtigungen" - chat: "Chat" - chatBg: "Chat (Hintergrund)" antenna: "Antennen" channel: "Kanalbenachrichtigung" _ago: diff --git a/locales/el-GR.yml b/locales/el-GR.yml index e46efcec1f..9392fd12fe 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -303,8 +303,6 @@ _theme: _sfx: note: "Σημειώματα" notification: "Ειδοποιήσεις" - chat: "Συνομιλία" - chatBg: "Συνομιλία (Παρασκήνιο)" antenna: "Αντένες" channel: "Ειδοποιήσεις καναλιών" _ago: diff --git a/locales/en-US.yml b/locales/en-US.yml index 7b529516f1..395b4a08a2 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1133,6 +1133,9 @@ edited: "Edited" notificationRecieveConfig: "Notification Settings" mutualFollow: "Mutual follow" fileAttachedOnly: "Only notes with files" +showRepliesToOthersInTimeline: "Show replies to others in TL" +hideRepliesToOthersInTimeline: "Hide replies to others from TL" +externalServices: "External Services" _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." @@ -1463,7 +1466,6 @@ _role: gtlAvailable: "Can view the global timeline" ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" - canEditNote: "Note editing" canInvite: "Can create instance invite codes" inviteLimit: "Invite limit" inviteLimitCycle: "Invite limit cooldown" @@ -1483,6 +1485,7 @@ _role: descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " canHideAds: "Can hide ads" canSearchNotes: "Usage of note search" + canUseTranslator: "Translator usage" _condition: isLocal: "Local user" isRemote: "Remote user" @@ -1617,11 +1620,6 @@ _wordMute: muteWords: "Muted words" muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Surround keywords with slashes to use regular expressions." - softDescription: "Hide notes that fulfil the set conditions from the timeline." - hardDescription: "Prevents notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed." - soft: "Soft" - hard: "Hard" - mutedNotes: "Muted notes" _instanceMute: instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance." instanceMuteDescription2: "Separate with newlines" @@ -1705,8 +1703,6 @@ _sfx: note: "New note" noteMy: "Own note" notification: "Notifications" - chat: "Chat" - chatBg: "Chat (Background)" antenna: "Antennas" channel: "Channel notifications" _ago: diff --git a/locales/es-ES.yml b/locales/es-ES.yml index d663bd829c..f3e4bd7c75 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1603,11 +1603,6 @@ _wordMute: muteWords: "Palabras que silenciar" muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。" muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones regulares" - softDescription: "Ocultar en la linea de tiempo las notas que cumplen las condiciones" - hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las condiciones." - soft: "Suave" - hard: "Duro" - mutedNotes: "Notas silenciadas" _instanceMute: instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas, incluyendo respuestas a los usuarios de las mismas" instanceMuteDescription2: "Separar por líneas" @@ -1691,8 +1686,6 @@ _sfx: note: "Notas" noteMy: "Nota (a mí mismo)" notification: "Notificaciones" - chat: "Chat" - chatBg: "Chat (Fondo)" antenna: "Antena receptora" channel: "Notificaciones del canal" _ago: diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index db19b66880..cd88084ced 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1267,11 +1267,6 @@ _wordMute: muteWords: "Mots à filtrer" muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR." muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez les mots-clés entre barres obliques." - softDescription: "Masquez les notes de votre fil selon les paramètres que vous définissez." - hardDescription: "Empêchez votre fil de charger les notes selon les paramètres que vous définissez. Cette action est irréversible : si vous modifiez ces paramètres plus tard, les notes précédemment filtrées ne seront pas récupérées." - soft: "Doux" - hard: "Strict" - mutedNotes: "Notes filtrées" _instanceMute: instanceMuteDescription: "Met en sourdine toutes les notes et renotes de l'instance configurée, y compris les réponses aux utilisateurs de l'instance muette." instanceMuteDescription2: "Séparer avec de nouvelles lignes" @@ -1355,8 +1350,6 @@ _sfx: note: "Nouvelle note" noteMy: "Ma note" notification: "Notifications" - chat: "Discuter" - chatBg: "Discussion (arrière-plan)" antenna: "Réception de l’antenne" channel: "Notifications de canal" _ago: diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 0e067c5383..109dfb7603 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1564,11 +1564,6 @@ _wordMute: muteWords: "Kata yang dibisukan" muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR." muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler." - softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa." - hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah." - soft: "Lembut" - hard: "Keras" - mutedNotes: "Catatan yang dibisukan" _instanceMute: instanceMuteDescription: "Pengaturan ini akan membisukan note/renote apa saja dari instansi yang terdaftar, termasuk pengguna yang membalas pengguna lain dalam instansi yang dibisukan." instanceMuteDescription2: "Pisah dengan baris baru" @@ -1652,8 +1647,6 @@ _sfx: note: "Catatan" noteMy: "Catatan (Saya)" notification: "Notifikasi" - chat: "Pesan" - chatBg: "Obrolan (Latar Belakang)" antenna: "Penerimaan Antenna" channel: "Notifikasi Kanal" _ago: diff --git a/locales/index.d.ts b/locales/index.d.ts index feb74dec35..014a98b7b3 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1144,6 +1144,13 @@ export interface Locale { "authenticationRequiredToContinue": string; "dateAndTime": string; "showRenotes": string; + "edited": string; + "notificationRecieveConfig": string; + "mutualFollow": string; + "fileAttachedOnly": string; + "showRepliesToOthersInTimeline": string; + "hideRepliesToOthersInTimeline": string; + "externalServices": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -1557,7 +1564,6 @@ export interface Locale { "gtlAvailable": string; "ltlAvailable": string; "canPublicNote": string; - "canEditNote": string; "canInvite": string; "inviteLimit": string; "inviteLimitCycle": string; @@ -1735,11 +1741,6 @@ export interface Locale { "muteWords": string; "muteWordsDescription": string; "muteWordsDescription2": string; - "softDescription": string; - "hardDescription": string; - "soft": string; - "hard": string; - "mutedNotes": string; }; "_instanceMute": { "instanceMuteDescription": string; @@ -1827,8 +1828,6 @@ export interface Locale { "note": string; "noteMy": string; "notification": string; - "chat": string; - "chatBg": string; "antenna": string; "channel": string; }; diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 83b63e15d2..a28d243566 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -78,7 +78,7 @@ download: "Scarica" driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?" unfollowConfirm: "Vuoi davvero smettere di seguire {name}?" exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive." -importRequested: "Hai richiesto un'importazione. Può volerci tempo. " +importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo." lists: "Liste" noLists: "Nessuna lista" note: "Nota" @@ -186,7 +186,7 @@ recipient: "Destinatario" annotation: "Annotazione preventiva" federation: "Federazione" instances: "Istanza" -registeredAt: "Registrato presso" +registeredAt: "Prima federazione" latestRequestReceivedAt: "Ultima richiesta ricevuta" latestStatus: "Ultimo stato" storageUsage: "Capienza dei dischi" @@ -461,7 +461,7 @@ invitationCode: "Codice di invito" checking: "Confermando" available: "Disponibile" unavailable: "Il nome utente è già in uso" -usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'" +usernameInvalidFormat: "Il nome utente deve avere solo caratteri alfanumerici e trattino basso '_'" tooShort: "Troppo breve" tooLong: "Troppo lungo" weakPassword: "Password debole" @@ -1122,6 +1122,13 @@ authentication: "Autenticazione" authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione" dateAndTime: "Data e Ora" showRenotes: "Leggi le Rinota" +edited: "Modificato" +notificationRecieveConfig: "Preferenze di notifica" +mutualFollow: "Follow reciproco" +fileAttachedOnly: "Con file in allegato" +showRepliesToOthersInTimeline: "Risposte altrui nella TL" +hideRepliesToOthersInTimeline: "Nascondi Riposte altrui nella TL" +externalServices: "Servizi esterni" _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." @@ -1451,14 +1458,14 @@ _role: _options: gtlAvailable: "Disponibilità della Timeline Federata" ltlAvailable: "Disponibilità della Timeline Locale" - canPublicNote: "Può scrivere Note con Visibilità Pubblica" - canInvite: "Genera codici di invito all'istanza" + canPublicNote: "Scrivere Note con Visibilità Pubblica" + canInvite: "Generare codici di invito all'istanza" inviteLimit: "Limite di codici invito" inviteLimitCycle: "Intervallo di emissione del codice di invito" inviteExpirationTime: "Scadenza del codice di invito" canManageCustomEmojis: "Gestire le emoji personalizzate" driveCapacity: "Capienza del Drive" - alwaysMarkNsfw: "Imposta sempre come NSFW" + alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)" pinMax: "Quantità massima di Note in primo piano" antennaMax: "Quantità massima di Antenne" wordMuteMax: "Lunghezza massima del filtro parole" @@ -1469,8 +1476,9 @@ _role: userEachUserListsMax: "Quantità massima di profili per lista" rateLimitFactor: "Limite del rapporto" descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più." - canHideAds: "Può nascondere i banner" + canHideAds: "Nascondere i banner" canSearchNotes: "Ricercare nelle Note" + canUseTranslator: "Tradurre le Note" _condition: isLocal: "Profilo locale" isRemote: "Profilo remoto" @@ -1604,11 +1612,6 @@ _wordMute: muteWords: "Parole da filtrare" muteWordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con una interruzione di riga, indica la condizione \"O\"" muteWordsDescription2: "Se vuoi indicare delle Espressioni Regolari (regexp), metti la condizione all'interno di due slash (/)" - softDescription: "Verranno nascoste da tutte le Timeline quelle Note che soddisfano le seguenti condizioni" - hardDescription: "Impedisci alla istanza di caricare Note che soddisfano le seguenti condizioni. Le Note già filtrate sono già scomparse in modo irreversibile, fino al cambiamento delle condizioni. Dopo di che scompariranno quelle che soddisfano le nuove condizioni." - soft: "Leggero" - hard: "Pesante" - mutedNotes: "Note filtrate" _instanceMute: instanceMuteDescription: "Disattiva tutte le note, le note di rinvio (condivisione) dell'istanza configurata, comprese le risposte agli utenti dell'istanza." instanceMuteDescription2: "Impostazione separata da una nuova riga" @@ -1692,8 +1695,6 @@ _sfx: note: "Nota" noteMy: "Mia nota" notification: "Notifiche" - chat: "Messaggi" - chatBg: "Chat (sfondo)" antenna: "Ricezione dell'antenna" channel: "Notifiche di canale" _ago: @@ -2130,3 +2131,6 @@ _moderationLogTypes: unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" resolveAbuseReport: "Segnalazione risolta" createInvitation: "Genera codice di invito" + createAd: "Banner creato" + deleteAd: "Banner eliminato" + updateAd: "Banner aggiornato" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ccf1a3f81d..c6f25764dd 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1141,6 +1141,13 @@ authentication: "認証" authenticationRequiredToContinue: "続けるには認証を行ってください" dateAndTime: "日時" showRenotes: "リノートを表示" +edited: "編集済み" +notificationRecieveConfig: "通知の受信設定" +mutualFollow: "相互フォロー" +fileAttachedOnly: "ファイル付きのみ" +showRepliesToOthersInTimeline: "TLに他の人への返信を含める" +hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" +externalServices: "外部サービス" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -1478,7 +1485,6 @@ _role: gtlAvailable: "グローバルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" - canEditNote: "ノートの編集" canInvite: "サーバー招待コードの発行" inviteLimit: "招待コードの作成可能数" inviteLimitCycle: "招待コードの発行間隔" @@ -1652,11 +1658,6 @@ _wordMute: muteWords: "ミュートするワード" muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。" - softDescription: "指定した条件のノートをタイムラインから隠します。" - hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。" - soft: "ソフト" - hard: "ハード" - mutedNotes: "ミュートされたノート" _instanceMute: instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。" @@ -1744,8 +1745,6 @@ _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - chat: "チャット" - chatBg: "チャット(バックグラウンド)" antenna: "アンテナ受信" channel: "チャンネル通知" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index bf945088f0..43d98fdd04 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1586,11 +1586,6 @@ _wordMute: muteWords: "ミュートするワード" muteWordsDescription: "スペースで区切るとAND指定になって、改行で区切るとOR指定になるで。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になるで。" - softDescription: "指定した条件のノートをタイムラインから隠すで。" - hardDescription: "指定した条件のノートをタイムラインに追加しないようにするで。追加せーへんかったかったノートは、条件を変えても除外されたままになるで。" - soft: "ソフト" - hard: "ハード" - mutedNotes: "ミュートされたノート" _instanceMute: instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。" instanceMuteDescription2: "改行で区切って設定するんやで" @@ -1674,8 +1669,6 @@ _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - chat: "チャット" - chatBg: "チャット(バックグラウンド)" antenna: "アンテナ受信" channel: "チャンネル通知" _ago: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index af7afb2c3e..4009b7df5f 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1600,11 +1600,6 @@ _wordMute: muteWords: "뮤트할 단어" muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다." muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요." - softDescription: "지정한 조건의 노트를 타임라인에서 숨깁니다." - hardDescription: "지정한 조건의 노트를 타임라인에 추가하지 않습니다. 타임라인에 추가되지 않은 노트는 조건을 변경해도 표시되지 않습니다." - soft: "보통" - hard: "보다 높은 수준" - mutedNotes: "뮤트된 노트" _instanceMute: instanceMuteDescription: "뮤트한 서버에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다." instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요" @@ -1688,8 +1683,6 @@ _sfx: note: "새 노트" noteMy: "내 노트" notification: "알림" - chat: "대화" - chatBg: "대화 (백그라운드)" antenna: "안테나 수신" channel: "채널 알림" _ago: diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 22cb5857f9..b22e047cfa 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -407,7 +407,6 @@ _theme: _sfx: note: "ບັນທຶກ" notification: "ການແຈ້ງເຕືອນ" - chat: "ແຊ໋ດ" _2fa: renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້" _widgets: diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index fd9ffa33f2..6f789dff10 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -438,7 +438,6 @@ _theme: _sfx: note: "Notities" notification: "Meldingen" - chat: "Chat" _2fa: renewTOTPCancel: "Nee, bedankt" _widgets: diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 00f22c0c4f..d99c61c1dd 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -575,9 +575,6 @@ _channel: nameAndDescription: "Navn og beskrivelse" _menuDisplay: hide: "Skjul" -_wordMute: - soft: "Myk" - hard: "Hard" _theme: description: "Beskrivelse" color: "Farge" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 1c7ebe8108..4e5eb147ea 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -982,9 +982,6 @@ _menuDisplay: _wordMute: muteWords: "Słowo do wyciszenia" muteWordsDescription2: "Otocz słowa kluczowe ukośnikami, aby używać wyrażeń regularnych." - soft: "Łagodny" - hard: "Twardy" - mutedNotes: "Wyciszone wpisy" _instanceMute: title: "Ukrywa wpisy z wymienionych instancji." heading: "Lista instancji do wyciszenia" @@ -1066,8 +1063,6 @@ _sfx: note: "Wpisy" noteMy: "Mój wpis" notification: "Powiadomienia" - chat: "Wiadomości" - chatBg: "Rozmowy (tło)" antenna: "Anteny" channel: "Powiadomienia kanału" _ago: diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index f9e777bc75..23864df1b8 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -1320,7 +1320,6 @@ _theme: _sfx: note: "Posts" notification: "Notificações" - chat: "Chat" _ago: invalid: "Não há nada aqui" _timelineTutorial: diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 51c33085af..77bccb7e6b 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -647,7 +647,6 @@ _theme: _sfx: note: "Note" notification: "Notificări" - chat: "Chat" _ago: invalid: "Nu e nimic de văzut aici" _widgets: diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 937158978d..ac30e3a98c 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1488,11 +1488,6 @@ _wordMute: muteWords: "Скрыть слово" muteWordsDescription: "Пишите слова через пробел в одной строке, чтобы фильтровать их появление вместе; а если хотите фильтровать любое из них, пишите в отдельных строках." muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)." - softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты." - hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся." - soft: "Мягко" - hard: "Жёстко" - mutedNotes: "Скрытые заметки" _instanceMute: instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться." instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке" @@ -1576,8 +1571,6 @@ _sfx: note: "Заметки" noteMy: "Собственные заметки" notification: "Уведомления" - chat: "Сообщения" - chatBg: "Сообщения (фон)" antenna: "Антенна" channel: "Канал" _ago: diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index e44aaafc0a..811f93e711 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1039,11 +1039,6 @@ _wordMute: muteWords: "Umlčané slová" muteWordsDescription: "Medzerami oddeľte pre podmienku AND a novými riadkami pre podmienku OR." muteWordsDescription2: "Regulárne výrazy sa použijú keď použijete okolo lomítka." - softDescription: "Skryje poznámky z časovej osi, ktoré spĺňajú podmienky." - hardDescription: "Zabráni poznámky spĺňajúce množinu podmienok, aby boli pridané do časovej osi. Navyše tieto poznámky nepribudnú v časovej osi ani keď sa podmienky zmenia." - soft: "Mäkké" - hard: "Tvrdé" - mutedNotes: "Umlčané poznámky" _instanceMute: instanceMuteDescription: "Toto umlčí všetky poznámky/preposlania zo zoznamu serverov, vrátane tých, na ktoré používatelia odpovedajú z umlčaného servera." instanceMuteDescription2: "Oddeľte novými riadkami" @@ -1127,8 +1122,6 @@ _sfx: note: "Poznámky" noteMy: "Vlastná poznámka" notification: "Oznámenia" - chat: "Chat" - chatBg: "Chat (pozadie)" antenna: "Antény" channel: "Upozornenia kanála" _ago: diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 62e7d412ab..92678afef8 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -507,7 +507,6 @@ _theme: _sfx: note: "Noter" notification: "Notifikationer" - chat: "Chatt" antenna: "Antenner" _2fa: renewTOTPCancel: "Nej tack" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index c2adcf8ec5..6a553dab52 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1150,6 +1150,7 @@ _serverRules: description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ" _serverSettings: iconUrl: "ไอคอน URL" + manifestJsonOverride: "manifest.json โอเวอร์ลาย" shortName: "ชื่อย่อ" _accountMigration: moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง" @@ -1407,6 +1408,7 @@ _achievements: flavor: "Misskey-Misskey La-Tu-Ma" _smashTestNotificationButton: title: "ทดสอบโอเวอร์โฟลว์" + description: "ทดสอบการแจ้งเตือนทริกเกอร์ซ้ำๆ ภายในระยะเวลาอันสั้นๆ" _role: new: "บทบาทใหม่" edit: "แก้ไขบทบาท" @@ -1445,7 +1447,6 @@ _role: gtlAvailable: "การดูไทม์ไลน์ทั่วโลก" ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" canPublicNote: "สามารถส่งโน้ตสาธารณะ" - canEditNote: "กำลังแก้ไขโน้ต" canInvite: "สร้างรหัสเชิญอินสแตนซ์" inviteLimit: "จำกัดการเชิญ" inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์" @@ -1465,6 +1466,7 @@ _role: descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า" canHideAds: "ซ่อนโฆษณา" canSearchNotes: "การใช้การค้นหาโน้ต" + canUseTranslator: "การใช้งานแปล" _condition: isLocal: "ผู้ใช้ภายใน" isRemote: "ผู้ใช้ระยะไกล" @@ -1598,11 +1600,6 @@ _wordMute: muteWords: "ปิดเสียงคำ" muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ" muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป" - softDescription: "ซ่อนโน้ตให้ตรงตามเงื่อนไขที่ตั้งไว้จากไทม์ไลน์" - hardDescription: "ป้องกันไม่ให้โน้ตย่อที่ตรงตามเงื่อนไขที่ตั้งไว้ไม่ให้ถูกเพิ่มลงในไทม์ไลน์ นอกจากนี้ โน้ตเหล่านี้จะไม่ถูกเพิ่มลงในไทม์ไลน์แม้ว่าจะมีการเปลี่ยนแปลงเงื่อนไขยังไงก็ตาม" - soft: "ซอฟ" - hard: "ยาก" - mutedNotes: "ปิดเสียงโน้ต" _instanceMute: instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง" instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่" @@ -1686,8 +1683,6 @@ _sfx: note: "หมายเหตุ" noteMy: "โน้ตของตัวเอง" notification: "การเเจ้งเตือน" - chat: "แชท" - chatBg: "แชท (พื้นหลัง)" antenna: "เสาอากาศ" channel: "การแจ้งเตือนช่อง" _ago: @@ -1792,6 +1787,7 @@ _antennaSources: homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม" users: "โน้ตจากผู้ใช้ที่เฉพาะเจาะจง" userList: "โน้ตจากรายชื่อผู้ใช้ที่ระบุ" + userBlacklist: "โน้ตทั้งหมดยกเว้นโน้ตของผู้ใช้ที่ต้องระบุเจาะจงตั้งแต่หนึ่งรายขึ้นไป" _weekday: sunday: "วันอาทิตย์" monday: "วันจันทร์" @@ -1891,6 +1887,7 @@ _profile: metadataContent: "เนื้อหา" changeAvatar: "เปลี่ยนอวาตาร์" changeBanner: "เปลี่ยนแบนเนอร์" + verifiedLinkDescription: "โดยการป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณตรงนี้ ส่วนไอคอนการยืนยันความเป็นเจ้าของนั้นก็สามารถแสดงถัดจากฟิลด์ได้นะ" _exportOrImport: allNotes: "โน้ตทั้งหมด" favoritedNotes: "บันทึกที่ชื่นชอบ" @@ -2104,7 +2101,17 @@ _moderationLogTypes: updateUserNote: "อัปเดตโน้ตการกลั่นกรองแล้ว" deleteDriveFile: "ลบไฟล์ออกแล้ว" deleteNote: "ลบโน้ตออกแล้ว" + createGlobalAnnouncement: "สร้างประกาศทั่วโลกแล้ว" + createUserAnnouncement: "สร้างประกาศผู้ใช้แล้ว" + updateGlobalAnnouncement: "อัปเดตประกาศทั่วโลกแล้ว" + updateUserAnnouncement: "อัปเดตประกาศผู้ใช้แล้ว" + deleteGlobalAnnouncement: "ลบประกาศทั่วโลกออกแล้ว" + deleteUserAnnouncement: "ลบประกาศผู้ใช้ออกแล้ว" resetPassword: "รีเซ็ตรหัสผ่าน" + suspendRemoteInstance: "อินสแตนซ์ระยะไกลถูกระงับ" + unsuspendRemoteInstance: "อินสแตนซ์ระยะไกลเลิกการระงับ" + markSensitiveDriveFile: "ทำเครื่องหมายไฟล์บอกว่าละเอียดอ่อน" + unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่าละเอียดอ่อน" resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว" createInvitation: "สร้างคำเชิญ" createAd: "สร้างโฆษณาแล้ว" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 1111c23091..90bee48a1f 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -386,7 +386,6 @@ _theme: _sfx: note: "notlar" notification: "Bildirim" - chat: "Mesajlar" _2fa: renewTOTPCancel: "Hayır, teşekkürler" _permissions: diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 09b3eba745..a0e2a52826 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1233,11 +1233,6 @@ _wordMute: muteWords: "Заглушені слова" muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\"" muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж слешів \"/\"." - softDescription: "Приховати записи які відповідають критеріям зі стрічки подій." - hardDescription: "Приховати записи які відповідають критеріям зі стрічки подій. Також приховані записи не будуть додані до стрічки подій навіть якщо критерії буде змінено." - soft: "М'яко" - hard: "Жорстко" - mutedNotes: "Заблоковані нотатки" _instanceMute: instanceMuteDescription2: "Розділяйте новими рядками" title: "Приховує нотатки з перелічених інстансів." @@ -1315,8 +1310,6 @@ _sfx: note: "Нотатки" noteMy: "Мої нотатки" notification: "Сповіщення" - chat: "Чати" - chatBg: "Чати (фон)" antenna: "Прийом антени" channel: "Повідомлення каналу" _ago: diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 726333958b..3a9e6ec5e7 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -910,7 +910,6 @@ _theme: _sfx: note: "Qaydlar" notification: "Xabarnomalar" - chat: "Suhbat" _ago: minutesAgo: "{n} daqiqa oldin" hoursAgo: "{n} soat oldin" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 3b34e4711c..0c32735c31 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1404,11 +1404,6 @@ _wordMute: muteWords: "Ẩn từ ngữ" muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sử dụng cụm từ thông dụng." - softDescription: "Ẩn các tút phù hợp điều kiện đã đặt khỏi bảng tin." - hardDescription: "Ngăn các tút đáp ứng các điều kiện đã đặt xuất hiện trên bảng tin. Lưu ý, những tút này sẽ không được thêm vào bảng tin ngay cả khi các điều kiện được thay đổi." - soft: "Yếu" - hard: "Mạnh" - mutedNotes: "Những tút đã ẩn" _instanceMute: instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn." instanceMuteDescription2: "Tách bằng cách xuống dòng" @@ -1492,8 +1487,6 @@ _sfx: note: "Tút" noteMy: "Tút của tôi" notification: "Thông báo" - chat: "Trò chuyện" - chatBg: "Chat (Nền)" antenna: "Trạm phát sóng" channel: "Kênh" _ago: diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index a04697e480..5f4db9192b 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1126,6 +1126,8 @@ edited: "已编辑" notificationRecieveConfig: "通知接收设置" mutualFollow: "互相关注" fileAttachedOnly: "仅限媒体" +showRepliesToOthersInTimeline: "在时间线上显示给其他人的回复" +hideRepliesToOthersInTimeline: "在时间线上隐藏给其他人的回复" _announcement: forExistingUsers: "仅限现有用户" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" @@ -1456,7 +1458,6 @@ _role: gtlAvailable: "查看全局时间线" ltlAvailable: "查看本地时间线" canPublicNote: "允许公开发帖" - canEditNote: "编辑帖子" canInvite: "发放服务器邀请码" inviteLimit: "可发行邀请码的数量" inviteLimitCycle: "邀请码的发行间隔" @@ -1609,11 +1610,6 @@ _wordMute: muteWords: "禁用词" muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" muteWordsDescription2: "正则表达式用斜线包裹" - softDescription: "隐藏时间线中指定条件的帖子。" - hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。" - soft: "软屏蔽" - hard: "硬屏蔽" - mutedNotes: "被屏蔽的帖子" _instanceMute: instanceMuteDescription: "屏蔽服务器中的所有帖子和转帖,包括这些服务器上的用户回复。" instanceMuteDescription2: "一行一个" @@ -1697,8 +1693,6 @@ _sfx: note: "帖子" noteMy: "我的帖子" notification: "通知" - chat: "聊天" - chatBg: "聊天背景" antenna: "天线接收" channel: "频道通知" _ago: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index c0bf1f7d1b..5e62cb1b7b 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -156,7 +156,7 @@ emojiUrl: "表情符號 URL" addEmoji: "新增表情符號" settingGuide: "推薦設定" cacheRemoteFiles: "快取遠端檔案" -cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true,以便產生圖片的縮圖並保護使用者的隱私,。" +cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true,以便產生圖片的縮圖並保護使用者的隱私。" youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。" cacheRemoteSensitiveFiles: "快取遠端的敏感檔案" cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。" @@ -1123,7 +1123,12 @@ authenticationRequiredToContinue: "請於繼續前完成驗證" dateAndTime: "日期與時間" showRenotes: "顯示轉發貼文" edited: "已編輯" +notificationRecieveConfig: "接受通知的設定" mutualFollow: "互相追隨" +fileAttachedOnly: "包含附件" +showRepliesToOthersInTimeline: "在時間軸上顯示給其他人的回覆" +hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆" +externalServices: "外部服務" _announcement: forExistingUsers: "僅限既有的使用者" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" @@ -1454,7 +1459,6 @@ _role: gtlAvailable: "瀏覽全域時間軸" ltlAvailable: "瀏覽本地時間軸" canPublicNote: "允許公開貼文" - canEditNote: "允許編輯貼文" canInvite: "發行實例邀請碼" inviteLimit: "可建立邀請碼的數量" inviteLimitCycle: "邀請碼的發放間隔" @@ -1474,6 +1478,7 @@ _role: descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。" canHideAds: "不顯示廣告" canSearchNotes: "可否搜尋貼文" + canUseTranslator: "使用翻譯功能" _condition: isLocal: "本地使用者" isRemote: "遠端使用者" @@ -1607,11 +1612,6 @@ _wordMute: muteWords: "加入靜音文字" muteWordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)。" muteWordsDescription2: "用斜線包圍關鍵字代表正規表達式。" - softDescription: "隱藏時間軸中符合特定條件的貼文。" - hardDescription: "符合特定條件的貼文將不會新增至時間軸。 即使您更改條件,未被新增的貼文也會被排除在外。" - soft: "軟性靜音" - hard: "硬性靜音" - mutedNotes: "已靜音的貼文" _instanceMute: instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。" instanceMuteDescription2: "設定時以換行進行分隔" @@ -1695,8 +1695,6 @@ _sfx: note: "貼文" noteMy: "我的貼文" notification: "通知" - chat: "聊天" - chatBg: "聊天背景" antenna: "天線接收" channel: "頻道通知" _ago: diff --git a/package.json b/package.json index 0a7efa9f5e..d6b9f610b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.9.3-prismisskey.3", + "version": "2023.10.0-beta.5", "codename": "nasubi", "repository": { "type": "git", @@ -48,12 +48,12 @@ "cssnano": "6.0.1", "js-yaml": "4.1.0", "postcss": "8.4.31", - "terser": "5.20.0", + "terser": "5.21.0", "typescript": "5.2.2" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", + "@typescript-eslint/eslint-plugin": "6.7.4", + "@typescript-eslint/parser": "6.7.4", "cross-env": "7.0.3", "cypress": "13.3.0", "eslint": "8.50.0", diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 6b1afec734..97d777c862 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -216,4 +216,6 @@ module.exports = { maxWorkers: 1, // Make it use worker (that can be killed and restarted) logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) + + maxConcurrency: 32, }; diff --git a/packages/backend/migration/1696222183852-withReplies.js b/packages/backend/migration/1696222183852-withReplies.js new file mode 100644 index 0000000000..9f65d5f6a1 --- /dev/null +++ b/packages/backend/migration/1696222183852-withReplies.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class WithReplies1696222183852 { + name = 'WithReplies1696222183852' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "following" ADD "withReplies" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "user_list_joining" ADD "withReplies" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`); + await queryRunner.query(`ALTER TABLE "user_list_joining" DROP COLUMN "withReplies"`); + await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "withReplies"`); + } +} diff --git a/packages/backend/migration/1696323464251-user-list-membership.js b/packages/backend/migration/1696323464251-user-list-membership.js new file mode 100644 index 0000000000..7534040c4c --- /dev/null +++ b/packages/backend/migration/1696323464251-user-list-membership.js @@ -0,0 +1,11 @@ +export class UserListMembership1696323464251 { + name = 'UserListMembership1696323464251' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_joining" RENAME TO "user_list_membership"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" RENAME TO "user_list_joining"`); + } +} diff --git a/packages/backend/migration/1696331570827-hibernation.js b/packages/backend/migration/1696331570827-hibernation.js new file mode 100644 index 0000000000..119d35913f --- /dev/null +++ b/packages/backend/migration/1696331570827-hibernation.js @@ -0,0 +1,17 @@ +export class Hibernation1696331570827 { + name = 'Hibernation1696331570827' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`); + await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`); + await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`); + await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `); + } +} diff --git a/packages/backend/migration/1696332072038-clean.js b/packages/backend/migration/1696332072038-clean.js new file mode 100644 index 0000000000..97dba655f4 --- /dev/null +++ b/packages/backend/migration/1696332072038-clean.js @@ -0,0 +1,33 @@ +export class Clean1696332072038 { + name = 'Clean1696332072038' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`); + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d844bfc6f3f523a05189076efa"`); + await queryRunner.query(`DROP INDEX "public"."IDX_605472305f26818cc93d1baaa7"`); + await queryRunner.query(`DROP INDEX "public"."IDX_90f7da835e4c10aca6853621e1"`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`); + await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListMembership.'`); + await queryRunner.query(`CREATE INDEX "IDX_021015e6683570ae9f6b0c62be" ON "user_list_membership" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_cddcaf418dc4d392ecfcca842a" ON "user_list_membership" ("userListId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e4f3094c43f2d665e6030b0337" ON "user_list_membership" ("userId", "userListId") `); + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_021015e6683570ae9f6b0c62bee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7"`); + await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_021015e6683570ae9f6b0c62bee"`); + await queryRunner.query(`DROP INDEX "public"."IDX_e4f3094c43f2d665e6030b0337"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cddcaf418dc4d392ecfcca842a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_021015e6683570ae9f6b0c62be"`); + await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListJoining.'`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_membership" ("userId", "userListId") `); + await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_membership" ("userListId") `); + await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_membership" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/migration/1696373953614-meta-cache-settings.js b/packages/backend/migration/1696373953614-meta-cache-settings.js new file mode 100644 index 0000000000..f994b76ef2 --- /dev/null +++ b/packages/backend/migration/1696373953614-meta-cache-settings.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MetaCacheSettings1696373953614 { + name = 'MetaCacheSettings1696373953614' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "perLocalUserUserTimelineCacheMax" integer NOT NULL DEFAULT '300'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "perRemoteUserUserTimelineCacheMax" integer NOT NULL DEFAULT '100'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "perUserHomeTimelineCacheMax" integer NOT NULL DEFAULT '300'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "perUserListTimelineCacheMax" integer NOT NULL DEFAULT '300'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserListTimelineCacheMax"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserHomeTimelineCacheMax"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perRemoteUserUserTimelineCacheMax"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perLocalUserUserTimelineCacheMax"`); + } +} diff --git a/packages/backend/migration/1696388600237-revert-note-edit.js b/packages/backend/migration/1696388600237-revert-note-edit.js new file mode 100644 index 0000000000..83bc552c35 --- /dev/null +++ b/packages/backend/migration/1696388600237-revert-note-edit.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RevertNoteEdit1696388600237 { + name = 'RevertNoteEdit1696388600237' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`); + } +} diff --git a/packages/backend/migration/1696405744672-clean-up.js b/packages/backend/migration/1696405744672-clean-up.js new file mode 100644 index 0000000000..5ec89b08f4 --- /dev/null +++ b/packages/backend/migration/1696405744672-clean-up.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class CleanUp1696405744672 { + name = 'CleanUp1696405744672' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_e7c0567f5261063592f022e9b5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_25dfc71b0369b003a4cd434d0b"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `); + await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `); + } +} diff --git a/packages/backend/migration/1696569742153-clean-up.js b/packages/backend/migration/1696569742153-clean-up.js new file mode 100644 index 0000000000..de48fab5aa --- /dev/null +++ b/packages/backend/migration/1696569742153-clean-up.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class CleanUp1696569742153 { + name = 'CleanUp1696569742153' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_01f4581f114e0ebd2bbb876f0b"`); + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "score"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "score" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `); + } +} diff --git a/packages/backend/migration/1696581429196-clean-up.js b/packages/backend/migration/1696581429196-clean-up.js new file mode 100644 index 0000000000..da69b4e9de --- /dev/null +++ b/packages/backend/migration/1696581429196-clean-up.js @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class CleanUp1696581429196 { + name = 'CleanUp1696581429196' + + async up(queryRunner) { + await queryRunner.query(`DROP TABLE IF EXISTS "muted_note"`); + } + + async down(queryRunner) { + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 2fbf9de8a6..2f0cf10374 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -71,14 +71,14 @@ "@fastify/multipart": "8.0.0", "@fastify/static": "6.11.2", "@fastify/view": "8.2.0", - "@nestjs/common": "10.2.6", - "@nestjs/core": "10.2.6", - "@nestjs/testing": "10.2.6", + "@nestjs/common": "10.2.7", + "@nestjs/core": "10.2.7", + "@nestjs/testing": "10.2.7", "@peertube/http-signature": "1.7.0", "@simplewebauthn/server": "8.2.0", "@sinonjs/fake-timers": "11.1.0", "@swc/cli": "0.1.62", - "@swc/core": "1.3.90", + "@swc/core": "1.3.92", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "6.0.1", @@ -86,7 +86,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "4.11.4", + "bullmq": "4.12.2", "cacheable-lookup": "7.0.0", "cbor": "9.0.1", "chalk": "5.3.0", @@ -155,7 +155,7 @@ "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.21.9", + "systeminformation": "5.21.11", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.8", @@ -189,7 +189,7 @@ "@types/jsrsasign": "10.5.9", "@types/mime-types": "2.1.2", "@types/ms": "0.7.32", - "@types/node": "20.7.1", + "@types/node": "20.8.2", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.11", "@types/oauth": "0.9.2", @@ -212,8 +212,8 @@ "@types/vary": "1.1.1", "@types/web-push": "3.6.1", "@types/ws": "8.5.6", - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", + "@typescript-eslint/eslint-plugin": "6.7.4", + "@typescript-eslint/parser": "6.7.4", "aws-sdk-client-mock": "3.0.0", "cross-env": "7.0.3", "eslint": "8.50.0", diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 9f1ee9fcaa..3e9d19f825 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -70,11 +70,19 @@ const $redisForSub: Provider = { inject: [DI.config], }; +const $redisForTimelines: Provider = { + provide: DI.redisForTimelines, + useFactory: (config: Config) => { + return new Redis.Redis(config.redisForTimelines); + }, + inject: [DI.config], +}; + @Global() @Module({ imports: [RepositoryModule], - providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub], - exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule], + providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines], + exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule], }) export class GlobalModule implements OnApplicationShutdown { constructor( @@ -82,6 +90,7 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, + @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, ) {} public async dispose(): Promise { @@ -98,6 +107,7 @@ export class GlobalModule implements OnApplicationShutdown { this.redisClient.disconnect(), this.redisForPub.disconnect(), this.redisForSub.disconnect(), + this.redisForTimelines.disconnect(), ]); } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index f89879d535..ef59a80950 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -47,6 +47,7 @@ type Source = { redis: RedisOptionsSource; redisForPubsub?: RedisOptionsSource; redisForJobQueue?: RedisOptionsSource; + redisForTimelines?: RedisOptionsSource; meilisearch?: { host: string; port: string; @@ -161,6 +162,7 @@ export type Config = { redis: RedisOptions & RedisOptionsSource; redisForPubsub: RedisOptions & RedisOptionsSource; redisForJobQueue: RedisOptions & RedisOptionsSource; + redisForTimelines: RedisOptions & RedisOptionsSource; perChannelMaxNoteCacheCount: number; perUserNotificationsMaxCount: number; deactivateAntennaThreshold: number; @@ -227,6 +229,7 @@ export function loadConfig(): Config { redis, redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, + redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis, id: config.id, proxy: config.proxy, proxySmtp: config.proxySmtp, diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index ec1d013922..ba3413007d 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import { IdService } from '@/core/IdService.js'; @@ -42,8 +42,8 @@ export class AccountMoveService { @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -215,40 +215,40 @@ export class AccountMoveService { @bindThis public async updateLists(src: ThinUser, dst: MiUser): Promise { // Return if there is no list to be updated. - const oldJoinings = await this.userListJoiningsRepository.find({ + const oldMemberships = await this.userListMembershipsRepository.find({ where: { userId: src.id, }, }); - if (oldJoinings.length === 0) return; + if (oldMemberships.length === 0) return; - const existingUserListIds = await this.userListJoiningsRepository.find({ + const existingUserListIds = await this.userListMembershipsRepository.find({ where: { userId: dst.id, }, - }).then(joinings => joinings.map(joining => joining.userListId)); + }).then(memberships => memberships.map(membership => membership.userListId)); - const newJoinings: Map = new Map(); + const newMemberships: Map = new Map(); // 重複しないようにIDを生成 const genId = (): string => { let id: string; do { id = this.idService.genId(); - } while (newJoinings.has(id)); + } while (newMemberships.has(id)); return id; }; - for (const joining of oldJoinings) { - if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list - newJoinings.set(genId(), { + for (const membership of oldMemberships) { + if (existingUserListIds.includes(membership.userListId)) continue; // skip if dst exists in this user's list + newMemberships.set(genId(), { createdAt: new Date(), userId: dst.id, - userListId: joining.userListId, + userListId: membership.userListId, }); } - const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] })); - await this.userListJoiningsRepository.insert(arrayToInsert); + const arrayToInsert = Array.from(newMemberships.entries()).map(entry => ({ ...entry[1], id: entry[0] })); + await this.userListMembershipsRepository.insert(arrayToInsert); // Have the proxy account follow the new account in the same way as UserListService.push if (this.userEntityService.isRemoteUser(dst)) { diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index ddacc0936f..a5330db53f 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -158,9 +158,13 @@ export class AnnouncementService { if (moderator) { if (announcement.userId) { + const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId }); this.moderationLogService.log(moderator, 'deleteUserAnnouncement', { announcementId: announcement.id, announcement: announcement, + userId: announcement.userId, + userUsername: user.username, + userHost: user.host, }); } else { this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', { diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index d9f27b8c63..95712b35b7 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -12,7 +12,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import * as Acct from '@/misc/acct.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js'; +import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; @@ -24,8 +24,8 @@ export class AntennaService implements OnApplicationShutdown { private antennas: MiAntenna[]; constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @@ -33,8 +33,8 @@ export class AntennaService implements OnApplicationShutdown { @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private utilityService: UtilityService, private globalEventService: GlobalEventService, @@ -81,7 +81,7 @@ export class AntennaService implements OnApplicationShutdown { const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const))); const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna); - const redisPipeline = this.redisClient.pipeline(); + const redisPipeline = this.redisForTimelines.pipeline(); for (const antenna of matchedAntennas) { redisPipeline.xadd( @@ -108,7 +108,7 @@ export class AntennaService implements OnApplicationShutdown { if (antenna.src === 'home') { // TODO } else if (antenna.src === 'list') { - const listUsers = (await this.userListJoiningsRepository.findBy({ + const listUsers = (await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId!, })).map(x => x.userId); diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 561979c4bf..22c510cc37 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; @@ -25,7 +25,7 @@ export class CacheService implements OnApplicationShutdown { public userBlockingCache: RedisKVCache>; public userBlockedCache: RedisKVCache>; // NOTE: 「被」Blockキャッシュ public renoteMutingsCache: RedisKVCache>; - public userFollowingsCache: RedisKVCache>; + public userFollowingsCache: RedisKVCache | undefined>>; public userFollowingChannelsCache: RedisKVCache>; constructor( @@ -136,12 +136,18 @@ export class CacheService implements OnApplicationShutdown { fromRedisConverter: (value) => new Set(JSON.parse(value)), }); - this.userFollowingsCache = new RedisKVCache>(this.redisClient, 'userFollowings', { + this.userFollowingsCache = new RedisKVCache | undefined>>(this.redisClient, 'userFollowings', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))), - toRedisConverter: (value) => JSON.stringify(Array.from(value)), - fromRedisConverter: (value) => new Set(JSON.parse(value)), + fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => { + const obj: Record | undefined> = {}; + for (const x of xs) { + obj[x.followeeId] = { withReplies: x.withReplies }; + } + return obj; + }), + toRedisConverter: (value) => JSON.stringify(value), + fromRedisConverter: (value) => JSON.parse(value), }); this.userFollowingChannelsCache = new RedisKVCache>(this.redisClient, 'userFollowingChannels', { @@ -188,6 +194,7 @@ export class CacheService implements OnApplicationShutdown { if (follower) follower.followingCount++; const followee = this.userByIdCache.get(body.followeeId); if (followee) followee.followersCount++; + this.userFollowingsCache.delete(body.followerId); break; } default: diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 78333e70a5..1984d9e6c2 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -46,6 +46,7 @@ import { SignupService } from './SignupService.js'; import { WebAuthnService } from './WebAuthnService.js'; import { UserBlockingService } from './UserBlockingService.js'; import { CacheService } from './CacheService.js'; +import { UserService } from './UserService.js'; import { UserFollowingService } from './UserFollowingService.js'; import { UserKeypairService } from './UserKeypairService.js'; import { UserListService } from './UserListService.js'; @@ -59,6 +60,7 @@ import { UtilityService } from './UtilityService.js'; import { FileInfoService } from './FileInfoService.js'; import { SearchService } from './SearchService.js'; import { ClipService } from './ClipService.js'; +import { FeaturedService } from './FeaturedService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -173,6 +175,7 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService }; +const $UserService: Provider = { provide: 'UserService', useExisting: UserService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; @@ -185,6 +188,7 @@ const $UtilityService: Provider = { provide: 'UtilityService', useExisting: Util const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; +const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -303,6 +307,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting WebAuthnService, UserBlockingService, CacheService, + UserService, UserFollowingService, UserKeypairService, UserListService, @@ -315,6 +320,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FileInfoService, SearchService, ClipService, + FeaturedService, ChartLoggerService, FederationChart, NotesChart, @@ -426,6 +432,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $WebAuthnService, $UserBlockingService, $CacheService, + $UserService, $UserFollowingService, $UserKeypairService, $UserListService, @@ -438,6 +445,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FileInfoService, $SearchService, $ClipService, + $FeaturedService, $ChartLoggerService, $FederationChart, $NotesChart, @@ -550,6 +558,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting WebAuthnService, UserBlockingService, CacheService, + UserService, UserFollowingService, UserKeypairService, UserListService, @@ -562,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FileInfoService, SearchService, ClipService, + FeaturedService, FederationChart, NotesChart, UsersChart, @@ -672,6 +682,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $WebAuthnService, $UserBlockingService, $CacheService, + $UserService, $UserFollowingService, $UserKeypairService, $UserListService, @@ -684,6 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FileInfoService, $SearchService, $ClipService, + $FeaturedService, $FederationChart, $NotesChart, $UsersChart, diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 83fe91fbe3..d071696479 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -48,7 +48,6 @@ export class CustomEmojiService implements OnApplicationShutdown { fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), fromRedisConverter: (value) => { - if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す) return new Map(JSON.parse(value).map((x: Serialized) => [x.name, { ...x, updatedAt: x.updatedAt ? new Date(x.updatedAt) : null, @@ -409,6 +408,20 @@ export class CustomEmojiService implements OnApplicationShutdown { } } + /** + * ローカル内の絵文字に重複がないかチェックします + * @param name 絵文字名 + */ + @bindThis + public checkDuplicate(name: string): Promise { + return this.emojisRepository.exist({ where: { name, host: IsNull() } }); + } + + @bindThis + public getEmojiById(id: string): Promise { + return this.emojisRepository.findOneBy({ id }); + } + @bindThis public dispose(): void { this.cache.dispose(); diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts new file mode 100644 index 0000000000..945c23b0e2 --- /dev/null +++ b/packages/backend/src/core/FeaturedService.ts @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import type { MiNote, MiUser } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; + +const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと +const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと + +@Injectable() +export class FeaturedService { + constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする + ) { + } + + @bindThis + private getCurrentWindow(windowRange: number): number { + const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime(); + return Math.floor(passed / windowRange); + } + + @bindThis + private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise { + const currentWindow = this.getCurrentWindow(windowRange); + const redisTransaction = this.redisClient.multi(); + redisTransaction.zincrby( + `${name}:${currentWindow}`, + score, + element); + redisTransaction.expire( + `${name}:${currentWindow}`, + (windowRange * 3) / 1000, + 'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定 + await redisTransaction.exec(); + } + + @bindThis + private async getRankingOf(name: string, windowRange: number, limit: number): Promise { + const currentWindow = this.getCurrentWindow(windowRange); + const previousWindow = currentWindow - 1; + + const [currentRankingResult, previousRankingResult] = await Promise.all([ + this.redisClient.zrange( + `${name}:${currentWindow}`, 0, limit, 'REV', 'WITHSCORES'), + this.redisClient.zrange( + `${name}:${previousWindow}`, 0, limit, 'REV', 'WITHSCORES'), + ]); + + const ranking = new Map(); + for (let i = 0; i < currentRankingResult.length; i += 2) { + const noteId = currentRankingResult[i]; + const score = parseInt(currentRankingResult[i + 1], 10); + ranking.set(noteId, score); + } + for (let i = 0; i < previousRankingResult.length; i += 2) { + const noteId = previousRankingResult[i]; + const score = parseInt(previousRankingResult[i + 1], 10); + const exist = ranking.get(noteId); + if (exist != null) { + ranking.set(noteId, (exist + score) / 2); + } else { + ranking.set(noteId, score); + } + } + + return Array.from(ranking.keys()); + } + + @bindThis + public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise { + return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); + } + + @bindThis + public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise { + return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score); + } + + @bindThis + public updatePerUserNotesRanking(userId: MiUser['id'], noteId: MiNote['id'], score = 1): Promise { + return this.updateRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, noteId, score); + } + + @bindThis + public getGlobalNotesRanking(limit: number): Promise { + return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, limit); + } + + @bindThis + public getInChannelNotesRanking(channelId: MiNote['channelId'], limit: number): Promise { + return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, limit); + } + + @bindThis + public getPerUserNotesRanking(userId: MiUser['id'], limit: number): Promise { + return this.getRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, limit); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index f20727ce41..b6fc4b3c49 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -5,7 +5,7 @@ import { setImmediate } from 'node:timers/promises'; import * as mfm from 'mfm-js'; -import { In, DataSource } from 'typeorm'; +import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import RE2 from 're2'; @@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -53,8 +53,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; - -const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5); +import { FeaturedService } from '@/core/FeaturedService.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -157,8 +156,8 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.db) private db: DataSource, - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -175,8 +174,8 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.mutedNotesRepository) - private mutedNotesRepository: MutedNotesRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -187,6 +186,9 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -199,6 +201,7 @@ export class NoteCreateService implements OnApplicationShutdown { private hashtagService: HashtagService, private antennaService: AntennaService, private webhookService: WebhookService, + private featuredService: FeaturedService, private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, @@ -251,19 +254,30 @@ export class NoteCreateService implements OnApplicationShutdown { } } - // Renote対象が「ホームまたは全体」以外の公開範囲ならreject - if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { - throw new Error('Renote target is not public or home'); - } + if (data.renote) { + switch (data.renote.visibility) { + case 'public': + // public noteは無条件にrenote可能 + break; + case 'home': + // home noteはhome以下にrenote可能 + if (data.visibility === 'public') { + data.visibility = 'home'; + } + break; + case 'followers': + // 他人のfollowers noteはreject + if (data.renote.userId !== user.id) { + throw new Error('Renote target is not public or home'); + } - // Renote対象がpublicではないならhomeにする - if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') { - data.visibility = 'home'; - } - - // Renote対象がfollowersならfollowersにする - if (data.renote && data.renote.visibility === 'followers') { - data.visibility = 'followers'; + // Renote対象がfollowersならfollowersにする + data.visibility = 'followers'; + break; + case 'specified': + // specified / direct noteはreject + throw new Error('Renote target is not public or home'); + } } // 返信対象がpublicではないならhomeにする @@ -334,7 +348,7 @@ export class NoteCreateService implements OnApplicationShutdown { const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); if (data.channel) { - this.redisClient.xadd( + this.redisForTimelines.xadd( `channelTimeline:${data.channel.id}`, 'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(), '*', @@ -480,26 +494,11 @@ export class NoteCreateService implements OnApplicationShutdown { // Increment notes count (user) this.incNotesCountOfUser(user); - // Word mute - mutedWordsCache.fetch(() => this.userProfilesRepository.find({ - where: { - enableWordMute: true, - }, - select: ['userId', 'mutedWords'], - })).then(us => { - for (const u of us) { - checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { - if (shouldMute) { - this.mutedNotesRepository.insert({ - id: this.idService.genId(), - userId: u.userId, - noteId: note.id, - reason: 'word', - }); - } - }); - } - }); + if (data.visibility === 'specified') { + // TODO? + } else { + this.pushToTl(note, user); + } this.antennaService.addNoteToAntennas(note, user); @@ -508,11 +507,13 @@ export class NoteCreateService implements OnApplicationShutdown { } if (data.reply == null) { + // TODO: キャッシュ this.followingsRepository.findBy({ followeeId: user.id, notify: 'normal', }).then(followings => { for (const following of followings) { + // TODO: ワードミュート考慮 this.notificationService.createNotification(following.followerId, 'note', { noteId: note.id, }, user.id); @@ -520,9 +521,8 @@ export class NoteCreateService implements OnApplicationShutdown { }); } - // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき - if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) { - if (!user.isBot) this.incRenoteCount(data.renote); + if (data.renote && data.renote.userId !== user.id && !user.isBot) { + this.incRenoteCount(data.renote); } if (data.poll && data.poll.expiresAt) { @@ -722,10 +722,19 @@ export class NoteCreateService implements OnApplicationShutdown { this.notesRepository.createQueryBuilder().update() .set({ renoteCount: () => '"renoteCount" + 1', - score: () => '"score" + 1', }) .where('id = :id', { id: renote.id }) .execute(); + + // 30%の確率でハイライト用ランキング更新 + if (Math.random() < 0.3) { + if (renote.channelId != null) { + this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5); + } else if (renote.visibility === 'public' && renote.userHost == null) { + this.featuredService.updateGlobalNotesRanking(renote.id, 5); + this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5); + } + } } @bindThis @@ -811,6 +820,211 @@ export class NoteCreateService implements OnApplicationShutdown { return mentionedUsers; } + @bindThis + private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { + const meta = await this.metaService.fetch(); + + const redisPipeline = this.redisForTimelines.pipeline(); + + if (note.channelId) { + redisPipeline.xadd( + `userTimelineWithChannel:${user.id}`, + 'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(), + '*', + 'note', note.id); + + const channelFollowings = await this.channelFollowingsRepository.find({ + where: { + followeeId: note.channelId, + }, + select: ['followerId'], + }); + + for (const channelFollowing of channelFollowings) { + redisPipeline.xadd( + `homeTimeline:${channelFollowing.followerId}`, + 'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(), + '*', + 'note', note.id); + + if (note.fileIds.length > 0) { + redisPipeline.xadd( + `homeTimelineWithFiles:${channelFollowing.followerId}`, + 'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(), + '*', + 'note', note.id); + } + } + } else { + // TODO: キャッシュ? + const followings = await this.followingsRepository.find({ + where: { + followeeId: user.id, + followerHost: IsNull(), + isFollowerHibernated: false, + }, + select: ['followerId', 'withReplies'], + }); + + const userListMemberships = await this.userListMembershipsRepository.find({ + where: { + userId: user.id, + }, + select: ['userListId', 'withReplies'], + }); + + // TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする + for (const following of followings) { + // 自分自身以外への返信 + if (note.replyId && note.replyUserId !== note.userId) { + if (!following.withReplies) continue; + } + + redisPipeline.xadd( + `homeTimeline:${following.followerId}`, + 'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(), + '*', + 'note', note.id); + + if (note.fileIds.length > 0) { + redisPipeline.xadd( + `homeTimelineWithFiles:${following.followerId}`, + 'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(), + '*', + 'note', note.id); + } + } + + // TODO + //if (note.visibility === 'followers') { + // // TODO: 重そうだから何とかしたい Set 使う? + // userLists = userLists.filter(x => followings.some(f => f.followerId === x.userListUserId)); + //} + + for (const userListMembership of userListMemberships) { + // 自分自身以外への返信 + if (note.replyId && note.replyUserId !== note.userId) { + if (!userListMembership.withReplies) continue; + } + + redisPipeline.xadd( + `userListTimeline:${userListMembership.userListId}`, + 'MAXLEN', '~', meta.perUserListTimelineCacheMax.toString(), + '*', + 'note', note.id); + + if (note.fileIds.length > 0) { + redisPipeline.xadd( + `userListTimelineWithFiles:${userListMembership.userListId}`, + 'MAXLEN', '~', (meta.perUserListTimelineCacheMax / 2).toString(), + '*', + 'note', note.id); + } + } + + { // 自分自身のHTL + redisPipeline.xadd( + `homeTimeline:${user.id}`, + 'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(), + '*', + 'note', note.id); + + if (note.fileIds.length > 0) { + redisPipeline.xadd( + `homeTimelineWithFiles:${user.id}`, + 'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(), + '*', + 'note', note.id); + } + } + + // 自分自身以外への返信 + if (note.replyId && note.replyUserId !== note.userId) { + redisPipeline.xadd( + `userTimelineWithReplies:${user.id}`, + 'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(), + '*', + 'note', note.id); + } else { + redisPipeline.xadd( + `userTimeline:${user.id}`, + 'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(), + '*', + 'note', note.id); + + if (note.fileIds.length > 0) { + redisPipeline.xadd( + `userTimelineWithFiles:${user.id}`, + 'MAXLEN', '~', note.userHost == null ? (meta.perLocalUserUserTimelineCacheMax / 2).toString() : (meta.perRemoteUserUserTimelineCacheMax / 2).toString(), + '*', + 'note', note.id); + } + + if (note.visibility === 'public' && note.userHost == null) { + redisPipeline.xadd( + 'localTimeline', + 'MAXLEN', '~', '1000', + '*', + 'note', note.id); + + if (note.fileIds.length > 0) { + redisPipeline.xadd( + 'localTimelineWithFiles', + 'MAXLEN', '~', '500', + '*', + 'note', note.id); + } + } + } + + if (Math.random() < 0.1) { + process.nextTick(() => { + this.checkHibernation(followings); + }); + } + } + + redisPipeline.exec(); + } + + @bindThis + public async checkHibernation(followings: MiFollowing[]) { + if (followings.length === 0) return; + + const shuffle = (array: MiFollowing[]) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + }; + + // ランダムに最大1000件サンプリング + const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000)); + + const hibernatedUsers = await this.usersRepository.find({ + where: { + id: In(samples.map(x => x.followerId)), + lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))), + }, + select: ['id'], + }); + + if (hibernatedUsers.length > 0) { + this.usersRepository.update({ + id: In(hibernatedUsers.map(x => x.id)), + }, { + isHibernated: true, + }); + + this.followingsRepository.update({ + followerId: In(hibernatedUsers.map(x => x.id)), + }, { + isFollowerHibernated: true, + }); + } + } + @bindThis public dispose(): void { this.#shutdownController.abort(); diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 87979f22ac..9a817ffd76 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -64,12 +64,6 @@ export class NoteDeleteService { const deletedAt = new Date(); const cascadingNotes = await this.findCascadingNotes(note); - // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき - if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) { - this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1); - if (!user.isBot) this.notesRepository.decrement({ id: note.renoteId }, 'score', 1); - } - if (note.replyId) { await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); } diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index ca05989a4a..32d54d2576 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -99,19 +99,19 @@ export class NotificationService implements OnApplicationShutdown { } if (recieveConfig?.type === 'following') { - const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)); + const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)); if (!isFollowing) { return null; } } else if (recieveConfig?.type === 'follower') { - const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)); + const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)); if (!isFollower) { return null; } } else if (recieveConfig?.type === 'mutualFollow') { const [isFollowing, isFollower] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)), - this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)), + this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)), + this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)), ]); if (!isFollowing && !isFollower) { return null; diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 9145726f86..18bd49286e 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets, ObjectLiteral } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiUser } from '@/models/User.js'; -import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js'; +import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import type { SelectQueryBuilder } from 'typeorm'; @@ -23,9 +23,6 @@ export class QueryService { @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, - @Inject(DI.mutedNotesRepository) - private mutedNotesRepository: MutedNotesRepository, - @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -108,39 +105,6 @@ export class QueryService { q.setParameters(blockedQuery.getParameters()); } - @bindThis - public generateChannelQuery(q: SelectQueryBuilder, me?: { id: MiUser['id'] } | null): void { - if (me == null) { - q.andWhere('note.channelId IS NULL'); - } else { - q.leftJoinAndSelect('note.channel', 'channel'); - - const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') - .select('channelFollowing.followeeId') - .where('channelFollowing.followerId = :followerId', { followerId: me.id }); - - q.andWhere(new Brackets(qb => { qb - // チャンネルのノートではない - .where('note.channelId IS NULL') - // または自分がフォローしているチャンネルのノート - .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); - })); - - q.setParameters(channelFollowingQuery.getParameters()); - } - } - - @bindThis - public generateMutedNoteQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { - const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') - .select('muted.noteId') - .where('muted.userId = :userId', { userId: me.id }); - - q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - - q.setParameters(mutedQuery.getParameters()); - } - @bindThis public generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') @@ -212,32 +176,6 @@ export class QueryService { q.setParameters(mutingQuery.getParameters()); } - @bindThis - public generateRepliesQuery(q: SelectQueryBuilder, withReplies: boolean, me?: Pick | null): void { - if (me == null) { - q.andWhere(new Brackets(qb => { qb - .where('note.replyId IS NULL') // 返信ではない - .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })); - })); - } else if (!withReplies) { - q.andWhere(new Brackets(qb => { qb - .where('note.replyId IS NULL') // 返信ではない - .orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信 - .orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.userId = :meId', { meId: me.id }); - })) - .orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 - .where('note.replyId IS NOT NULL') - .andWhere('note.replyUserId = note.userId'); - })); - })); - } - } - @bindThis public generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: MiUser['id'] } | null): void { // This code must always be synchronized with the checks in Notes.isVisibleForMe. diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 25464b19a8..63cf4be322 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; @@ -26,6 +27,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { RoleService } from '@/core/RoleService.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; const FALLBACK = '❤'; @@ -66,6 +68,9 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/; @Injectable() export class ReactionService { constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -86,6 +91,7 @@ export class ReactionService { private noteEntityService: NoteEntityService, private userBlockingService: UserBlockingService, private idService: IdService, + private featuredService: FeaturedService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, @@ -182,11 +188,20 @@ export class ReactionService { await this.notesRepository.createQueryBuilder().update() .set({ reactions: () => sql, - ... (!user.isBot ? { score: () => '"score" + 1' } : {}), }) .where('id = :id', { id: note.id }) .execute(); + // 30%の確率でハイライト用ランキング更新 + if (Math.random() < 0.3 && note.userId !== user.id) { + if (note.channelId != null) { + this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1); + } else if (note.visibility === 'public' && note.userHost == null) { + this.featuredService.updateGlobalNotesRanking(note.id, 1); + this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1); + } + } + const meta = await this.metaService.fetch(); if (meta.enableChartsForRemoteUser || (user.host == null)) { @@ -275,8 +290,6 @@ export class ReactionService { .where('id = :id', { id: note.id }) .execute(); - if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); - this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, userId: user.id, diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index ad40fbaecd..f2bd9de5ee 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -26,7 +26,6 @@ export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; canPublicNote: boolean; - canEditNote: boolean; canInvite: boolean; inviteLimit: number; inviteLimitCycle: number; @@ -52,7 +51,6 @@ export const DEFAULT_POLICIES: RolePolicies = { gtlAvailable: true, ltlAvailable: true, canPublicNote: true, - canEditNote: true, canInvite: false, inviteLimit: 0, inviteLimitCycle: 60 * 24 * 7, @@ -298,7 +296,6 @@ export class RoleService implements OnApplicationShutdown { gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), - canEditNote: calc('canEditNote', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 37031e341e..087dfd9214 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -11,7 +11,7 @@ import type { MiBlocking } from '@/models/Blocking.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; +import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js'; import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -38,8 +38,8 @@ export class UserBlockingService implements OnModuleInit { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private cacheService: CacheService, private userEntityService: UserEntityService, @@ -149,7 +149,7 @@ export class UserBlockingService implements OnModuleInit { }); for (const userList of userLists) { - await this.userListJoiningsRepository.delete({ + await this.userListMembershipsRepository.delete({ userListId: userList.id, userId: user.id, }); diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 230f6ef261..beffcc2e9c 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -123,7 +123,11 @@ export class UserFollowingService implements OnModuleInit { // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく - if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) { + if ( + followee.isLocked || + (followeeProfile.carefulBot && follower.isBot) || + (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') + ) { let autoAccept = false; // 鍵アカウントであっても、既にフォローされていた場合はスルー diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 93dc5edbba..bece1e442e 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -5,10 +5,10 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { UserListJoiningsRepository } from '@/models/_.js'; +import type { UserListMembershipsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import type { MiUserList } from '@/models/UserList.js'; -import type { MiUserListJoining } from '@/models/UserListJoining.js'; +import type { MiUserListMembership } from '@/models/UserListMembership.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -33,8 +33,8 @@ export class UserListService implements OnApplicationShutdown { @Inject(DI.redisForSub) private redisForSub: Redis.Redis, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private userEntityService: UserEntityService, private idService: IdService, @@ -46,7 +46,7 @@ export class UserListService implements OnApplicationShutdown { this.membersCache = new RedisKVCache>(this.redisClient, 'userListMembers', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m - fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))), + fetcher: (key) => this.userListMembershipsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))), toRedisConverter: (value) => JSON.stringify(Array.from(value)), fromRedisConverter: (value) => new Set(JSON.parse(value)), }); @@ -85,19 +85,19 @@ export class UserListService implements OnApplicationShutdown { @bindThis public async addMember(target: MiUser, list: MiUserList, me: MiUser) { - const currentCount = await this.userListJoiningsRepository.countBy({ + const currentCount = await this.userListMembershipsRepository.countBy({ userListId: list.id, }); if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { throw new UserListService.TooManyUsersError(); } - await this.userListJoiningsRepository.insert({ + await this.userListMembershipsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), userId: target.id, userListId: list.id, - } as MiUserListJoining); + } as MiUserListMembership); this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id }); this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); @@ -113,7 +113,7 @@ export class UserListService implements OnApplicationShutdown { @bindThis public async removeMember(target: MiUser, list: MiUserList) { - await this.userListJoiningsRepository.delete({ + await this.userListMembershipsRepository.delete({ userId: target.id, userListId: list.id, }); @@ -122,6 +122,24 @@ export class UserListService implements OnApplicationShutdown { this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target)); } + @bindThis + public async updateMembership(target: MiUser, list: MiUserList, options: { withReplies?: boolean }) { + const membership = await this.userListMembershipsRepository.findOneBy({ + userId: target.id, + userListId: list.id, + }); + + if (membership == null) { + throw new Error('User is not a member of the list'); + } + + await this.userListMembershipsRepository.update({ + id: membership.id, + }, { + withReplies: options.withReplies, + }); + } + @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts new file mode 100644 index 0000000000..d16e1be615 --- /dev/null +++ b/packages/backend/src/core/UserService.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class UserService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + ) { + } + + @bindThis + public async updateLastActiveDate(user: MiUser): Promise { + if (user.isHibernated) { + const result = await this.usersRepository.createQueryBuilder().update() + .set({ + lastActiveDate: new Date(), + }) + .where('id = :id', { id: user.id }) + .returning('*') + .execute() + .then((response) => { + return response.raw[0]; + }); + const wokeUp = result.isHibernated; + if (wokeUp) { + this.usersRepository.update(user.id, { + isHibernated: false, + }); + this.followingsRepository.update({ + followerId: user.id, + }, { + isFollowerHibernated: false, + }); + } + } else { + this.usersRepository.update(user.id, { + lastActiveDate: new Date(), + }); + } + } +} diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index a024286b48..e45a7992bb 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -98,13 +98,13 @@ export class NoteEntityService implements OnModuleInit { } else if (meId === packedNote.userId) { hide = false; } else if (packedNote.reply && (meId === packedNote.reply.userId)) { - // 自分の投稿に対するリプライ + // 自分の投稿に対するリプライ hide = false; } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { - // 自分へのメンション + // 自分へのメンション hide = false; } else { - // フォロワーかどうか + // フォロワーかどうか const isFollowing = await this.followingsRepository.exist({ where: { followeeId: packedNote.userId, @@ -308,7 +308,6 @@ export class NoteEntityService implements OnModuleInit { const packed: Packed<'Note'> = await awaitAll({ id: note.id, createdAt: note.createdAt.toISOString(), - updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined, userId: note.userId, user: this.userEntityService.pack(note.user ?? note.userId, me, { detail: false, @@ -451,19 +450,4 @@ export class NoteEntityService implements OnModuleInit { } return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[]; } - - @bindThis - public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise { - // 指定したユーザーの指定したノートのリノートがいくつあるか数える - const query = this.notesRepository.createQueryBuilder('note') - .where('note.userId = :userId', { userId }) - .andWhere('note.renoteId = :renoteId', { renoteId }); - - // 指定した投稿を除く - if (excludeNoteId) { - query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); - } - - return await query.getCount(); - } } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index a47b3d51ac..ee67634da5 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -146,64 +146,76 @@ export class UserEntityService implements OnModuleInit { @bindThis public async getRelation(me: MiUser['id'], target: MiUser['id']) { - const following = await this.followingsRepository.findOneBy({ - followerId: me, - followeeId: target, - }); - return awaitAll({ - id: target, + const [ following, - isFollowing: following != null, - isFollowed: this.followingsRepository.count({ + isFollowed, + hasPendingFollowRequestFromYou, + hasPendingFollowRequestToYou, + isBlocking, + isBlocked, + isMuted, + isRenoteMuted, + ] = await Promise.all([ + this.followingsRepository.findOneBy({ + followerId: me, + followeeId: target, + }), + this.followingsRepository.exist({ where: { followerId: target, followeeId: me, }, - take: 1, - }).then(n => n > 0), - hasPendingFollowRequestFromYou: this.followRequestsRepository.count({ + }), + this.followRequestsRepository.exist({ where: { followerId: me, followeeId: target, }, - take: 1, - }).then(n => n > 0), - hasPendingFollowRequestToYou: this.followRequestsRepository.count({ + }), + this.followRequestsRepository.exist({ where: { followerId: target, followeeId: me, }, - take: 1, - }).then(n => n > 0), - isBlocking: this.blockingsRepository.count({ + }), + this.blockingsRepository.exist({ where: { blockerId: me, blockeeId: target, }, - take: 1, - }).then(n => n > 0), - isBlocked: this.blockingsRepository.count({ + }), + this.blockingsRepository.exist({ where: { blockerId: target, blockeeId: me, }, - take: 1, - }).then(n => n > 0), - isMuted: this.mutingsRepository.count({ + }), + this.mutingsRepository.exist({ where: { muterId: me, muteeId: target, }, - take: 1, - }).then(n => n > 0), - isRenoteMuted: this.renoteMutingsRepository.count({ + }), + this.renoteMutingsRepository.exist({ where: { muterId: me, muteeId: target, }, - take: 1, - }).then(n => n > 0), - }); + }), + ]); + + return { + id: target, + following, + isFollowing: following != null, + isFollowed, + hasPendingFollowRequestFromYou, + hasPendingFollowRequestToYou, + isBlocking, + isBlocked, + isMuted, + isRenoteMuted, + }; } @bindThis @@ -290,24 +302,6 @@ export class UserEntityService implements OnModuleInit { const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src }); - // migration - if (user.avatarId != null && user.avatarUrl === null) { - const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId }); - user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar'); - this.usersRepository.update(user.id, { - avatarUrl: user.avatarUrl, - avatarBlurhash: avatar.blurhash, - }); - } - if (user.bannerId != null && user.bannerUrl === null) { - const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId }); - user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner); - this.usersRepository.update(user.id, { - bannerUrl: user.bannerUrl, - bannerBlurhash: banner.blurhash, - }); - } - const meId = me ? me.id : null; const isMe = meId === user.id; const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; @@ -487,6 +481,7 @@ export class UserEntityService implements OnModuleInit { isMuted: relation.isMuted, isRenoteMuted: relation.isRenoteMuted, notify: relation.following?.notify ?? 'none', + withReplies: relation.following?.withReplies ?? false, } : {}), } as Promiseable> as Promiseable>; diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index a7f2885194..06b6e852b1 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -5,11 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/Blocking.js'; import type { MiUserList } from '@/models/UserList.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class UserListEntityService { @@ -17,8 +18,10 @@ export class UserListEntityService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, + + private userEntityService: UserEntityService, ) { } @@ -28,7 +31,7 @@ export class UserListEntityService { ): Promise> { const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src }); - const users = await this.userListJoiningsRepository.findBy({ + const users = await this.userListMembershipsRepository.findBy({ userListId: userList.id, }); @@ -40,5 +43,18 @@ export class UserListEntityService { isPublic: userList.isPublic, }; } + + @bindThis + public async packMembershipsMany( + memberships: MiUserListMembership[], + ) { + return Promise.all(memberships.map(async x => ({ + id: x.id, + createdAt: x.createdAt.toISOString(), + userId: x.userId, + user: await this.userEntityService.pack(x.userId), + withReplies: x.withReplies, + }))); + } } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 72ec98cebe..edcdd21d60 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -10,6 +10,7 @@ export const DI = { redis: Symbol('redis'), redisForPub: Symbol('redisForPub'), redisForSub: Symbol('redisForSub'), + redisForTimelines: Symbol('redisForTimelines'), //#region Repositories usersRepository: Symbol('usersRepository'), @@ -30,7 +31,7 @@ export const DI = { userPublickeysRepository: Symbol('userPublickeysRepository'), userListsRepository: Symbol('userListsRepository'), userListFavoritesRepository: Symbol('userListFavoritesRepository'), - userListJoiningsRepository: Symbol('userListJoiningsRepository'), + userListMembershipsRepository: Symbol('userListMembershipsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'), userIpsRepository: Symbol('userIpsRepository'), usedUsernamesRepository: Symbol('usedUsernamesRepository'), @@ -63,7 +64,6 @@ export const DI = { promoNotesRepository: Symbol('promoNotesRepository'), promoReadsRepository: Symbol('promoReadsRepository'), relaysRepository: Symbol('relaysRepository'), - mutedNotesRepository: Symbol('mutedNotesRepository'), channelsRepository: Symbol('channelsRepository'), channelFollowingsRepository: Symbol('channelFollowingsRepository'), channelFavoritesRepository: Symbol('channelFavoritesRepository'), diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts index edd65a3c1c..6efb1194d3 100644 --- a/packages/backend/src/misc/is-user-related.ts +++ b/packages/backend/src/misc/is-user-related.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export function isUserRelated(note: any, userIds: Set): boolean { - if (userIds.has(note.userId)) { +export function isUserRelated(note: any, userIds: Set, ignoreAuthor = false): boolean { + if (userIds.has(note.userId) && !ignoreAuthor) { return true; } - if (note.reply != null && userIds.has(note.reply.userId)) { + if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) { return true; } - if (note.renote != null && userIds.has(note.renote.userId)) { + if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) { return true; } diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts index 8c9f965fad..607538b1e7 100644 --- a/packages/backend/src/models/Following.ts +++ b/packages/backend/src/models/Following.ts @@ -9,6 +9,7 @@ import { MiUser } from './User.js'; @Entity('following') @Index(['followerId', 'followeeId'], { unique: true }) +@Index(['followeeId', 'followerHost', 'isFollowerHibernated']) export class MiFollowing { @PrimaryColumn(id()) public id: string; @@ -45,6 +46,17 @@ export class MiFollowing { @JoinColumn() public follower: MiUser | null; + @Column('boolean', { + default: false, + }) + public isFollowerHibernated: boolean; + + // タイムラインにその人のリプライまで含めるかどうか + @Column('boolean', { + default: false, + }) + public withReplies: boolean; + @Index() @Column('varchar', { length: 32, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index e69bef8e98..491d446723 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -471,4 +471,24 @@ export class MiMeta { length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', }) public preservedUsernames: string[]; + + @Column('integer', { + default: 300, + }) + public perLocalUserUserTimelineCacheMax: number; + + @Column('integer', { + default: 100, + }) + public perRemoteUserUserTimelineCacheMax: number; + + @Column('integer', { + default: 300, + }) + public perUserHomeTimelineCacheMax: number; + + @Column('integer', { + default: 300, + }) + public perUserListTimelineCacheMax: number; } diff --git a/packages/backend/src/models/MutedNote.ts b/packages/backend/src/models/MutedNote.ts deleted file mode 100644 index 89a678a2a7..0000000000 --- a/packages/backend/src/models/MutedNote.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { mutedNoteReasons } from '@/types.js'; -import { id } from './util/id.js'; -import { MiNote } from './Note.js'; -import { MiUser } from './User.js'; - -@Entity('muted_note') -@Index(['noteId', 'userId'], { unique: true }) -export class MiMutedNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: MiNote['id']; - - @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - /** - * ミュートされた理由。 - */ - @Index() - @Column('enum', { - enum: mutedNoteReasons, - comment: 'The reason of the MutedNote.', - }) - public reason: typeof mutedNoteReasons[number]; -} diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index f396a0cd7a..3e2adf4d82 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -18,17 +18,11 @@ export class MiNote { @PrimaryColumn(id()) public id: string; - @Index() @Column('timestamp with time zone', { comment: 'The created date of the Note.', }) public createdAt: Date; - @Column('timestamp with time zone', { - default: null, - }) - public updatedAt: Date | null; - @Index() @Column({ ...id(), @@ -144,11 +138,6 @@ export class MiNote { }) public url: string | null; - @Column('integer', { - default: 0, select: false, - }) - public score: number; - @Index() @Column({ ...id(), @@ -156,7 +145,6 @@ export class MiNote { }) public fileIds: MiDriveFile['id'][]; - @Index() @Column('varchar', { length: 256, array: true, default: '{}', }) diff --git a/packages/backend/src/models/NoteReaction.ts b/packages/backend/src/models/NoteReaction.ts index 7c08d31c6d..43323f8a43 100644 --- a/packages/backend/src/models/NoteReaction.ts +++ b/packages/backend/src/models/NoteReaction.ts @@ -14,7 +14,6 @@ export class MiNoteReaction { @PrimaryColumn(id()) public id: string; - @Index() @Column('timestamp with time zone', { comment: 'The created date of the NoteReaction.', }) diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 766e7ce21c..9efd6841b1 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -5,7 +5,7 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js'; +import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -117,9 +117,9 @@ const $userListFavoritesRepository: Provider = { inject: [DI.db], }; -const $userListJoiningsRepository: Provider = { - provide: DI.userListJoiningsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserListJoining), +const $userListMembershipsRepository: Provider = { + provide: DI.userListMembershipsRepository, + useFactory: (db: DataSource) => db.getRepository(MiUserListMembership), inject: [DI.db], }; @@ -315,12 +315,6 @@ const $relaysRepository: Provider = { inject: [DI.db], }; -const $mutedNotesRepository: Provider = { - provide: DI.mutedNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiMutedNote), - inject: [DI.db], -}; - const $channelsRepository: Provider = { provide: DI.channelsRepository, useFactory: (db: DataSource) => db.getRepository(MiChannel), @@ -421,7 +415,7 @@ const $userMemosRepository: Provider = { $userPublickeysRepository, $userListsRepository, $userListFavoritesRepository, - $userListJoiningsRepository, + $userListMembershipsRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -454,7 +448,6 @@ const $userMemosRepository: Provider = { $promoNotesRepository, $promoReadsRepository, $relaysRepository, - $mutedNotesRepository, $channelsRepository, $channelFollowingsRepository, $channelFavoritesRepository, @@ -488,7 +481,7 @@ const $userMemosRepository: Provider = { $userPublickeysRepository, $userListsRepository, $userListFavoritesRepository, - $userListJoiningsRepository, + $userListMembershipsRepository, $userNotePiningsRepository, $userIpsRepository, $usedUsernamesRepository, @@ -521,7 +514,6 @@ const $userMemosRepository: Provider = { $promoNotesRepository, $promoReadsRepository, $relaysRepository, - $mutedNotesRepository, $channelsRepository, $channelFollowingsRepository, $channelFavoritesRepository, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index b040d302ce..4d961c4290 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -187,6 +187,11 @@ export class MiUser { }) public isExplorable: boolean; + @Column('boolean', { + default: false, + }) + public isHibernated: boolean; + // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ @Column('boolean', { default: false, diff --git a/packages/backend/src/models/UserListJoining.ts b/packages/backend/src/models/UserListMembership.ts similarity index 76% rename from packages/backend/src/models/UserListJoining.ts rename to packages/backend/src/models/UserListMembership.ts index 4918f2f700..f337f19a47 100644 --- a/packages/backend/src/models/UserListJoining.ts +++ b/packages/backend/src/models/UserListMembership.ts @@ -8,14 +8,14 @@ import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiUserList } from './UserList.js'; -@Entity('user_list_joining') +@Entity('user_list_membership') @Index(['userId', 'userListId'], { unique: true }) -export class MiUserListJoining { +export class MiUserListMembership { @PrimaryColumn(id()) public id: string; @Column('timestamp with time zone', { - comment: 'The created date of the UserListJoining.', + comment: 'The created date of the UserListMembership.', }) public createdAt: Date; @@ -44,4 +44,10 @@ export class MiUserListJoining { }) @JoinColumn() public userList: MiUserList | null; + + // タイムラインにその人のリプライまで含めるかどうか + @Column('boolean', { + default: false, + }) + public withReplies: boolean; } diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 6be7bd0df6..f974f95ed8 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -28,7 +28,6 @@ import { MiHashtag } from '@/models/Hashtag.js'; import { MiInstance } from '@/models/Instance.js'; import { MiMeta } from '@/models/Meta.js'; import { MiModerationLog } from '@/models/ModerationLog.js'; -import { MiMutedNote } from '@/models/MutedNote.js'; import { MiMuting } from '@/models/Muting.js'; import { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { MiNote } from '@/models/Note.js'; @@ -53,7 +52,7 @@ import { MiUser } from '@/models/User.js'; import { MiUserIp } from '@/models/UserIp.js'; import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUserList } from '@/models/UserList.js'; -import { MiUserListJoining } from '@/models/UserListJoining.js'; +import { MiUserListMembership } from '@/models/UserListMembership.js'; import { MiUserNotePining } from '@/models/UserNotePining.js'; import { MiUserPending } from '@/models/UserPending.js'; import { MiUserProfile } from '@/models/UserProfile.js'; @@ -96,7 +95,6 @@ export { MiInstance, MiMeta, MiModerationLog, - MiMutedNote, MiMuting, MiRenoteMuting, MiNote, @@ -122,7 +120,7 @@ export { MiUserKeypair, MiUserList, MiUserListFavorite, - MiUserListJoining, + MiUserListMembership, MiUserNotePining, MiUserPending, MiUserProfile, @@ -163,7 +161,6 @@ export type HashtagsRepository = Repository; export type InstancesRepository = Repository; export type MetasRepository = Repository; export type ModerationLogsRepository = Repository; -export type MutedNotesRepository = Repository; export type MutingsRepository = Repository; export type RenoteMutingsRepository = Repository; export type NotesRepository = Repository; @@ -189,7 +186,7 @@ export type UserIpsRepository = Repository; export type UserKeypairsRepository = Repository; export type UserListsRepository = Repository; export type UserListFavoritesRepository = Repository; -export type UserListJoiningsRepository = Repository; +export type UserListMembershipsRepository = Repository; export type UserNotePiningsRepository = Repository; export type UserPendingsRepository = Repository; export type UserProfilesRepository = Repository; diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index ad0cb3c45d..2caf0d0c3d 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -17,11 +17,6 @@ export const packedNoteSchema = { optional: false, nullable: false, format: 'date-time', }, - updatedAt: { - type: 'string', - optional: true, nullable: true, - format: 'date-time', - }, deletedAt: { type: 'string', optional: true, nullable: true, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 0181ea50e8..57d2d976ff 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -277,6 +277,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'string', nullable: false, optional: true, }, + withReplies: { + type: 'boolean', + nullable: false, optional: true, + }, //#endregion }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 10126eab2b..d4c6ad82ce 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -36,7 +36,6 @@ import { MiHashtag } from '@/models/Hashtag.js'; import { MiInstance } from '@/models/Instance.js'; import { MiMeta } from '@/models/Meta.js'; import { MiModerationLog } from '@/models/ModerationLog.js'; -import { MiMutedNote } from '@/models/MutedNote.js'; import { MiMuting } from '@/models/Muting.js'; import { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { MiNote } from '@/models/Note.js'; @@ -62,7 +61,7 @@ import { MiUserIp } from '@/models/UserIp.js'; import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUserList } from '@/models/UserList.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; -import { MiUserListJoining } from '@/models/UserListJoining.js'; +import { MiUserListMembership } from '@/models/UserListMembership.js'; import { MiUserNotePining } from '@/models/UserNotePining.js'; import { MiUserPending } from '@/models/UserPending.js'; import { MiUserProfile } from '@/models/UserProfile.js'; @@ -138,7 +137,7 @@ export const entities = [ MiUserPublickey, MiUserList, MiUserListFavorite, - MiUserListJoining, + MiUserListMembership, MiUserNotePining, MiUserSecurityKey, MiUsedUsername, @@ -174,7 +173,6 @@ export const entities = [ MiPromoNote, MiPromoRead, MiRelay, - MiMutedNote, MiChannel, MiChannelFollowing, MiChannelFavorite, diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index f0453f7054..e252c5d8a1 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js'; +import type { AntennasRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; @@ -25,9 +25,6 @@ export class CleanProcessorService { @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, - @Inject(DI.mutedNotesRepository) - private mutedNotesRepository: MutedNotesRepository, - @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -48,16 +45,6 @@ export class CleanProcessorService { createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))), }); - this.mutedNotesRepository.delete({ - id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), - reason: 'word', - }); - - this.mutedNotesRepository.delete({ - id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), - reason: 'word', - }); - // 使われてないアンテナを停止 if (this.config.deactivateAntennaThreshold > 0) { this.antennasRepository.update({ diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index f941fb6e85..a0afbee3ba 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { format as DateFormat } from 'date-fns'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, MiUser } from '@/models/_.js'; +import type { AntennasRepository, UsersRepository, UserListMembershipsRepository, MiUser } from '@/models/_.js'; import Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -29,8 +29,8 @@ export class ExportAntennasProcessorService { @Inject(DI.antennasRepository) private antennsRepository: AntennasRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private driveService: DriveService, private utilityService: UtilityService, @@ -65,9 +65,9 @@ export class ExportAntennasProcessorService { for (const [index, antenna] of antennas.entries()) { let users: MiUser[] | undefined; if (antenna.userListId !== null) { - const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId }); + const memberships = await this.userListMembershipsRepository.findBy({ userListId: antenna.userListId }); users = await this.usersRepository.findBy({ - id: In(joinings.map(j => j.userId)), + id: In(memberships.map(j => j.userId)), }); } write(JSON.stringify({ diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index 7baaa7081a..a3f9441dc2 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/_.js'; +import type { UserListMembershipsRepository, UserListsRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; @@ -29,8 +29,8 @@ export class ExportUserListsProcessorService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private utilityService: UtilityService, private driveService: DriveService, @@ -61,9 +61,9 @@ export class ExportUserListsProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); for (const list of lists) { - const joinings = await this.userListJoiningsRepository.findBy({ userListId: list.id }); + const memberships = await this.userListMembershipsRepository.findBy({ userListId: list.id }); const users = await this.usersRepository.findBy({ - id: In(joinings.map(j => j.userId)), + id: In(memberships.map(j => j.userId)), }); for (const u of users) { diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index 60a0d1605f..9be36a9d0d 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -33,8 +33,8 @@ export class ImportUserListsProcessorService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private utilityService: UtilityService, private idService: IdService, @@ -99,7 +99,7 @@ export class ImportUserListsProcessorService { target = await this.remoteUserResolveService.resolveUser(username, host); } - if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; + if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; this.userListService.addMember(target, list!, user); } catch (e) { diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index d17da2208f..523bd0bf86 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -207,7 +207,6 @@ import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; -import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; import * as ep___i_importMuting from './endpoints/i/import-muting.js'; @@ -260,7 +259,6 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; -import * as ep___notes_update from './endpoints/notes/update.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; @@ -330,6 +328,7 @@ import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; +import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; @@ -339,7 +338,9 @@ import * as ep___users_lists_show from './endpoints/users/lists/show.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; -import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; +import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js'; +import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js'; +import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; import * as ep___users_flashs from './endpoints/users/flashs.js'; @@ -559,7 +560,6 @@ const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default }; const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default }; const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default }; -const $i_getWordMutedNotesCount: Provider = { provide: 'ep:i/get-word-muted-notes-count', useClass: ep___i_getWordMutedNotesCount.default }; const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default }; const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default }; const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default }; @@ -612,7 +612,6 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; -const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default }; const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; @@ -682,6 +681,7 @@ const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; +const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default }; const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default }; const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default }; @@ -691,7 +691,9 @@ const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default }; const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default }; const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default }; -const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default }; +const $users_lists_createFromPublic: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_createFromPublic.default }; +const $users_lists_updateMembership: Provider = { provide: 'ep:users/lists/update-membership', useClass: ep___users_lists_updateMembership.default }; +const $users_lists_getMemberships: Provider = { provide: 'ep:users/lists/get-memberships', useClass: ep___users_lists_getMemberships.default }; const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default }; @@ -915,7 +917,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_favorites, $i_gallery_likes, $i_gallery_posts, - $i_getWordMutedNotesCount, $i_importBlocking, $i_importFollowing, $i_importMuting, @@ -968,7 +969,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_conversation, $notes_create, $notes_delete, - $notes_update, $notes_favorites_create, $notes_favorites_delete, $notes_featured, @@ -1038,6 +1038,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_following, $users_gallery_posts, $users_getFrequentlyRepliedUsers, + $users_featuredNotes, $users_lists_create, $users_lists_delete, $users_lists_list, @@ -1047,7 +1048,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_update, $users_lists_favorite, $users_lists_unfavorite, - $users_lists_create_from_public, + $users_lists_createFromPublic, + $users_lists_updateMembership, + $users_lists_getMemberships, $users_notes, $users_pages, $users_flashs, @@ -1265,7 +1268,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_favorites, $i_gallery_likes, $i_gallery_posts, - $i_getWordMutedNotesCount, $i_importBlocking, $i_importFollowing, $i_importMuting, @@ -1318,7 +1320,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_conversation, $notes_create, $notes_delete, - $notes_update, $notes_favorites_create, $notes_favorites_delete, $notes_featured, @@ -1385,6 +1386,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_following, $users_gallery_posts, $users_getFrequentlyRepliedUsers, + $users_featuredNotes, $users_lists_create, $users_lists_delete, $users_lists_list, @@ -1394,7 +1396,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_update, $users_lists_favorite, $users_lists_unfavorite, - $users_lists_create_from_public, + $users_lists_createFromPublic, + $users_lists_updateMembership, + $users_lists_getMemberships, $users_notes, $users_pages, $users_flashs, diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 9acaa688c5..badcec1b33 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -14,6 +14,7 @@ import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; import { MiLocalUser } from '@/models/User.js'; +import { UserService } from '@/core/UserService.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; import MainStreamConnection from './stream/Connection.js'; import { ChannelsService } from './stream/ChannelsService.js'; @@ -37,6 +38,7 @@ export class StreamingApiServerService { private authenticateService: AuthenticateService, private channelsService: ChannelsService, private notificationService: NotificationService, + private usersService: UserService, ) { } @@ -130,14 +132,10 @@ export class StreamingApiServerService { this.#connections.set(connection, Date.now()); const userUpdateIntervalId = user ? setInterval(() => { - this.usersRepository.update(user.id, { - lastActiveDate: new Date(), - }); + this.usersService.updateLastActiveDate(user); }, 1000 * 60 * 5) : null; if (user) { - this.usersRepository.update(user.id, { - lastActiveDate: new Date(), - }); + this.usersService.updateLastActiveDate(user); } connection.once('close', () => { diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 0a426f3767..7087c75ddf 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -206,7 +206,6 @@ import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; -import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; import * as ep___i_importMuting from './endpoints/i/import-muting.js'; @@ -259,7 +258,6 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; -import * as ep___notes_update from './endpoints/notes/update.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; @@ -329,6 +327,7 @@ import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; +import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js'; @@ -337,8 +336,10 @@ import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; -import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; +import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; +import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js'; +import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; import * as ep___users_flashs from './endpoints/users/flashs.js'; @@ -556,7 +557,6 @@ const eps = [ ['i/favorites', ep___i_favorites], ['i/gallery/likes', ep___i_gallery_likes], ['i/gallery/posts', ep___i_gallery_posts], - ['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount], ['i/import-blocking', ep___i_importBlocking], ['i/import-following', ep___i_importFollowing], ['i/import-muting', ep___i_importMuting], @@ -609,7 +609,6 @@ const eps = [ ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], ['notes/delete', ep___notes_delete], - ['notes/update', ep___notes_update], ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], @@ -679,6 +678,7 @@ const eps = [ ['users/following', ep___users_following], ['users/gallery/posts', ep___users_gallery_posts], ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers], + ['users/featured-notes', ep___users_featuredNotes], ['users/lists/create', ep___users_lists_create], ['users/lists/delete', ep___users_lists_delete], ['users/lists/list', ep___users_lists_list], @@ -688,7 +688,9 @@ const eps = [ ['users/lists/favorite', ep___users_lists_favorite], ['users/lists/unfavorite', ep___users_lists_unfavorite], ['users/lists/update', ep___users_lists_update], - ['users/lists/create-from-public', ep___users_lists_create_from_public], + ['users/lists/create-from-public', ep___users_lists_createFromPublic], + ['users/lists/update-membership', ep___users_lists_updateMembership], + ['users/lists/get-memberships', ep___users_lists_getMemberships], ['users/notes', ep___users_notes], ['users/pages', ep___users_pages], ['users/flashs', ep___users_flashs], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 66b8bcbe9d..faab8ee608 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository, EmojisRepository } from '@/models/_.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; @@ -23,11 +23,11 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', }, - duplicationEmojiAdd: { - message: 'This emoji is already added.', - code: 'DUPLICATION_EMOJI_ADD', - id: 'mattyaski_emoji_duplication_error', - } + duplicateName: { + message: 'Duplicate name.', + code: 'DUPLICATE_NAME', + id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', + }, }, } as const; @@ -61,8 +61,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, + private customEmojiService: CustomEmojiService, private emojiEntityService: EmojiEntityService, @@ -70,6 +69,8 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + if (isDuplicate) throw new ApiError(meta.errors.duplicateName); const emoji = await this.customEmojiService.add({ driveFile, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 8bf9076a6c..0bcf30a1c6 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -78,6 +78,15 @@ export default class extends Endpoint { // eslint- driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } + const emoji = await this.customEmojiService.getEmojiById(ps.id); + if (emoji != null) { + if (ps.name !== emoji.name) { + const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); + if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); + } + } else { + throw new ApiError(meta.errors.noSuchEmoji); + } await this.customEmojiService.update(ps.id, { driveFile, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index c3ba07cdd0..53e3672784 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -105,40 +105,32 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - userStarForReactionFallback: { - type: 'boolean', - optional: true, nullable: false, - }, pinnedUsers: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, hiddenTags: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, blockedHosts: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, sensitiveWords: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, preservedUsernames: { @@ -146,129 +138,124 @@ export const meta = { optional: false, nullable: false, items: { type: 'string', - optional: false, nullable: false, }, }, hcaptchaSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, recaptchaSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, turnstileSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, sensitiveMediaDetection: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, sensitiveMediaDetectionSensitivity: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, setSensitiveFlagAutomatically: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableSensitiveMediaDetectionForVideos: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, proxyAccountId: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, format: 'id', }, - summaryProxy: { - type: 'string', - optional: true, nullable: true, - }, email: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpSecure: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, smtpHost: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpPort: { type: 'number', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpUser: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, smtpPass: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, swPrivateKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, useObjectStorage: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageBaseUrl: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageBucket: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStoragePrefix: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageEndpoint: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageRegion: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStoragePort: { type: 'number', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageAccessKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageSecretKey: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, objectStorageUseSSL: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageUseProxy: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, objectStorageSetPublicRead: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableIpLogging: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableActiveEmailValidation: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, enableChartsForRemoteUser: { type: 'boolean', @@ -288,12 +275,28 @@ export const meta = { }, manifestJsonOverride: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: false, }, policies: { type: 'object', optional: false, nullable: false, }, + perLocalUserUserTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perRemoteUserUserTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perUserHomeTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, + perUserListTimelineCacheMax: { + type: 'number', + optional: false, nullable: false, + }, }, }, } as const; @@ -313,7 +316,7 @@ export default class extends Endpoint { // eslint- private metaService: MetaService, ) { - super(meta, paramDef, async (ps, me) => { + super(meta, paramDef, async () => { const instance = await this.metaService.fetch(true); return { @@ -399,6 +402,10 @@ export default class extends Endpoint { // eslint- enableIdenticonGeneration: instance.enableIdenticonGeneration, policies: { ...DEFAULT_POLICIES, ...instance.policies }, manifestJsonOverride: instance.manifestJsonOverride, + perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax, + perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, + perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, + perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 3454597532..0731413d05 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -85,6 +85,7 @@ export default class extends Endpoint { // eslint- isModerator: isModerator, isSilenced: isSilenced, isSuspended: user.isSuspended, + isHibernated: user.isHibernated, lastActiveDate: user.lastActiveDate, moderationNote: profile.moderationNote ?? '', signins, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index ea6ebdd1fe..247d3ba4e0 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -108,6 +108,10 @@ export const paramDef = { serverRules: { type: 'array', items: { type: 'string' } }, preservedUsernames: { type: 'array', items: { type: 'string' } }, manifestJsonOverride: { type: 'string' }, + perLocalUserUserTimelineCacheMax: { type: 'integer' }, + perRemoteUserUserTimelineCacheMax: { type: 'integer' }, + perUserHomeTimelineCacheMax: { type: 'integer' }, + perUserListTimelineCacheMax: { type: 'integer' }, }, required: [], } as const; @@ -441,6 +445,22 @@ export default class extends Endpoint { // eslint- set.manifestJsonOverride = ps.manifestJsonOverride; } + if (ps.perLocalUserUserTimelineCacheMax !== undefined) { + set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax; + } + + if (ps.perRemoteUserUserTimelineCacheMax !== undefined) { + set.perRemoteUserUserTimelineCacheMax = ps.perRemoteUserUserTimelineCacheMax; + } + + if (ps.perUserHomeTimelineCacheMax !== undefined) { + set.perUserHomeTimelineCacheMax = ps.perUserHomeTimelineCacheMax; + } + + if (ps.perUserListTimelineCacheMax !== undefined) { + set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index eaae7bff62..63e542cb62 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -56,8 +56,8 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -86,7 +86,7 @@ export default class extends Endpoint { // eslint- }); const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - const noteIdsRes = await this.redisClient.xrevrange( + const noteIdsRes = await this.redisForTimelines.xrevrange( `antennaTimeline:${antenna.id}`, ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 026b649537..f0b14d4fd8 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -54,8 +54,8 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -79,14 +79,14 @@ export default class extends Endpoint { // eslint- let timeline: MiNote[] = []; - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 let noteIdsRes: [string, string[]][] = []; if (!ps.sinceId && !ps.sinceDate) { - noteIdsRes = await this.redisClient.xrevrange( + noteIdsRes = await this.redisForTimelines.xrevrange( `channelTimeline:${channel.id}`, ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', - '-', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', 'COUNT', limit); } @@ -104,14 +104,13 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); } //#endregion timeline = await query.limit(ps.limit).getMany(); } else { - const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); if (noteIds.length === 0) { return []; @@ -129,7 +128,6 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); } //#endregion diff --git a/packages/backend/src/server/api/endpoints/following/update.ts b/packages/backend/src/server/api/endpoints/following/update.ts index 25f393e517..db17d151df 100644 --- a/packages/backend/src/server/api/endpoints/following/update.ts +++ b/packages/backend/src/server/api/endpoints/following/update.ts @@ -57,8 +57,9 @@ export const paramDef = { properties: { userId: { type: 'string', format: 'misskey:id' }, notify: { type: 'string', enum: ['normal', 'none'] }, + withReplies: { type: 'boolean' }, }, - required: ['userId', 'notify'], + required: ['userId'], } as const; @Injectable() @@ -98,7 +99,8 @@ export default class extends Endpoint { // eslint- await this.followingsRepository.update({ id: exist.id, }, { - notify: ps.notify === 'none' ? null : ps.notify, + notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined, + withReplies: ps.withReplies != null ? ps.withReplies : undefined, }); return await this.userEntityService.pack(follower.id, me); diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts deleted file mode 100644 index d62bfbb3ed..0000000000 --- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutedNotesRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['account'], - - requireCredential: true, - - kind: 'read:account', - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - count: { - type: 'number', - optional: false, nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.mutedNotesRepository) - private mutedNotesRepository: MutedNotesRepository, - ) { - super(meta, paramDef, async (ps, me) => { - return { - count: await this.mutedNotesRepository.countBy({ - userId: me.id, - reason: 'word', - }), - }; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index fa6486ed18..271b3f6fb2 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -214,11 +214,11 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - localTimeLine: { + localTimeline: { type: 'boolean', optional: false, nullable: false, }, - globalTimeLine: { + globalTimeline: { type: 'boolean', optional: false, nullable: false, }, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 137040beb5..117f1952e3 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -58,6 +58,12 @@ export const meta = { id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', }, + cannotRenoteDueToVisibility: { + message: 'You can not Renote due to target visibility.', + code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY', + id: 'be9529e9-fe72-4de0-ae43-0b363c4938af', + }, + noSuchReplyTarget: { message: 'No such reply target.', code: 'NO_SUCH_REPLY_TARGET', @@ -232,6 +238,14 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.youHaveBeenBlocked); } } + + if (renote.visibility === 'followers' && renote.userId !== me.id) { + // 他人のfollowers noteはreject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (renote.visibility === 'specified') { + // specified / direct noteはreject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } } let visibility = ps.visibility; let reply: MiNote | null = null; diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 5283b0e0bc..c456874309 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -6,9 +6,9 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; export const meta = { tags: ['notes'], @@ -32,7 +32,7 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - offset: { type: 'integer', default: 0 }, + untilId: { type: 'string', format: 'misskey:id' }, channelId: { type: 'string', nullable: true, format: 'misskey:id' }, }, required: [], @@ -40,41 +40,53 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export + private globalNotesRankingCache: string[] = []; + private globalNotesRankingCacheLastFetchedAt = 0; + constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, - private queryService: QueryService, + private featuredService: FeaturedService, ) { super(meta, paramDef, async (ps, me) => { - const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで + let noteIds: string[]; + if (ps.channelId) { + noteIds = await this.featuredService.getInChannelNotesRanking(ps.channelId, 50); + } else { + if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) { + noteIds = this.globalNotesRankingCache; + } else { + noteIds = await this.featuredService.getGlobalNotesRanking(100); + this.globalNotesRankingCache = noteIds; + this.globalNotesRankingCacheLastFetchedAt = Date.now(); + } + } + + if (noteIds.length === 0) { + return []; + } + + noteIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + noteIds = noteIds.filter(id => id < ps.untilId!); + } + noteIds = noteIds.slice(0, ps.limit); const query = this.notesRepository.createQueryBuilder('note') - .addSelect('note.score') - .where('note.userHost IS NULL') - .andWhere('note.score > 0') - .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) - .andWhere('note.visibility = \'public\'') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId }); + const notes = await query.getMany(); + notes.sort((a, b) => a.id > b.id ? -1 : 1); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); - - let notes = await query - .orderBy('note.score', 'DESC') - .limit(100) - .getMany(); - - notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - - notes = notes.slice(ps.offset, ps.offset + ps.limit); + // TODO: ミュート等考慮 return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 8784e86153..e5a86905d6 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -40,7 +40,6 @@ export const paramDef = { type: 'object', properties: { withFiles: { type: 'boolean', default: false }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -68,49 +67,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.gtlDisabled); } - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.visibility = \'public\'') - .andWhere('note.channelId IS NULL') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); - - this.queryService.generateRepliesQuery(query, ps.withReplies, me); - if (me) { - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - //#endregion - - const timeline = await query.limit(ps.limit).getMany(); - - process.nextTick(() => { - if (me) { - this.activeUsersChart.read(me); - } - }); - - return await this.noteEntityService.packMany(timeline, me); + // TODO? + return []; }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 9bde5dee21..8e7f2a2a98 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,14 +5,16 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -51,7 +53,6 @@ export const paramDef = { includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, }, required: [], @@ -60,17 +61,17 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private noteEntityService: NoteEntityService, - private queryService: QueryService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, + private cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const policies = await this.roleService.getUserPolicies(me.id); @@ -78,79 +79,77 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.stlDisabled); } - //#region Construct query - const followingQuery = this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]); - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで - .andWhere(new Brackets(qb => { - qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id }) - .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); - })) + let timeline: MiNote[] = []; + + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + let htlNoteIdsRes: [string, string[]][] = []; + let ltlNoteIdsRes: [string, string[]][] = []; + + if (!ps.sinceId && !ps.sinceDate) { + [htlNoteIdsRes, ltlNoteIdsRes] = await Promise.all([ + this.redisForTimelines.xrevrange( + ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit), + this.redisForTimelines.xrevrange( + ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline', + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit), + ]); + } + + const htlNoteIds = htlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); + const ltlNoteIds = ltlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); + let noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); + noteIds.sort((a, b) => a > b ? -1 : 1); + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser') - .setParameters(followingQuery.getParameters()); + .leftJoinAndSelect('note.channel', 'channel'); - this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, ps.withReplies, me); - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + timeline = await query.getMany(); - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + timeline = timeline.filter(note => { + if (note.userId === me.id) { + return true; + } + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } + } - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + return true; + }); - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + // TODO: フィルタした結果件数が足りなかった場合の対応 - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - //#endregion - - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); process.nextTick(() => { this.activeUsersChart.read(me); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 0fefddc51b..ac8f8d610e 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,14 +5,16 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { MiNote, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -41,11 +43,7 @@ export const paramDef = { type: 'object', properties: { withFiles: { type: 'boolean', default: false }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, - fileType: { type: 'array', items: { - type: 'string', - } }, excludeNsfw: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -59,14 +57,17 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, - private queryService: QueryService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, + private cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const policies = await this.roleService.getUserPolicies(me ? me.id : null); @@ -74,56 +75,63 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.ltlDisabled); } - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで - .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]) : [new Set(), new Set(), new Set()]; + + let timeline: MiNote[] = []; + + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + let noteIdsRes: [string, string[]][] = []; + + if (!ps.sinceId && !ps.sinceDate) { + noteIdsRes = await this.redisForTimelines.xrevrange( + ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline', + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit); + } + + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, ps.withReplies, me); - this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateMutedNoteQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); - if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + timeline = await query.getMany(); - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); - } - })); - - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); + timeline = timeline.filter(note => { + if (me && (note.userId === me.id)) { + return true; + } + if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } } - } - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - //#endregion + return true; + }); - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); process.nextTick(() => { if (me) { diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 0d47cc1702..7442356978 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,13 +5,16 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; export const meta = { tags: ['notes'], @@ -41,7 +44,6 @@ export const paramDef = { includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, }, required: [], @@ -50,96 +52,82 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private noteEntityService: NoteEntityService, - private queryService: QueryService, private activeUsersChart: ActiveUsersChart, private idService: IdService, + private cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { - const followees = await this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }) - .getMany(); + const [ + followings, + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = await Promise.all([ + this.cacheService.userFollowingsCache.fetch(me.id), + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]); - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - // パフォーマンス上の利点が無さそう? - //.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで + let timeline: MiNote[] = []; + + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + let noteIdsRes: [string, string[]][] = []; + + if (!ps.sinceId && !ps.sinceDate) { + noteIdsRes = await this.redisForTimelines.xrevrange( + ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit); + } + + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - if (followees.length > 0) { - const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; + timeline = await query.getMany(); - query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); - } else { - query.andWhere('note.userId = :meId', { meId: me.id }); - } + timeline = timeline.filter(note => { + if (note.userId === me.id) { + return true; + } + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } + } + if (note.reply && note.reply.visibility === 'followers') { + if (!Object.hasOwn(followings, note.reply.userId)) return false; + } - this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, ps.withReplies, me); - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + return true; + }); - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + // TODO: フィルタした結果件数が足りなかった場合の対応 - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - //#endregion - - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); process.nextTick(() => { this.activeUsersChart.read(me); diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts deleted file mode 100644 index cdf7f085e0..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import ms from 'ms'; -import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, NotesRepository } from '@/models/_.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteDeleteService } from '@/core/NoteDeleteService.js'; -import { DI } from '@/di-symbols.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['notes'], - - requireCredential: true, - requireRolePolicy: 'canEditNote', - - kind: 'write:notes', - - limit: { - duration: ms('1hour'), - max: 10, - minInterval: ms('1sec'), - }, - - errors: { - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - text: { - type: 'string', - minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, - nullable: false, - }, - cw: { type: 'string', nullable: true, maxLength: 100 }, - }, - required: ['noteId', 'text', 'cw'], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - private getterService: GetterService, - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNote(ps.noteId).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - if (note.userId !== me.id) { - throw new ApiError(meta.errors.noSuchNote); - } - - await this.notesRepository.update({ id: note.id }, { - updatedAt: new Date(), - cw: ps.cw, - text: ps.text, - }); - - this.globalEventService.publishNoteStream(note.id, 'updated', { - cw: ps.cw, - text: ps.text, - }); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index c20274b2ba..d11e9751b3 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -5,12 +5,16 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; +import { CacheService } from '@/core/CacheService.js'; +import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -49,7 +53,6 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', @@ -63,18 +66,19 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, - private noteEntityService: NoteEntityService, - private queryService: QueryService, private activeUsersChart: ActiveUsersChart, + private cacheService: CacheService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { const list = await this.userListsRepository.findOneBy({ @@ -86,72 +90,65 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchList); } - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId') + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.renoteMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]); + + let timeline: MiNote[] = []; + + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + let noteIdsRes: [string, string[]][] = []; + + if (!ps.sinceId && !ps.sinceDate) { + noteIdsRes = await this.redisForTimelines.xrevrange( + ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit); + } + + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser') - .andWhere('userListJoining.userListId = :userListId', { userListId: list.id }); + .leftJoinAndSelect('note.channel', 'channel'); - this.queryService.generateVisibilityQuery(query, me); - this.queryService.generateMutedUserQuery(query, me); - this.queryService.generateMutedNoteQuery(query, me); - this.queryService.generateBlockedUserQuery(query, me); - this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + timeline = await query.getMany(); - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + timeline = timeline.filter(note => { + if (note.userId === me.id) { + return true; + } + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; + if (ps.withRenotes === false) return false; + } + } - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + return true; + }); - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } + // TODO: フィルタした結果件数が足りなかった場合の対応 - if (!ps.withReplies) { - query.andWhere('note.replyId IS NULL'); - } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); this.activeUsersChart.read(me); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 6dc35907e1..f2533efa36 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -53,8 +53,8 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -79,7 +79,7 @@ export default class extends Endpoint { // eslint- return []; } const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 - const noteIdsRes = await this.redisClient.xrevrange( + const noteIdsRes = await this.redisForTimelines.xrevrange( `roleTimeline:${role.id}`, ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts new file mode 100644 index 0000000000..fdf36a6ae0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: false, + allowGet: true, + cacheSec: 3600, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + untilId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private featuredService: FeaturedService, + ) { + super(meta, paramDef, async (ps, me) => { + let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50); + + if (noteIds.length === 0) { + return []; + } + + noteIds.sort((a, b) => a > b ? -1 : 1); + if (ps.untilId) { + noteIds = noteIds.filter(id => id < ps.untilId!); + } + noteIds = noteIds.slice(0, ps.limit); + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + const notes = await query.getMany(); + notes.sort((a, b) => a.id > b.id ? -1 : 1); + + // TODO: ミュート等考慮 + + return await this.noteEntityService.packMany(notes, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index eae55905d3..f2f6c4303a 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js'; +import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import type { MiUserList } from '@/models/UserList.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -76,8 +76,8 @@ export default class extends Endpoint { // eslint- @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -110,7 +110,7 @@ export default class extends Endpoint { // eslint- name: ps.name, } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); - const users = (await this.userListJoiningsRepository.findBy({ + const users = (await this.userListMembershipsRepository.findBy({ userListId: ps.listId, })).map(x => x.userId); @@ -132,7 +132,7 @@ export default class extends Endpoint { // eslint- } } - const exist = await this.userListJoiningsRepository.exist({ + const exist = await this.userListMembershipsRepository.exist({ where: { userListId: userList.id, userId: currentUser.id, diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts new file mode 100644 index 0000000000..ae8b4e9b81 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UserListsRepository, UserListFavoritesRepository, UserListMembershipsRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { QueryService } from '@/core/QueryService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['lists', 'account'], + + requireCredential: false, + + kind: 'read:account', + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + forPublic: { type: 'boolean', default: false }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: ['listId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, + + private userListEntityService: UserListEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the list + const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? { + id: ps.listId, + userId: me.id, + } : { + id: ps.listId, + isPublic: true, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + const query = this.queryService.makePaginationQuery(this.userListMembershipsRepository.createQueryBuilder('membership'), ps.sinceId, ps.untilId) + .andWhere('membership.userListId = :userListId', { userListId: userList.id }) + .innerJoinAndSelect('membership.user', 'user'); + + const memberships = await query + .limit(ps.limit) + .getMany(); + + return this.userListEntityService.packMembershipsMany(memberships); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 72a6a7380d..c4ceec575b 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js'; +import type { UserListsRepository, UserListMembershipsRepository, BlockingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserListService } from '@/core/UserListService.js'; @@ -76,8 +76,8 @@ export default class extends Endpoint { // eslint- @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -115,7 +115,7 @@ export default class extends Endpoint { // eslint- } } - const exist = await this.userListJoiningsRepository.exist({ + const exist = await this.userListMembershipsRepository.exist({ where: { userListId: userList.id, userId: user.id, diff --git a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts new file mode 100644 index 0000000000..b69465b940 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UserListsRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { DI } from '@/di-symbols.js'; +import { UserListService } from '@/core/UserListService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['lists', 'users'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'write:account', + + errors: { + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02', + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '588e7f72-c744-4a61-b180-d354e912bda2', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + withReplies: { type: 'boolean' }, + }, + required: ['listId', 'userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + private userListService: UserListService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + // Fetch the list + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + userId: me.id, + }); + + if (userList == null) { + throw new ApiError(meta.errors.noSuchList); + } + + // Fetch the user + const user = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + await this.userListService.updateMembership(user, userList, { + withReplies: ps.withReplies, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index e660a0bb25..23c2811fb9 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -5,19 +5,20 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import type { MiNote, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { IdService } from '@/core/IdService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { ApiError } from '../../error.js'; export const meta = { tags: ['users', 'notes'], - description: 'Show all notes that this user created.', - res: { type: 'array', optional: false, nullable: false, @@ -43,6 +44,7 @@ export const paramDef = { userId: { type: 'string', format: 'misskey:id' }, withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, + withChannelNotes: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -50,9 +52,6 @@ export const paramDef = { untilDate: { type: 'integer' }, includeMyRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, - fileType: { type: 'array', items: { - type: 'string', - } }, excludeNsfw: { type: 'boolean', default: false }, }, required: ['userId'], @@ -61,87 +60,95 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, - private queryService: QueryService, private getterService: GetterService, + private cacheService: CacheService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - // Lookup user - const user = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); + const [ + userIdsWhoMeMuting, + ] = me ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + ]) : [new Set()]; - //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.userId = :userId', { userId: user.id }) + let timeline: MiNote[] = []; + + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + let noteIdsRes: [string, string[]][] = []; + let repliesNoteIdsRes: [string, string[]][] = []; + let channelNoteIdsRes: [string, string[]][] = []; + + if (!ps.sinceId && !ps.sinceDate) { + [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([ + this.redisForTimelines.xrevrange( + ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit), + ps.withReplies + ? this.redisForTimelines.xrevrange( + `userTimelineWithReplies:${ps.userId}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit) + : Promise.resolve([]), + ps.withChannelNotes + ? this.redisForTimelines.xrevrange( + `userTimelineWithChannel:${ps.userId}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-', + 'COUNT', limit) + : Promise.resolve([]), + ]); + } + + let noteIds = Array.from(new Set([ + ...noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId), + ...repliesNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId), + ...channelNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId), + ])); + noteIds.sort((a, b) => a > b ? -1 : 1); + noteIds = noteIds.slice(0, ps.limit); + + if (noteIds.length === 0) { + return []; + } + + const isFollowing = me ? Object.hasOwn(await this.cacheService.userFollowingsCache.fetch(me.id), ps.userId) : false; + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('note.channel', 'channel') .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); - query.andWhere(new Brackets(qb => { - qb.orWhere('note.channelId IS NULL'); - qb.orWhere('channel.isSensitive = false'); - })); + timeline = await query.getMany(); - this.queryService.generateVisibilityQuery(query, me); - if (me) { - this.queryService.generateMutedUserQuery(query, me, user); - this.queryService.generateBlockedUserQuery(query, me); - } + timeline = timeline.filter(note => { + if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - - if (ps.fileType != null) { - query.andWhere('note.fileIds != \'{}\''); - query.andWhere(new Brackets(qb => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + if (note.renoteId) { + if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { + if (ps.withRenotes === false) return false; } - })); - - if (ps.excludeNsfw) { - query.andWhere('note.cw IS NULL'); - query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); } - } - if (!ps.withReplies) { - query.andWhere('note.replyId IS NULL'); - } + if (note.visibility === 'followers' && !isFollowing) return false; - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } + return true; + }); - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :userId', { userId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - //#endregion - - const timeline = await query.limit(ps.limit).getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); return await this.noteEntityService.packMany(timeline, me); }); diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index a73071ea99..f981e63871 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -11,7 +11,7 @@ import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; -import { MiUserProfile } from '@/models/_.js'; +import { MiFollowing, MiUserProfile } from '@/models/_.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; @@ -30,7 +30,7 @@ export default class Connection { private subscribingNotes: any = {}; private cachedNotes: Packed<'Note'>[] = []; public userProfile: MiUserProfile | null = null; - public following: Set = new Set(); + public following: Record | undefined> = {}; public followingChannels: Set = new Set(); public userIdsWhoMeMuting: Set = new Set(); public userIdsWhoBlockingMe: Set = new Set(); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 05641d6f57..5a0a09cdce 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -69,7 +69,7 @@ class GlobalTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && !this.withReplies) { + if (note.reply && !this.following[note.userId]?.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; @@ -87,13 +87,6 @@ class GlobalTimelineChannel extends Channel { if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 72c066e1e6..0c355b1cd5 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -45,7 +45,7 @@ class HomeTimelineChannel extends Channel { if (!this.followingChannels.has(note.channelId)) return; } else { // その投稿のユーザーをフォローしていなかったら弾く - if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return; + if ((this.user!.id !== note.userId) && !Object.hasOwn(this.following, note.userId)) return; } // Ignore notes from instances the user has muted @@ -78,7 +78,7 @@ class HomeTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && !this.withReplies) { + if (note.reply && !this.following[note.userId]?.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; @@ -93,13 +93,6 @@ class HomeTimelineChannel extends Channel { if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (await checkWordMute(note, this.user, this.userProfile!.mutedWords)) return; - this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 7e7a6ae056..7b0a0d47e8 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -55,7 +55,7 @@ class HybridTimelineChannel extends Channel { // フォローしているチャンネルの投稿 の場合だけ if (!( (note.channelId == null && this.user!.id === note.userId) || - (note.channelId == null && this.following.has(note.userId)) || + (note.channelId == null && Object.hasOwn(this.following, note.userId)) || (note.channelId == null && (note.user.host == null && note.visibility === 'public')) || (note.channelId != null && this.followingChannels.has(note.channelId)) )) return; @@ -90,7 +90,7 @@ class HybridTimelineChannel extends Channel { if (isInstanceMuted(note, new Set(this.userProfile!.mutedInstances ?? []))) return; // 関係ない返信は除外 - if (note.reply && !this.withReplies) { + if (note.reply && !this.following[note.userId]?.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; @@ -105,13 +105,6 @@ class HybridTimelineChannel extends Channel { if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 65bc4c4acc..2c111a31e2 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -69,7 +69,7 @@ class LocalTimelineChannel extends Channel { } // 関係ない返信は除外 - if (note.reply && this.user && !this.withReplies) { + if (note.reply && this.user && !this.following[note.userId]?.withReplies) { const reply = note.reply; // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; @@ -84,13 +84,6 @@ class LocalTimelineChannel extends Channel { if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - this.connection.cacheNote(note); this.send('note', note); diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 8bbba0b6db..03f7760d8e 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -18,12 +18,12 @@ class UserListChannel extends Channel { public static shouldShare = false; public static requireCredential = false; private listId: string; - public listUsers: MiUser['id'][] = []; + public membershipsMap: Record | undefined> = {}; private listUsersClock: NodeJS.Timeout; constructor( private userListsRepository: UserListsRepository, - private userListJoiningsRepository: UserListJoiningsRepository, + private userListMembershipsRepository: UserListMembershipsRepository, private noteEntityService: NoteEntityService, id: string, @@ -58,19 +58,25 @@ class UserListChannel extends Channel { @bindThis private async updateListUsers() { - const users = await this.userListJoiningsRepository.find({ + const memberships = await this.userListMembershipsRepository.find({ where: { userListId: this.listId, }, select: ['userId'], }); - this.listUsers = users.map(x => x.userId); + const membershipsMap: Record | undefined> = {}; + for (const membership of memberships) { + membershipsMap[membership.userId] = { + withReplies: membership.withReplies, + }; + } + this.membershipsMap = membershipsMap; } @bindThis private async onNote(note: Packed<'Note'>) { - if (!this.listUsers.includes(note.userId)) return; + if (!Object.hasOwn(this.membershipsMap, note.userId)) return; if (['followers', 'specified'].includes(note.visibility)) { note = await this.noteEntityService.pack(note.id, this.user, { @@ -95,6 +101,13 @@ class UserListChannel extends Channel { } } + // 関係ない返信は除外 + if (note.reply && !this.membershipsMap[note.userId]?.withReplies) { + const reply = note.reply; + // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 + if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; + } + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する @@ -124,8 +137,8 @@ export class UserListChannelService { @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.userListMembershipsRepository) + private userListMembershipsRepository: UserListMembershipsRepository, private noteEntityService: NoteEntityService, ) { @@ -135,7 +148,7 @@ export class UserListChannelService { public create(id: string, connection: Channel['connection']): UserListChannel { return new UserListChannel( this.userListsRepository, - this.userListJoiningsRepository, + this.userListMembershipsRepository, this.noteEntityService, id, connection, diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index a9b9a55bc0..316073c992 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -171,6 +171,9 @@ export type ModerationLogPayloads = { deleteUserAnnouncement: { announcementId: string; announcement: any; + userId: string; + userUsername: string; + userHost: string | null; }; resetPassword: { userId: string; diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index c9e1ccc304..7d57ba17b6 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -6,7 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; @@ -42,6 +42,9 @@ describe('Renote Mute', () => { const carolRenote = await post(carol, { renoteId: bobNote.id }); const carolNote = await post(carol, { text: 'hi' }); + // redisに追加されるのを待つ + await sleep(100); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); @@ -56,6 +59,9 @@ describe('Renote Mute', () => { const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' }); const carolNote = await post(carol, { text: 'hi' }); + // redisに追加されるのを待つ + await sleep(100); + const res = await api('/notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts new file mode 100644 index 0000000000..49256cba34 --- /dev/null +++ b/packages/backend/test/e2e/timelines.ts @@ -0,0 +1,945 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; +process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true'; + +import * as assert from 'assert'; +import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl, randomString } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; + +function genHost() { + return randomString() + '.example.com'; +} + +function waitForPushToTl() { + return sleep(300); +} + +let app: INestApplicationContext; + +beforeAll(async () => { + app = await startServer(); +}, 1000 * 60 * 2); + +afterAll(async () => { + await app.close(); +}); + +describe('Timelines', () => { + describe('Home TL', () => { + test.concurrent('自分の visibility: followers なノートが含まれる', async () => { + const [alice] = await Promise.all([signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); + }); + + test.concurrent('フォローしているユーザーのノートが含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); + const carolNote = await post(carol, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + const carolNote = await post(carol, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/create', { userId: carol.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi'); + }); + + test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/create', { userId: carol.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + }); + + test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }); + + test.concurrent('自分の他人への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi' }); + const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }); + + test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', { + withRenotes: false, + }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', { + withRenotes: false, + }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const [bobFile, carolFile] = await Promise.all([ + uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), + uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), + ]); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [bobFile.id] }); + const carolNote1 = await post(carol, { text: 'hi' }); + const carolNote2 = await post(carol, { fileIds: [carolFile.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', { withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false); + }, 1000 * 10); + + test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/notes/timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + }); + + describe('Local TL', () => { + test.concurrent('visibility: home なノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('チャンネル投稿が含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('リモートユーザーのノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + // 含まれても良いと思うけど実装が面倒なので含まれない + test.concurrent('フォローしているユーザーの visibility: home なノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('ミュートしているユーザーのノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await api('/following/update', { userId: bob.id, withReplies: true }, alice); + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', { withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }, 1000 * 10); + }); + + describe('Social TL', () => { + test.concurrent('ローカルユーザーのノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リモートユーザーのノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', {}, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/hybrid-timeline', { withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }, 1000 * 10); + }); + + describe('User List TL', () => { + test.concurrent('リスインしているフォローしていないユーザーのノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + /* 未実装 + test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + */ + + test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれるが隠される', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, null); + }); + + test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }); + + test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('/users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + }); + + test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('/notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }, 1000 * 10); + }); + + describe('User TL', () => { + test.concurrent('ノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/following/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + }); + + test.concurrent('チャンネル投稿が含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); + }); + + test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }); + + test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified' }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); + }); + + test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withFiles: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + }, 1000 * 10); + + test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('/mute/create', { userId: carol.id }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + + test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('/mute/create', { userId: bob.id }, alice); + await sleep(1000); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true); + }); + }); + + // TODO: リノートミュート済みユーザーのテスト + // TODO: ページネーションのテスト +}); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index 121070787d..b5f00a6327 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -38,23 +38,10 @@ describe('users/notes', () => { await app.close(); }); - test('ファイルタイプ指定 (jpg)', async () => { + test('withFiles', async () => { const res = await api('/users/notes', { userId: alice.id, - fileType: ['image/jpeg'], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 2); - assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); - }); - - test('ファイルタイプ指定 (jpg or png)', async () => { - const res = await api('/users/notes', { - userId: alice.id, - fileType: ['image/jpeg', 'image/png'], + withFiles: true, }, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 0f5d5f7344..53db1ac28a 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -133,6 +133,7 @@ describe('ユーザー', () => { isMuted: user.isMuted ?? false, isRenoteMuted: user.isRenoteMuted ?? false, notify: user.notify ?? 'none', + withReplies: user.withReplies ?? false, }); }; diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index dbc446d12d..2e9454927c 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -92,6 +92,9 @@ describe('ActivityPub', () => { const metaInitial = { cacheRemoteFiles: true, cacheRemoteSensitiveFiles: true, + perUserHomeTimelineCacheMax: 100, + perLocalUserUserTimelineCacheMax: 100, + perRemoteUserUserTimelineCacheMax: 100, blockedHosts: [] as string[], sensitiveWords: [] as string[], } as MiMeta; diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index adc532bbe7..121c4711d3 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -99,9 +99,17 @@ export const relativeFetch = async (path: string, init?: RequestInit | undefined return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init); }; +export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', length = 16) { + let randomString = ''; + for (let i = 0; i < length; i++) { + randomString += chars[Math.floor(Math.random() * chars.length)]; + } + return randomString; +} + export const signup = async (params?: Partial): Promise> => { const q = Object.assign({ - username: 'test', + username: randomString(), password: 'test', }, params); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 9204bda32d..b69ed7aba5 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -18,13 +18,13 @@ "dependencies": { "@discordapp/twemoji": "14.1.2", "@github/webauthn-json": "2.1.1", - "@rollup/plugin-alias": "5.0.0", - "@rollup/plugin-json": "6.0.0", - "@rollup/plugin-replace": "5.0.2", - "@rollup/pluginutils": "5.0.4", + "@rollup/plugin-alias": "5.0.1", + "@rollup/plugin-json": "6.0.1", + "@rollup/plugin-replace": "5.0.3", + "@rollup/pluginutils": "5.0.5", "@syuilo/aiscript": "0.16.0", "@tabler/icons-webfont": "2.37.0", - "@vitejs/plugin-vue": "4.3.4", + "@vitejs/plugin-vue": "4.4.0", "@vue-macros/reactivity-transform": "0.3.23", "@vue/compiler-sfc": "3.3.4", "@vueuse/core": "^10.4.1", @@ -39,7 +39,7 @@ "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "7.2.0", + "chromatic": "7.2.2", "compare-versions": "6.1.0", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", @@ -54,13 +54,13 @@ "matter-js": "0.19.0", "mfm-js": "0.23.3", "misskey-js": "workspace:*", - "photoswipe": "5.4.1", + "photoswipe": "5.4.2", "prismjs": "1.29.0", "punycode": "2.3.0", "querystring": "0.2.1", - "rollup": "3.29.4", + "rollup": "4.0.0", "sanitize-html": "2.11.0", - "sass": "1.68.0", + "sass": "1.69.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", "three": "0.157.0", @@ -73,37 +73,37 @@ "uuid": "9.0.1", "v-code-diff": "1.7.1", "vanilla-tilt": "1.8.1", - "vite": "4.4.9", + "vite": "4.4.11", "vue": "3.3.4", "vue-multiselect": "^2.1.7", "vue-prism-editor": "2.0.0-alpha.2", "vuedraggable": "next" }, "devDependencies": { - "@storybook/addon-actions": "7.4.5", - "@storybook/addon-essentials": "7.4.5", - "@storybook/addon-interactions": "7.4.5", - "@storybook/addon-links": "7.4.5", - "@storybook/addon-storysource": "7.4.5", - "@storybook/addons": "7.4.5", - "@storybook/blocks": "7.4.5", - "@storybook/core-events": "7.4.5", - "@storybook/jest": "0.2.2", - "@storybook/manager-api": "7.4.5", - "@storybook/preview-api": "7.4.5", - "@storybook/react": "7.4.5", - "@storybook/react-vite": "7.4.5", - "@storybook/testing-library": "0.2.1", - "@storybook/theming": "7.4.5", - "@storybook/types": "7.4.5", - "@storybook/vue3": "7.4.5", - "@storybook/vue3-vite": "7.4.5", + "@storybook/addon-actions": "7.4.6", + "@storybook/addon-essentials": "7.4.6", + "@storybook/addon-interactions": "7.4.6", + "@storybook/addon-links": "7.4.6", + "@storybook/addon-storysource": "7.4.6", + "@storybook/addons": "7.4.6", + "@storybook/blocks": "7.4.6", + "@storybook/core-events": "7.4.6", + "@storybook/jest": "0.2.3", + "@storybook/manager-api": "7.4.6", + "@storybook/preview-api": "7.4.6", + "@storybook/react": "7.4.6", + "@storybook/react-vite": "7.4.6", + "@storybook/testing-library": "0.2.2", + "@storybook/theming": "7.4.6", + "@storybook/types": "7.4.6", + "@storybook/vue3": "7.4.6", + "@storybook/vue3-vite": "7.4.6", "@testing-library/vue": "7.0.0", "@types/escape-regexp": "0.0.1", "@types/estree": "1.0.2", "@types/matter-js": "0.19.1", "@types/micromatch": "4.0.3", - "@types/node": "20.7.1", + "@types/node": "20.8.2", "@types/punycode": "2.1.0", "@types/sanitize-html": "2.9.1", "@types/throttle-debounce": "5.0.0", @@ -111,9 +111,9 @@ "@types/uuid": "9.0.4", "@types/websocket": "1.0.7", "@types/ws": "8.5.6", - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", - "@vitest/coverage-v8": "0.34.5", + "@typescript-eslint/eslint-plugin": "6.7.4", + "@typescript-eslint/parser": "6.7.4", + "@vitest/coverage-v8": "0.34.6", "@vue/runtime-core": "3.3.4", "acorn": "8.10.0", "cross-env": "7.0.3", @@ -124,18 +124,18 @@ "fast-glob": "3.3.1", "happy-dom": "10.0.3", "micromatch": "4.0.5", - "msw": "1.3.1", + "msw": "1.3.2", "msw-storybook-addon": "1.8.0", "nodemon": "3.0.1", "prettier": "3.0.3", "react": "18.2.0", "react-dom": "18.2.0", "start-server-and-test": "2.0.1", - "storybook": "7.4.5", + "storybook": "7.4.6", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", "vite-plugin-turbosnap": "1.0.3", - "vitest": "0.34.5", + "vitest": "0.34.6", "vitest-fetch-mock": "0.2.2", "vue-eslint-parser": "9.3.1", "vue-tsc": "1.8.15" diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 611796d035..b8b52c3084 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -186,7 +186,7 @@ import {deepClone} from '@/scripts/clone.js'; import {useTooltip} from '@/scripts/use-tooltip.js'; import {claimAchievement} from '@/scripts/achievements.js'; import {getNoteSummary} from '@/scripts/get-note-summary.js'; -import {MenuItem} from '@/types/menu'; +import {MenuItem} from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import {showMovedDialog} from '@/scripts/show-moved-dialog.js'; import {shouldCollapsed} from '@/scripts/collapsed.js'; @@ -232,11 +232,11 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n const isLong = shouldCollapsed(appearNote); const collapsed = ref(appearNote.cw == null && isLong); const isDeleted = ref(false); -const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords)); +const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const translation = ref(null); const translating = ref(false); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); -const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id)); let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null))); const keymap = { @@ -519,7 +519,6 @@ function focusAfter() { focusNext(el.value); } - function readPromo() { os.api('promo/read', { noteId: appearNote.id, @@ -529,7 +528,6 @@ function readPromo() { diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 80306f48f8..6aff826a04 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -212,6 +212,11 @@ const menuDef = $computed(() => [{ text: i18n.ts.proxyAccount, to: '/admin/proxy-account', active: currentPage?.route.name === 'proxy-account', + }, { + icon: 'ti ti-link', + text: i18n.ts.externalServices, + to: '/admin/external-services', + active: currentPage?.route.name === 'external-services', }, { icon: 'ti ti-adjustments', text: i18n.ts.other, diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 66561c969e..0af226f02e 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -29,8 +29,12 @@ SPDX-License-Identifier: AGPL-3.0-only : @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }} : {{ log.info.host }} : {{ log.info.host }} + : {{ log.info.announcement.title }} + : {{ log.info.before.title }} + : {{ log.info.announcement.title }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} + : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} : @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }} : @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }} @@ -88,6 +92,16 @@ SPDX-License-Identifier: AGPL-3.0-only + +
raw diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 8015bb7a7f..ead2250af2 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -160,26 +160,6 @@ SPDX-License-Identifier: AGPL-3.0-only - - - -
- - - - - - - - - -
-
- - + diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 5db1e4f57c..6937f73e4f 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -24,10 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -53,7 +52,6 @@ let disabled = $ref(false); const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); const withRenotes = $ref(props.column.withRenotes ?? true); -const withReplies = $ref(props.column.withReplies ?? false); const onlyFiles = $ref(props.column.onlyFiles ?? false); watch($$(withRenotes), v => { @@ -62,12 +60,6 @@ watch($$(withRenotes), v => { }); }); -watch($$(withReplies), v => { - updateColumn(props.column.id, { - withReplies: v, - }); -}); - watch($$(onlyFiles), v => { updateColumn(props.column.id, { onlyFiles: v, @@ -118,10 +110,6 @@ const menu = [{ type: 'switch', text: i18n.ts.showRenotes, ref: $$(withRenotes), -}, { - type: 'switch', - text: i18n.ts.withReplies, - ref: $$(withReplies), }, { type: 'switch', text: i18n.ts.fileAttachedOnly, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index f0fc47c207..fa09fd94e6 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1381,10 +1381,6 @@ export type Endpoints = { req: TODO; res: TODO; }; - 'i/get-word-muted-notes-count': { - req: TODO; - res: TODO; - }; 'i/import-following': { req: TODO; res: TODO; @@ -2643,7 +2639,6 @@ export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"]; type Note = { id: ID; createdAt: DateString; - updatedAt?: DateString | null; text: string | null; cw: string | null; user: User; @@ -2981,9 +2976,9 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/api.types.ts:630:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts -// src/entities.ts:595:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:594:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index cdbb5f38ce..1aefc898f5 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -20,12 +20,12 @@ "url": "git+https://github.com/misskey-dev/misskey.js.git" }, "devDependencies": { - "@microsoft/api-extractor": "7.37.2", + "@microsoft/api-extractor": "7.38.0", "@swc/jest": "0.2.29", "@types/jest": "29.5.5", - "@types/node": "20.7.1", - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", + "@types/node": "20.8.2", + "@typescript-eslint/eslint-plugin": "6.7.4", + "@typescript-eslint/parser": "6.7.4", "eslint": "8.50.0", "jest": "29.7.0", "jest-fetch-mock": "3.0.3", @@ -39,7 +39,7 @@ ], "dependencies": { "@swc/cli": "0.1.62", - "@swc/core": "1.3.90", + "@swc/core": "1.3.92", "eventemitter3": "5.0.1", "reconnecting-websocket": "4.4.0" } diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index e69d8324a1..a7a2ea1b36 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -371,7 +371,6 @@ export type Endpoints = { 'i/favorites': { req: { limit?: number; sinceId?: NoteFavorite['id']; untilId?: NoteFavorite['id']; }; res: NoteFavorite[]; }; 'i/gallery/likes': { req: TODO; res: TODO; }; 'i/gallery/posts': { req: TODO; res: TODO; }; - 'i/get-word-muted-notes-count': { req: TODO; res: TODO; }; 'i/import-following': { req: TODO; res: TODO; }; 'i/import-user-lists': { req: TODO; res: TODO; }; 'i/move': { req: TODO; res: TODO; }; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 271a64274f..ccc55537d2 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -189,6 +189,9 @@ export type ModerationLogPayloads = { deleteUserAnnouncement: { announcementId: string; announcement: any; + userId: string; + userUsername: string; + userHost: string | null; }; resetPassword: { userId: string; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index e6bac2a5f4..8097618c33 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -177,7 +177,6 @@ export type GalleryPost = { export type Note = { id: ID; createdAt: DateString; - updatedAt?: DateString | null; text: string | null; cw: string | null; user: User; diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index ce29a00032..96ac7787e1 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -133,13 +133,6 @@ export type NoteUpdatedEvent = { body: { deletedAt: string; }; -} | { - id: Note['id']; - type: 'updated'; - body: { - cw: string | null; - text: string; - }; } | { id: Note['id']; type: 'pollVoted'; diff --git a/packages/sw/package.json b/packages/sw/package.json index 4499e9f38e..7d4c16c164 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -14,7 +14,7 @@ "misskey-js": "workspace:*" }, "devDependencies": { - "@typescript-eslint/parser": "6.7.3", + "@typescript-eslint/parser": "6.7.4", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67", "eslint": "8.50.0", "eslint-plugin-import": "2.28.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf7a537469..49ac89d59c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: specifier: 8.4.31 version: 8.4.31 terser: - specifier: 5.20.0 - version: 5.20.0 + specifier: 5.21.0 + version: 5.21.0 typescript: specifier: 5.2.2 version: 5.2.2 @@ -36,11 +36,11 @@ importers: version: 4.4.0 devDependencies: '@typescript-eslint/eslint-plugin': - specifier: 6.7.3 - version: 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(eslint@8.50.0)(typescript@5.2.2) cross-env: specifier: 7.0.3 version: 7.0.3 @@ -99,14 +99,14 @@ importers: specifier: 8.2.0 version: 8.2.0 '@nestjs/common': - specifier: 10.2.6 - version: 10.2.6(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 10.2.7 + version: 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': - specifier: 10.2.6 - version: 10.2.6(@nestjs/common@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 10.2.7 + version: 10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/testing': - specifier: 10.2.6 - version: 10.2.6(@nestjs/common@10.2.6)(@nestjs/core@10.2.6) + specifier: 10.2.7 + version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 @@ -121,10 +121,10 @@ importers: version: 2.1.5 '@swc/cli': specifier: 0.1.62 - version: 0.1.62(@swc/core@1.3.90)(chokidar@3.5.3) + version: 0.1.62(@swc/core@1.3.92)(chokidar@3.5.3) '@swc/core': - specifier: 1.3.90 - version: 1.3.90 + specifier: 1.3.92 + version: 1.3.92 accepts: specifier: 1.3.8 version: 1.3.8 @@ -147,8 +147,8 @@ importers: specifier: 1.20.2 version: 1.20.2 bullmq: - specifier: 4.11.4 - version: 4.11.4 + specifier: 4.12.2 + version: 4.12.2 cacheable-lookup: specifier: 7.0.0 version: 7.0.0 @@ -354,8 +354,8 @@ importers: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/d2d8db49943ccb201c1b1b283e9d0a630519fac7 systeminformation: - specifier: 5.21.9 - version: 5.21.9 + specifier: 5.21.11 + version: 5.21.11 tinycolor2: specifier: 1.6.0 version: 1.6.0 @@ -489,7 +489,7 @@ importers: version: 8.0.0 '@swc/jest': specifier: 0.2.29 - version: 0.2.29(@swc/core@1.3.90) + version: 0.2.29(@swc/core@1.3.92) '@types/accepts': specifier: 1.3.5 version: 1.3.5 @@ -539,8 +539,8 @@ importers: specifier: 0.7.32 version: 0.7.32 '@types/node': - specifier: 20.7.1 - version: 20.7.1 + specifier: 20.8.2 + version: 20.8.2 '@types/node-fetch': specifier: 3.0.3 version: 3.0.3 @@ -608,11 +608,11 @@ importers: specifier: 8.5.6 version: 8.5.6 '@typescript-eslint/eslint-plugin': - specifier: 6.7.3 - version: 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(eslint@8.50.0)(typescript@5.2.2) aws-sdk-client-mock: specifier: 3.0.0 version: 3.0.0 @@ -624,13 +624,13 @@ importers: version: 8.50.0 eslint-plugin-import: specifier: 2.28.1 - version: 2.28.1(@typescript-eslint/parser@6.7.3)(eslint@8.50.0) + version: 2.28.1(@typescript-eslint/parser@6.7.4)(eslint@8.50.0) execa: specifier: 8.0.1 version: 8.0.1 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.7.1) + version: 29.7.0(@types/node@20.8.2) jest-mock: specifier: 29.7.0 version: 29.7.0 @@ -647,17 +647,17 @@ importers: specifier: 2.1.1 version: 2.1.1 '@rollup/plugin-alias': - specifier: 5.0.0 - version: 5.0.0(rollup@3.29.4) + specifier: 5.0.1 + version: 5.0.1(rollup@4.0.0) '@rollup/plugin-json': - specifier: 6.0.0 - version: 6.0.0(rollup@3.29.4) + specifier: 6.0.1 + version: 6.0.1(rollup@4.0.0) '@rollup/plugin-replace': - specifier: 5.0.2 - version: 5.0.2(rollup@3.29.4) + specifier: 5.0.3 + version: 5.0.3(rollup@4.0.0) '@rollup/pluginutils': - specifier: 5.0.4 - version: 5.0.4(rollup@3.29.4) + specifier: 5.0.5 + version: 5.0.5(rollup@4.0.0) '@syuilo/aiscript': specifier: 0.16.0 version: 0.16.0 @@ -665,17 +665,14 @@ importers: specifier: 2.37.0 version: 2.37.0 '@vitejs/plugin-vue': - specifier: 4.3.4 - version: 4.3.4(vite@4.4.9)(vue@3.3.4) + specifier: 4.4.0 + version: 4.4.0(vite@4.4.11)(vue@3.3.4) '@vue-macros/reactivity-transform': specifier: 0.3.23 - version: 0.3.23(rollup@3.29.4)(vue@3.3.4) + version: 0.3.23(rollup@4.0.0)(vue@3.3.4) '@vue/compiler-sfc': specifier: 3.3.4 version: 3.3.4 - '@vueuse/core': - specifier: ^10.4.1 - version: 10.4.1(vue@3.3.4) astring: specifier: 1.8.6 version: 1.8.6 @@ -710,8 +707,8 @@ importers: specifier: 2.0.1 version: 2.0.1(chart.js@4.4.0) chromatic: - specifier: 7.2.0 - version: 7.2.0 + specifier: 7.2.2 + version: 7.2.2 compare-versions: specifier: 6.1.0 version: 6.1.0 @@ -755,8 +752,8 @@ importers: specifier: workspace:* version: link:../misskey-js photoswipe: - specifier: 5.4.1 - version: 5.4.1 + specifier: 5.4.2 + version: 5.4.2 prismjs: specifier: 1.29.0 version: 1.29.0 @@ -767,14 +764,14 @@ importers: specifier: 0.2.1 version: 0.2.1 rollup: - specifier: 3.29.4 - version: 3.29.4 + specifier: 4.0.0 + version: 4.0.0 sanitize-html: specifier: 2.11.0 version: 2.11.0 sass: - specifier: 1.68.0 - version: 1.68.0 + specifier: 1.69.0 + version: 1.69.0 strict-event-emitter-types: specifier: 2.0.0 version: 2.0.0 @@ -812,14 +809,11 @@ importers: specifier: 1.8.1 version: 1.8.1 vite: - specifier: 4.4.9 - version: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + specifier: 4.4.11 + version: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) vue: specifier: 3.3.4 version: 3.3.4 - vue-multiselect: - specifier: ^2.1.7 - version: 2.1.7 vue-prism-editor: specifier: 2.0.0-alpha.2 version: 2.0.0-alpha.2(vue@3.3.4) @@ -828,59 +822,59 @@ importers: version: 4.1.0(vue@3.3.4) devDependencies: '@storybook/addon-actions': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-essentials': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-interactions': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-links': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-storysource': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/addons': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/blocks': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/core-events': - specifier: 7.4.5 - version: 7.4.5 + specifier: 7.4.6 + version: 7.4.6 '@storybook/jest': - specifier: 0.2.2 - version: 0.2.2(vitest@0.34.5) + specifier: 0.2.3 + version: 0.2.3(vitest@0.34.6) '@storybook/manager-api': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/preview-api': - specifier: 7.4.5 - version: 7.4.5 + specifier: 7.4.6 + version: 7.4.6 '@storybook/react': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) '@storybook/react-vite': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0)(rollup@3.29.4)(typescript@5.2.2)(vite@4.4.9) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0)(rollup@4.0.0)(typescript@5.2.2)(vite@4.4.11) '@storybook/testing-library': - specifier: 0.2.1 - version: 0.2.1 + specifier: 0.2.2 + version: 0.2.2 '@storybook/theming': - specifier: 7.4.5 - version: 7.4.5(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.6 + version: 7.4.6(react-dom@18.2.0)(react@18.2.0) '@storybook/types': - specifier: 7.4.5 - version: 7.4.5 + specifier: 7.4.6 + version: 7.4.6 '@storybook/vue3': - specifier: 7.4.5 - version: 7.4.5(@vue/compiler-core@3.3.4)(vue@3.3.4) + specifier: 7.4.6 + version: 7.4.6(@vue/compiler-core@3.3.4)(vue@3.3.4) '@storybook/vue3-vite': - specifier: 7.4.5 - version: 7.4.5(@vue/compiler-core@3.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(vite@4.4.9)(vue@3.3.4) + specifier: 7.4.6 + version: 7.4.6(@vue/compiler-core@3.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(vite@4.4.11)(vue@3.3.4) '@testing-library/vue': specifier: 7.0.0 version: 7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4) @@ -897,8 +891,8 @@ importers: specifier: 4.0.3 version: 4.0.3 '@types/node': - specifier: 20.7.1 - version: 20.7.1 + specifier: 20.8.2 + version: 20.8.2 '@types/punycode': specifier: 2.1.0 version: 2.1.0 @@ -921,14 +915,14 @@ importers: specifier: 8.5.6 version: 8.5.6 '@typescript-eslint/eslint-plugin': - specifier: 6.7.3 - version: 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(eslint@8.50.0)(typescript@5.2.2) '@vitest/coverage-v8': - specifier: 0.34.5 - version: 0.34.5(vitest@0.34.5) + specifier: 0.34.6 + version: 0.34.6(vitest@0.34.6) '@vue/runtime-core': specifier: 3.3.4 version: 3.3.4 @@ -946,7 +940,7 @@ importers: version: 8.50.0 eslint-plugin-import: specifier: 2.28.1 - version: 2.28.1(@typescript-eslint/parser@6.7.3)(eslint@8.50.0) + version: 2.28.1(@typescript-eslint/parser@6.7.4)(eslint@8.50.0) eslint-plugin-vue: specifier: 9.17.0 version: 9.17.0(eslint@8.50.0) @@ -960,11 +954,11 @@ importers: specifier: 4.0.5 version: 4.0.5 msw: - specifier: 1.3.1 - version: 1.3.1(typescript@5.2.2) + specifier: 1.3.2 + version: 1.3.2(typescript@5.2.2) msw-storybook-addon: specifier: 1.8.0 - version: 1.8.0(msw@1.3.1) + version: 1.8.0(msw@1.3.2) nodemon: specifier: 3.0.1 version: 3.0.1 @@ -981,11 +975,11 @@ importers: specifier: 2.0.1 version: 2.0.1 storybook: - specifier: 7.4.5 - version: 7.4.5 + specifier: 7.4.6 + version: 7.4.6 storybook-addon-misskey-theme: specifier: github:misskey-dev/storybook-addon-misskey-theme - version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.4.5)(@storybook/components@7.4.5)(@storybook/core-events@7.4.5)(@storybook/manager-api@7.4.5)(@storybook/preview-api@7.4.5)(@storybook/theming@7.4.5)(@storybook/types@7.4.5)(react-dom@18.2.0)(react@18.2.0) + version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.4.6)(@storybook/components@7.4.6)(@storybook/core-events@7.4.6)(@storybook/manager-api@7.4.6)(@storybook/preview-api@7.4.6)(@storybook/theming@7.4.6)(@storybook/types@7.4.6)(react-dom@18.2.0)(react@18.2.0) summaly: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/d2d8db49943ccb201c1b1b283e9d0a630519fac7 @@ -993,11 +987,11 @@ importers: specifier: 1.0.3 version: 1.0.3 vitest: - specifier: 0.34.5 - version: 0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0) + specifier: 0.34.6 + version: 0.34.6(happy-dom@10.0.3)(sass@1.69.0)(terser@5.21.0) vitest-fetch-mock: specifier: 0.2.2 - version: 0.2.2(vitest@0.34.5) + version: 0.2.2(vitest@0.34.6) vue-eslint-parser: specifier: 9.3.1 version: 9.3.1(eslint@8.50.0) @@ -1009,10 +1003,10 @@ importers: dependencies: '@swc/cli': specifier: 0.1.62 - version: 0.1.62(@swc/core@1.3.90)(chokidar@3.5.3) + version: 0.1.62(@swc/core@1.3.92)(chokidar@3.5.3) '@swc/core': - specifier: 1.3.90 - version: 1.3.90 + specifier: 1.3.92 + version: 1.3.92 eventemitter3: specifier: 5.0.1 version: 5.0.1 @@ -1021,29 +1015,29 @@ importers: version: 4.4.0 devDependencies: '@microsoft/api-extractor': - specifier: 7.37.2 - version: 7.37.2(@types/node@20.7.1) + specifier: 7.38.0 + version: 7.38.0(@types/node@20.8.2) '@swc/jest': specifier: 0.2.29 - version: 0.2.29(@swc/core@1.3.90) + version: 0.2.29(@swc/core@1.3.92) '@types/jest': specifier: 29.5.5 version: 29.5.5 '@types/node': - specifier: 20.7.1 - version: 20.7.1 + specifier: 20.8.2 + version: 20.8.2 '@typescript-eslint/eslint-plugin': - specifier: 6.7.3 - version: 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(eslint@8.50.0)(typescript@5.2.2) eslint: specifier: 8.50.0 version: 8.50.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.7.1) + version: 29.7.0(@types/node@20.8.2) jest-fetch-mock: specifier: 3.0.3 version: 3.0.3 @@ -1073,8 +1067,8 @@ importers: version: link:../misskey-js devDependencies: '@typescript-eslint/parser': - specifier: 6.7.3 - version: 6.7.3(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.4 + version: 6.7.4(eslint@8.50.0)(typescript@5.2.2) '@typescript/lib-webworker': specifier: npm:@types/serviceworker@0.0.67 version: /@types/serviceworker@0.0.67 @@ -1083,7 +1077,7 @@ importers: version: 8.50.0 eslint-plugin-import: specifier: 2.28.1 - version: 2.28.1(@typescript-eslint/parser@6.7.3)(eslint@8.50.0) + version: 2.28.1(@typescript-eslint/parser@6.7.4)(eslint@8.50.0) typescript: specifier: 5.2.2 version: 5.2.2 @@ -4014,7 +4008,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -4035,14 +4029,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.7.1) + jest-config: 29.7.0(@types/node@20.8.2) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4077,7 +4071,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 jest-mock: 29.7.0 dev: true @@ -4104,7 +4098,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.7.1 + '@types/node': 20.8.2 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4137,7 +4131,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 20.7.1 + '@types/node': 20.8.2 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -4231,7 +4225,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.7.1 + '@types/node': 20.8.2 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -4243,12 +4237,12 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.7.1 + '@types/node': 20.8.2 '@types/yargs': 17.0.19 chalk: 4.1.2 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.2.2)(vite@4.4.9): + /@joshwooding/vite-plugin-react-docgen-typescript@0.2.1(typescript@5.2.2)(vite@4.4.11): resolution: {integrity: sha512-ou4ZJSXMMWHqGS4g8uNRbC5TiTWxAgQZiVucoUrOCWuPrTbkpJbmVyIi9jU72SBry7gQtuMEDp4YR8EEXAg7VQ==} peerDependencies: typescript: '>= 4.3.x' @@ -4262,7 +4256,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.2.2) typescript: 5.2.2 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) dev: true /@jridgewell/gen-mapping@0.3.2: @@ -4347,24 +4341,24 @@ packages: react: 18.2.0 dev: true - /@microsoft/api-extractor-model@7.28.2(@types/node@20.7.1): + /@microsoft/api-extractor-model@7.28.2(@types/node@20.8.2): resolution: {integrity: sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==} dependencies: '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.61.0(@types/node@20.7.1) + '@rushstack/node-core-library': 3.61.0(@types/node@20.8.2) transitivePeerDependencies: - '@types/node' dev: true - /@microsoft/api-extractor@7.37.2(@types/node@20.7.1): - resolution: {integrity: sha512-b4tr1rTto9/utTjbuqRwfQP2mzP0ACCmJMUY0JIOfOQ3tewGOkMCIRpIS5kcv5/nURekoAY06hNwHmkVsv/s1g==} + /@microsoft/api-extractor@7.38.0(@types/node@20.8.2): + resolution: {integrity: sha512-e1LhZYnfw+JEebuY2bzhw0imDCl1nwjSThTrQqBXl40hrVo6xm3j/1EpUr89QyzgjqmAwek2ZkIVZbrhaR+cqg==} hasBin: true dependencies: - '@microsoft/api-extractor-model': 7.28.2(@types/node@20.7.1) + '@microsoft/api-extractor-model': 7.28.2(@types/node@20.8.2) '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.61.0(@types/node@20.7.1) + '@rushstack/node-core-library': 3.61.0(@types/node@20.8.2) '@rushstack/rig-package': 0.5.1 '@rushstack/ts-command-line': 4.16.1 colors: 1.2.5 @@ -4484,8 +4478,8 @@ packages: tar-fs: 2.1.1 dev: true - /@nestjs/common@10.2.6(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-ma8R7n+FXsWM4XF9QXjjrsRceyRzid/xKmNKVOa/sTJntkVG8lL71BHBEfjtFvO6EJUqjs/15LbDc0iaN5nCwA==} + /@nestjs/common@10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-cUtCRXiUstDmh4bSBhVbq4cI439Gngp4LgLGLBmd5dqFQodfXKnSD441ldYfFiLz4rbUsnoMJz/8ZjuIEI+B7A==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -4504,8 +4498,8 @@ packages: uid: 2.0.2 dev: false - /@nestjs/core@10.2.6(@nestjs/common@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-oGQ2CoBeFRT7egG47MFqS89xlXBTIRZBkRpKRTPMftEfL1RMXhXIcIIaGfzp11wx6qxrBVxBXpVLM09oaqHpaQ==} + /@nestjs/core@10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-5GSu53QUUcwX17sNmlJPa1I0wIeAZOKbedyVuQx0ZAwWVa9g0wJBbsNP+R4EJ+j5Dkdzt/8xkiZvnKt8RFRR8g==} requiresBuild: true peerDependencies: '@nestjs/common': ^10.0.0 @@ -4522,7 +4516,7 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 10.2.6(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -4535,8 +4529,8 @@ packages: - encoding dev: false - /@nestjs/testing@10.2.6(@nestjs/common@10.2.6)(@nestjs/core@10.2.6): - resolution: {integrity: sha512-uxlxHhpSvG4yDTPmuPneoQL1/UnBkOkzE+Zaz6bwURg7lc3uS4ZsXl75OL3pYaJH37rHYXYT9bGcYSpxVbwIrg==} + /@nestjs/testing@10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7): + resolution: {integrity: sha512-d2SIqiJIf/7NSILeNNWSdRvTTpHSouGgisGHwf5PVDC7z4/yXZw/wPO9eJhegnxFlqk6n2LW4QBTmMzbqjAfHA==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 @@ -4548,8 +4542,8 @@ packages: '@nestjs/platform-express': optional: true dependencies: - '@nestjs/common': 10.2.6(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.6(@nestjs/common@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) tslib: 2.6.2 dev: false @@ -5195,51 +5189,51 @@ packages: '@babel/runtime': 7.22.10 dev: true - /@rollup/plugin-alias@5.0.0(rollup@3.29.4): - resolution: {integrity: sha512-l9hY5chSCjuFRPsnRm16twWBiSApl2uYFLsepQYwtBuAxNMQ/1dJqADld40P0Jkqm65GRTLy/AC6hnpVebtLsA==} + /@rollup/plugin-alias@5.0.1(rollup@4.0.0): + resolution: {integrity: sha512-JObvbWdOHoMy9W7SU0lvGhDtWq9PllP5mjpAy+TUslZG/WzOId9u80Hsqq1vCUn9pFJ0cxpdcnAv+QzU2zFH3Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true dependencies: - rollup: 3.29.4 + rollup: 4.0.0 slash: 4.0.0 dev: false - /@rollup/plugin-json@6.0.0(rollup@3.29.4): - resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==} + /@rollup/plugin-json@6.0.1(rollup@4.0.0): + resolution: {integrity: sha512-RgVfl5hWMkxN1h/uZj8FVESvPuBJ/uf6ly6GTj0GONnkfoBN5KC0MSz+PN2OLDgYXMhtG0mWpTrkiOjoxAIevw==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) - rollup: 3.29.4 + '@rollup/pluginutils': 5.0.5(rollup@4.0.0) + rollup: 4.0.0 dev: false - /@rollup/plugin-replace@5.0.2(rollup@3.29.4): - resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==} + /@rollup/plugin-replace@5.0.3(rollup@4.0.0): + resolution: {integrity: sha512-je7fu05B800IrMlWjb2wzJcdXzHYW46iTipfChnBDbIbDXhASZs27W1B58T2Yf45jZtJUONegpbce+9Ut2Ti/Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) + '@rollup/pluginutils': 5.0.5(rollup@4.0.0) magic-string: 0.27.0 - rollup: 3.29.4 + rollup: 4.0.0 dev: false - /@rollup/pluginutils@5.0.4(rollup@3.29.4): - resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} + /@rollup/pluginutils@5.0.5(rollup@4.0.0): + resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true @@ -5247,9 +5241,86 @@ packages: '@types/estree': 1.0.2 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 3.29.4 + rollup: 4.0.0 - /@rushstack/node-core-library@3.61.0(@types/node@20.7.1): + /@rollup/rollup-android-arm-eabi@4.0.0: + resolution: {integrity: sha512-rN3qt1JzOx0v7JWyK68zkb3yf1k1f1OhhHR0i7vLlGlediTtM3FKsOkestQN6HwJ9nEaP3KxPHxH5Xv7yr6f4w==} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@rollup/rollup-android-arm64@4.0.0: + resolution: {integrity: sha512-dcdg6Zp2bqIS/+2FHhdSS+lbcySufP2fYYoXkDa4W6uHE22L15psftdQZtFhxvvqRWPD1HsK0xIj5f07zuujkg==} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@rollup/rollup-darwin-arm64@4.0.0: + resolution: {integrity: sha512-mOz75DpOOHGk4+xYbh1E23vmSOrOqskTwq9s/e2Z46eYbTZ0+s/UVoS42cLG8dUe6enF2Xh3hTtiIEzLhO9kmA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@rollup/rollup-darwin-x64@4.0.0: + resolution: {integrity: sha512-rEBuHQ2ejl9gb0//19F88gR7Z9HY2kcCX8jT5LhCHqGqAvlloETXO1FD7DKEdqGz98UtJy6pVAxxeVBN4tlWag==} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.0.0: + resolution: {integrity: sha512-W4Elp0SGWqWOkdgoYniOp6ERrhHYRfMPikUZmnU/kAdLXQ9p0M0meF648Z6Y7ClHJr8pIQpcCdmr7E2h8Kn7Fw==} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.0.0: + resolution: {integrity: sha512-/BTevM/UKprMJgFse0nm+YXQ83iDqArru+k3kZtQlvaNMWdkLcyscOP8SwWPpR0CJuLlXr8Gtpps+EgH3TUqLA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.0.0: + resolution: {integrity: sha512-Pz2FD/4FUZM98+rcpuGAJgatW5/dW/pXXrbanjtir38EYqqmdVc0odHwqlQ+KFY2C5P+B6PJO5vom8PmJQLdug==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.0.0: + resolution: {integrity: sha512-Xs2tOshU5MD7nK5WnaSBUwiFdBlMtyKdXOOnBno4IRbDIyrjLtx9lnSIO47FNP0LtpGfyOcsK/lE/ZsLlnXyIg==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.0.0: + resolution: {integrity: sha512-h2r04SsqVMbmaIRSMN3HKQLYpKewJ7rWQx1SwEZQMeXRkecWFBBNOfoB3iMlvvUfc3VUOonR/3Dm/Op6yOD2Lg==} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.0.0: + resolution: {integrity: sha512-1pl05L51RbVLnqZTEpbgG2RxeS7VLysF7vhU8v1EOAMqbLzko64r8+S2SxsNDKODsgusFqHO8rc3w+G9VUjodw==} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.0.0: + resolution: {integrity: sha512-GDi4TkL95/J0ven1wt+q2cfdg1k9UEIQiF58lSC36KUdA0xtlqgLPEDlNAhu6NTXJ491eiZ71lQbLu1D7hlz9w==} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@rushstack/node-core-library@3.61.0(@types/node@20.8.2): resolution: {integrity: sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==} peerDependencies: '@types/node': '*' @@ -5257,7 +5328,7 @@ packages: '@types/node': optional: true dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 colors: 1.2.5 fs-extra: 7.0.1 import-lazy: 4.0.0 @@ -5812,8 +5883,8 @@ packages: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} dev: false - /@storybook/addon-actions@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FkjJWmPN/+duLSkRwfa2bwlwjKfY6yCXYn7CRzn3rb64B8f50NB79zAgVLHjkJh9l6T3DIlWtol6vqPHj1aRpw==} + /@storybook/addon-actions@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-SsqZr3js5NinKPnC8AeNI7Ij+Q6fIl9tRdRmSulEgjksjOg7E5S1/Wsn5Bb2CCgj7MaX6VxGyC7s3XskQtDiIQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5823,14 +5894,14 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 dequal: 2.0.3 lodash: 4.17.21 polished: 4.2.2 @@ -5846,8 +5917,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-backgrounds@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-fTq9E1WrYH/9hwDemFVLVcaI2iSSuwWnvY/8tqGrY9xhQF5dIpeHf+z8+HWXpau7e6Z0/WiYR+1vwAcIKt95LQ==} + /@storybook/addon-backgrounds@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+LHTZB/ZYMAzkyD5ZxSriBsqmsrvIaW/Nnd/BeuXGbkrVKKqM0qAKiFZAfjc2WchA1piVNy0/1Rsf+kuYCEiJw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5857,14 +5928,14 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5874,8 +5945,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-controls@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Mxs56jt44HIbZ4gJa0AII1U8GqEGFsvcM5Iob0ETNpxCW5Kj5iHly/4Ws0RFWPH/krrQKaLpWXaUxKmbtEzhJA==} + /@storybook/addon-controls@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-4lq3sycEUIsK8SUWDYc60QgF4vV9FZZ3lDr6M7j2W9bOnvGw49d2fbdlnq+bX1ZprZZ9VgglQpBAorQB3BXZRw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5885,16 +5956,16 @@ packages: react-dom: optional: true dependencies: - '@storybook/blocks': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.4.5 - '@storybook/core-events': 7.4.5 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/blocks': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.4.6 + '@storybook/core-events': 7.4.6 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/node-logger': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 lodash: 4.17.21 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5906,27 +5977,27 @@ packages: - supports-color dev: true - /@storybook/addon-docs@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-KjFVeq8oL7ZC1gsk8iY3Nn0RrHHUpczmOTCd8FeVNmKD4vq+dkPb/8bJLy+jArmIZ8vRhknpTh6kp1BqB7qHGQ==} + /@storybook/addon-docs@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-dLaub+XWFq4hChw+xfuF9yYg0Txp77FUawKoAigccfjWXx+OOhRV3XTuAcknpXkYq94GWynHgUFXosXT9kbDNA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@jest/transform': 29.7.0 '@mdx-js/react': 2.3.0(react@18.2.0) - '@storybook/blocks': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/csf-plugin': 7.4.5 - '@storybook/csf-tools': 7.4.5 + '@storybook/blocks': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/csf-plugin': 7.4.6 + '@storybook/csf-tools': 7.4.6 '@storybook/global': 5.0.0 '@storybook/mdx2-csf': 1.0.0 - '@storybook/node-logger': 7.4.5 - '@storybook/postinstall': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/react-dom-shim': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/node-logger': 7.4.6 + '@storybook/postinstall': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/react-dom-shim': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 fs-extra: 11.1.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5940,25 +6011,25 @@ packages: - supports-color dev: true - /@storybook/addon-essentials@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-H7zZWJXZP0UU2kXfo9zlQfjIKHuuqYBK7PZ2/SL5y08mTrbtt1BfqYScz3xRvHocaFcsBWCXdy8jJULT4KFUpw==} + /@storybook/addon-essentials@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-dWodufrt71TK7ELkeIvVae/x4PzECUlbOm57Iqqt4yQCyR291CgvI4PjeB8un2HbpcXCGZ+N/Oj3YkytvzBi4A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/addon-actions': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-backgrounds': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-controls': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-docs': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-highlight': 7.4.5 - '@storybook/addon-measure': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-outline': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-toolbars': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-viewport': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.4.5 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 7.4.5 - '@storybook/preview-api': 7.4.5 + '@storybook/addon-actions': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-backgrounds': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-controls': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-docs': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-highlight': 7.4.6 + '@storybook/addon-measure': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-outline': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-toolbars': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-viewport': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.4.6 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/node-logger': 7.4.6 + '@storybook/preview-api': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 @@ -5969,16 +6040,16 @@ packages: - supports-color dev: true - /@storybook/addon-highlight@7.4.5: - resolution: {integrity: sha512-6Ru411+Iis4m2weKb8kB1eEssLvCHwFqAf4fjcOC//O5Vaf5+beHYZJUm/rzD0k/oUHfLCBwDBSBY5TLRegkdA==} + /@storybook/addon-highlight@7.4.6: + resolution: {integrity: sha512-zCufxxD2KS5VwczxfkcBxe1oR/juTTn2H1Qm8kYvWCJQx3UxzX0+G9cwafbpV7eivqaufLweEwROkH+0KjAtkQ==} dependencies: - '@storybook/core-events': 7.4.5 + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.4.5 + '@storybook/preview-api': 7.4.6 dev: true - /@storybook/addon-interactions@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-KDdV/THxj38VsuOevrUefev0rZPhzqUXCgrw1Jc2PsJGidHf9d9nnB7wbA9ZFYsxTz90M/Vk5sm7i1QkMmsquA==} + /@storybook/addon-interactions@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zVZYrEPZPhNrXBuPqM7HbQvr6jwsje1sbCYj3wnp83U5wjciuqrngqHIlaSZ30zOWSfRVyzbyqL+JQZKA58BNA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5988,16 +6059,16 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-common': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/instrumenter': 7.4.5 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/instrumenter': 7.4.6 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 jest-mock: 27.5.1 polished: 4.2.2 react: 18.2.0 @@ -6010,8 +6081,8 @@ packages: - supports-color dev: true - /@storybook/addon-links@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-eKczq3U5KfPLaxMUzzVQQrGVtzDshUmrSEEuWKf9ZbK3mh5yVuagIBb88edgUX58vZ3TJMvqQzq1+BtUoPHQ6Q==} + /@storybook/addon-links@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BPygElZKX+CPI9Se6GJNk1dYc5oxuhA+vHigO1tBqhiM6VkHyFP3cvezJNQvpNYhkUnu3cxnZXb3UJnlRbPY3g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6021,22 +6092,22 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/router': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/router': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 dev: true - /@storybook/addon-measure@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FQGZniTH67nC1YPR4ep0p+isgxwLaNAmIAyCZWXPRTkZssIrnXVwNgi0A2QkHdxZvxj8yXGFTOVXLWEPT9YvFQ==} + /@storybook/addon-measure@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-nCymMLaHnxv8TE3yEM1A9Tulb1NuRXRNmtsdHTkjv7P1aWCxZo8A/GZaottKe/GLT8jSRjZ+dnpYWrbAhw6wTQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6046,13 +6117,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) tiny-invariant: 1.3.1 @@ -6061,8 +6132,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-outline@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-eOH9BZzpehUz5FXD98OLnWgzmBFMvEB2kFfw5JiO7IRx7Fan80fx/WDQuMSNDOgLBCTTvsZ4TBMMXZHpw91WAw==} + /@storybook/addon-outline@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-errNUblRVDLpuEaHQPr/nsrnsUkD2ARmXawkRvizgDWLIDMDJYjTON3MUCaVx3x+hlZ3I6X//G5TVcma8tCc8A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6072,13 +6143,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 @@ -6087,8 +6158,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-storysource@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-aWQkW4IzDHRXdUyHPfksSdk4zK4gIJvXpxVCqX+oz3FuadmwZmhK1vWxNdm4Jo/0EZdwe2YZOBJwXHIwpZtigg==} + /@storybook/addon-storysource@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-qkfwvh/pgVBReuWqO25WyaD7jd6LVqhoIJ6rBWnmx+NBpTds+h3Yt3UJCHgvweIrfSF8J3IqzaTxmmNjnkcrRw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6098,13 +6169,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/router': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/source-loader': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/router': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/source-loader': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) estraverse: 5.3.0 prop-types: 15.8.1 react: 18.2.0 @@ -6116,8 +6187,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-toolbars@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-PZlwUTIdQ18de3zNb+627VSF4UrCGIXDdikyO9O5j2Cd0xfr5uhS6tgQ+3AT0DfUj0UIkKxilwcAt+agpNyicA==} + /@storybook/addon-toolbars@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-L9m2FBcKeteGq7qIYsMJr0LEfiH7Wdrv5IDcldZTn68eZUJTh1p4GdJZcOmzX1P5IFRr76hpu03iWsNlWQjpbQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6127,11 +6198,11 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -6139,8 +6210,8 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-viewport@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-SBLnUMIztVrqJ0fRCsVg9KZ29APLIxqAvTsYHF3twy5KB2naeCFuX3K9LxSH7vbROI6zHEfnPduz/Ykyvu9yUg==} + /@storybook/addon-viewport@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-INDtk54j7bi7NgxMfd2ATmbA0J7nAd6X8itMkLIyPuPJtx8bYHPDORyemDOd0AojgmAdTOAyUtDYdI/PFeo4Cw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6150,13 +6221,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) memoizerific: 1.11.3 prop-types: 15.8.1 react: 18.2.0 @@ -6166,36 +6237,36 @@ packages: - '@types/react-dom' dev: true - /@storybook/addons@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-jmdQf39XhwVi8d0J99qpk51fOAwNhYlCtVctvFWPX4qC1cq1d1pxLmTb5OBV2VHQ11BKwlKLzA7coiOgAQmNRg==} + /@storybook/addons@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-c+4awrtwNlJayFdgLkEXa5H2Gj+KNlxuN+Z5oDAdZBLqXI8g0gn7eYO2F/eCSIDWdd/+zcU2uq57XPFKc8veHQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/blocks@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FhAIkCT2HrzJcKsC3mL5+uG3GrbS23mYAT1h3iyPjCliZzxfCCI9UCMUXqYx4Z/FmAGJgpsQQXiBFZuoTHO9aQ==} + /@storybook/blocks@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HxBSAeOiTZW2jbHQlo1upRWFgoMsaAyKijUFf5MwwMNIesXCuuTGZDJ3xTABwAVLK2qC9Ektfbo0CZCiPVuDRQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 - '@storybook/docs-tools': 7.4.5 + '@storybook/docs-tools': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 '@types/lodash': 4.14.191 color-convert: 2.0.1 dequal: 2.0.3 @@ -6217,13 +6288,13 @@ packages: - supports-color dev: true - /@storybook/builder-manager@7.4.5: - resolution: {integrity: sha512-Jhql8iZgK9cxDmG9NSTejsj5FptHni2TBa5Sea2Uz1NIBQ0OpzNdUfYVX6TN/PEq3QrWXTrAEKPqsL2qGjOrxw==} + /@storybook/builder-manager@7.4.6: + resolution: {integrity: sha512-zylZCD2rmyLOOFBFmUgtJg6UNUKmRNgXiig1XApzS2TkIbTZP827DsVEUl0ey/lskCe0uArkrEBR6ICba8p/Rw==} dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 7.4.5 - '@storybook/manager': 7.4.5 - '@storybook/node-logger': 7.4.5 + '@storybook/core-common': 7.4.6 + '@storybook/manager': 7.4.6 + '@storybook/node-logger': 7.4.6 '@types/ejs': 3.1.2 '@types/find-cache-dir': 3.2.1 '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.18.17) @@ -6241,8 +6312,8 @@ packages: - supports-color dev: true - /@storybook/builder-vite@7.4.5(typescript@5.2.2)(vite@4.4.9): - resolution: {integrity: sha512-0aIMvBIx2U/DhDjdjWCW/KIG3HAJpus8NIUIvkVAUCaA7Vn8XvnSsdaRSTTxaaJReFZcIxDf7MebHSCJ0UEKqQ==} + /@storybook/builder-vite@7.4.6(typescript@5.2.2)(vite@4.4.11): + resolution: {integrity: sha512-xV9STYK+TkqWWTf2ydm6jx+7P70fjD2UPd1XTUw08uKszIjhuuxk+bG/OF5R1E25mPunAKXm6kBFh351AKejBg==} peerDependencies: '@preact/preset-vite': '*' typescript: '>= 4.3.x' @@ -6256,15 +6327,15 @@ packages: vite-plugin-glimmerx: optional: true dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/core-common': 7.4.5 - '@storybook/csf-plugin': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/core-common': 7.4.6 + '@storybook/csf-plugin': 7.4.6 '@storybook/mdx2-csf': 1.0.0 - '@storybook/node-logger': 7.4.5 - '@storybook/preview': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/node-logger': 7.4.6 + '@storybook/preview': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 0.9.3 @@ -6276,39 +6347,39 @@ packages: remark-slug: 6.1.0 rollup: 3.29.4 typescript: 5.2.2 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) transitivePeerDependencies: - encoding - supports-color dev: true - /@storybook/channels@7.4.5: - resolution: {integrity: sha512-zWPZn4CxPFXsrrSRQ9JD8GmTeWeFYgr3sTBpe23hnhYookCXVNJ6AcaXogrT9b2ALfbB6MiFDbZIHHTgIgbWpg==} + /@storybook/channels@7.4.6: + resolution: {integrity: sha512-yPv/sfo2c18fM3fvG0i1xse63vG8l33Al/OU0k/dtovltPu001/HVa1QgBgsb/QrEfZtvGjGhmtdVeYb39fv3A==} dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 qs: 6.11.1 telejson: 7.2.0 tiny-invariant: 1.3.1 dev: true - /@storybook/cli@7.4.5: - resolution: {integrity: sha512-PlTkcHdKCugg3pD1zkBP/oFazcZsr7F3wdEmTvygfH0Cx/sQWg5wXBZCYKmf0ONRK4RKL3LVM8DRpeYiQVEFWg==} + /@storybook/cli@7.4.6: + resolution: {integrity: sha512-rRwaH8pOL+FHz/pJMEkNpMH2xvZvWsrl7obBYw26NQiHmiVSAkfHJicndSN1mwc+p5w+9iXthrgzbLtSAOSvkA==} hasBin: true dependencies: '@babel/core': 7.22.11 '@babel/preset-env': 7.22.9(@babel/core@7.22.11) '@babel/types': 7.22.17 '@ndelangen/get-tarball': 3.0.7 - '@storybook/codemod': 7.4.5 - '@storybook/core-common': 7.4.5 - '@storybook/core-events': 7.4.5 - '@storybook/core-server': 7.4.5 - '@storybook/csf-tools': 7.4.5 - '@storybook/node-logger': 7.4.5 - '@storybook/telemetry': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/codemod': 7.4.6 + '@storybook/core-common': 7.4.6 + '@storybook/core-events': 7.4.6 + '@storybook/core-server': 7.4.6 + '@storybook/csf-tools': 7.4.6 + '@storybook/node-logger': 7.4.6 + '@storybook/telemetry': 7.4.6 + '@storybook/types': 7.4.6 '@types/semver': 7.5.3 '@yarnpkg/fslib': 2.10.3 '@yarnpkg/libzip': 2.3.0 @@ -6345,22 +6416,22 @@ packages: - utf-8-validate dev: true - /@storybook/client-logger@7.4.5: - resolution: {integrity: sha512-Bn6eTAjhPDUfLpvuxhKkpDpOtkadfkSmkBNBZRu3r0Dzk2J1nNyKV5K6D8dOU4PFVof4z/gXYj5bktT29jKsmw==} + /@storybook/client-logger@7.4.6: + resolution: {integrity: sha512-XDw31ZziU//86PKuMRnmc+L/G0VopaGKENQOGEpvAXCU9IZASwGKlKAtcyosjrpi+ZiUXlMgUXCpXM7x3b1Ehw==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/codemod@7.4.5: - resolution: {integrity: sha512-gyI2xliSv4vvnfNQN+0e3tRmT7beiq8q8iGjcBtpOhA2xrStyCR7PjbOfLXtRx2I/b50MDZMRTcckzeM3BLoWQ==} + /@storybook/codemod@7.4.6: + resolution: {integrity: sha512-lxmwEpwksCaAq96APN2YlooSDfKjJ1vKzN5Ni2EqQzf2TEXl7XQjLacHd7OOaII1kfsy+D5gNG4N5wBo7Ub30g==} dependencies: '@babel/core': 7.22.11 '@babel/preset-env': 7.22.9(@babel/core@7.22.11) '@babel/types': 7.22.17 '@storybook/csf': 0.1.0 - '@storybook/csf-tools': 7.4.5 - '@storybook/node-logger': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/csf-tools': 7.4.6 + '@storybook/node-logger': 7.4.6 + '@storybook/types': 7.4.6 '@types/cross-spawn': 6.0.2 cross-spawn: 7.0.3 globby: 11.1.0 @@ -6372,19 +6443,19 @@ packages: - supports-color dev: true - /@storybook/components@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-boskkfvMBB8CFYY9+1ofFNyKrdWXTY/ghzt7oK80dz6f2Eseo/WXK3OsCdCq5vWbLRCdbgJ8zXG8pAFi4yBsxA==} + /@storybook/components@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-nIRBhewAgrJJVafyCzuaLx1l+YOfvvD5dOZ0JxZsxJsefOdw1jFpUqUZ5fIpQ2moyvrR0mAUFw378rBfMdHz5Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@radix-ui/react-select': 1.2.2(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-toolbar': 1.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.4.5 + '@storybook/client-logger': 7.4.6 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -6395,19 +6466,19 @@ packages: - '@types/react-dom' dev: true - /@storybook/core-client@7.4.5: - resolution: {integrity: sha512-d/qiCUZeOKY0HX/YmomxlccxJ2NKC3ttRrAsAXzJGypClKabv20X+qbeO/E7Kp5UQxIEJx1wuwJPcnlCvjgPDA==} + /@storybook/core-client@7.4.6: + resolution: {integrity: sha512-tfgxAHeCvMcs6DsVgtb4hQSDaCHeAPJOsoyhb47eDQfk4OmxzriM0qWucJV5DePSMi+KutX/rN2u0JxfOuN68g==} dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/preview-api': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/preview-api': 7.4.6 dev: true - /@storybook/core-common@7.4.5: - resolution: {integrity: sha512-c4pBuILMD4YhSpJ+QpKtsUZpK+/rfolwOvzXfJwlN5EpYzMz6FjVR/LyX0cCT2YLI3X5YWRoCdvMxy5Aeryb8g==} + /@storybook/core-common@7.4.6: + resolution: {integrity: sha512-05MJFmOM86qvTLtgDskokIFz9txe0Lbhq4L3by1FtF0GwgH+p+W6I94KI7c6ANER+kVZkXQZhiRzwBFnVTW+Cg==} dependencies: - '@storybook/core-events': 7.4.5 - '@storybook/node-logger': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/core-events': 7.4.6 + '@storybook/node-logger': 7.4.6 + '@storybook/types': 7.4.6 '@types/find-cache-dir': 3.2.1 '@types/node': 16.18.46 '@types/node-fetch': 2.6.4 @@ -6433,30 +6504,30 @@ packages: - supports-color dev: true - /@storybook/core-events@7.4.5: - resolution: {integrity: sha512-Jzy/adSC95saYCZlgXE5j7jmiMLAXYpnBFBxEtBdXwSWEBb0zt21n1nyWBEAv9s/k2gqDXlPHKHeL5Mn6y40zA==} + /@storybook/core-events@7.4.6: + resolution: {integrity: sha512-r5vrE+32lwrJh1NGFr1a0mWjvxo7q8FXYShylcwRWpacmL5NTtLkrXOoJSeGvJ4yKNYkvxQFtOPId4lzDxa32w==} dependencies: ts-dedent: 2.2.0 dev: true - /@storybook/core-server@7.4.5: - resolution: {integrity: sha512-cW+Qx9Ls823577bd/s9Kv4M1MdKS8mkk6/+nYbwtAwH3hkdlb077rlk/ue0X4O9NZmCrtaJ84KNrBkeDUdFyLQ==} + /@storybook/core-server@7.4.6: + resolution: {integrity: sha512-jqmRTGCJ1W0WReImivkisPVaLFT5sjtLnFoAk0feHp6QS5j7EYOPN7CYzliyQmARWTLUEXOVaFf3VD6nJZQhJQ==} dependencies: '@aw-web-design/x-default-browser': 1.4.126 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 7.4.5 - '@storybook/channels': 7.4.5 - '@storybook/core-common': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/builder-manager': 7.4.6 + '@storybook/channels': 7.4.6 + '@storybook/core-common': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 - '@storybook/csf-tools': 7.4.5 + '@storybook/csf-tools': 7.4.6 '@storybook/docs-mdx': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager': 7.4.5 - '@storybook/node-logger': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/telemetry': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/manager': 7.4.6 + '@storybook/node-logger': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/telemetry': 7.4.6 + '@storybook/types': 7.4.6 '@types/detect-port': 1.3.2 '@types/node': 16.18.46 '@types/pretty-hrtime': 1.0.1 @@ -6476,7 +6547,6 @@ packages: prompts: 2.4.2 read-pkg-up: 7.0.1 semver: 7.5.4 - serve-favicon: 2.5.0 telejson: 7.2.0 tiny-invariant: 1.3.1 ts-dedent: 2.2.0 @@ -6491,24 +6561,24 @@ packages: - utf-8-validate dev: true - /@storybook/csf-plugin@7.4.5: - resolution: {integrity: sha512-8p3AnwIm3xXtQhiF7OQ0rBiP/Pn5OCMHRiT4FytRnNimGaw7gxRZ2xzM608QZHQ4A8rHfmgoM2FTwgxdC15ulA==} + /@storybook/csf-plugin@7.4.6: + resolution: {integrity: sha512-yi7Qa4NSqKOyiJTWCxlB0ih2ijXq6oY5qZKW6MuMMBP14xJNRGLbH5KabpfXgN2T7YECcOWG1uWaGj2veJb1KA==} dependencies: - '@storybook/csf-tools': 7.4.5 + '@storybook/csf-tools': 7.4.6 unplugin: 1.4.0 transitivePeerDependencies: - supports-color dev: true - /@storybook/csf-tools@7.4.5: - resolution: {integrity: sha512-xbm5HGYvlwF0Efivx37v9rO7Exel1/Tdb/Yv/vXn4D/hQeljNVLNz4Bomfy4EQ207rRsrGDSOHEhLUbHDimnxg==} + /@storybook/csf-tools@7.4.6: + resolution: {integrity: sha512-ocKpcIUtTBy6hlLY34RUFQyX403cWpB2gGfqvkHbpGe2BQj7EyV0zpWnjsfVxvw+M9OWlCdxHWDOPUgXM33ELw==} dependencies: '@babel/generator': 7.22.10 '@babel/parser': 7.22.16 '@babel/traverse': 7.22.11 '@babel/types': 7.22.17 '@storybook/csf': 0.1.0 - '@storybook/types': 7.4.5 + '@storybook/types': 7.4.6 fs-extra: 11.1.1 recast: 0.23.1 ts-dedent: 2.2.0 @@ -6526,12 +6596,12 @@ packages: resolution: {integrity: sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==} dev: true - /@storybook/docs-tools@7.4.5: - resolution: {integrity: sha512-ctK+yGb2nvWISSvCCzj3ZhDaAb7I2BLjbxuBGTyNPvl4V9UQ9LBYzdJwR50q+DfscxdwSHMSOE/0OnzmJdaSJA==} + /@storybook/docs-tools@7.4.6: + resolution: {integrity: sha512-nZj1L/8WwKWWJ41FW4MaKGajZUtrhnr9UwflRCkQJaWhAKmDfOb5M5TqI93uCOULpFPOm5wpoMBz2IHInQ2Lrg==} dependencies: - '@storybook/core-common': 7.4.5 - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/core-common': 7.4.6 + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 '@types/doctrine': 0.0.3 doctrine: 3.0.0 lodash: 4.17.21 @@ -6550,21 +6620,21 @@ packages: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} dev: true - /@storybook/instrumenter@7.4.5: - resolution: {integrity: sha512-VLFOcmG75QhWa7MtmfEybIJEz5oT2Ry8xAy/pIVhQwyBaeW0kRT0MHWkixRTtWQmJs/78FmHE3FlgMnqpa5JoA==} + /@storybook/instrumenter@7.4.6: + resolution: {integrity: sha512-K5atRoVFCl6HEgkSxIbwygpzgE/iROc7BrtJ3z3a7E70sanFr6Jxt6Egu6fz2QkL3ef4EWpXMnle2vhEfG29pA==} dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.4.5 + '@storybook/preview-api': 7.4.6 dev: true - /@storybook/jest@0.2.2(vitest@0.34.5): - resolution: {integrity: sha512-PUfp9WoqUA8NdAmiz3UahUsyAMr+g1Dv3BB0fqJZsE2IuE5o1Mgsv4iLGzFm+ohcQLIDQvwvvbQIpxe8eY7TNw==} + /@storybook/jest@0.2.3(vitest@0.34.6): + resolution: {integrity: sha512-ov5izrmbAFObzKeh9AOC5MlmFxAcf0o5i6YFGae9sDx6DGh6alXsRM+chIbucVkUwVHVlSzdfbLDEFGY/ShaYw==} dependencies: '@storybook/expect': 28.1.3-5 - '@testing-library/jest-dom': 6.1.2(@types/jest@28.1.3)(vitest@0.34.5) + '@testing-library/jest-dom': 6.1.2(@types/jest@28.1.3)(vitest@0.34.6) '@types/jest': 28.1.3 jest-mock: 27.5.1 transitivePeerDependencies: @@ -6573,20 +6643,20 @@ packages: - vitest dev: true - /@storybook/manager-api@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-8Hdh5Tutet8xRy2fAknczfvpshz09eVnLd8m34vcFceUOYvEnvDbWerufhlEzovsF4v7U32uqbDHKdKTamWEQQ==} + /@storybook/manager-api@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-inrm3DIbCp8wjXSN/wK6e6i2ysQ/IEmtC7IN0OJ7vdrp+USCooPT448SQTUmVctUGCFmOU3fxXByq8g77oIi7w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/router': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/router': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -6598,31 +6668,31 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/manager@7.4.5: - resolution: {integrity: sha512-yoqVktWzzC0f8cXsxErOEUfT+VFfWV/W7soytIPQuJFqNaq+BqR5A7WCeoY7BIv3mdpRjo4GKwerCsgoHYeHhg==} + /@storybook/manager@7.4.6: + resolution: {integrity: sha512-kA1hUDxpn1i2SO9OinvLvVXDeL4xgJkModp+pbE8IXv4NJWReNq1ecMeQCzPLS3Sil2gnrullQ9uYXsnZ9bxxA==} dev: true /@storybook/mdx2-csf@1.0.0: resolution: {integrity: sha512-dBAnEL4HfxxJmv7LdEYUoZlQbWj9APZNIbOaq0tgF8XkxiIbzqvgB0jhL/9UOrysSDbQWBiCRTu2wOVxedGfmw==} dev: true - /@storybook/node-logger@7.4.5: - resolution: {integrity: sha512-fJSykphbryuEYj1qihbaTH5oOzD4NkptRxyf2uyBrpgkr5tCTq9d7GHheqaBuIdi513dsjlcIR7z5iHxW7ZD+Q==} + /@storybook/node-logger@7.4.6: + resolution: {integrity: sha512-djZb310Q27GviDug1XBv0jOEDLCiwr4hhDE0aifCEKZpfNCi/EaP31nbWimFzZwxu4hE/YAPWExzScruR1zw9Q==} dev: true - /@storybook/postinstall@7.4.5: - resolution: {integrity: sha512-MWRjnKkUpEe2VkHNNpv3zkuMvxM2Zu9DMxFENQaEmhqUHkIFh5klfFwzhSBRexVLzIh7DA1p7mntIpY5A6lh+Q==} + /@storybook/postinstall@7.4.6: + resolution: {integrity: sha512-TqI5BucPAGRWrkh55BYiG2/gHLFtC0In4cuu0GsUzB/1jc4i51npLRorCwhmT7r7YliGl5F7JaP0Bni/qHN3Lg==} dev: true - /@storybook/preview-api@7.4.5: - resolution: {integrity: sha512-6xXQZPyilkGVddfZBI7tMbMMgOyIoZTYgTnwSPTMsXxO0f0TvtNDmGdwhn0I1nREHKfiQGpcQe6gwddEMnGtSg==} + /@storybook/preview-api@7.4.6: + resolution: {integrity: sha512-byUS/Opt3ytWD4cWz3sNEKw5Yks8MkQgRN+GDSyIomaEAQkLAM0rchPC0MYjwCeUSecV7IIQweNX5RbV4a34BA==} dependencies: - '@storybook/channels': 7.4.5 - '@storybook/client-logger': 7.4.5 - '@storybook/core-events': 7.4.5 + '@storybook/channels': 7.4.6 + '@storybook/client-logger': 7.4.6 + '@storybook/core-events': 7.4.6 '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/types': 7.4.5 + '@storybook/types': 7.4.6 '@types/qs': 6.9.7 dequal: 2.0.3 lodash: 4.17.21 @@ -6633,12 +6703,12 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/preview@7.4.5: - resolution: {integrity: sha512-hCVFoPJP0d7vFCJKaWEsDMa6LcRFcEikQ8Cy6Vo+trS8xXwvwE+vIBqyuPozl4O/MYD9iOlzjgZFNwaUUgX0Jg==} + /@storybook/preview@7.4.6: + resolution: {integrity: sha512-2RPXusJ4CTDrIipIKKvbotD7fP0+8VzoFjImunflIrzN9rni+2rq5eMjqlXAaB+77w064zIR4uDUzI9fxsMDeQ==} dev: true - /@storybook/react-dom-shim@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-/hGe8yuiWbT7L3ZsllmJPgxT9MEQE3k23FhliyKx6IGHsWoYaEsPYPZ9tygqtKY8RpqqMUKWz8+kbO79zUxaoQ==} + /@storybook/react-dom-shim@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-DSq8l9FDocUF1ooVI+TF83pddj1LynE/Hv0/y8XZhc3IgJ/HkuOQuUmfz29ezgfAi9gFYUR8raTIBi3/xdoRmw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6647,25 +6717,25 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/react-vite@7.4.5(react-dom@18.2.0)(react@18.2.0)(rollup@3.29.4)(typescript@5.2.2)(vite@4.4.9): - resolution: {integrity: sha512-VfEktqZlSiAcM0oqUnXvQDIFM/G3pOZSW9VCcdQp2NWbsG/UVH42++ZkT0qJmQtW+Kkr8mTofLK5H1v5si5Z1A==} + /@storybook/react-vite@7.4.6(react-dom@18.2.0)(react@18.2.0)(rollup@4.0.0)(typescript@5.2.2)(vite@4.4.11): + resolution: {integrity: sha512-jkjnrf3FxzR5wcmebXRPflrsM4WIDjWyW/NVFJwxi5PeIOk7fE7/QAPrm4NFRUu2Q7DeuH3oLKsw8bigvUI9RA==} engines: {node: '>=16'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.2.2)(vite@4.4.9) - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) - '@storybook/builder-vite': 7.4.5(typescript@5.2.2)(vite@4.4.9) - '@storybook/react': 7.4.5(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) - '@vitejs/plugin-react': 3.1.0(vite@4.4.9) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.2.1(typescript@5.2.2)(vite@4.4.11) + '@rollup/pluginutils': 5.0.5(rollup@4.0.0) + '@storybook/builder-vite': 7.4.6(typescript@5.2.2)(vite@4.4.11) + '@storybook/react': 7.4.6(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) + '@vitejs/plugin-react': 3.1.0(vite@4.4.11) ast-types: 0.14.2 magic-string: 0.30.3 react: 18.2.0 react-docgen: 6.0.0-alpha.3 react-dom: 18.2.0(react@18.2.0) - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -6675,8 +6745,8 @@ packages: - vite-plugin-glimmerx dev: true - /@storybook/react@7.4.5(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2): - resolution: {integrity: sha512-Tiylrs3uFO8QSvH1w3ueSxlAgh2fteH0edRVKaX01M/h47+QqEiZqq/dYkVDvLHngF+CCCwE3OY8nNe6L14Xkw==} + /@storybook/react@7.4.6(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2): + resolution: {integrity: sha512-w0dVo64baFFPTGpUOWFqkKsu6pQincoymegSNgqaBd5DxEyMDRiRoTWSJHMKE9BwgE8SyWhRkP1ak1mkccSOhQ==} engines: {node: '>=16.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6686,13 +6756,13 @@ packages: typescript: optional: true dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/core-client': 7.4.5 - '@storybook/docs-tools': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/core-client': 7.4.6 + '@storybook/docs-tools': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.4.5 - '@storybook/react-dom-shim': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/preview-api': 7.4.6 + '@storybook/react-dom-shim': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 '@types/node': 16.18.46 @@ -6715,27 +6785,27 @@ packages: - supports-color dev: true - /@storybook/router@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-IM4IhiPiXsx3FAUeUOAB47uiuUS8Yd37VQcNlXLBO28GgHoTSYOrjS+VTGLIV5cAGKr8+H5pFB+q35BnlFUpkQ==} + /@storybook/router@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Vl1esrHkcHxDKqc+HY7+6JQpBPW3zYvGk0cQ2rxVMhWdLZTAz1hss9DqzN9tFnPyfn0a1Q77EpMySkUrvWKKNQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/client-logger': 7.4.5 + '@storybook/client-logger': 7.4.6 memoizerific: 1.11.3 qs: 6.11.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/source-loader@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ieo/aPgIXAJfg2raDtsboX43IXiXYHDm0MSXvNXoFE7F1jtRe7gXRi8z7O9xTX4hlIuYea0+kHe+198adgLlWA==} + /@storybook/source-loader@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-tBso55luaKIsZmIsgYyT7HJcjbgjxf0pdzbYqdThZhY3oSl3d56xbcFDCWW+yWjFONuFY8RGPCT7iGywwmaBdQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@storybook/csf': 0.1.0 - '@storybook/types': 7.4.5 + '@storybook/types': 7.4.6 estraverse: 5.3.0 lodash: 4.17.21 prettier: 2.8.8 @@ -6743,12 +6813,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/telemetry@7.4.5: - resolution: {integrity: sha512-JbhQXZF5sqS2c7Cf+vAtuKTdTSBDco+liUP2UGQFjqdacTRLVzxyj+YY2UH4aAQn7wpmnQ67iHnqFp0+fdYmAA==} + /@storybook/telemetry@7.4.6: + resolution: {integrity: sha512-c8p/C1NIH8EMBviZkBCx8MMDk6rrITJ+b29DEp5MaWSRlklIVyhGiC4RPIRv6sxJwlD41PnqWVFtfu2j2eXLdQ==} dependencies: - '@storybook/client-logger': 7.4.5 - '@storybook/core-common': 7.4.5 - '@storybook/csf-tools': 7.4.5 + '@storybook/client-logger': 7.4.6 + '@storybook/core-common': 7.4.6 + '@storybook/csf-tools': 7.4.6 chalk: 4.1.2 detect-package-manager: 2.0.1 fetch-retry: 5.0.4 @@ -6759,53 +6829,53 @@ packages: - supports-color dev: true - /@storybook/testing-library@0.2.1: - resolution: {integrity: sha512-AdbfLCm1C2nEFrhA3ScdicfW6Fjcorehr6RlGwECMiWwaXisnP971Wd4psqtWxlAqQo4tYBZ0f6rJ3J78JLtsg==} + /@storybook/testing-library@0.2.2: + resolution: {integrity: sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==} dependencies: '@testing-library/dom': 9.2.0 '@testing-library/user-event': 14.4.3(@testing-library/dom@9.2.0) ts-dedent: 2.2.0 dev: true - /@storybook/theming@7.4.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-QSIJDIMzOegzlhubIBaYIovf4mlf+AVL0SmQOskPS8GZ6s9t77yUUI6gZTEjO+S4eB3djXRsfTTijQ8+z4XmRA==} + /@storybook/theming@7.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0) - '@storybook/client-logger': 7.4.5 + '@storybook/client-logger': 7.4.6 '@storybook/global': 5.0.0 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/types@7.4.5: - resolution: {integrity: sha512-DTWFNjfRTpncjufDoUs0QnNkgHG2qThGKWL1D6sO18cYI02zWPyHWD8/cbqlvtT7XIGe3s1iUEfCTdU5GcwWBA==} + /@storybook/types@7.4.6: + resolution: {integrity: sha512-6QLXtMVsFZFpzPkdGWsu/iuc8na9dnS67AMOBKm5qCLPwtUJOYkwhMdFRSSeJthLRpzV7JLAL8Kwvl7MFP3QSw==} dependencies: - '@storybook/channels': 7.4.5 + '@storybook/channels': 7.4.6 '@types/babel__core': 7.20.0 '@types/express': 4.17.17 file-system-cache: 2.3.0 dev: true - /@storybook/vue3-vite@7.4.5(@vue/compiler-core@3.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(vite@4.4.9)(vue@3.3.4): - resolution: {integrity: sha512-hNuzSd7EAGpLNGekKjOfuMpir1CpMbSvro4q+04G34CGw2O6awoQKqE+gaOeAHIsSPffio5eeaBR1nKjoKYEog==} + /@storybook/vue3-vite@7.4.6(@vue/compiler-core@3.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2)(vite@4.4.11)(vue@3.3.4): + resolution: {integrity: sha512-r/mUDdCifpN99Cqmvm7IvPZGnur7lYiTxbQPhV8NdRBpQGxm3JC0life9yIvvHV9mYRCjn5MEzC65zWx03Nzig==} engines: {node: ^14.18 || >=16} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 dependencies: - '@storybook/builder-vite': 7.4.5(typescript@5.2.2)(vite@4.4.9) - '@storybook/core-server': 7.4.5 - '@storybook/vue3': 7.4.5(@vue/compiler-core@3.3.4)(vue@3.3.4) - '@vitejs/plugin-vue': 4.3.4(vite@4.4.9)(vue@3.3.4) + '@storybook/builder-vite': 7.4.6(typescript@5.2.2)(vite@4.4.11) + '@storybook/core-server': 7.4.6 + '@storybook/vue3': 7.4.6(@vue/compiler-core@3.3.4)(vue@3.3.4) + '@vitejs/plugin-vue': 4.4.0(vite@4.4.11)(vue@3.3.4) magic-string: 0.30.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) vue-docgen-api: 4.64.1(vue@3.3.4) transitivePeerDependencies: - '@preact/preset-vite' @@ -6819,18 +6889,18 @@ packages: - vue dev: true - /@storybook/vue3@7.4.5(@vue/compiler-core@3.3.4)(vue@3.3.4): - resolution: {integrity: sha512-9vmGSg+jwpTYeBneC3XAL5zJW7/kfA/3tXNfIOkqA4oJ087TBoo5XztzbtT6pSNq8fB9AY8VyPTG8ZE5IaJ4xQ==} + /@storybook/vue3@7.4.6(@vue/compiler-core@3.3.4)(vue@3.3.4): + resolution: {integrity: sha512-Azv/GhmPlAUy8UbXZHKubrBlKhGimuJTT2O6zUvIzggR6sJdsRmdWaEv2S90ZpMBkVYyyM9oKS1fZ4eKi/Ds8g==} engines: {node: '>=16.0.0'} peerDependencies: '@vue/compiler-core': ^3.0.0 vue: ^3.0.0 dependencies: - '@storybook/core-client': 7.4.5 - '@storybook/docs-tools': 7.4.5 + '@storybook/core-client': 7.4.6 + '@storybook/docs-tools': 7.4.6 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.4.5 - '@storybook/types': 7.4.5 + '@storybook/preview-api': 7.4.6 + '@storybook/types': 7.4.6 '@vue/compiler-core': 3.3.4 lodash: 4.17.21 ts-dedent: 2.2.0 @@ -6842,7 +6912,7 @@ packages: - supports-color dev: true - /@swc/cli@0.1.62(@swc/core@1.3.90)(chokidar@3.5.3): + /@swc/cli@0.1.62(@swc/core@1.3.92)(chokidar@3.5.3): resolution: {integrity: sha512-kOFLjKY3XH1DWLfXL1/B5MizeNorHR8wHKEi92S/Zi9Md/AK17KSqR8MgyRJ6C1fhKHvbBCl8wboyKAFXStkYw==} engines: {node: '>= 12.13'} hasBin: true @@ -6854,7 +6924,7 @@ packages: optional: true dependencies: '@mole-inc/bin-wrapper': 8.0.1 - '@swc/core': 1.3.90 + '@swc/core': 1.3.92 chokidar: 3.5.3 commander: 7.2.0 fast-glob: 3.3.1 @@ -6883,8 +6953,8 @@ packages: dev: false optional: true - /@swc/core-darwin-arm64@1.3.90: - resolution: {integrity: sha512-he0w74HvcoufE6CZrB/U/VGVbc7021IQvYrn1geMACnq/OqMBqjdczNtdNfJAy87LZ4AOUjHDKEIjsZZu7o8nQ==} + /@swc/core-darwin-arm64@1.3.92: + resolution: {integrity: sha512-v7PqZUBtIF6Q5Cp48gqUiG8zQQnEICpnfNdoiY3xjQAglCGIQCjJIDjreZBoeZQZspB27lQN4eZ43CX18+2SnA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] @@ -6900,8 +6970,8 @@ packages: dev: false optional: true - /@swc/core-darwin-x64@1.3.90: - resolution: {integrity: sha512-hKNM0Ix0qMlAamPe0HUfaAhQVbZEL5uK6Iw8v9ew0FtVB4v7EifQ9n41wh+yCj0CjcHBPEBbQU0P6mNTxJu/RQ==} + /@swc/core-darwin-x64@1.3.92: + resolution: {integrity: sha512-Q3XIgQfXyxxxms3bPN+xGgvwk0TtG9l89IomApu+yTKzaIIlf051mS+lGngjnh9L0aUiCp6ICyjDLtutWP54fw==} engines: {node: '>=10'} cpu: [x64] os: [darwin] @@ -6928,8 +6998,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm-gnueabihf@1.3.90: - resolution: {integrity: sha512-HumvtrqTWE8rlFuKt7If0ZL7145H/jVc4AeziVjcd+/ajpqub7IyfrLCYd5PmKMtfeSVDMsxjG0BJ0HLRxrTJA==} + /@swc/core-linux-arm-gnueabihf@1.3.92: + resolution: {integrity: sha512-tnOCoCpNVXC+0FCfG84PBZJyLlz0Vfj9MQhyhCvlJz9hQmvpf8nTdKH7RHrOn8VfxtUBLdVi80dXgIFgbvl7qA==} engines: {node: '>=10'} cpu: [arm] os: [linux] @@ -6945,8 +7015,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm64-gnu@1.3.90: - resolution: {integrity: sha512-tA7DqCS7YCwngwXZQeqQhhMm8BbydpaABw8Z/EDQ7KPK1iZ1rNjZw+aWvSpmNmEGmH1RmQ9QDS9mGRDp0faAeg==} + /@swc/core-linux-arm64-gnu@1.3.92: + resolution: {integrity: sha512-lFfGhX32w8h1j74Iyz0Wv7JByXIwX11OE9UxG+oT7lG0RyXkF4zKyxP8EoxfLrDXse4Oop434p95e3UNC3IfCw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -6962,8 +7032,8 @@ packages: dev: false optional: true - /@swc/core-linux-arm64-musl@1.3.90: - resolution: {integrity: sha512-p2Vtid5BZA36fJkNUwk5HP+HJlKgTru+Ghna7pRe45ghKkkRIUk3fhkgudEvfKfhT+3AvP+GTVQ+T9k0gc9S8w==} + /@swc/core-linux-arm64-musl@1.3.92: + resolution: {integrity: sha512-rOZtRcLj57MSAbiecMsqjzBcZDuaCZ8F6l6JDwGkQ7u1NYR57cqF0QDyU7RKS1Jq27Z/Vg21z5cwqoH5fLN+Sg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -6979,8 +7049,8 @@ packages: dev: false optional: true - /@swc/core-linux-x64-gnu@1.3.90: - resolution: {integrity: sha512-J6pDtWaulYGXuANERuvv4CqmUbZOQrRZBCRQGZQJ6a86RWpesZqckBelnYx48wYmkgvMkF95Y3xbI3WTfoSHzw==} + /@swc/core-linux-x64-gnu@1.3.92: + resolution: {integrity: sha512-qptoMGnBL6v89x/Qpn+l1TH1Y0ed+v0qhNfAEVzZvCvzEMTFXphhlhYbDdpxbzRmCjH6GOGq7Y+xrWt9T1/ARg==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -6996,8 +7066,8 @@ packages: dev: false optional: true - /@swc/core-linux-x64-musl@1.3.90: - resolution: {integrity: sha512-3Gh6EA3+0K+l3MqnRON7h5bZ32xLmfcVM6QiHHJ9dBttq7YOEeEoMOCdIPMaQxJmK1VfLgZCsPYRd66MhvUSkw==} + /@swc/core-linux-x64-musl@1.3.92: + resolution: {integrity: sha512-g2KrJ43bZkCZHH4zsIV5ErojuV1OIpUHaEyW1gf7JWKaFBpWYVyubzFPvPkjcxHGLbMsEzO7w/NVfxtGMlFH/Q==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -7013,8 +7083,8 @@ packages: dev: false optional: true - /@swc/core-win32-arm64-msvc@1.3.90: - resolution: {integrity: sha512-BNaw/iJloDyaNOFV23Sr53ULlnbmzSoerTJ10v0TjSZOEIpsS0Rw6xOK1iI0voDJnRXeZeWRSxEC9DhefNtN/g==} + /@swc/core-win32-arm64-msvc@1.3.92: + resolution: {integrity: sha512-3MCRGPAYDoQ8Yyd3WsCMc8eFSyKXY5kQLyg/R5zEqA0uthomo0m0F5/fxAJMZGaSdYkU1DgF73ctOWOf+Z/EzQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] @@ -7030,8 +7100,8 @@ packages: dev: false optional: true - /@swc/core-win32-ia32-msvc@1.3.90: - resolution: {integrity: sha512-SiyTethWAheE/JbxXCukAAciU//PLcmVZ2ME92MRuLMLmOhrwksjbaa7ukj9WEF3LWrherhSqTXnpj3VC1l/qw==} + /@swc/core-win32-ia32-msvc@1.3.92: + resolution: {integrity: sha512-zqTBKQhgfWm73SVGS8FKhFYDovyRl1f5dTX1IwSKynO0qHkRCqJwauFJv/yevkpJWsI2pFh03xsRs9HncTQKSA==} engines: {node: '>=10'} cpu: [ia32] os: [win32] @@ -7047,16 +7117,16 @@ packages: dev: false optional: true - /@swc/core-win32-x64-msvc@1.3.90: - resolution: {integrity: sha512-OpWAW5ljKcPJ3SQ0pUuKqYfwXv7ssIhVgrH9XP9ONtdgXKWZRL9hqJQkcL55FARw/gDjKanoCM47wsTNQL+ZZA==} + /@swc/core-win32-x64-msvc@1.3.92: + resolution: {integrity: sha512-41bE66ddr9o/Fi1FBh0sHdaKdENPTuDpv1IFHxSg0dJyM/jX8LbkjnpdInYXHBxhcLVAPraVRrNsC4SaoPw2Pg==} engines: {node: '>=10'} cpu: [x64] os: [win32] requiresBuild: true optional: true - /@swc/core@1.3.90: - resolution: {integrity: sha512-wptBxP4PldOnhmyDVj8qUcn++GRqyw1qc9wOTGtPNHz8cpuTfdfIgYGlhI4La0UYqecuaaIfLfokyuNePOMHPg==} + /@swc/core@1.3.92: + resolution: {integrity: sha512-vx0vUrf4YTEw59njOJ46Ha5i0cZTMYdRHQ7KXU29efN1MxcmJH2RajWLPlvQarOP1ab9iv9cApD7SMchDyx2vA==} engines: {node: '>=10'} requiresBuild: true peerDependencies: @@ -7068,28 +7138,28 @@ packages: '@swc/counter': 0.1.1 '@swc/types': 0.1.5 optionalDependencies: - '@swc/core-darwin-arm64': 1.3.90 - '@swc/core-darwin-x64': 1.3.90 - '@swc/core-linux-arm-gnueabihf': 1.3.90 - '@swc/core-linux-arm64-gnu': 1.3.90 - '@swc/core-linux-arm64-musl': 1.3.90 - '@swc/core-linux-x64-gnu': 1.3.90 - '@swc/core-linux-x64-musl': 1.3.90 - '@swc/core-win32-arm64-msvc': 1.3.90 - '@swc/core-win32-ia32-msvc': 1.3.90 - '@swc/core-win32-x64-msvc': 1.3.90 + '@swc/core-darwin-arm64': 1.3.92 + '@swc/core-darwin-x64': 1.3.92 + '@swc/core-linux-arm-gnueabihf': 1.3.92 + '@swc/core-linux-arm64-gnu': 1.3.92 + '@swc/core-linux-arm64-musl': 1.3.92 + '@swc/core-linux-x64-gnu': 1.3.92 + '@swc/core-linux-x64-musl': 1.3.92 + '@swc/core-win32-arm64-msvc': 1.3.92 + '@swc/core-win32-ia32-msvc': 1.3.92 + '@swc/core-win32-x64-msvc': 1.3.92 /@swc/counter@0.1.1: resolution: {integrity: sha512-xVRaR4u9hcYjFvcSg71Lz5Bo4//CyjAAfMxa7UsaDSYxAshflUkVJWiyVWrfxC59z2kP1IzI4/1BEpnhI9o3Mw==} - /@swc/jest@0.2.29(@swc/core@1.3.90): + /@swc/jest@0.2.29(@swc/core@1.3.92): resolution: {integrity: sha512-8reh5RvHBsSikDC3WGCd5ZTd2BXKkyOdK7QwynrCH58jk2cQFhhHhFBg/jvnWZehUQe/EoOImLENc9/DwbBFow==} engines: {npm: '>= 7.0.0'} peerDependencies: '@swc/core': '*' dependencies: '@jest/create-cache-key-function': 27.5.1 - '@swc/core': 1.3.90 + '@swc/core': 1.3.92 jsonc-parser: 3.2.0 dev: true @@ -7266,7 +7336,7 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.1.2(@types/jest@28.1.3)(vitest@0.34.5): + /@testing-library/jest-dom@6.1.2(@types/jest@28.1.3)(vitest@0.34.6): resolution: {integrity: sha512-NP9jl1Q2qDDtx+cqogowtQtmgD2OVs37iMSIsTv5eN5ETRkf26Kj6ugVwA93/gZzzFWQAsgkKkcftDe91BJCkQ==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: @@ -7293,7 +7363,7 @@ packages: dom-accessibility-api: 0.5.16 lodash: 4.17.21 redent: 3.0.0 - vitest: 0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0) + vitest: 0.34.6(happy-dom@10.0.3)(sass@1.69.0)(terser@5.21.0) dev: true /@testing-library/user-event@14.4.3(@testing-library/dom@9.2.0): @@ -7341,7 +7411,7 @@ packages: /@types/accepts@1.3.5: resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/archiver@5.3.3: @@ -7395,7 +7465,7 @@ packages: resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/braces@3.0.1: @@ -7407,7 +7477,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 20.7.1 + '@types/node': 20.8.2 '@types/responselike': 1.0.0 dev: false @@ -7440,7 +7510,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/content-disposition@0.5.6: @@ -7454,7 +7524,7 @@ packages: /@types/cross-spawn@6.0.2: resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/debug@4.1.7: @@ -7508,7 +7578,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -7529,20 +7599,20 @@ packages: /@types/fluent-ffmpeg@2.1.22: resolution: {integrity: sha512-ZZPDDrDOb2Ahp5fxZzuw64f0rCcviv+SDuCyJ1PIF/UFn9wNHtb/bY8Dj/2nrbQ7SNsGI7gaO2wJVkkU2HBcMg==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/hast@2.3.4: @@ -7557,7 +7627,7 @@ packages: /@types/http-link-header@1.0.3: resolution: {integrity: sha512-y8HkoD/vyid+5MrJ3aas0FvU3/BVBGcyG9kgxL0Zn4JwstA8CglFPnrR0RuzOjRCXwqzL5uxWC2IO7Ub0rMU2A==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/istanbul-lib-coverage@2.0.4: @@ -7601,7 +7671,7 @@ packages: /@types/jsdom@21.1.3: resolution: {integrity: sha512-1zzqSP+iHJYV4lB3lZhNBa012pubABkj9yG/GuXuf6LZH1cSPIJBqFDrm5JX65HHt6VOnNYdTui/0ySerRbMgA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 dev: true @@ -7625,7 +7695,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: false /@types/lodash@4.14.191: @@ -7674,7 +7744,7 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 form-data: 3.0.1 /@types/node-fetch@3.0.3: @@ -7691,13 +7761,13 @@ packages: resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==} dev: true - /@types/node@20.7.1: - resolution: {integrity: sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg==} + /@types/node@20.8.2: + resolution: {integrity: sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==} /@types/nodemailer@6.4.11: resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/normalize-package-data@2.4.1: @@ -7714,13 +7784,13 @@ packages: resolution: {integrity: sha512-U3L0c4eQA6lTSZRgW4LYfhKlR084Aw19akmYHrMdYzaqg9mQDfc2b/1iyqm9+1FJDEnVS5ONi5fxdDrB4/7CpQ==} dependencies: '@types/express': 4.17.17 - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/oauth@0.9.2: resolution: {integrity: sha512-Nu3/abQ6yR9VlsCdX3aiGsWFkj6OJvJqDvg/36t8Gwf2mFXdBZXPDN3K+2yfeA6Lo2m1Q12F8Qil9TZ48nWhOQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/offscreencanvas@2019.3.0: @@ -7736,7 +7806,7 @@ packages: /@types/pg@8.10.3: resolution: {integrity: sha512-BACzsw64lCZesclRpZGu55tnqgFAYcrCBP92xLh1KLypZLCOsvJTSTgaoFVTy3lCys/aZTQzfeDxtjwrvdzL2g==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 pg-protocol: 1.6.0 pg-types: 4.0.1 dev: true @@ -7760,7 +7830,7 @@ packages: /@types/qrcode@1.5.2: resolution: {integrity: sha512-W4KDz75m7rJjFbyCctzCtRzZUj+PrUHV+YjqDp50sSRezTbrtEAIq2iTzC6lISARl3qw+8IlcCyljdcVJE0Wug==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/qs@6.9.7: @@ -7790,7 +7860,7 @@ packages: /@types/readdir-glob@1.1.1: resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/rename@1.0.5: @@ -7800,7 +7870,7 @@ packages: /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: false /@types/sanitize-html@2.9.1: @@ -7826,7 +7896,7 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/serviceworker@0.0.67: @@ -7836,7 +7906,7 @@ packages: /@types/set-cookie-parser@2.4.3: resolution: {integrity: sha512-7QhnH7bi+6KAhBB+Auejz1uV9DHiopZqu7LfR/5gZZTkejJV5nYeZZpgfFoE0N8aDsXuiYpfKyfyMatCwQhyTQ==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/sharp@0.32.0: @@ -7899,17 +7969,13 @@ packages: /@types/vary@1.1.1: resolution: {integrity: sha512-XL8U62BpXBMMuFzFBYsWekQwo+dqcyN117IwFVMCkBCvc6HY1ODdRKNA0JHxnuTM5lX3kpqsnBH5OuEeXSN3aA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true - /@types/web-bluetooth@0.0.17: - resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} - dev: false - /@types/web-push@3.6.1: resolution: {integrity: sha512-Zu6Iju7c4IlE8I8eEeFLYRb7XFqvHFmWWAYr1cmug9EX3c6CDarxIXWN/GO0sxjbJLkHPwozUzp6cLdXsrq7Ew==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/webgl-ext@0.0.30: @@ -7920,13 +7986,13 @@ packages: /@types/websocket@1.0.7: resolution: {integrity: sha512-62Omr8U0PO+hgjLCpPnMsmjh2/FRwIGOktZHyYAUzooEJotwkXHMp7vCacdYi8haxBNOiw9bc2HIHI+b/MPNjA==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/ws@8.5.6: resolution: {integrity: sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /@types/yargs-parser@21.0.0: @@ -7949,12 +8015,12 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true optional: true - /@typescript-eslint/eslint-plugin@6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==} + /@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.2.2): + resolution: {integrity: sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -7965,11 +8031,11 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.6.2 - '@typescript-eslint/parser': 6.7.3(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.7.3 - '@typescript-eslint/type-utils': 6.7.3(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.3(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.7.3 + '@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.7.4 + '@typescript-eslint/type-utils': 6.7.4(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.7.4 debug: 4.3.4(supports-color@8.1.1) eslint: 8.50.0 graphemer: 1.4.0 @@ -7982,8 +8048,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.7.3(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==} + /@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.2.2): + resolution: {integrity: sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -7992,10 +8058,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.7.3 - '@typescript-eslint/types': 6.7.3 - '@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.7.3 + '@typescript-eslint/scope-manager': 6.7.4 + '@typescript-eslint/types': 6.7.4 + '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.7.4 debug: 4.3.4(supports-color@8.1.1) eslint: 8.50.0 typescript: 5.2.2 @@ -8003,16 +8069,16 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager@6.7.3: - resolution: {integrity: sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==} + /@typescript-eslint/scope-manager@6.7.4: + resolution: {integrity: sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.7.3 - '@typescript-eslint/visitor-keys': 6.7.3 + '@typescript-eslint/types': 6.7.4 + '@typescript-eslint/visitor-keys': 6.7.4 dev: true - /@typescript-eslint/type-utils@6.7.3(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==} + /@typescript-eslint/type-utils@6.7.4(eslint@8.50.0)(typescript@5.2.2): + resolution: {integrity: sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -8021,8 +8087,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.3(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@5.2.2) debug: 4.3.4(supports-color@8.1.1) eslint: 8.50.0 ts-api-utils: 1.0.1(typescript@5.2.2) @@ -8031,13 +8097,13 @@ packages: - supports-color dev: true - /@typescript-eslint/types@6.7.3: - resolution: {integrity: sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==} + /@typescript-eslint/types@6.7.4: + resolution: {integrity: sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.7.3(typescript@5.2.2): - resolution: {integrity: sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==} + /@typescript-eslint/typescript-estree@6.7.4(typescript@5.2.2): + resolution: {integrity: sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -8045,8 +8111,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.7.3 - '@typescript-eslint/visitor-keys': 6.7.3 + '@typescript-eslint/types': 6.7.4 + '@typescript-eslint/visitor-keys': 6.7.4 debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 @@ -8057,8 +8123,8 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.7.3(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==} + /@typescript-eslint/utils@6.7.4(eslint@8.50.0)(typescript@5.2.2): + resolution: {integrity: sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -8066,9 +8132,9 @@ packages: '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.3 - '@typescript-eslint/scope-manager': 6.7.3 - '@typescript-eslint/types': 6.7.3 - '@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.7.4 + '@typescript-eslint/types': 6.7.4 + '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.2.2) eslint: 8.50.0 semver: 7.5.4 transitivePeerDependencies: @@ -8076,15 +8142,15 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@6.7.3: - resolution: {integrity: sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==} + /@typescript-eslint/visitor-keys@6.7.4: + resolution: {integrity: sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.7.3 + '@typescript-eslint/types': 6.7.4 eslint-visitor-keys: 3.4.3 dev: true - /@vitejs/plugin-react@3.1.0(vite@4.4.9): + /@vitejs/plugin-react@3.1.0(vite@4.4.11): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -8095,23 +8161,23 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.22.11) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) transitivePeerDependencies: - supports-color dev: true - /@vitejs/plugin-vue@4.3.4(vite@4.4.9)(vue@3.3.4): - resolution: {integrity: sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==} + /@vitejs/plugin-vue@4.4.0(vite@4.4.11)(vue@3.3.4): + resolution: {integrity: sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.0.0 vue: ^3.2.25 dependencies: - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) vue: 3.3.4 - /@vitest/coverage-v8@0.34.5(vitest@0.34.5): - resolution: {integrity: sha512-97xjhRTSdmeeHCm2nNHhT3hLsMYkAhHXm/rwj6SZ3voka8xiCJrwgtfIjoZIFEL4OO0KezGmVuHWQXcMunULIA==} + /@vitest/coverage-v8@0.34.6(vitest@0.34.6): + resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==} peerDependencies: vitest: '>=0.32.0 <1' dependencies: @@ -8126,43 +8192,43 @@ packages: std-env: 3.3.3 test-exclude: 6.0.0 v8-to-istanbul: 9.1.0 - vitest: 0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0) + vitest: 0.34.6(happy-dom@10.0.3)(sass@1.69.0)(terser@5.21.0) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@0.34.5: - resolution: {integrity: sha512-/3RBIV9XEH+nRpRMqDJBufKIOQaYUH2X6bt0rKSCW0MfKhXFLYsR5ivHifeajRSTsln0FwJbitxLKHSQz/Xwkw==} + /@vitest/expect@0.34.6: + resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} dependencies: - '@vitest/spy': 0.34.5 - '@vitest/utils': 0.34.5 - chai: 4.3.7 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + chai: 4.3.10 dev: true - /@vitest/runner@0.34.5: - resolution: {integrity: sha512-RDEE3ViVvl7jFSCbnBRyYuu23XxmvRTSZWW6W4M7eC5dOsK75d5LIf6uhE5Fqf809DQ1+9ICZZNxhIolWHU4og==} + /@vitest/runner@0.34.6: + resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} dependencies: - '@vitest/utils': 0.34.5 + '@vitest/utils': 0.34.6 p-limit: 4.0.0 pathe: 1.1.1 dev: true - /@vitest/snapshot@0.34.5: - resolution: {integrity: sha512-+ikwSbhu6z2yOdtKmk/aeoDZ9QPm2g/ZO5rXT58RR9Vmu/kB2MamyDSx77dctqdZfP3Diqv4mbc/yw2kPT8rmA==} + /@vitest/snapshot@0.34.6: + resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} dependencies: magic-string: 0.30.3 pathe: 1.1.1 pretty-format: 29.7.0 dev: true - /@vitest/spy@0.34.5: - resolution: {integrity: sha512-epsicsfhvBjRjCMOC/3k00mP/TBGQy8/P0DxOFiWyLt55gnZ99dqCfCiAsKO17BWVjn4eZRIjKvcqNmSz8gvmg==} + /@vitest/spy@0.34.6: + resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} dependencies: tinyspy: 2.1.1 dev: true - /@vitest/utils@0.34.5: - resolution: {integrity: sha512-ur6CmmYQoeHMwmGb0v+qwkwN3yopZuZyf4xt1DBBSGBed8Hf9Gmbm/5dEWqgpLPdRx6Av6jcWXrjcKfkTzg/pw==} + /@vitest/utils@0.34.6: + resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} dependencies: diff-sequences: 29.6.3 loupe: 2.3.6 @@ -8187,7 +8253,7 @@ packages: '@volar/language-core': 1.10.0 dev: true - /@vue-macros/common@1.8.0(rollup@3.29.4)(vue@3.3.4): + /@vue-macros/common@1.8.0(rollup@4.0.0)(vue@3.3.4): resolution: {integrity: sha512-auDJJzE0z3uRe3867e0DsqcseKImktNf5ojCZgUKqiVxb2yTlwlgOVAYCgoep9oITqxkXQymSvFeKhedi8PhaA==} engines: {node: '>=16.14.0'} peerDependencies: @@ -8197,9 +8263,9 @@ packages: optional: true dependencies: '@babel/types': 7.22.17 - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) + '@rollup/pluginutils': 5.0.5(rollup@4.0.0) '@vue/compiler-sfc': 3.3.4 - ast-kit: 0.11.2(rollup@3.29.4) + ast-kit: 0.11.2(rollup@4.0.0) local-pkg: 0.4.3 magic-string-ast: 0.3.0 vue: 3.3.4 @@ -8207,14 +8273,14 @@ packages: - rollup dev: false - /@vue-macros/reactivity-transform@0.3.23(rollup@3.29.4)(vue@3.3.4): + /@vue-macros/reactivity-transform@0.3.23(rollup@4.0.0)(vue@3.3.4): resolution: {integrity: sha512-SubIg1GsNpQdIDJusrcA2FWBgwSY+4jmL0j6SJ6PU85r3rlS+uDhn6AUkqxeZRAdmJnrbGHXDyWUdygOZmWrSg==} engines: {node: '>=16.14.0'} peerDependencies: vue: ^2.7.0 || ^3.2.25 dependencies: '@babel/parser': 7.22.16 - '@vue-macros/common': 1.8.0(rollup@3.29.4)(vue@3.3.4) + '@vue-macros/common': 1.8.0(rollup@4.0.0)(vue@3.3.4) '@vue/compiler-core': 3.3.4 '@vue/shared': 3.3.4 magic-string: 0.30.3 @@ -8337,31 +8403,6 @@ packages: - typescript dev: true - /@vueuse/core@10.4.1(vue@3.3.4): - resolution: {integrity: sha512-DkHIfMIoSIBjMgRRvdIvxsyboRZQmImofLyOHADqiVbQVilP8VVHDhBX2ZqoItOgu7dWa8oXiNnScOdPLhdEXg==} - dependencies: - '@types/web-bluetooth': 0.0.17 - '@vueuse/metadata': 10.4.1 - '@vueuse/shared': 10.4.1(vue@3.3.4) - vue-demi: 0.14.6(vue@3.3.4) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: false - - /@vueuse/metadata@10.4.1: - resolution: {integrity: sha512-2Sc8X+iVzeuMGHr6O2j4gv/zxvQGGOYETYXEc41h0iZXIRnRbJZGmY/QP8dvzqUelf8vg0p/yEA5VpCEu+WpZg==} - dev: false - - /@vueuse/shared@10.4.1(vue@3.3.4): - resolution: {integrity: sha512-vz5hbAM4qA0lDKmcr2y3pPdU+2EVw/yzfRsBdu+6+USGa4PxqSQRYIUC9/NcT06y+ZgaTsyURw2I9qOFaaXHAg==} - dependencies: - vue-demi: 0.14.6(vue@3.3.4) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: false - /@webgpu/types@0.1.30: resolution: {integrity: sha512-9AXJSmL3MzY8ZL//JjudA//q+2kBRGhLBFpkdGksWIuxrMy81nFrCzj2Am+mbh8WoU6rXmv7cY5E3rdlyru2Qg==} requiresBuild: true @@ -8814,12 +8855,12 @@ packages: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true - /ast-kit@0.11.2(rollup@3.29.4): + /ast-kit@0.11.2(rollup@4.0.0): resolution: {integrity: sha512-Q0DjXK4ApbVoIf9GLyCo252tUH44iTnD/hiJ2TQaJeydYWSpKk0sI34+WMel8S9Wt5pbLgG02oJ+gkgX5DV3sQ==} engines: {node: '>=16.14.0'} dependencies: '@babel/parser': 7.22.16 - '@rollup/pluginutils': 5.0.4(rollup@3.29.4) + '@rollup/pluginutils': 5.0.5(rollup@4.0.0) pathe: 1.1.1 transitivePeerDependencies: - rollup @@ -9283,8 +9324,8 @@ packages: dependencies: node-gyp-build: 4.6.0 - /bullmq@4.11.4: - resolution: {integrity: sha512-LuCR3ILngYa3CLC5jyf8DU4Yokj9T12MWwBogP3S4IiJUtbJsQ9GTGFxho3imRxXfcd9DUfrABT/pSoqVigXiQ==} + /bullmq@4.12.2: + resolution: {integrity: sha512-0YhOtg1lvdqBtYQgh7NNKisFckbxKaHSDqifXvx90OAa5qa6sqNoFbvOgaUitRsAiJgg2fDdT1DnMdQHsQiCeQ==} dependencies: cron-parser: 4.8.1 glob: 8.1.0 @@ -9480,14 +9521,14 @@ packages: dependencies: nofilter: 3.1.0 - /chai@4.3.7: - resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + /chai@4.3.10: + resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 - check-error: 1.0.2 + check-error: 1.0.3 deep-eql: 4.1.3 - get-func-name: 2.0.0 + get-func-name: 2.0.2 loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 @@ -9596,8 +9637,10 @@ packages: hammerjs: 2.0.8 dev: false - /check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 dev: true /check-more-types@2.24.0: @@ -9649,8 +9692,8 @@ packages: engines: {node: '>=10'} requiresBuild: true - /chromatic@7.2.0: - resolution: {integrity: sha512-EbuvmsM6XAVFC4EQpqR2AT2PaXY4IS8qWxxg6N10AhpRulfX2b2AtW1hUc88cCosRyztd6esxkBdj3FSKR7zVw==} + /chromatic@7.2.2: + resolution: {integrity: sha512-o9EIMV/EAe6bI7osYi4DfD1zuVovYR/vrY8CXNB5OdcT+alpHZmEZ4+ysTrvL9Bgk6zP/z/2YMVz5ZYdV/gagA==} hasBin: true dev: false @@ -9987,7 +10030,7 @@ packages: readable-stream: 3.6.0 dev: false - /create-jest@29.7.0(@types/node@20.7.1): + /create-jest@29.7.0(@types/node@20.8.2): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -9996,7 +10039,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.7.1) + jest-config: 29.7.0(@types/node@20.8.2) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -11011,7 +11054,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.3)(eslint-import-resolver-node@0.3.7)(eslint@8.50.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.4)(eslint-import-resolver-node@0.3.7)(eslint@8.50.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -11032,7 +11075,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.7.3(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.2.2) debug: 3.2.7(supports-color@5.5.0) eslint: 8.50.0 eslint-import-resolver-node: 0.3.7 @@ -11040,7 +11083,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.3)(eslint@8.50.0): + /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.4)(eslint@8.50.0): resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} engines: {node: '>=4'} peerDependencies: @@ -11050,7 +11093,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.7.3(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.2.2) array-includes: 3.1.6 array.prototype.findlastindex: 1.2.2 array.prototype.flat: 1.3.1 @@ -11059,7 +11102,7 @@ packages: doctrine: 2.1.0 eslint: 8.50.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.3)(eslint-import-resolver-node@0.3.7)(eslint@8.50.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.4)(eslint-import-resolver-node@0.3.7)(eslint@8.50.0) has: 1.0.3 is-core-module: 2.13.0 is-glob: 4.0.3 @@ -11966,8 +12009,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - /get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true /get-intrinsic@1.2.0: @@ -12253,8 +12296,8 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /graphql@16.6.0: - resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} + /graphql@16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} dev: true @@ -13216,7 +13259,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 chalk: 4.1.2 co: 4.6.0 dedent: 1.3.0 @@ -13237,7 +13280,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@20.7.1): + /jest-cli@29.7.0(@types/node@20.8.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -13251,10 +13294,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.7.1) + create-jest: 29.7.0(@types/node@20.8.2) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.7.1) + jest-config: 29.7.0(@types/node@20.8.2) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.6.2 @@ -13265,7 +13308,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.7.1): + /jest-config@29.7.0(@types/node@20.8.2): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -13280,7 +13323,7 @@ packages: '@babel/core': 7.22.11 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 babel-jest: 29.7.0(@babel/core@7.22.11) chalk: 4.1.2 ci-info: 3.7.1 @@ -13360,7 +13403,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -13390,7 +13433,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 - '@types/node': 20.7.1 + '@types/node': 20.8.2 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -13451,7 +13494,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.7.1 + '@types/node': 20.8.2 dev: true /jest-mock@29.7.0: @@ -13459,7 +13502,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 jest-util: 29.7.0 dev: true @@ -13514,7 +13557,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -13545,7 +13588,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -13597,7 +13640,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 chalk: 4.1.2 ci-info: 3.7.1 graceful-fs: 4.2.11 @@ -13622,7 +13665,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.7.1 + '@types/node': 20.8.2 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -13641,13 +13684,13 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@20.7.1): + /jest@29.7.0(@types/node@20.8.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -13660,7 +13703,7 @@ packages: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.7.1) + jest-cli: 29.7.0(@types/node@20.8.2) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -14154,7 +14197,7 @@ packages: /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: - get-func-name: 2.0.0 + get-func-name: 2.0.2 dev: true /lowercase-keys@2.0.0: @@ -14630,10 +14673,6 @@ packages: /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - /ms@2.1.1: - resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==} - dev: true - /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -14667,17 +14706,17 @@ packages: msgpackr-extract: 3.0.2 dev: false - /msw-storybook-addon@1.8.0(msw@1.3.1): + /msw-storybook-addon@1.8.0(msw@1.3.2): resolution: {integrity: sha512-dw3vZwqjixmiur0vouRSOax7wPSu9Og2Hspy9JZFHf49bZRjwDiLF0Pfn2NXEkGviYJOJiGxS1ejoTiUwoSg4A==} peerDependencies: msw: '>=0.35.0 <2.0.0' dependencies: is-node-process: 1.0.1 - msw: 1.3.1(typescript@5.2.2) + msw: 1.3.2(typescript@5.2.2) dev: true - /msw@1.3.1(typescript@5.2.2): - resolution: {integrity: sha512-GhP5lHSTXNlZb9EaKgPRJ01YAnVXwzkvnTzRn4W8fxU2DXuJrRO+Nb6OHdYqB4fCkwSNpIJH9JkON5Y6rHqJMQ==} + /msw@1.3.2(typescript@5.2.2): + resolution: {integrity: sha512-wKLhFPR+NitYTkQl5047pia0reNGgf0P6a1eTnA5aNlripmiz0sabMvvHcicE8kQ3/gZcI0YiPFWmYfowfm3lA==} engines: {node: '>=14'} hasBin: true requiresBuild: true @@ -14695,7 +14734,7 @@ packages: chalk: 4.1.2 chokidar: 3.5.3 cookie: 0.4.2 - graphql: 16.6.0 + graphql: 16.8.1 headers-polyfill: 3.2.5 inquirer: 8.2.5 is-node-process: 1.2.0 @@ -15619,8 +15658,8 @@ packages: split2: 4.1.0 dev: false - /photoswipe@5.4.1: - resolution: {integrity: sha512-iauO0fP4oMdZvjlXzeIe8um1fZatkGE0bqdoIwpb65jlo/KK1KhfD7Z51+0YhS2tC4FOoOtE1p0c4o/HbY1s2Q==} + /photoswipe@5.4.2: + resolution: {integrity: sha512-z5hr36nAIPOZbHJPbCJ/mQ3+ZlizttF9za5gKXKH/us1k4KNHaRbC63K1Px5sVVKUtGb/2+ixHpKqtwl0WAwvA==} engines: {node: '>= 0.12.0'} dev: false @@ -17126,6 +17165,24 @@ packages: optionalDependencies: fsevents: 2.3.2 + /rollup@4.0.0: + resolution: {integrity: sha512-dtlkoIdp/g2glVlQb6FzhMAMzhMYVIJ3KLGjhWKkwz/ambEuHeVZ7Eg6GALhHZOsDRD+ZWSjnUikZXPyb22puQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.0.0 + '@rollup/rollup-android-arm64': 4.0.0 + '@rollup/rollup-darwin-arm64': 4.0.0 + '@rollup/rollup-darwin-x64': 4.0.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.0.0 + '@rollup/rollup-linux-arm64-gnu': 4.0.0 + '@rollup/rollup-linux-x64-gnu': 4.0.0 + '@rollup/rollup-linux-x64-musl': 4.0.0 + '@rollup/rollup-win32-arm64-msvc': 4.0.0 + '@rollup/rollup-win32-ia32-msvc': 4.0.0 + '@rollup/rollup-win32-x64-msvc': 4.0.0 + fsevents: 2.3.2 + /rrweb-cssom@0.6.0: resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} dev: false @@ -17162,10 +17219,6 @@ packages: isarray: 2.0.5 dev: true - /safe-buffer@5.1.1: - resolution: {integrity: sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==} - dev: true - /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -17205,8 +17258,8 @@ packages: postcss: 8.4.31 dev: false - /sass@1.68.0: - resolution: {integrity: sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==} + /sass@1.69.0: + resolution: {integrity: sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==} engines: {node: '>=14.0.0'} hasBin: true dependencies: @@ -17287,17 +17340,6 @@ packages: transitivePeerDependencies: - supports-color - /serve-favicon@2.5.0: - resolution: {integrity: sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==} - engines: {node: '>= 0.8.0'} - dependencies: - etag: 1.8.1 - fresh: 0.5.2 - ms: 2.1.1 - parseurl: 1.3.3 - safe-buffer: 5.1.1 - dev: true - /serve-static@1.15.0: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} @@ -17812,11 +17854,11 @@ packages: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true - /storybook@7.4.5: - resolution: {integrity: sha512-J7fidphTJ6SJHlR8f/USQE30K6ipbynLVLsTOz0bNYW/0Ua2t9u6dAYGbbq6bLikl3zxzQbdm9lXMUzmaYAdIA==} + /storybook@7.4.6: + resolution: {integrity: sha512-YkFSpnR47j5zz7yElA+2axLjXN7K7TxDGJRHHlqXmG5iQ0PXzmjrj2RxMDKFz4Ybp/QjEUoJ4rx//ESEY0Nb5A==} hasBin: true dependencies: - '@storybook/cli': 7.4.5 + '@storybook/cli': 7.4.6 transitivePeerDependencies: - bufferutil - encoding @@ -18088,8 +18130,8 @@ packages: resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} dev: true - /systeminformation@5.21.9: - resolution: {integrity: sha512-7pI4mu9P/2MGDV0T49B52E7IULBGj+kRVk6JSYUj5qfAk7N7C7aNX15fXziqrbgZntc6/jjYzWeb/x41jhg/eA==} + /systeminformation@5.21.11: + resolution: {integrity: sha512-dIJEGoP5W7k4JJGje/b+inJrOL5hV9LPsUi5ndBvJydI80CVEcu2DZYgt6prdRErDi2SA4SqYd/WMR4b+u34mA==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -18182,8 +18224,8 @@ packages: unique-string: 2.0.0 dev: true - /terser@5.20.0: - resolution: {integrity: sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==} + /terser@5.21.0: + resolution: {integrity: sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==} engines: {node: '>=10'} hasBin: true dependencies: @@ -18986,8 +19028,8 @@ packages: core-util-is: 1.0.2 extsprintf: 1.3.0 - /vite-node@0.34.5(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0): - resolution: {integrity: sha512-RNZ+DwbCvDoI5CbCSQSyRyzDTfFvFauvMs6Yq4ObJROKlIKuat1KgSX/Ako5rlDMfVCyMcpMRMTkJBxd6z8YRA==} + /vite-node@0.34.6(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0): + resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} hasBin: true dependencies: @@ -18996,7 +19038,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) transitivePeerDependencies: - '@types/node' - less @@ -19012,8 +19054,8 @@ packages: resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==} dev: true - /vite@4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0): - resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} + /vite@4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0): + resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -19040,29 +19082,29 @@ packages: terser: optional: true dependencies: - '@types/node': 20.7.1 + '@types/node': 20.8.2 esbuild: 0.18.17 postcss: 8.4.31 rollup: 3.29.4 - sass: 1.68.0 - terser: 5.20.0 + sass: 1.69.0 + terser: 5.21.0 optionalDependencies: fsevents: 2.3.2 - /vitest-fetch-mock@0.2.2(vitest@0.34.5): + /vitest-fetch-mock@0.2.2(vitest@0.34.6): resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==} engines: {node: '>=14.14.0'} peerDependencies: vitest: '>=0.16.0' dependencies: cross-fetch: 3.1.5 - vitest: 0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0) + vitest: 0.34.6(happy-dom@10.0.3)(sass@1.69.0)(terser@5.21.0) transitivePeerDependencies: - encoding dev: true - /vitest@0.34.5(happy-dom@10.0.3)(sass@1.68.0)(terser@5.20.0): - resolution: {integrity: sha512-CPI68mmnr2DThSB3frSuE5RLm9wo5wU4fbDrDwWQQB1CWgq9jQVoQwnQSzYAjdoBOPoH2UtXpOgHVge/uScfZg==} + /vitest@0.34.6(happy-dom@10.0.3)(sass@1.69.0)(terser@5.21.0): + resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} hasBin: true peerDependencies: @@ -19094,16 +19136,16 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.7.1 - '@vitest/expect': 0.34.5 - '@vitest/runner': 0.34.5 - '@vitest/snapshot': 0.34.5 - '@vitest/spy': 0.34.5 - '@vitest/utils': 0.34.5 + '@types/node': 20.8.2 + '@vitest/expect': 0.34.6 + '@vitest/runner': 0.34.6 + '@vitest/snapshot': 0.34.6 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 acorn: 8.10.0 acorn-walk: 8.2.0 cac: 6.7.14 - chai: 4.3.7 + chai: 4.3.10 debug: 4.3.4(supports-color@8.1.1) happy-dom: 10.0.3 local-pkg: 0.4.3 @@ -19114,8 +19156,8 @@ packages: strip-literal: 1.0.1 tinybench: 2.5.0 tinypool: 0.7.0 - vite: 4.4.9(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) - vite-node: 0.34.5(@types/node@20.7.1)(sass@1.68.0)(terser@5.20.0) + vite: 4.4.11(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) + vite-node: 0.34.6(@types/node@20.8.2)(sass@1.69.0)(terser@5.21.0) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -19150,21 +19192,6 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.6(vue@3.3.4): - resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - dependencies: - vue: 3.3.4 - dev: false - /vue-docgen-api@4.64.1(vue@3.3.4): resolution: {integrity: sha512-jbOf7ByE3Zvtuk+429Jorl+eIeh2aB2Fx1GUo3xJd1aByJWE8KDlSEa6b11PB1ze8f0sRUBraRDinICCk0KY7g==} dependencies: @@ -19209,11 +19236,6 @@ packages: vue: 3.3.4 dev: true - /vue-multiselect@2.1.7: - resolution: {integrity: sha512-KIegcN+Ntwg3cbkY/jhw2s/+XJUM0Lpi/LcKFYCS8PrZHcWBl2iKCVze7ZCnRj3w8H7/lUJ9v7rj9KQiNxApBw==} - engines: {node: '>= 4.0.0', npm: '>= 3.0.0'} - dev: false - /vue-prism-editor@2.0.0-alpha.2(vue@3.3.4): resolution: {integrity: sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==} engines: {node: '>=10'} @@ -19693,7 +19715,7 @@ packages: sharp: 0.31.3 dev: false - github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.4.5)(@storybook/components@7.4.5)(@storybook/core-events@7.4.5)(@storybook/manager-api@7.4.5)(@storybook/preview-api@7.4.5)(@storybook/theming@7.4.5)(@storybook/types@7.4.5)(react-dom@18.2.0)(react@18.2.0): + github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.4.6)(@storybook/components@7.4.6)(@storybook/core-events@7.4.6)(@storybook/manager-api@7.4.6)(@storybook/preview-api@7.4.6)(@storybook/theming@7.4.6)(@storybook/types@7.4.6)(react-dom@18.2.0)(react@18.2.0): resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640} id: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640 name: storybook-addon-misskey-theme @@ -19714,13 +19736,13 @@ packages: react-dom: optional: true dependencies: - '@storybook/blocks': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/components': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.4.5 - '@storybook/manager-api': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.4.5 - '@storybook/theming': 7.4.5(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.5 + '@storybook/blocks': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.4.6 + '@storybook/manager-api': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.4.6 + '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.4.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true