diff --git a/CHANGELOG.md b/CHANGELOG.md index 66b2d50dc08c1fcdab6a4c3907b9cf1f1964bd3b..9b42370b50b4ef2c62732c05364cfd68d3f15aca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,35 @@ --> +## 13.10.3 + +### General +- コンディショナルãƒãƒ¼ãƒ«ã®æ¡ä»¶ã«ã€ŒæŠ•ç¨¿æ•°ãŒï½žä»¥ä¸‹ã€ã€ŒæŠ•ç¨¿æ•°ãŒï½žä»¥ä¸Šã€ã‚’è¿½åŠ +- リアクションéžå¯¾å¿œAP実装ã‹ã‚‰ã®Likeアクティビティã®è§£é‡ˆã‚’ðŸ‘ã‹ã‚‰â™¥ã« + +### Client +- クリップボタンをノートアクションã«è¿½åŠ ã§ãるよã†ã« +- センシティブワードã®ä¸€è¦§ã«ãƒ”ン留ã‚ユーザーã®IDãŒè¡¨ç¤ºã•ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ + +### Server +- リモートユーザーã®ãƒãƒ£ãƒ¼ãƒˆç”Ÿæˆã‚’無効ã«ã™ã‚‹ã‚ªãƒ—ã‚·ãƒ§ãƒ³ã‚’è¿½åŠ +- リモートサーãƒãƒ¼ã®ãƒãƒ£ãƒ¼ãƒˆç”Ÿæˆã‚’無効ã«ã™ã‚‹ã‚ªãƒ—ã‚·ãƒ§ãƒ³ã‚’è¿½åŠ +- ドライブã®ãƒãƒ£ãƒ¼ãƒˆã¯ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ç”Ÿæˆã™ã‚‹ã‚ˆã†ã« +- 空ã®ã‚¢ãƒ³ãƒ†ãƒŠãŒä½œæˆã§ãã‚‹ã®ã‚’ä¿®æ£ + +## 13.10.2 + +### Server +- 絵文å—を編集ã™ã‚‹ã¨ä¿å˜ã§ããªã„ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ + +### Client +- ドライブファイルã®ãƒ¡ãƒ‹ãƒ¥ãƒ¼ãŒæ£å¸¸ã«å‹•ä½œã—ãªã„å•é¡Œã‚’ä¿®æ£ + +## 13.10.1 + +### Client +- Misskey Playã®Playボタンを押ã—ãŸæ™‚ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã™ã‚‹å•é¡Œã‚’ä¿®æ£ + ## 13.10.0 ### General @@ -22,15 +51,19 @@ - ãƒãƒ¼ãƒ«ã®ä¸¦ã³é †ã‚’è¨å®šå¯èƒ½ã« - カスタム絵文å—ã«ãƒ©ã‚¤ã‚»ãƒ³ã‚¹æƒ…å ±ã‚’ä»˜ä¸Žã§ãるよã†ã« - 指定ã—ãŸæ–‡å—列をå«ã‚€æŠ•ç¨¿ã®å…¬é–‹ç¯„囲をホームã«ã§ãるよã†ã« +- 使ã‚ã‚Œã¦ãªã„アンテナã¯è‡ªå‹•åœæ¢ã•ã‚Œã‚‹ã‚ˆã†ã« ### Client - è¨å®šã‹ã‚‰è‡ªåˆ†ã®ãƒãƒ¼ãƒ«ã‚’確èªã§ãるよã†ã« - åºƒå‘Šä¸€è¦§ãƒšãƒ¼ã‚¸ã‚’è¿½åŠ +- ãƒ‰ãƒ©ã‚¤ãƒ–ã‚¯ãƒªãƒ¼ãƒŠãƒ¼ã‚’è¿½åŠ - DM作æˆæ™‚ã«ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã‚‚å«ã‚€ã‚ˆã†ã« - フォãƒãƒ¼ç”³è«‹ã®ãƒœã‚¿ãƒ³ã®ãƒ‡ã‚¶ã‚¤ãƒ³ã‚’改善 - 付箋ウィジェットã®é«˜ã•ã‚’è¨å®šå¯èƒ½ã« - APオブジェクトを入力ã—ã¦ãƒ•ã‚§ãƒƒãƒã™ã‚‹æ©Ÿèƒ½ã¨ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚„ノートã®æ¤œç´¢æ©Ÿèƒ½ã‚’分離 - ナビゲーションãƒãƒ¼ã®é …ç›®ã«ã€Œãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã€ã‚’è¿½åŠ ã§ãるよã†ã« +- ナビゲーションãƒãƒ¼ã®ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºã‚’ドラッグ&ドãƒãƒƒãƒ—ã§è¡Œãˆã‚‹ã‚ˆã†ã« +- ジョブã‚ューã®å†è©¦è¡Œã‚’ワンクリックã§ã§ãるよã†ã« - AiScriptã‚’0.13.1ã«æ›´æ–° - oEmbedをサãƒãƒ¼ãƒˆã—ã¦ã„るウェブサイトã®ãƒ—レビューãŒã§ãるよã†ã« - YouTubeã‚’oEmbedã§ãƒãƒ¼ãƒ‰ã—ã€ãƒ—レビューã§å…±æœ‰ãƒœã‚¿ãƒ³ã‚’押ã™ã¨OSã®å…±æœ‰ç”»é¢ãŒã§ã‚‹ã‚ˆã†ã« @@ -42,6 +75,7 @@ - Safariã§ãƒ—ラグインãŒè¤‡æ•°ã‚ã‚‹å ´åˆã«æ£å¸¸ã«èªã¿è¾¼ã¾ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ - Bookwyrmã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ãƒšãƒ¼ã‚¸ã§ã€Œãƒªãƒ¢ãƒ¼ãƒˆã§è¡¨ç¤ºã€ã‚’タップã—ã¦ã‚‚åå¿œãŒãªã„å•é¡Œã‚’ä¿®æ£ - éžãƒã‚°ã‚¤ãƒ³æ™‚ã®ã€ŒMisskeyã«ã¤ã„ã¦ã€ã®è¡¨ç¤ºã‚’ä¿®æ£ +- PC版ã«ã¦ã€Œè¨å®šã€ã€Œã‚³ãƒ³ãƒˆãƒãƒ¼ãƒ«ãƒ‘ãƒãƒ«ã€ã®ãƒªãƒ³ã‚¯ã‚’2度以上続ã‘ã¦ã‚¯ãƒªãƒƒã‚¯ã—ãŸéš›ã«ç©ºç™½ã®ãƒšãƒ¼ã‚¸ãŒè¡¨ç¤ºã•ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ ### Server - OpenAPIエンドãƒã‚¤ãƒ³ãƒˆã‚’復旧 @@ -59,6 +93,7 @@ - リテンション分æžãŒä¸Šæ‰‹ã機能ã—ãªã„ã“ã¨ãŒã‚ã‚‹ã®ã‚’ä¿®æ£ - 空ã®ã‚¢ãƒ³ãƒ†ãƒŠãŒä½œæˆã§ããªã„よã†ã«ä¿®æ£ - 特定ã®æ¡ä»¶ã§é€šå ±ãŒè¦‹ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- 絵文å—ã®åå‰ã«ä»»æ„ã®æ–‡å—ãŒä½¿ç”¨ã§ãã‚‹å•é¡Œã‚’ä¿®æ£ ## 13.9.2 (2023/03/06) diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 5254b20ef0a66409dfff354ef0d646980b15f822..c2910b90cd61b8ea38a0ba1cb73033d435df508f 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -545,7 +545,6 @@ tokenRequested: "Ù…Ù†Ø ØÙ‚ الوصول إلى الØساب" pluginTokenRequestedDescription: "ستتمكن الإضاÙØ© من استخدام هذه الأذونات." notificationType: "أنواع الإشعارات" edit: "التعديل" -useStarForReactionFallback: "استخدم ★ كبديل إذا كان التÙاعل مجهولًا" emailServer: "خادم البريد الإلكتروني" emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها." email: "البريد الإلكتروني " @@ -1275,3 +1274,7 @@ _deck: channel: "القنوات" mentions: "الإشارات" direct: "مباشرة" +_webhookSettings: + name: "الإسم" + active: "Ù…Ùعّل" + diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 49b76b8ab356678fa218afa81dd927bae445c857..40af5a33260840ad0839b069739ecd44d84ca279 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -562,7 +562,6 @@ tokenRequested: "অà§à¦¯à¦¾à¦•à¦¾à¦‰à¦¨à§à¦Ÿà§‡ অà§à¦¯à¦¾à¦•à§à¦¸à§‡à¦¸ pluginTokenRequestedDescription: "à¦à¦‡ পà§à¦²à¦¾à¦—ইনটি à¦à¦–ানে দেওয়া অনà§à¦®à§à¦¤à¦¿à¦¸à¦®à§‚হ বà§à¦¯à¦¾à¦¬à¦¹à¦¾à¦° করবে" notificationType: "বিজà§à¦žà¦ªà§à¦¤à¦¿à¦° ধরন" edit: "সমà§à¦ªà¦¾à¦¦à¦¨à¦¾" -useStarForReactionFallback: "রিঅà§à¦¯à¦¾à¦•à¦¶à¦¨à§‡à¦° ইমোজি না জানলে ★ বà§à¦¯à¦¬à¦¹à¦¾à¦° করà§à¦¨" emailServer: "ইমেইল সারà§à¦à¦¾à¦°" enableEmail: "ইমেইল বিতরণ চালৠকরà§à¦¨" emailConfigInfo: "আপনার ইমেল ঠিকানা নিশà§à¦šà¦¿à¦¤ করতে à¦à¦¬à¦‚ আপনার পাসওয়ারà§à¦¡ পà§à¦¨à¦°à¦¾à¦¯à¦¼ সেট করতে বà§à¦¯à¦¬à¦¹à§ƒà¦¤ হয়" @@ -1354,3 +1353,7 @@ _deck: channel: "চà§à¦¯à¦¾à¦¨à§‡à¦²à¦—à§à¦²à¦¿" mentions: "উলà§à¦²à§‡à¦–সমূহ" direct: "ডাইরেকà§à¦Ÿ নোটগà§à¦²à¦¿" +_webhookSettings: + name: "নাম" + active: "চালà§" + diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 2b1168f7803f5c53fc88f7b78e0cfb140e8ebde3..bc9e66249390fabd230ce07b27358f42d405ecb2 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -460,3 +460,4 @@ _deck: list: "Llistes" mentions: "Mencions" direct: "Publicacions directes" + diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 7f665895b90f49ed434b0868d868f59e732d0ad9..4b59192474a7b703ce411eed3c04072238ec7c96 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -776,3 +776,7 @@ _deck: list: "Seznamy" channel: "Kanály" mentions: "ZmÃnÄ›nÃ" +_webhookSettings: + name: "Jméno" + active: "Zapnuto" + diff --git a/locales/da-DK.yml b/locales/da-DK.yml index 08c15ed092fc217aec263728d823d1f62a0fb88f..d1fbec9f6791cea281989c9cb2a38ef4c6c44588 100644 --- a/locales/da-DK.yml +++ b/locales/da-DK.yml @@ -1,2 +1,3 @@ --- _lang_: "Dansk" + diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 08808ea6a4fb60c4befae07df833b780e4a533f6..f6c28fbded87bb63f78a2de003cfd210b4224490 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -594,7 +594,6 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren" pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können." notificationType: "Art der Benachrichtigung" edit: "Bearbeiten" -useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist" emailServer: "Email-Server" enableEmail: "Email-Versand aktivieren" emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet" @@ -977,6 +976,10 @@ notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar." license: "Lizenz" unfavoriteConfirm: "Wirklich aus Favoriten entfernen?" myClips: "Meine Clips" +drivecleaner: "Drive-Reiniger" +retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen" +retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?" +retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen." _achievements: earnedAt: "Freigeschaltet am" _types: @@ -1273,6 +1276,8 @@ _role: followersMoreThanOrEq: "Hat X oder mehr Follower" followingLessThanOrEq: "Folgt X oder weniger Benutzern" followingMoreThanOrEq: "Folgt X oder mehr Benutzern" + notesLessThanOrEq: "Beitragszahl ist kleiner-gleich" + notesMoreThanOrEq: "Beitragszahl ist größer-gleich" and: "UND-Bedingung" or: "ODER-Bedingung" not: "NICHT-Bedingung" @@ -1868,3 +1873,10 @@ _dialog: _disabledTimeline: title: "Chronik deaktiviert" description: "Mit deinen jetzigen Rollen ist diese Chronik nicht verfügbar." +_drivecleaner: + orderBySizeDesc: "Absteigende Dateigrößen" + orderByCreatedAtAsc: "Aufsteigendes Erstelldatum" +_webhookSettings: + name: "Name" + active: "Aktiviert" + diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 0721ba6e999bdd409e02038fa8e865599abfbc37..634e36c29eedc26ff845fdc2b4fd2d34eb17dc94 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -392,3 +392,6 @@ _deck: antenna: "ΑντÎνες" list: "Λίστα" mentions: "Επισημάνσεις" +_webhookSettings: + name: "Όνομα" + diff --git a/locales/en-US.yml b/locales/en-US.yml index 9e018ce2acdb92972b9a625db8c19a644485b7c1..6489919e8a7d65e7a309cfa5995756679a9ce575 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -530,7 +530,7 @@ nothing: "There's nothing to see here" installedDate: "Authorized at" lastUsedDate: "Last used at" state: "State" -sort: "Sort" +sort: "Sorting order" ascendingOrder: "Ascending" descendingOrder: "Descending" scratchpad: "Scratchpad" @@ -594,7 +594,6 @@ tokenRequested: "Grant access to account" pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here." notificationType: "Notification type" edit: "Edit" -useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown" emailServer: "Email server" enableEmail: "Enable email distribution" emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password" @@ -977,6 +976,13 @@ notesSearchNotAvailable: "Note search is unavailable." license: "License" unfavoriteConfirm: "Really remove from favorites?" myClips: "My clips" +drivecleaner: "Drive Cleaner" +retryAllQueuesNow: "Retry running all queues" +retryAllQueuesConfirmTitle: "Really retry all?" +retryAllQueuesConfirmText: "This will temporarily increase the server load." +enableChartsForRemoteUser: "Generate remote user data charts" +enableChartsForFederatedInstances: "Generate remote instance data charts" +showClipButtonInNoteFooter: "Add \"Clip\" to note action menu" _achievements: earnedAt: "Unlocked at" _types: @@ -1273,6 +1279,8 @@ _role: followersMoreThanOrEq: "Has X or more followers" followingLessThanOrEq: "Follows X or fewer accounts" followingMoreThanOrEq: "Follows X or more accounts" + notesLessThanOrEq: "Post count is less than/equal to" + notesMoreThanOrEq: "Post count is greater than/equal to" and: "AND-Condition" or: "OR-Condition" not: "NOT-Condition" @@ -1868,3 +1876,21 @@ _dialog: _disabledTimeline: title: "Timeline disabled" description: "You cannot use this timeline under your current roles." +_drivecleaner: + orderBySizeDesc: "Descending Filesizes" + orderByCreatedAtAsc: "Ascending Dates" +_webhookSettings: + createWebhook: "Create Webhook" + name: "Name" + secret: "Secret" + events: "Webhook Events" + active: "Enabled" + _events: + follow: "When following a user" + followed: "When being followed" + note: "When posting a note" + reply: "When receiving a reply" + renote: "When renoted" + reaction: "When receiving a reaction" + mention: "When being mentioned" + diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 3e73e4c5ea9e379c2aa7e650ef92d006144616ea..70327c5eac11ea05a35032b579ca7777d666cf73 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -594,7 +594,6 @@ tokenRequested: "Permiso de acceso a la cuenta" pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquÃ" notificationType: "Tipo de notificación" edit: "Editar" -useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella" emailServer: "Servidor de correo" enableEmail: "Activar el envÃo de correos electrónicos" emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña" @@ -973,6 +972,14 @@ rolesAssignedToMe: "Roles asignados a mÃ" resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" sensitiveWords: "Palabras sensibles" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de lÃnea" +notesSearchNotAvailable: "No se puede buscar una nota" +license: "Licencia" +unfavoriteConfirm: "¿Desea quitar de favoritos?" +myClips: "Mis clips" +drivecleaner: "Limpiador del Drive" +retryAllQueuesNow: "Reintentar inmediatamente todas las colas" +retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?" +retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente " _achievements: earnedAt: "Desbloqueado el" _types: @@ -1864,3 +1871,10 @@ _dialog: _disabledTimeline: title: "LÃnea de tiempo deshabilitada" description: "No puedes usar esta lÃnea de tiempo con tus roles actuales." +_drivecleaner: + orderBySizeDesc: "Más grandes" + orderByCreatedAtAsc: "Más antiguos" +_webhookSettings: + name: "Nombre" + active: "Activado" + diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index f9b8939e8b2ebd60aa265dabdeeb5751ea6f1b5b..11573e0ce0034cfc5755257115e9bb62e4487527 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -575,7 +575,6 @@ tokenRequested: "Autoriser l'accès au compte" pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." notificationType: "Type de notifications" edit: "Editer" -useStarForReactionFallback: "Utiliser ★ comme alternative si l’émoji de réaction est inconnu" emailServer: "Serveur mail" enableEmail: "Activer la distribution de courriel" emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas d’oubli." @@ -1468,3 +1467,7 @@ _deck: channel: "Canaux" mentions: "Mentions" direct: "Direct" +_webhookSettings: + name: "Nom" + active: "Activé" + diff --git a/locales/hr-HR.yml b/locales/hr-HR.yml index ed97d539c095cf1413af30cc23dea272095b97dd..cd21505a47e530a967e3c44bd2a772d1b8d08bd7 100644 --- a/locales/hr-HR.yml +++ b/locales/hr-HR.yml @@ -1 +1,2 @@ --- + diff --git a/locales/ht-HT.yml b/locales/ht-HT.yml index ed97d539c095cf1413af30cc23dea272095b97dd..cd21505a47e530a967e3c44bd2a772d1b8d08bd7 100644 --- a/locales/ht-HT.yml +++ b/locales/ht-HT.yml @@ -1 +1,2 @@ --- + diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 5d74cf5389a22f52e8da2b7c53363abe55f04616..e5a057477a238fe35c96670fc0ba304d3969644d 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -579,7 +579,6 @@ tokenRequested: "Berikan ijin akses ke akun" pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini." notificationType: "Jenis pemberitahuan" edit: "Sunting" -useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui" emailServer: "Peladen surel" enableEmail: "Nyalakan distribusi surel" emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi" @@ -1804,3 +1803,7 @@ _deck: channel: "Kanal" mentions: "Sebutan" direct: "Langsung" +_webhookSettings: + name: "Nama" + active: "Aktif" + diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 44499fa3dd9cf01e286943430718ad297983169a..ddd1e5e90a5e7225055d6cd976d1443938252b81 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -565,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito" visibility: "Visibilità " poll: "Sondaggio" useCw: "Nascondere media" -enablePlayer: "Apri in lettore video" -disablePlayer: "Chiudi il lettore" +enablePlayer: "Visualizza" +disablePlayer: "Chiudi" expandTweet: "Espandi tweet" themeEditor: "Editor di temi" description: "Descrizione" @@ -594,7 +594,6 @@ tokenRequested: "Autorizza accesso al profilo" pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui." notificationType: "Tipo di notifiche" edit: "Modifica" -useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa." emailServer: "Server email" enableEmail: "Abilita consegna email" emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password" @@ -977,6 +976,10 @@ notesSearchNotAvailable: "Non è possibile cercare tra le Note." license: "Licenza" unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?" myClips: "Le mie Clip" +drivecleaner: "Drive cleaner" +retryAllQueuesNow: "Ritenta di consumare tutte le code" +retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?" +retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente." _achievements: earnedAt: "Data di conseguimento" _types: @@ -1868,3 +1871,10 @@ _dialog: _disabledTimeline: title: "Timeline disabilitata" description: "Il tuo ruolo non ha i permessi per accedere a questa timeline" +_drivecleaner: + orderBySizeDesc: "Dal più grande al più piccolo" + orderByCreatedAtAsc: "Dal più vecchio al più recente" +_webhookSettings: + name: "Nome" + active: "Attivo" + diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c4e86fc64ab2426c5a4b2da558d64d296e223687..cf4ede30b62977ef54566e9f8523baeb8c71217f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -460,7 +460,7 @@ aboutX: "{x}ã«ã¤ã„ã¦" emojiStyle: "絵文å—ã®ã‚¹ã‚¿ã‚¤ãƒ«" native: "ãƒã‚¤ãƒ†ã‚£ãƒ–" disableDrawer: "メニューをドãƒãƒ¯ãƒ¼ã§è¡¨ç¤ºã—ãªã„" -showNoteActionsOnlyHover: "ノートã®æ“作部をホãƒãƒ¼æ™‚ã®ã¿è¡¨ç¤ºã™ã‚‹" +showNoteActionsOnlyHover: "ノートã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ホãƒãƒ¼æ™‚ã®ã¿è¡¨ç¤ºã™ã‚‹" noHistory: "å±¥æ´ã¯ã‚ã‚Šã¾ã›ã‚“" signinHistory: "ãƒã‚°ã‚¤ãƒ³å±¥æ´" enableAdvancedMfm: "高度ãªMFMを有効ã«ã™ã‚‹" @@ -594,7 +594,6 @@ tokenRequested: "アカウントã¸ã®ã‚¢ã‚¯ã‚»ã‚¹è¨±å¯" pluginTokenRequestedDescription: "ã“ã®ãƒ—ラグインã¯ã“ã“ã§è¨å®šã—ãŸæ¨©é™ã‚’行使ã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚" notificationType: "通知ã®ç¨®é¡ž" edit: "編集" -useStarForReactionFallback: "リアクション絵文å—ãŒä¸æ˜Žãªå ´åˆã€ä»£ã‚ã‚Šã«â˜…を使ã†" emailServer: "メールサーãƒãƒ¼" enableEmail: "メールé…信機能を有効化ã™ã‚‹" emailConfigInfo: "メールアドレスã®ç¢ºèªã‚„パスワードリセットã®éš›ã«ä½¿ã„ã¾ã™" @@ -977,6 +976,13 @@ notesSearchNotAvailable: "ノート検索ã¯åˆ©ç”¨ã§ãã¾ã›ã‚“。" license: "ライセンス" unfavoriteConfirm: "ãŠæ°—ã«å…¥ã‚Šè§£é™¤ã—ã¾ã™ã‹ï¼Ÿ" myClips: "自分ã®ã‚¯ãƒªãƒƒãƒ—" +drivecleaner: "ドライブクリーナー" +retryAllQueuesNow: "ã™ã¹ã¦ã®ã‚ューを今ã™ãå†è©¦è¡Œ" +retryAllQueuesConfirmTitle: "今ã™ãå†è©¦è¡Œã—ã¾ã™ã‹ï¼Ÿ" +retryAllQueuesConfirmText: "一時的ã«ã‚µãƒ¼ãƒãƒ¼ã®è² è·ãŒå¢—大ã™ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚" +enableChartsForRemoteUser: "リモートユーザーã®ãƒãƒ£ãƒ¼ãƒˆã‚’生æˆ" +enableChartsForFederatedInstances: "リモートサーãƒãƒ¼ã®ãƒãƒ£ãƒ¼ãƒˆã‚’生æˆ" +showClipButtonInNoteFooter: "ノートã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«ã‚¯ãƒªãƒƒãƒ—ã‚’è¿½åŠ " _achievements: earnedAt: "ç²å¾—日時" @@ -1275,6 +1281,8 @@ _role: followersMoreThanOrEq: "フォãƒãƒ¯ãƒ¼æ•°ãŒï½žä»¥ä¸Š" followingLessThanOrEq: "フォãƒãƒ¼æ•°ãŒï½žä»¥ä¸‹" followingMoreThanOrEq: "フォãƒãƒ¼æ•°ãŒï½žä»¥ä¸Š" + notesLessThanOrEq: "投稿数ãŒï½žä»¥ä¸‹" + notesMoreThanOrEq: "投稿数ãŒï½žä»¥ä¸Š" and: "~ã‹ã¤ï½ž" or: "~ã¾ãŸã¯ï½ž" not: "~ã§ã¯ãªã„" @@ -1922,3 +1930,23 @@ _dialog: _disabledTimeline: title: "無効化ã•ã‚ŒãŸã‚¿ã‚¤ãƒ ライン" description: "ç¾åœ¨ã®ãƒãƒ¼ãƒ«ã§ã¯ã€ã“ã®ã‚¿ã‚¤ãƒ ラインを使用ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。" + +_drivecleaner: + orderBySizeDesc: "サイズãŒå¤§ãã„é †" + orderByCreatedAtAsc: "è¿½åŠ æ—¥ãŒå¤ã„é †" + +_webhookSettings: + createWebhook: "Webhookを作æˆ" + name: "åå‰" + secret: "シークレット" + events: "Webhookを実行ã™ã‚‹ã‚¿ã‚¤ãƒŸãƒ³ã‚°" + active: "有効" + _events: + follow: "フォãƒãƒ¼ã—ãŸã¨ã" + followed: "フォãƒãƒ¼ã•ã‚ŒãŸã¨ã" + note: "ノートを投稿ã—ãŸã¨ã" + reply: "返信ã•ã‚ŒãŸã¨ã" + renote: "Renoteã•ã‚ŒãŸã¨ã" + reaction: "リアクションãŒã‚ã£ãŸã¨ã" + mention: "メンションã•ã‚ŒãŸã¨ã" + diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index bd9ae46d34e66d76b046a98490d0f40dc1280dc7..5b1b312b780f754164fae75d5211fa6d6202b454 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -594,7 +594,6 @@ tokenRequested: "アカウントã¸ã®ã‚¢ã‚¯ã‚»ã‚¹è¨±ã—ã¦ã‚„ã£ãŸã‚‰ã©ã†ã‚„ pluginTokenRequestedDescription: "ã“ã®ãƒ—ラグインã¯ã“ã“ã§è¨å®šã—ãŸæ¨©é™ã‚’使ãˆã‚‹ã‚ˆã†ã«ãªã‚‹ã§ã€‚" notificationType: "通知ã®ç¨®é¡ž" edit: "編集" -useStarForReactionFallback: "リアクションãŒã‚ˆã†ã‚ã‹ã‚‰ã‚“å ´åˆã€â˜…を使ã†" emailServer: "メールサーãƒãƒ¼" enableEmail: "メールé…ä¿¡ã‚’å—ã‘å–ã‚‹" emailConfigInfo: "メールアドレスã®ç¢ºèªã¨ã‹ãƒ‘スワードリセットã®æ™‚ã«ä½¿ã†ã§" @@ -977,6 +976,13 @@ notesSearchNotAvailable: "ノート検索ã¯ä½¿ã‚ã‚Œã¸ã‚“ã§ã€‚" license: "ライセンス" unfavoriteConfirm: "ã»ã‚“ã¾ã«æ°—ã«å…¥ã‚‰ã‚“ã®ï¼Ÿ" myClips: "自分ã®ã‚¯ãƒªãƒƒãƒ—" +drivecleaner: "ドライブã‚レイã‚レイ" +retryAllQueuesNow: "ã‚ューを全部もã£ã‹ã„ã‚„ã‚Šç›´ã™" +retryAllQueuesConfirmTitle: "ã‚‚ã£ã‹ã„ã‚„ã£ã¦ã¿ã‚‹ã‹ï¼Ÿ" +retryAllQueuesConfirmText: "一時的ã«ã‚µãƒ¼ãƒãƒ¼é‡ãªã‚‹ã‹ã‚‚ã—ã‚Œã¸ã‚“ã§ã€‚" +enableChartsForRemoteUser: "リモートユーザーã®ãƒãƒ£ãƒ¼ãƒˆã‚’作る" +enableChartsForFederatedInstances: "リモートサーãƒãƒ¼ã®ãƒãƒ£ãƒ¼ãƒˆã‚’作る" +showClipButtonInNoteFooter: "ノートã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«ã‚¯ãƒªãƒƒãƒ—ã‚’è¿½åŠ " _achievements: earnedAt: "è²°ã£ãŸæ—¥ãƒ" _types: @@ -1273,6 +1279,8 @@ _role: followersMoreThanOrEq: "フォãƒãƒ¯ãƒ¼æ•°ãŒï½žä»¥ä¸Š" followingLessThanOrEq: "フォãƒãƒ¼æ•°ãŒï½žä»¥ä¸‹" followingMoreThanOrEq: "フォãƒãƒ¼æ•°ãŒï½žä»¥ä¸Š" + notesLessThanOrEq: "投稿数ãŒï½žä»¥ä¸‹ã—ã‹ãªã„" + notesMoreThanOrEq: "投稿を~以上ã—ã¨ã‚‹" and: "~ã‹ã¤ï½ž" or: "~ã¾ãŸã¯ï½ž" not: "~ã§ã¯ãªã„" @@ -1868,3 +1876,21 @@ _dialog: _disabledTimeline: title: "使ã‚ã‚Œã¸ã‚“タイムライン" description: "ã‚ã‚“ãŸã®ä»Šã®ãƒãƒ¼ãƒ«ã‚„ã£ãŸã‚‰ã€ã“ã®ã‚¿ã‚¤ãƒ ラインã¯ä½¿ã‚ã‚Œã¸ã‚“ã§ã€‚" +_drivecleaner: + orderBySizeDesc: "サイズã®ã§ã‹ã„é †" + orderByCreatedAtAsc: "è¿½åŠ æ—¥ã®å¤ã„é †" +_webhookSettings: + createWebhook: "Webhookã‚’ã¤ãã‚‹" + name: "åå‰" + secret: "シークレット" + events: "Webhookを投ã’るタイミング" + active: "有効" + _events: + follow: "フォãƒãƒ¼ã—ãŸã¨ã~ï¼" + followed: "フォãƒãƒ¼ã‚‚らã£ãŸã¨ã~ï¼" + note: "ノートを投稿ã—ãŸã¨ã~ï¼" + reply: "返信ãŒã‚ã‚‹ã¨ã~ï¼" + renote: "Renoteã•ã‚Œã‚‹ã¨ã~ï¼" + reaction: "リアクションãŒã‚ã‚‹ã¨ã~ï¼" + mention: "メンションãŒã‚ã‚‹ã¨ã~ï¼" + diff --git a/locales/jbo-EN.yml b/locales/jbo-EN.yml index ed97d539c095cf1413af30cc23dea272095b97dd..cd21505a47e530a967e3c44bd2a772d1b8d08bd7 100644 --- a/locales/jbo-EN.yml +++ b/locales/jbo-EN.yml @@ -1 +1,2 @@ --- + diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index 18fd8f5a58d1651258fd7ce54959ab49cc9f1b60..8b43041e4c66fbcef8145a99ac166f3958f0b1b1 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -103,3 +103,4 @@ _deck: _columns: notifications: "IlÉ£uyen" list: "Tibdarin" + diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index ef66f3fbd234b5c028500b7a71a678b71dbb7d31..63a75302a159b51226438bd1002274169438d6ae 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -83,3 +83,4 @@ _deck: notifications: "ಅಧಿಸೂಚನೆಗಳà³" tl: "ಸಮಯಸಾಲà³" mentions: "ಹೆಸರಿಸಿದ" + diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index e52d619f8c65a43fff7819180d58a2b0386d5bce..31b4200c2e79f97dd5ad70c9d630de9facb28e51 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -592,7 +592,6 @@ tokenRequested: "ê³„ì • ì ‘ê·¼ 허용" pluginTokenRequestedDescription: "ì´ í”ŒëŸ¬ê·¸ì¸ì€ 여기서 ì„¤ì •í•œ ê¶Œí•œì„ ì‚¬ìš©í• ìˆ˜ 있게 ë©ë‹ˆë‹¤." notificationType: "알림 ìœ í˜•" edit: "편집" -useStarForReactionFallback: "ì•Œ 수 없는 리액션 ì´ëª¨ì§€ ëŒ€ì‹ â˜… 사용" emailServer: "ë©”ì¼ ì„œë²„" enableEmail: "ì´ë©”ì¼ ì†¡ì‹ ê¸°ëŠ¥ 활성화" emailConfigInfo: "가입 ì‹œ ë©”ì¼ ì£¼ì†Œ 확ì¸ì´ë‚˜ 비밀번호 초기화 ì‹œì— ì‚¬ìš©í•©ë‹ˆë‹¤." @@ -1849,3 +1848,7 @@ _deck: _dialog: charactersExceeded: "최대 글ìžìˆ˜ë¥¼ 초과하였습니다! 현재 {current} / 최대 {min}" charactersBelow: "최소 글ìžìˆ˜ 미만입니다! 현재 {current} / 최소 {min}" +_webhookSettings: + name: "ì´ë¦„" + active: "활성화" + diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 9c1a48c67ccfb14a6c778c6932833735606db957..5736fa67a7b678a0a171984958ed843525c1dc5f 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -368,3 +368,4 @@ _deck: list: "ລາàºàºàº²àº™" channel: "ຊ່àºàº‡" mentions: "àºà»ˆàº²àº§à»€àº–ິງ" + diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 3d33b5227e6b6956065901b1a9072ebe29537d64..efbb83c70ffe2c6439e2096a62fc34c9e25304f7 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -483,3 +483,6 @@ _deck: antenna: "Antennes" list: "Lijsten" mentions: "Vermeldingen" +_webhookSettings: + name: "Naam" + diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 83e189b9cf1c78ce3d50251015448b388294ddff..36a0a2e0e3c7177665f4f9fa0a335f61a69e5513 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -1,2 +1,3 @@ --- _lang_: "Norsk BokmÃ¥l" + diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 1dc818d45919c776f31d5c01355ee182d87f312a..cc0bbb1fac376db8ce9e98c1756d2302f4de81de 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -564,7 +564,6 @@ tokenRequested: "Przydziel dostÄ™p do konta" pluginTokenRequestedDescription: "Ta wtyczka bÄ™dzie mogÅ‚a korzystać z ustawionych tu uprawnieÅ„." notificationType: "Rodzaj powiadomieÅ„" edit: "Edytuj" -useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane" emailServer: "Serwer poczty e-mail" enableEmail: "WÅ‚Ä…cz dostarczanie wiadomoÅ›ci e-mail" emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasÅ‚a" @@ -1358,3 +1357,7 @@ _deck: channel: "KanaÅ‚y" mentions: "Wspomnienia" direct: "BezpoÅ›redni" +_webhookSettings: + name: "Nazwa" + active: "WÅ‚aczono" + diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 40b4aee7e6adbb188d0e5179bc12e59e3d2f0016..870ad501507f55d3b8c8be3f370b035e6405cfb2 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -555,3 +555,6 @@ _deck: list: "Listas" mentions: "Menções" direct: "Notas diretas" +_webhookSettings: + name: "Nome" + diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 10cb085f3fa13727a9122b6fc37bacd5fa264494..89f8afac9a3104b61121f338c6137257244ce591 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -561,7 +561,6 @@ tokenRequested: "Acordă acces la cont" pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici." notificationType: "Tipul notificării" edit: "Editează" -useStarForReactionFallback: "FoloseÈ™te ★ ca fallback dacă emoji-ul este necunoscut" emailServer: "Server email" enableEmail: "Activează distribuÈ›ia de emailuri" emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiÈ›i parola" @@ -702,3 +701,6 @@ _deck: list: "Liste" channel: "Canale" mentions: "MenÈ›iuni" +_webhookSettings: + name: "Nume" + diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 81ea01179b7a92ac544df6cbbb0b46ad14ede272..6a1756f8a2151a83cf6ba16b7df91e159b6c1813 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -585,7 +585,6 @@ tokenRequested: "Открыть доÑтуп к учётной запиÑи" pluginTokenRequestedDescription: "Ðто раÑширение Ñможет пользоватьÑÑ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñми, уÑтановленными здеÑÑŒ." notificationType: "Тип уведомлениÑ" edit: "Изменить" -useStarForReactionFallback: "Ставить ★ в качеÑтве реакции вмеÑто неизвеÑтного Ñмодзи" emailServer: "Сервер Ñлектронной почты" enableEmail: "Включить обмен Ñлектронной почтой" emailConfigInfo: "ИÑпользуетÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ Ð°Ð´Ñ€ÐµÑа Ñлектронной почты и ÑброÑа паролÑ." @@ -1837,3 +1836,7 @@ _deck: _dialog: charactersExceeded: "Превышено макÑимальное количеÑтво Ñимволов! У Ð²Ð°Ñ {current} / из {max}" charactersBelow: "Ðто ниже минимального количеÑтва Ñимволов! У Ð²Ð°Ñ {current} / из {min}" +_webhookSettings: + name: "Ðазвание" + active: "Вкл." + diff --git a/locales/si-LK.yml b/locales/si-LK.yml index ed97d539c095cf1413af30cc23dea272095b97dd..cd21505a47e530a967e3c44bd2a772d1b8d08bd7 100644 --- a/locales/si-LK.yml +++ b/locales/si-LK.yml @@ -1 +1,2 @@ --- + diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index d4be5540b8fce14e6638e1055601b6cca2d29f4f..ff6075b7038408736127c7128cd3d14407703a08 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -586,7 +586,6 @@ tokenRequested: "PovoliÅ¥ prÃstup k úÄtu" pluginTokenRequestedDescription: "Tento plugin bude môcÅ¥ použÃvaÅ¥ oprávnenia nastavené tu." notificationType: "Typ oznámenia" edit: "UpraviÅ¥" -useStarForReactionFallback: "PoužiÅ¥ ★ keÄ emoji reakcie nie je známe" emailServer: "Email server" enableEmail: "Zapnúť email" emailConfigInfo: "PoužÃva sa na overenie emaily pri registrácii alebo pri zabudnutà hesla" @@ -1475,3 +1474,7 @@ _deck: channel: "Kanály" mentions: "Zmienky" direct: "Priame poznámky" +_webhookSettings: + name: "Názov" + active: "Zapnuté" + diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 5e66df207639c2b198b794930070f27030bf1dd3..6ea5f77c21310b162b409840170cc98674281a69 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -442,3 +442,6 @@ _deck: antenna: "Antenner" list: "Listor" mentions: "Omnämningar" +_webhookSettings: + active: "Aktiverad" + diff --git a/locales/th-TH.yml b/locales/th-TH.yml index cf33e6642bb23f84acee2a82d9d1eb8f3057dd0e..a8b47843980c6324941712e5c80847e83c6676f3 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -544,6 +544,8 @@ userSuspended: "ผู้ใช้รายนี้ถูà¸à¸£à¸°à¸‡à¸±à¸šà¸ userSilenced: "ผู้ใช้รายนี้à¸à¸³à¸¥à¸±à¸‡à¸–ูà¸à¸›à¸´à¸”à¸à¸±à¹‰à¸™" yourAccountSuspendedTitle: "บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸™à¸±à¹‰à¸™à¸–ูà¸à¸£à¸°à¸‡à¸±à¸š" yourAccountSuspendedDescription: "บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸–ูà¸à¸£à¸°à¸‡à¸±à¸š เนื่à¸à¸‡à¸ˆà¸²à¸à¸¥à¸°à¹€à¸¡à¸´à¸”ข้à¸à¸à¸³à¸«à¸™à¸”ในà¸à¸²à¸£à¹ƒà¸«à¹‰à¸šà¸£à¸´à¸à¸²à¸£à¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸«à¸£à¸·à¸à¸à¸²à¸ˆà¸ˆà¸°à¸¥à¸°à¹€à¸¡à¸´à¸”หลัà¸à¹€à¸à¸“ฑ์ชุมชน หรืภà¸à¸²à¸ˆà¸ˆà¸°à¹‚ดนร้à¸à¸‡à¹€à¸£à¸µà¸¢à¸™à¹€à¸£à¸·à¹ˆà¸à¸‡à¸à¸²à¸£à¸¥à¸°à¹€à¸¡à¸´à¸”ลิขสิทธิ์à¹à¸¥à¸°à¸à¸·à¹ˆà¸™à¹†à¸à¸¢à¹ˆà¸²à¸‡à¸•à¹ˆà¸à¹€à¸™à¸·à¹ˆà¸à¸‡à¸‹à¹‰à¸³à¹† หาà¸à¸„ุณคิดว่าไม่ได้ทำผิดจริงๆหรืà¸à¸•à¸±à¸”สินผิดพลาด ได้โปรดà¸à¸£à¸¸à¸“าติดต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¸«à¸²à¸à¸„ุณต้à¸à¸‡à¸à¸²à¸£à¸—ราบเหตุผลโดยละเà¸à¸µà¸¢à¸”เพิ่มเติม à¹à¸¥à¸°à¸‚à¸à¸„วามà¸à¸£à¸¸à¸“าà¸à¸¢à¹ˆà¸²à¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µà¹ƒà¸«à¸¡à¹ˆ" +tokenRevoked: "โทเค็นไม่ถูà¸à¸•à¹‰à¸à¸‡" +accountDeleted: "ลบบัà¸à¸Šà¸µà¹à¸¥à¹‰à¸§" menu: "เมนู" divider: "ตัวà¹à¸šà¹ˆà¸‡" addItem: "เพิ่มรายà¸à¸²à¸£" @@ -587,7 +589,6 @@ tokenRequested: "ให้สิทธิ์à¸à¸²à¸£à¹€à¸‚้าถึงบั pluginTokenRequestedDescription: "ปลั๊à¸à¸à¸´à¸™à¸™à¸µà¹‰à¸ˆà¸°à¸ªà¸²à¸¡à¸²à¸£à¸–ใช้à¸à¸²à¸£à¸à¸™à¸¸à¸à¸²à¸•à¸—ี่ตั้งค่าไว้ที่นี่นะ" notificationType: "ประเภทà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" edit: "à¹à¸à¹‰à¹„ข" -useStarForReactionFallback: "ใช้ ★ เป็นทางเลืà¸à¸à¹à¸—นถ้าหาà¸à¹„ม่ทราบà¸à¸´à¹‚มจิ" emailServer: "à¸à¸µà¹€à¸¡à¸¥à¹Œà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" enableEmail: "เปิดใช้งานà¸à¸²à¸£à¸à¸£à¸°à¸ˆà¸²à¸¢à¸à¸µà¹€à¸¡à¸¥" emailConfigInfo: "ใช้เพื่à¸à¸¢à¸·à¸™à¸¢à¸±à¸™à¸à¸µà¹€à¸¡à¸¥à¸‚à¸à¸‡à¸„ุณระหว่างà¸à¸²à¸£à¸ªà¸¡à¸±à¸„รหรืà¸à¸–้าหาà¸à¸„ุณลืมรหัสผ่าน" @@ -959,6 +960,18 @@ invitationRequiredToRegister: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰à¹€à¸›à¹‡à¸™ emailNotSupported: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰à¹„ม่รà¸à¸‡à¸£à¸±à¸šà¸à¸²à¸£à¸ªà¹ˆà¸‡à¸à¸µà¹€à¸¡à¸¥à¸™à¸°à¸„่ะ" postToTheChannel: "โพสต์ลงช่à¸à¸‡" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนà¹à¸›à¸¥à¸‡à¹„ด้ในภายหลังนะ" +likeOnly: "ที่ชà¸à¸šà¹€à¸—่านั้น" +resetPasswordConfirm: "รีเซ็ตรหัสผ่านขà¸à¸‡à¸„ุณจริงๆหรà¸?" +sensitiveWords: "คำที่ละเà¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" +sensitiveWordsDescription: "à¸à¸²à¸£à¹€à¸›à¸´à¸”เผยโน้ตทั้งหมดที่มีคำที่à¸à¸³à¸«à¸™à¸”ค่าไว้จะถูà¸à¸•à¸±à¹‰à¸‡à¸„่าเป็น \"หน้าà¹à¸£à¸\" โดยà¸à¸±à¸•à¹‚นมัติ คุณยังสามารถà¹à¸ªà¸”งหลายรายà¸à¸²à¸£à¹„ด้โดยà¹à¸¢à¸à¸£à¸²à¸¢à¸à¸²à¸£à¹‚ดยใช้ตัวà¹à¸šà¹ˆà¸‡à¸šà¸£à¸£à¸—ัดได้นะ" +notesSearchNotAvailable: "à¸à¸²à¸£à¸„้นหาโน้ตไม่พร้à¸à¸¡à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸™à¸°à¸„่ะ" +license: "ใบà¸à¸™à¸¸à¸à¸²à¸•" +unfavoriteConfirm: "ลบà¸à¸à¸à¸ˆà¸²à¸à¸£à¸²à¸¢à¸à¸²à¸£à¹‚ปรดà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸?" +myClips: "คลิปขà¸à¸‡à¸‰à¸±à¸™" +drivecleaner: "ทำความสะà¸à¸²à¸”ไดรฟ์" +retryAllQueuesNow: "ลà¸à¸‡à¹€à¸£à¸µà¸¢à¸à¹ƒà¸Šà¹‰à¸„ิวทั้งหมดà¸à¸µà¸à¸„รั้ง" +retryAllQueuesConfirmTitle: "ลà¸à¸‡à¹ƒà¸«à¸¡à¹ˆà¸—ั้งหมดจริงๆหรà¸à¹à¸™à¹ˆà¹ƒà¸ˆà¸™à¸°?" +retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มà¸à¸²à¸£à¹‚หลดเซิร์ฟเวà¸à¸£à¹Œà¸Šà¸±à¹ˆà¸§à¸„ราวนะ" _achievements: earnedAt: "ได้รับเมื่à¸" _types: @@ -1218,6 +1231,8 @@ _role: iconUrl: "ไà¸à¸„à¸à¸™ URL" asBadge: "à¹à¸ªà¸”งเป็นตรา" descriptionOfAsBadge: "ไà¸à¸„à¸à¸™à¸‚à¸à¸‡à¸šà¸—บาทนี้จะปราà¸à¸à¸–ัดจาà¸à¸Šà¸·à¹ˆà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸”้วยบทบาทนี้ถ้าหาà¸à¹€à¸›à¸´à¸”ใช้งาน" + displayOrder: "ตำà¹à¸«à¸™à¹ˆà¸‡" + descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำà¹à¸«à¸™à¹ˆà¸‡ UI à¸à¹‡à¸¢à¸´à¹ˆà¸‡à¸ªà¸¹à¸‡à¸‚ึ้นนะ" canEditMembersByModerator: "à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¹à¸à¹‰à¹„ขสมาชิà¸" descriptionOfCanEditMembersByModerator: "เมื่à¸à¹€à¸›à¸´à¸”ใช้ ผู้ดูà¹à¸¥à¸™à¸à¸à¹€à¸«à¸™à¸·à¸à¸ˆà¸²à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¹à¸¥à¹‰à¸§ จะสามารถà¸à¸³à¸«à¸™à¸”à¹à¸¥à¸°à¸¢à¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸¡à¸à¸šà¸«à¸¡à¸²à¸¢à¸šà¸—บาทนี้ให้à¸à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹„ด้ เมื่à¸à¸›à¸´à¸” เฉพาะผู้ดูà¹à¸¥à¸£à¸°à¸šà¸šà¹€à¸—่านั้นที่จะสามารถà¸à¸³à¸«à¸™à¸”ผู้ใช้ได้นะ" priority: "ลำดับความสำคัà¸" @@ -1243,6 +1258,7 @@ _role: rateLimitFactor: "ขีดจำà¸à¸±à¸”à¸à¸±à¸•à¸£à¸²" descriptionOfRateLimitFactor: "ขีดจà¹à¸²à¸à¸±à¸”à¸à¸±à¸•à¸£à¸²à¸—ี่ต่ำà¸à¸§à¹ˆà¸²à¸¡à¸µà¸‚้à¸à¸ˆà¹à¸²à¸à¸±à¸”น้à¸à¸¢à¸à¸§à¹ˆà¸²à¸‚้à¸à¸ˆà¹à¸²à¸à¸±à¸”ที่สูงà¸à¸§à¹ˆà¸²" canHideAds: "ซ่à¸à¸™à¹‚ฆษณา" + canSearchNotes: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸à¸²à¸£à¸„้นหาโน้ต" _condition: isLocal: "ผู้ใช้ภายใน" isRemote: "ผู้ใช้ระยะไà¸à¸¥" @@ -1844,3 +1860,13 @@ _deck: _dialog: charactersExceeded: "คุณà¸à¸³à¸¥à¸±à¸‡à¸¡à¸µà¸•à¸±à¸§à¸à¸±à¸à¸‚ระเà¸à¸´à¸™à¸‚ีดจำà¸à¸±à¸”สูงสุดà¹à¸¥à¹‰à¸§à¸™à¸°! ปัจจุบันà¸à¸¢à¸¹à¹ˆà¸—ี่ {current} จาภ{max}" charactersBelow: "คุณà¸à¸³à¸¥à¸±à¸‡à¹ƒà¸Šà¹‰à¸à¸±à¸à¸‚ระต่ำà¸à¸§à¹ˆà¸²à¸‚ีดจำà¸à¸±à¸”ขั้นต่ำเลยนะ! ปัจจุบันà¸à¸¢à¸¹à¹ˆà¸—ี่ {current} จาภ{min}" +_disabledTimeline: + title: "ปิดใช้งานไทม์ไลน์" + description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันขà¸à¸‡à¸„ุณได้" +_drivecleaner: + orderBySizeDesc: "ขนาดไฟล์จาà¸à¸¡à¸²à¸à¹„ปหาน้à¸à¸¢" + orderByCreatedAtAsc: "วันที่จาà¸à¸™à¹‰à¸à¸¢à¹„ปหามาà¸" +_webhookSettings: + name: "ชื่à¸" + active: "เปิดใช้งาน" + diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 7bd8188a48f5be6cd209a619c07481d1fe35fca3..0f53dbafcbd639c437bdbcaf54f29c5b3f37fe93 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -60,3 +60,4 @@ _deck: _columns: notifications: "Bildirim" tl: "Zaman çizelgesi" + diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml index 65ef84125907178fa6b7d801d4f33b981c97e0a7..5b825d7bf39da1a52814a3d828f4dda49e4879b2 100644 --- a/locales/ug-CN.yml +++ b/locales/ug-CN.yml @@ -2,3 +2,4 @@ _lang_: "ياپونچە" search: "ئىزدەش" searchByGoogle: "ئىزدەش" + diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 56e3f024a1a86e66d75bf07cec1bb99208c91d12..7b2ee6d891c5aac554d1be862a7f7ac35d3c1ab0 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -576,7 +576,6 @@ tokenRequested: "Ðадати доÑтуп до акаунту" pluginTokenRequestedDescription: "Цей плагін зможе викориÑтовувати дозволи Ñкі тут вказані." notificationType: "Тип ÑповіщеннÑ" edit: "Редагувати" -useStarForReactionFallback: "ВикориÑтовувати ★ Ñк запаÑний варіант, Ñкщо емодзі реакції невідомий" emailServer: "Email Ñервер" enableEmail: "Увімкнути функцію доÑтавки пошти" emailConfigInfo: "ВикориÑтовуєтьÑÑ Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾Ñ— пошти Ð¿Ñ–Ð´Ñ‡Ð°Ñ Ñ€ÐµÑ”Ñтрації, а також Ð´Ð»Ñ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»ÑŽ." @@ -1639,3 +1638,7 @@ _deck: channel: "Канали" mentions: "Згадки" direct: "ОÑобиÑте" +_webhookSettings: + name: "Ім'Ñ" + active: "Увімкнено" + diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index ce36de03db4b2af703e4b4d2465029a599fa1253..f81445473293108c95ef26dc35b8f312bf19fdf7 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -585,7 +585,6 @@ tokenRequested: "Cấp quyá»n truy cáºp và o tà i khoản" pluginTokenRequestedDescription: "Plugin nà y sẽ có thể sá» dụng các quyá»n được đặt ở đây." notificationType: "Loại thông báo" edit: "Sá»a" -useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có" emailServer: "Email máy chủ" enableEmail: "Báºt phân phối email" emailConfigInfo: "Äược dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên máºt khẩu của mình" @@ -1705,3 +1704,7 @@ _deck: _dialog: charactersExceeded: "Bạn nhắn quá giá»›i hạn ký tá»±!! Hiện nay {current} / giá»›i hạn {max}" charactersBelow: "Bạn nhắn quá Ãt tối thiểu ký tá»±!! Hiện nay {current} / Tối thiểu {min}" +_webhookSettings: + name: "Tên" + active: "Äã báºt" + diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 73b36f8ec2c67295388d120a940912e0525e4610..c687c47a17920e10f0f647de99bfb6a28f7ccecd 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -594,7 +594,6 @@ tokenRequested: "å…许访问账户" pluginTokenRequestedDescription: "æ¤æ’件将能够拥有æ¤å¤„设置的æƒé™" notificationType: "通知类型" edit: "编辑" -useStarForReactionFallback: "如果回应的是未知表情符å·ï¼Œåˆ™ä½¿ç”¨â˜…作为代替" emailServer: "邮件æœåŠ¡å™¨" enableEmail: "å¯ç”¨å‘é€é‚®ä»¶åŠŸèƒ½" emailConfigInfo: "用于确认电å邮件和密ç é‡ç½®" @@ -977,6 +976,10 @@ notesSearchNotAvailable: "帖å检索ä¸å¯ç”¨" license: "许å¯ä¿¡æ¯" unfavoriteConfirm: "确定è¦å–消收è—å—?" myClips: "我的便ç¾" +drivecleaner: "网盘整ç†" +retryAllQueuesNow: "立刻é‡è¯•æ‰€æœ‰é˜Ÿåˆ—" +retryAllQueuesConfirmTitle: "è¦å†å°è¯•ä¸€æ¬¡å—?" +retryAllQueuesConfirmText: "å¯èƒ½ä¼šä½¿æœåŠ¡å™¨è´Ÿè·åœ¨ä¸€å®šæ—¶é—´å†…å¢žåŠ " _achievements: earnedAt: "è¾¾æˆæ—¶é—´" _types: @@ -1868,3 +1871,10 @@ _dialog: _disabledTimeline: title: "时间线已ç¦ç”¨" description: "您ä¸èƒ½åœ¨å½“å‰è§’色使用时间线。" +_drivecleaner: + orderBySizeDesc: "按大å°é™åºæŽ’列" + orderByCreatedAtAsc: "æŒ‰æ·»åŠ æ—¥æœŸé™åºæŽ’列" +_webhookSettings: + name: "å称" + active: "å·²å¯ç”¨" + diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 6109bdbeec6c418806f420c5f6b1b6a08bba84f9..e031a88f4b978f5ee4b4a7af02dbc061f88663b3 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -531,8 +531,8 @@ installedDate: "安è£æ™‚é–“" lastUsedDate: "最後上線日期" state: "狀態" sort: "排åº" -ascendingOrder: "éžå¢ž" -descendingOrder: "éžæ¸›" +ascendingOrder: "昇冪" +descendingOrder: "é™å†ª" scratchpad: "æš«å˜è¨˜æ†¶é«”" scratchpadDescription: "AiScript控制å°ç‚ºAiScriptæ供了實驗環境。您å¯ä»¥åœ¨æ¤ç·¨å¯«ã€åŸ·è¡Œå’Œç¢ºèªä»£ç¢¼èˆ‡Misskey互動的结果。" output: "輸出" @@ -594,7 +594,6 @@ tokenRequested: "å…許å˜å–帳戶" pluginTokenRequestedDescription: "æ¤å¤–掛將æ“有在æ¤è¨å®šçš„權é™ã€‚" notificationType: "通知形å¼" edit: "編輯" -useStarForReactionFallback: "以★代替未知的表情符號" emailServer: "電郵伺æœå™¨" enableEmail: "啟用發é€é›»éƒµåŠŸèƒ½" emailConfigInfo: "用於確èªé›»éƒµåœ°å€åŠå¯†ç¢¼é‡ç½®" @@ -678,8 +677,8 @@ sentReactionsCount: "å應發é€æ¬¡æ•¸" receivedReactionsCount: "收到å應次數" pollVotesCount: "已統計的投票數" pollVotedCount: "已投票數" -yes: "確定" -no: "å–消" +yes: "是" +no: "å¦" driveFilesCount: "雲端硬碟檔案數é‡" driveUsage: "雲端硬碟使用é‡" noCrawle: "拒絕æœå°‹å¼•æ“Žç´¢å¼•" @@ -973,6 +972,14 @@ rolesAssignedToMe: "指派給自己的角色" resetPasswordConfirm: "é‡è¨å¯†ç¢¼ï¼Ÿ" sensitiveWords: "æ•æ„Ÿè©ž" sensitiveWordsDescription: "å°‡å«æœ‰è¨å®šè©žå½™çš„貼文å¯è¦‹æ€§è¨ç‚ºç™¼é€è‡³é¦–é 。å¯ä»¥ç”¨æ›è¡Œä¾†é€²è¡Œè¤‡æ•¸çš„è¨å®šã€‚" +notesSearchNotAvailable: "無法使用æœå°‹è²¼æ–‡åŠŸèƒ½ã€‚" +license: "授權" +unfavoriteConfirm: "è¦å–消收錄我的最愛嗎?" +myClips: "我的摘錄" +drivecleaner: "雲端硬碟清掃器" +retryAllQueuesNow: "立刻é‡è©¦æ‰€æœ‰ä½‡åˆ—" +retryAllQueuesConfirmTitle: "è¦ç¾åœ¨é‡è©¦å—Žï¼Ÿ" +retryAllQueuesConfirmText: "伺æœå™¨çš„è² è·å¯èƒ½æœƒæš«æ™‚å¢žåŠ ã€‚" _achievements: earnedAt: "ç²å¾—日期" _types: @@ -1498,7 +1505,7 @@ _time: _tutorial: title: "Misskey使用方法" step1_1: "æ¡è¿Žï¼" - step1_2: "æ¤ç‚ºã€Œæ™‚間軸ã€é é¢ï¼Œå®ƒæœƒæŒ‰ç…§æ™‚é–“é †åºé¡¯ç¤ºä½ 「追隨ã€çš„人發出的「貼文ã€" + step1_2: "æ¤ç‚ºã€Œæ™‚間軸ã€é é¢ï¼Œå®ƒæœƒæŒ‰ç…§æ™‚é–“é †åºé¡¯ç¤ºä½ 「追隨ã€çš„人發出的「貼文ã€ã€‚" step1_3: "ç”±æ–¼ä½ æ²’æœ‰ç™¼ä½ˆä»»ä½•è²¼æ–‡ï¼Œä¹Ÿæ²’æœ‰è¿½éš¨ä»»ä½•äººï¼Œæ‰€ä»¥ä½ çš„æ™‚é–“è»¸ç›®å‰æ˜¯ç©ºçš„。" step2_1: "在發文或追隨其他人之å‰å…ˆè®“我們è¨å®šä¸€ä¸‹å€‹äººè³‡æ–™å§ã€‚" step2_2: "æä¾›ä¸€äº›é—œæ–¼è‡ªå·±çš„è³‡è¨Šä¾†è®“å…¶ä»–äººæ›´æœ‰è¿½éš¨ä½ çš„æ„願。" @@ -1864,3 +1871,10 @@ _dialog: _disabledTimeline: title: "åœç”¨çš„時間軸" description: "ç›®å‰çš„角色無法使用這個時間軸。" +_drivecleaner: + orderBySizeDesc: "檔案由大到å°" + orderByCreatedAtAsc: "ä¾ç…§åŠ å…¥çš„æ—¥æœŸé †åº" +_webhookSettings: + name: "å稱" + active: "已啟用" + diff --git a/package.json b/package.json index f68608911cde4855fce9bc964719336a1d6a214b..e4cf9c85d219ad60db3ad52647267c554179d64c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.10.0", + "version": "13.10.3", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1679309757174-antenna-active.js b/packages/backend/migration/1679309757174-antenna-active.js new file mode 100644 index 0000000000000000000000000000000000000000..69e845c1421febd42818e8080a6f26b9d5b76811 --- /dev/null +++ b/packages/backend/migration/1679309757174-antenna-active.js @@ -0,0 +1,17 @@ +export class antennaActive1679309757174 { + name = 'antennaActive1679309757174' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now'`); + await queryRunner.query(`ALTER TABLE "antenna" ADD "isActive" boolean NOT NULL DEFAULT true`); + await queryRunner.query(`CREATE INDEX "IDX_084c2abb8948ef59a37dce6ac1" ON "antenna" ("lastUsedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_36ef5192a1ce55ed0e40aa4db5" ON "antenna" ("isActive") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_36ef5192a1ce55ed0e40aa4db5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_084c2abb8948ef59a37dce6ac1"`); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "isActive"`); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "lastUsedAt"`); + } +} diff --git a/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js new file mode 100644 index 0000000000000000000000000000000000000000..42faab74664ea0bd397d0dc6a5744be7c6abe6be --- /dev/null +++ b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js @@ -0,0 +1,11 @@ +export class enableChartsForRemoteUser1679639483253 { + name = 'enableChartsForRemoteUser1679639483253' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForRemoteUser" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForRemoteUser"`); + } +} diff --git a/packages/backend/migration/1679651580149-cleanup.js b/packages/backend/migration/1679651580149-cleanup.js new file mode 100644 index 0000000000000000000000000000000000000000..1f00f3cc1fadecae829091242056cab56d484da6 --- /dev/null +++ b/packages/backend/migration/1679651580149-cleanup.js @@ -0,0 +1,11 @@ +export class cleanup1679651580149 { + name = 'cleanup1679651580149' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`); + } +} diff --git a/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js new file mode 100644 index 0000000000000000000000000000000000000000..0733339841c447da254e3d2d6892a9e4f46d6e8e --- /dev/null +++ b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js @@ -0,0 +1,11 @@ +export class enableChartsForFederatedInstances1679652081809 { + name = 'enableChartsForFederatedInstances1679652081809' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForFederatedInstances" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForFederatedInstances"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index a6fe2a07a8f8ec034b18dc8c1adcd6e1a18f49b9..162acd9f805dd98f546b98487e4552169ca9bb98 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,6 +37,9 @@ "@tensorflow/tfjs-node": "4.2.0" }, "dependencies": { + "@aws-sdk/client-s3": "^3.294.0", + "@aws-sdk/lib-storage": "^3.294.0", + "@aws-sdk/node-http-handler": "^3.292.0", "@bull-board/api": "5.0.0", "@bull-board/fastify": "5.0.0", "@bull-board/ui": "5.0.0", @@ -59,7 +62,6 @@ "ajv": "8.12.0", "archiver": "5.3.1", "autwh": "0.1.0", - "aws-sdk": "2.1318.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", "bull": "4.10.4", @@ -190,6 +192,7 @@ "@types/ws": "8.5.4", "@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/parser": "5.54.1", + "aws-sdk-client-mock": "^2.1.1", "cross-env": "7.0.3", "eslint": "8.35.0", "eslint-plugin-import": "2.27.5", diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 35fbb53e81acc914b7e5adeb3cf92be0a1a86a0c..aaa26a832173ef497d829f533e2078e95b97e88b 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -71,12 +71,14 @@ export class AntennaService implements OnApplicationShutdown { this.antennas.push({ ...body, createdAt: new Date(body.createdAt), + lastUsedAt: new Date(body.lastUsedAt), }); break; case 'antennaUpdated': this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { ...body, createdAt: new Date(body.createdAt), + lastUsedAt: new Date(body.lastUsedAt), }; break; case 'antennaDeleted': @@ -217,7 +219,9 @@ export class AntennaService implements OnApplicationShutdown { @bindThis public async getAntennas() { if (!this.antennasFetched) { - this.antennas = await this.antennasRepository.find(); + this.antennas = await this.antennasRepository.findBy({ + isActive: true, + }); this.antennasFetched = true; } diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index b404848d7db04e9c2c6a340cc66db3351f2c9244..a62854c61c6ddec9e4644e4ea12b8172b4f4173d 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -8,7 +8,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import type { EmojisRepository, Note } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import type { Config } from '@/config.js'; import { ReactionService } from '@/core/ReactionService.js'; @@ -16,7 +16,7 @@ import { query } from '@/misc/prelude/url.js'; @Injectable() export class CustomEmojiService { - private cache: Cache<Emoji | null>; + private cache: KVCache<Emoji | null>; constructor( @Inject(DI.config) @@ -34,7 +34,7 @@ export class CustomEmojiService { private globalEventService: GlobalEventService, private reactionService: ReactionService, ) { - this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12); + this.cache = new KVCache<Emoji | null>(1000 * 60 * 60 * 12); } @bindThis diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index f1e93d6dd9d5019b341c373dedf2a98ac68c6551..c6258474ec055654ea571e05d12c3c61ccc15f1f 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid'; import sharp from 'sharp'; import { sharpBmp } from 'sharp-read-bmp'; import { IsNull } from 'typeorm'; +import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -36,7 +37,6 @@ import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; -import type S3 from 'aws-sdk/clients/s3.js'; type AddFileArgs = { /** User who wish to add file */ @@ -81,6 +81,7 @@ type UploadFromUrlArgs = { export class DriveService { private registerLogger: Logger; private downloaderLogger: Logger; + private deleteLogger: Logger; constructor( @Inject(DI.config) @@ -118,6 +119,7 @@ export class DriveService { const logger = new Logger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); this.downloaderLogger = logger.createSubLogger('downloader'); + this.deleteLogger = logger.createSubLogger('delete'); } /*** @@ -368,7 +370,7 @@ export class DriveService { Body: stream, ContentType: type, CacheControl: 'max-age=31536000, immutable', - } as S3.PutObjectRequest; + } as PutObjectCommandInput; if (filename) params.ContentDisposition = contentDisposition( 'inline', @@ -378,21 +380,16 @@ export class DriveService { ); if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; - const s3 = this.s3Service.getS3(meta); - - const upload = s3.upload(params, { - partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, - }); - - await upload.promise() + await this.s3Service.upload(meta, params) .then( result => { - if (result) { + if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); - } else { - this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`); + } else { // AbortMultipartUploadCommandOutput + this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`); } - }, + }) + .catch( err => { this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err); }, @@ -528,10 +525,10 @@ export class DriveService { }; const properties: { - width?: number; - height?: number; - orientation?: number; - } = {}; + width?: number; + height?: number; + orientation?: number; + } = {}; if (info.width) { properties['width'] = info.width; @@ -616,17 +613,20 @@ export class DriveService { if (user) { this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { - // Publish driveFileCreated event + // Publish driveFileCreated event this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile); this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile); }); } - // 統計を更新 this.driveChart.update(file, true); - this.perUserDriveChart.update(file, true); - if (file.userHost !== null) { - this.instanceChart.updateDrive(file, true); + if (file.userHost == null) { + // ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ + this.perUserDriveChart.update(file, true); + } else { + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateDrive(file, true); + } } return file; @@ -692,7 +692,7 @@ export class DriveService { @bindThis private async deletePostProcess(file: DriveFile, isExpired = false) { - // リモートファイル期é™åˆ‡ã‚Œå‰Šé™¤å¾Œã¯ç›´ãƒªãƒ³ã‚¯ã«ã™ã‚‹ + // リモートファイル期é™åˆ‡ã‚Œå‰Šé™¤å¾Œã¯ç›´ãƒªãƒ³ã‚¯ã«ã™ã‚‹ if (isExpired && file.userHost !== null && file.uri != null) { this.driveFilesRepository.update(file.id, { isLink: true, @@ -709,33 +709,36 @@ export class DriveService { this.driveFilesRepository.delete(file.id); } - // 統計を更新 this.driveChart.update(file, false); - this.perUserDriveChart.update(file, false); - if (file.userHost !== null) { - this.instanceChart.updateDrive(file, false); + if (file.userHost == null) { + // ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ + this.perUserDriveChart.update(file, false); + } else { + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateDrive(file, false); + } } } @bindThis public async deleteObjectStorageFile(key: string) { const meta = await this.metaService.fetch(); - - const s3 = this.s3Service.getS3(meta); - try { - await s3.deleteObject({ - Bucket: meta.objectStorageBucket!, + const param = { + Bucket: meta.objectStorageBucket, Key: key, - }).promise(); + } as DeleteObjectCommandInput; + + await this.s3Service.delete(meta, param); } catch (err: any) { - if (err.code === 'NoSuchKey') { - console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err); + if (err.name === 'NoSuchKey') { + this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); return; + } else { + throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, { + cause: err, + }); } - throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, { - cause: err, - }); } } diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index e83b037dd7cb8b79fd76ab30f07f4da203757197..b85791e43f12f37969cf08f1265eee6db957646f 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { InstancesRepository } from '@/models/index.js'; import type { Instance } from '@/models/entities/Instance.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js'; @Injectable() export class FederatedInstanceService { - private cache: Cache<Instance>; + private cache: KVCache<Instance>; constructor( @Inject(DI.instancesRepository) @@ -18,7 +18,7 @@ export class FederatedInstanceService { private utilityService: UtilityService, private idService: IdService, ) { - this.cache = new Cache<Instance>(1000 * 60 * 60); + this.cache = new KVCache<Instance>(1000 * 60 * 60); } @bindThis diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index ee9ae0733fbc8e46f671d5fc0c7e820fa0f4ced8..ef87051a74045b54904901a2cffeebc1dfe8b1a2 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import type { LocalUser } from '@/models/entities/User.js'; import type { UsersRepository } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { DI } from '@/di-symbols.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { bindThis } from '@/decorators.js'; @@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const; @Injectable() export class InstanceActorService { - private cache: Cache<LocalUser>; + private cache: KVCache<LocalUser>; constructor( @Inject(DI.usersRepository) @@ -19,7 +19,7 @@ export class InstanceActorService { private createSystemUserService: CreateSystemUserService, ) { - this.cache = new Cache<LocalUser>(Infinity); + this.cache = new KVCache<LocalUser>(Infinity); } @bindThis diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 2fc2a3d54fa4655fda4991179d70842f67037908..7d080537610c0bef6b88c0f8104e6bff9c6b0dfd 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -19,7 +19,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js import { checkWordMute } from '@/misc/check-word-mute.js'; import type { Channel } from '@/models/entities/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { UserProfile } from '@/models/entities/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -46,7 +46,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; -const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); +const mutedWordsCache = new KVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -435,15 +435,20 @@ export class NoteCreateService implements OnApplicationShutdown { createdAt: User['createdAt']; isBot: User['isBot']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { - // 統計を更新 + const meta = await this.metaService.fetch(); + this.notesChart.update(note, true); - this.perUserNotesChart.update(user, note, true); + if (meta.enableChartsForRemoteUser || (user.host == null)) { + this.perUserNotesChart.update(user, note, true); + } // Register host if (this.userEntityService.isRemoteUser(user)) { - this.federatedInstanceService.fetch(user.host).then(i => { + this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); - this.instanceChart.updateNote(i.host, note, true); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateNote(i.host, note, true); + } }); } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 571b62552336de22d12ee721f22373c0659c812b..dd878f7bba0a48aee4dd37df9eb61ef7e0a5d426 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -16,6 +16,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; @Injectable() export class NoteDeleteService { @@ -39,6 +40,7 @@ export class NoteDeleteService { private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, + private metaService: MetaService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, @@ -95,14 +97,19 @@ export class NoteDeleteService { } //#endregion - // 統計を更新 + const meta = await this.metaService.fetch(); + this.notesChart.update(note, false); - this.perUserNotesChart.update(user, note, false); + if (meta.enableChartsForRemoteUser || (user.host == null)) { + this.perUserNotesChart.update(user, note, false); + } if (this.userEntityService.isRemoteUser(user)) { - this.federatedInstanceService.fetch(user.host).then(i => { + this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); - this.instanceChart.updateNote(i.host, note, false); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateNote(i.host, note, false); + } }); } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 271ba79176886172dfef98004ee85e7f9bf7262e..b3aea878d6c4f5bba748815fd83a0dccc012c286 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -21,6 +21,8 @@ import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +const FALLBACK = 'â¤'; + const legacies: Record<string, string> = { 'like': 'ðŸ‘', 'love': 'â¤', // ã“ã“ã«è¨˜è¿°ã™ã‚‹å ´åˆã¯ç•°ä½“å—セレクタを入れãªã„ @@ -147,7 +149,11 @@ export class ReactionService { .where('id = :id', { id: note.id }) .execute(); - this.perUserReactionsChart.update(user, note); + const meta = await this.metaService.fetch(); + + if (meta.enableChartsForRemoteUser || (user.host == null)) { + this.perUserReactionsChart.update(user, note); + } // カスタム絵文å—リアクションã ã£ãŸã‚‰çµµæ–‡å—æƒ…å ±ã‚‚é€ã‚‹ const decodedReaction = this.decodeReaction(reaction); @@ -251,12 +257,6 @@ export class ReactionService { //#endregion } - @bindThis - public async getFallbackReaction(): Promise<string> { - const meta = await this.metaService.fetch(); - return meta.useStarForReactionFallback ? 'â' : 'ðŸ‘'; - } - @bindThis public convertLegacyReactions(reactions: Record<string, number>) { const _reactions = {} as Record<string, number>; @@ -290,7 +290,7 @@ export class ReactionService { @bindThis public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> { - if (reaction == null) return await this.getFallbackReaction(); + if (reaction == null) return FALLBACK; reacterHost = this.utilityService.toPunyNullable(reacterHost); @@ -318,7 +318,7 @@ export class ReactionService { if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; } - return await this.getFallbackReaction(); + return FALLBACK; } @bindThis diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 86f983cc780e18c1a0ab73ff9ef13b92b917b800..4537f1b81a53bb76c6b054067e1f3a272c1a2165 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -3,7 +3,7 @@ import { IsNull } from 'typeorm'; import type { LocalUser, User } from '@/models/entities/User.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { Relay } from '@/models/entities/Relay.js'; import { QueueService } from '@/core/QueueService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; @@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const; @Injectable() export class RelayService { - private relaysCache: Cache<Relay[]>; + private relaysCache: KVCache<Relay[]>; constructor( @Inject(DI.usersRepository) @@ -30,7 +30,7 @@ export class RelayService { private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new Cache<Relay[]>(1000 * 60 * 10); + this.relaysCache = new KVCache<Relay[]>(1000 * 60 * 10); } @bindThis diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 4775196c6fdd7df616e992c10566e10395180f41..7b63e43cb186e18b25c431fced9dbadfc038515c 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { In } from 'typeorm'; import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -57,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = { @Injectable() export class RoleService implements OnApplicationShutdown { - private rolesCache: Cache<Role[]>; - private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>; + private rolesCache: KVCache<Role[]>; + private roleAssignmentByUserIdCache: KVCache<RoleAssignment[]>; public static AlreadyAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {}; @@ -84,8 +84,8 @@ export class RoleService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.rolesCache = new Cache<Role[]>(Infinity); - this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity); + this.rolesCache = new KVCache<Role[]>(Infinity); + this.roleAssignmentByUserIdCache = new KVCache<RoleAssignment[]>(Infinity); this.redisSubscriber.on('message', this.onMessage); } @@ -192,6 +192,12 @@ export class RoleService implements OnApplicationShutdown { case 'followingMoreThanOrEq': { return user.followingCount >= value.value; } + case 'notesLessThanOrEq': { + return user.notesCount <= value.value; + } + case 'notesMoreThanOrEq': { + return user.notesCount >= value.value; + } default: return false; } diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index cc8f95081314a0086ece2892baadea365f9374ec..629278d9157c9d6eca6aa47734c17f6d6e243df2 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -1,11 +1,16 @@ import { URL } from 'node:url'; +import * as http from 'node:http'; +import * as https from 'node:https'; import { Inject, Injectable } from '@nestjs/common'; -import S3 from 'aws-sdk/clients/s3.js'; +import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { Upload } from '@aws-sdk/lib-storage'; +import { NodeHttpHandler, NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Meta } from '@/models/entities/Meta.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; +import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3'; @Injectable() export class S3Service { @@ -18,25 +23,47 @@ export class S3Service { } @bindThis - public getS3(meta: Meta) { + public getS3Client(meta: Meta): S3Client { const u = meta.objectStorageEndpoint - ? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}` - : `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`; + ? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}` + : `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent - return new S3({ - endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0 - ? meta.objectStorageEndpoint - : undefined, - accessKeyId: meta.objectStorageAccessKey!, - secretAccessKey: meta.objectStorageSecretKey!, + const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy); + const handlerOption: NodeHttpHandlerOptions = {}; + if (meta.objectStorageUseSSL) { + handlerOption.httpsAgent = agent as https.Agent; + } else { + handlerOption.httpAgent = agent as http.Agent; + } + + return new S3Client({ + endpoint: meta.objectStorageEndpoint ? u : undefined, + credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? { + accessKeyId: meta.objectStorageAccessKey, + secretAccessKey: meta.objectStorageSecretKey, + } : undefined, region: meta.objectStorageRegion ?? undefined, - sslEnabled: meta.objectStorageUseSSL, - s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted - ? false - : meta.objectStorageS3ForcePathStyle, - httpOptions: { - agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy), - }, + tls: meta.objectStorageUseSSL, + forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted + requestHandler: new NodeHttpHandler(handlerOption), }); } + + @bindThis + public async upload(meta: Meta, input: PutObjectCommandInput) { + const client = this.getS3Client(meta); + return new Upload({ + client, + params: input, + partSize: (client.config.endpoint && (await client.config.endpoint()).hostname === 'storage.googleapis.com') + ? 500 * 1024 * 1024 + : 8 * 1024 * 1024, + }).done(); + } + + @bindThis + public delete(meta: Meta, input: DeleteObjectCommandInput) { + const client = this.getS3Client(meta); + return client.send(new DeleteObjectCommand(input)); + } } diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 92408da3429495b21824f3fb67fafecd0a0c841a..33b51537a668650c19b0260a0a2c9cf181e027bc 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -15,7 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { bindThis } from '@/decorators.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @Injectable() @@ -23,7 +23,7 @@ export class UserBlockingService implements OnApplicationShutdown { private logger: Logger; // ã‚ーãŒãƒ¦ãƒ¼ã‚¶ãƒ¼IDã§ã€å€¤ãŒãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒ–ãƒãƒƒã‚¯ã—ã¦ã„るユーザーã®IDã®ãƒªã‚¹ãƒˆãªã‚ャッシュ - private blockingsByUserIdCache: Cache<User['id'][]>; + private blockingsByUserIdCache: KVCache<User['id'][]>; constructor( @Inject(DI.redisSubscriber) @@ -58,7 +58,7 @@ export class UserBlockingService implements OnApplicationShutdown { ) { this.logger = this.loggerService.getLogger('user-block'); - this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity); + this.blockingsByUserIdCache = new KVCache<User['id'][]>(Infinity); this.redisSubscriber.on('message', this.onMessage); } diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index fc383d1c0861299927a7fc9ddd35a4cdb9ec2df3..631eb44062ccb65d1cc5a2c32cbe54db33a36b79 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import type { UsersRepository } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { LocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class UserCacheService implements OnApplicationShutdown { - public userByIdCache: Cache<User>; - public localUserByNativeTokenCache: Cache<LocalUser | null>; - public localUserByIdCache: Cache<LocalUser>; - public uriPersonCache: Cache<User | null>; + public userByIdCache: KVCache<User>; + public localUserByNativeTokenCache: KVCache<LocalUser | null>; + public localUserByIdCache: KVCache<LocalUser>; + public uriPersonCache: KVCache<User | null>; constructor( @Inject(DI.redisSubscriber) @@ -27,10 +27,10 @@ export class UserCacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new Cache<User>(Infinity); - this.localUserByNativeTokenCache = new Cache<LocalUser | null>(Infinity); - this.localUserByIdCache = new Cache<LocalUser>(Infinity); - this.uriPersonCache = new Cache<User | null>(Infinity); + this.userByIdCache = new KVCache<User>(Infinity); + this.localUserByNativeTokenCache = new KVCache<LocalUser | null>(Infinity); + this.localUserByIdCache = new KVCache<LocalUser>(Infinity); + this.uriPersonCache = new KVCache<User | null>(Infinity); this.redisSubscriber.on('message', this.onMessage); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 1c8550435300dee37717f5644e93ca25eb234e57..b51b553c7082e3fd0a39f418d22d71e62c8fb25f 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { MetaService } from '@/core/MetaService.js'; import Logger from '../logger.js'; const logger = new Logger('following/create'); @@ -57,6 +58,7 @@ export class UserFollowingService { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, + private metaService: MetaService, private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, @@ -200,14 +202,18 @@ export class UserFollowingService { //#region Update instance stats if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetch(follower.host).then(i => { + this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); - this.instanceChart.updateFollowing(i.host, true); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, true); + } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetch(followee.host).then(i => { + this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); - this.instanceChart.updateFollowers(i.host, true); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, true); + } }); } //#endregion @@ -320,14 +326,18 @@ export class UserFollowingService { //#region Update instance stats if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetch(follower.host).then(i => { + this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); - this.instanceChart.updateFollowing(i.host, false); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, false); + } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetch(followee.host).then(i => { + this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); - this.instanceChart.updateFollowers(i.host, false); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, false); + } }); } //#endregion diff --git a/packages/backend/src/core/UserKeypairStoreService.ts b/packages/backend/src/core/UserKeypairStoreService.ts index 1d3cc87c8db25f6d9b3f0eb76b6051b16c5da948..61c9293f86cd3305d0fcb4c9e557aabf817d93a1 100644 --- a/packages/backend/src/core/UserKeypairStoreService.ts +++ b/packages/backend/src/core/UserKeypairStoreService.ts @@ -1,20 +1,20 @@ import { Inject, Injectable } from '@nestjs/common'; import type { User } from '@/models/entities/User.js'; import type { UserKeypairsRepository } from '@/models/index.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class UserKeypairStoreService { - private cache: Cache<UserKeypair>; + private cache: KVCache<UserKeypair>; constructor( @Inject(DI.userKeypairsRepository) private userKeypairsRepository: UserKeypairsRepository, ) { - this.cache = new Cache<UserKeypair>(Infinity); + this.cache = new KVCache<UserKeypair>(Infinity); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index d0a4ad7a7581dc239d36a292a79a8aebe8e73856..c3b3875613f854ccd2b728f60020963f44def4cc 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -3,7 +3,7 @@ import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import type { Note } from '@/models/entities/Note.js'; @@ -31,8 +31,8 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService { - private publicKeyCache: Cache<UserPublickey | null>; - private publicKeyByUserIdCache: Cache<UserPublickey | null>; + private publicKeyCache: KVCache<UserPublickey | null>; + private publicKeyByUserIdCache: KVCache<UserPublickey | null>; constructor( @Inject(DI.config) @@ -50,8 +50,8 @@ export class ApDbResolverService { private userCacheService: UserCacheService, private apPersonService: ApPersonService, ) { - this.publicKeyCache = new Cache<UserPublickey | null>(Infinity); - this.publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity); + this.publicKeyCache = new KVCache<UserPublickey | null>(Infinity); + this.publicKeyByUserIdCache = new KVCache<UserPublickey | null>(Infinity); } @bindThis diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index d06958da0cb4071eb5c9d17e41cd68adfab14a7f..41f7eafa41cf35f724de42ac6b7f9b568f1691cf 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -30,6 +30,7 @@ import { StatusError } from '@/misc/status-error.js'; import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -50,6 +51,7 @@ export class ApPersonService implements OnModuleInit { private userEntityService: UserEntityService; private idService: IdService; private globalEventService: GlobalEventService; + private metaService: MetaService; private federatedInstanceService: FederatedInstanceService; private fetchInstanceMetadataService: FetchInstanceMetadataService; private userCacheService: UserCacheService; @@ -92,6 +94,7 @@ export class ApPersonService implements OnModuleInit { //private userEntityService: UserEntityService, //private idService: IdService, //private globalEventService: GlobalEventService, + //private metaService: MetaService, //private federatedInstanceService: FederatedInstanceService, //private fetchInstanceMetadataService: FetchInstanceMetadataService, //private userCacheService: UserCacheService, @@ -112,6 +115,7 @@ export class ApPersonService implements OnModuleInit { this.userEntityService = this.moduleRef.get('UserEntityService'); this.idService = this.moduleRef.get('IdService'); this.globalEventService = this.moduleRef.get('GlobalEventService'); + this.metaService = this.moduleRef.get('MetaService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.userCacheService = this.moduleRef.get('UserCacheService'); @@ -327,10 +331,12 @@ export class ApPersonService implements OnModuleInit { } // Register host - this.federatedInstanceService.fetch(host).then(i => { + this.federatedInstanceService.fetch(host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); - this.instanceChart.newUser(i.host); this.fetchInstanceMetadataService.fetchInstanceMetadata(i); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.newUser(i.host); + } }); this.usersChart.update(user!, true); diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 89137c0ec0ad5672168e0f706bc77949ff8332a0..e02daefd6458f15367a5c70923efb18ac0a04698 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -37,6 +37,7 @@ export class AntennaEntityService { notify: antenna.notify, withReplies: antenna.withReplies, withFile: antenna.withFile, + isActive: antenna.isActive, hasUnreadNote, }; } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 068ffad09d891f6dd6e4f79c4610e9a7d81c17c8..b693883e06fe138b73452d1d1755fd6dc9e841b5 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -8,7 +8,7 @@ import type { Packed } from '@/misc/json-schema.js'; import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; @@ -52,7 +52,7 @@ export class UserEntityService implements OnModuleInit { private customEmojiService: CustomEmojiService; private antennaService: AntennaService; private roleService: RoleService; - private userInstanceCache: Cache<Instance | null>; + private userInstanceCache: KVCache<Instance | null>; constructor( private moduleRef: ModuleRef, @@ -121,7 +121,7 @@ export class UserEntityService implements OnModuleInit { //private antennaService: AntennaService, //private roleService: RoleService, ) { - this.userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3); + this.userInstanceCache = new KVCache<Instance | null>(1000 * 60 * 60 * 3); } onModuleInit() { diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 43a71a2b572dc6fa7b2b5e5ffb10c1eb71b4aacd..b249cf448055d23619fe90e4d5cbbe39a9d54e92 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -2,11 +2,11 @@ import { bindThis } from '@/decorators.js'; // TODO: メモリ節約ã®ãŸã‚ã‚ã¾ã‚Šå‚ç…§ã•ã‚Œãªã„ã‚ーを定期的ã«å‰Šé™¤ã§ãるよã†ã«ã™ã‚‹ï¼Ÿ -export class Cache<T> { +export class KVCache<T> { public cache: Map<string | null, { date: number; value: T; }>; private lifetime: number; - constructor(lifetime: Cache<never>['lifetime']) { + constructor(lifetime: KVCache<never>['lifetime']) { this.cache = new Map(); this.lifetime = lifetime; } @@ -87,3 +87,88 @@ export class Cache<T> { return value; } } + +export class Cache<T> { + private cachedAt: number | null = null; + private value: T | undefined; + private lifetime: number; + + constructor(lifetime: Cache<never>['lifetime']) { + this.lifetime = lifetime; + } + + @bindThis + public set(value: T): void { + this.cachedAt = Date.now(); + this.value = value; + } + + @bindThis + public get(): T | undefined { + if (this.cachedAt == null) return undefined; + if ((Date.now() - this.cachedAt) > this.lifetime) { + this.value = undefined; + this.cachedAt = null; + return undefined; + } + return this.value; + } + + @bindThis + public delete() { + this.value = undefined; + this.cachedAt = null; + } + + /** + * ã‚ャッシュãŒã‚ã‚Œã°ãれを返ã—ã€ç„¡ã‘ã‚Œã°fetcherを呼ã³å‡ºã—ã¦çµæžœã‚’ã‚ャッシュ&è¿”ã—ã¾ã™ + * optional: ã‚ャッシュãŒå˜åœ¨ã—ã¦ã‚‚validatorã§falseã‚’è¿”ã™ã¨ã‚ャッシュ無効扱ã„ã«ã—ã¾ã™ + */ + @bindThis + public async fetch(fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> { + const cachedValue = this.get(); + if (cachedValue !== undefined) { + if (validator) { + if (validator(cachedValue)) { + // Cache HIT + return cachedValue; + } + } else { + // Cache HIT + return cachedValue; + } + } + + // Cache MISS + const value = await fetcher(); + this.set(value); + return value; + } + + /** + * ã‚ャッシュãŒã‚ã‚Œã°ãれを返ã—ã€ç„¡ã‘ã‚Œã°fetcherを呼ã³å‡ºã—ã¦çµæžœã‚’ã‚ャッシュ&è¿”ã—ã¾ã™ + * optional: ã‚ャッシュãŒå˜åœ¨ã—ã¦ã‚‚validatorã§falseã‚’è¿”ã™ã¨ã‚ャッシュ無効扱ã„ã«ã—ã¾ã™ + */ + @bindThis + public async fetchMaybe(fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> { + const cachedValue = this.get(); + if (cachedValue !== undefined) { + if (validator) { + if (validator(cachedValue)) { + // Cache HIT + return cachedValue; + } + } else { + // Cache HIT + return cachedValue; + } + } + + // Cache MISS + const value = await fetcher(); + if (value !== undefined) { + this.set(value); + } + return value; + } +} diff --git a/packages/backend/src/misc/correct-filename.ts b/packages/backend/src/misc/correct-filename.ts index 3357d8c1bd5c25664c2d4f3fd591106503c25fa8..23a0699f3971e4e468c379c67158f539d5f3c88d 100644 --- a/packages/backend/src/misc/correct-filename.ts +++ b/packages/backend/src/misc/correct-filename.ts @@ -1,15 +1,15 @@ // 与ãˆã‚‰ã‚ŒãŸæ‹¡å¼µåã¨ãƒ•ã‚¡ã‚¤ãƒ«åãŒä¸€è‡´ã—ã¦ã„ã‚‹ã‹ã©ã†ã‹ã‚’確èªã—〠// 一致ã—ã¦ã„ãªã„å ´åˆã¯æ‹¡å¼µåを付与ã—ã¦è¿”ã™ export function correctFilename(filename: string, ext: string | null) { - const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown'; - if (filename.endsWith(dotExt)) { - return filename; - } - if (ext === 'jpg' && filename.endsWith('.jpeg')) { - return filename; - } - if (ext === 'tif' && filename.endsWith('.tiff')) { - return filename; - } - return `${filename}${dotExt}`; + const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown'; + if (filename.endsWith(dotExt)) { + return filename; + } + if (ext === 'jpg' && filename.endsWith('.jpeg')) { + return filename; + } + if (ext === 'tif' && filename.endsWith('.tiff')) { + return filename; + } + return `${filename}${dotExt}`; } diff --git a/packages/backend/src/models/entities/Antenna.ts b/packages/backend/src/models/entities/Antenna.ts index 5b2164ef17c8587d6e29b6fa25243eadbabb2186..e63e7f2c72e3c7f3c57cbc4adaf5b87116365f79 100644 --- a/packages/backend/src/models/entities/Antenna.ts +++ b/packages/backend/src/models/entities/Antenna.ts @@ -13,6 +13,10 @@ export class Antenna { }) public createdAt: Date; + @Index() + @Column('timestamp with time zone') + public lastUsedAt: Date; + @Index() @Column({ ...id(), @@ -83,4 +87,10 @@ export class Antenna { @Column('boolean') public notify: boolean; + + @Index() + @Column('boolean', { + default: true, + }) + public isActive: boolean; } diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index 57338ecbd2771d857c22503d3a62466feafff88e..2e4f90b57f8d6e56adc1a6327c9733e1f910aef7 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -42,11 +42,6 @@ export class Meta { }) public disableRegistration: boolean; - @Column('boolean', { - default: false, - }) - public useStarForReactionFallback: boolean; - @Column('varchar', { length: 1024, array: true, default: '{}', }) @@ -396,6 +391,16 @@ export class Meta { }) public enableActiveEmailValidation: boolean; + @Column('boolean', { + default: true, + }) + public enableChartsForRemoteUser: boolean; + + @Column('boolean', { + default: true, + }) + public enableChartsForFederatedInstances: boolean; + @Column('jsonb', { default: { }, }) diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts index 85ff2667405082724f905beb05041814b1f620c6..eca9bcf2706d5ab54e182968cfff1859b27596ea 100644 --- a/packages/backend/src/models/entities/Role.ts +++ b/packages/backend/src/models/entities/Role.ts @@ -54,6 +54,16 @@ type CondFormulaValueFollowingMoreThanOrEq = { value: number; }; +type CondFormulaValueNotesLessThanOrEq = { + type: 'notesLessThanOrEq'; + value: number; +}; + +type CondFormulaValueNotesMoreThanOrEq = { + type: 'notesMoreThanOrEq'; + value: number; +}; + export type RoleCondFormulaValue = CondFormulaValueAnd | CondFormulaValueOr | @@ -65,7 +75,9 @@ export type RoleCondFormulaValue = CondFormulaValueFollowersLessThanOrEq | CondFormulaValueFollowersMoreThanOrEq | CondFormulaValueFollowingLessThanOrEq | - CondFormulaValueFollowingMoreThanOrEq; + CondFormulaValueFollowingMoreThanOrEq | + CondFormulaValueNotesLessThanOrEq | + CondFormulaValueNotesMoreThanOrEq; @Entity() export class Role { diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index f0994e48f7e53556dd37276d87acb0b639594b64..4483510610a00a93b8cf880b80815ce5f630c416 100644 --- a/packages/backend/src/models/json-schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -75,6 +75,10 @@ export const packedAntennaSchema = { type: 'boolean', optional: false, nullable: false, }, + isActive: { + type: 'boolean', + optional: false, nullable: false, + }, hasUnreadNote: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 7fd2cde9c0c21939b77dff2567c247edbca9bcb5..9534454fd75de93c63f2c5fa5024133de7aaa3cf 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; +import type { AntennaNotesRepository, AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; @@ -26,6 +26,9 @@ export class CleanProcessorService { @Inject(DI.mutedNotesRepository) private mutedNotesRepository: MutedNotesRepository, + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + @Inject(DI.antennaNotesRepository) private antennaNotesRepository: AntennaNotesRepository, @@ -55,8 +58,16 @@ export class CleanProcessorService { reason: 'word', }); - this.antennaNotesRepository.delete({ + this.mutedNotesRepository.delete({ id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), + reason: 'word', + }); + + // 7日以上使ã‚ã‚Œã¦ãªã„アンテナをåœæ¢ + this.antennasRepository.update({ + lastUsedAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 7))), + }, { + isActive: false, }); const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 43a92bb2672de8099709c1dd851789d0e6702a59..f637bf88188ffdcf18eaeed3b115fbcbc3e6585d 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; @@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js'; @Injectable() export class DeliverProcessorService { private logger: Logger; - private suspendedHostsCache: Cache<Instance[]>; + private suspendedHostsCache: KVCache<Instance[]>; private latest: string | null; constructor( @@ -46,7 +46,7 @@ export class DeliverProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); - this.suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60); + this.suspendedHostsCache = new KVCache<Instance[]>(1000 * 60 * 60); } @bindThis @@ -88,10 +88,12 @@ export class DeliverProcessorService { } this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - - this.instanceChart.requestSent(i.host, true); this.apRequestChart.deliverSucc(); this.federationChart.deliverd(i.host, true); + + if (meta.enableChartsForFederatedInstances) { + this.instanceChart.requestSent(i.host, true); + } }); return 'Success'; @@ -107,9 +109,12 @@ export class DeliverProcessorService { }); } - this.instanceChart.requestSent(i.host, false); this.apRequestChart.deliverFail(); this.federationChart.deliverd(i.host, false); + + if (meta.enableChartsForFederatedInstances) { + this.instanceChart.requestSent(i.host, false); + } }); if (res instanceof StatusError) { diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 41fe06b7c361d054a4b112716d860828341101f0..ed7f38d0137242e97504122089d5dad62c7c99da 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -184,9 +184,12 @@ export class InboxProcessorService { this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - this.instanceChart.requestReceived(i.host); this.apRequestChart.inbox(); this.federationChart.inbox(i.host); + + if (meta.enableChartsForFederatedInstances) { + this.instanceChart.requestReceived(i.host); + } }); // ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ãƒ†ã‚£ã‚’å‡¦ç† diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 364b46696de4154dbeebcfcf6826d8798cca3034..86019d4166c39744e31b410fc1e934ea5c7c5113 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -118,7 +118,7 @@ export class NodeinfoServerService { }; }; - const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); + const cache = new KVCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); fastify.get(nodeinfo2_1path, async (request, reply) => { const base = await cache.fetch(null, () => nodeinfo2()); diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 87438c348da90c653e52abb034d1b9c7b30a9302..a1895e3705661d672ea4eb41b755135b8441ce9d 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; -import { Cache } from '@/misc/cache.js'; +import { KVCache } from '@/misc/cache.js'; import type { App } from '@/models/entities/App.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import isNativeToken from '@/misc/is-native-token.js'; @@ -18,7 +18,7 @@ export class AuthenticationError extends Error { @Injectable() export class AuthenticateService { - private appCache: Cache<App>; + private appCache: KVCache<App>; constructor( @Inject(DI.usersRepository) @@ -32,7 +32,7 @@ export class AuthenticateService { private userCacheService: UserCacheService, ) { - this.appCache = new Cache<App>(Infinity); + this.appCache = new KVCache<App>(Infinity); } @bindThis diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 516e90dcb376464943a023ad78f4dcb18aad76cb..835e88419302e89f25ee358e7b7cb1b3f4b5f3d6 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -42,6 +42,7 @@ import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js'; import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; @@ -370,6 +371,7 @@ const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useCla const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default }; const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default }; const $admin_queue_inboxDelayed: Provider = { provide: 'ep:admin/queue/inbox-delayed', useClass: ep___admin_queue_inboxDelayed.default }; +const $admin_queue_promote: Provider = { provide: 'ep:admin/queue/promote', useClass: ep___admin_queue_promote.default }; const $admin_queue_stats: Provider = { provide: 'ep:admin/queue/stats', useClass: ep___admin_queue_stats.default }; const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: ep___admin_relays_add.default }; const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default }; @@ -702,6 +704,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_queue_clear, $admin_queue_deliverDelayed, $admin_queue_inboxDelayed, + $admin_queue_promote, $admin_queue_stats, $admin_relays_add, $admin_relays_list, @@ -1028,6 +1031,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_queue_clear, $admin_queue_deliverDelayed, $admin_queue_inboxDelayed, + $admin_queue_promote, $admin_queue_stats, $admin_relays_add, $admin_relays_list, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 2930468a2215a30da4d165cc4532bcd004c7a591..f6fc79fc70b46c512ada61a6c3689f109093a2b9 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -42,6 +42,7 @@ import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js'; import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; @@ -368,6 +369,7 @@ const eps = [ ['admin/queue/clear', ep___admin_queue_clear], ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed], ['admin/queue/inbox-delayed', ep___admin_queue_inboxDelayed], + ['admin/queue/promote', ep___admin_queue_promote], ['admin/queue/stats', ep___admin_queue_stats], ['admin/relays/add', ep___admin_relays_add], ['admin/relays/list', ep___admin_relays_list], 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 dad0e3ef860f1fbf4889d33ed18477e1499dca35..bc0475e05c8284e1540e5ff7ea3a991df04fea94 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { DataSource, IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { EmojisRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; @@ -19,6 +19,11 @@ export const meta = { code: 'NO_SUCH_EMOJI', id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', }, + sameNameEmojiExists: { + message: 'Emoji that have same name already exists.', + code: 'SAME_NAME_EMOJI_EXISTS', + id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8', + }, }, } as const; @@ -26,7 +31,7 @@ export const paramDef = { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, - name: { type: 'string' }, + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, category: { type: 'string', nullable: true, @@ -57,9 +62,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { ) { super(meta, paramDef, async (ps, me) => { const emoji = await this.emojisRepository.findOneBy({ id: ps.id }); - + const sameNameEmoji = await this.emojisRepository.findOneBy({ name: ps.name, host: IsNull() }); if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - + if (sameNameEmoji != null && sameNameEmoji.id !== ps.id) throw new ApiError(meta.errors.sameNameEmojiExists); await this.emojisRepository.update(emoji.id, { updatedAt: new Date(), name: ps.name, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index ce7e0d569dcd291dcd3ebc5a02928e7e03595dbb..fc318a621a7766d974f2a171911f6db0534a6864 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -239,6 +239,14 @@ export const meta = { type: 'boolean', optional: true, nullable: false, }, + enableChartsForRemoteUser: { + type: 'boolean', + optional: false, nullable: false, + }, + enableChartsForFederatedInstances: { + type: 'boolean', + optional: false, nullable: false, + }, policies: { type: 'object', optional: false, nullable: false, @@ -299,7 +307,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, cacheRemoteFiles: instance.cacheRemoteFiles, - useStarForReactionFallback: instance.useStarForReactionFallback, pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, @@ -337,6 +344,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { deeplIsPro: instance.deeplIsPro, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, + enableChartsForRemoteUser: instance.enableChartsForRemoteUser, + enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, policies: { ...DEFAULT_POLICIES, ...instance.policies }, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e57e6613e8ccb29365b9e67d8b4ec83b1b28ae1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + type: { type: 'string', enum: ['deliver', 'inbox'] }, + }, + required: ['type'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + private moderationLogService: ModerationLogService, + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + let delayedQueues; + + switch (ps.type) { + case 'deliver': + delayedQueues = await this.queueService.deliverQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + await queue.promote(); + } + break; + + case 'inbox': + delayedQueues = await this.queueService.inboxQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + await queue.promote(); + } + break; + } + + this.moderationLogService.insertModerationLog(me, 'promoteQueue'); + }); + } +} 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 2f23aca243fc8f966bec86ac1d025186b2a29552..11de29bf834c8fd6e6d376ac0a542d527e9a8ce6 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -17,7 +17,6 @@ export const paramDef = { type: 'object', properties: { disableRegistration: { type: 'boolean', nullable: true }, - useStarForReactionFallback: { type: 'boolean', nullable: true }, pinnedUsers: { type: 'array', nullable: true, items: { type: 'string', } }, @@ -93,6 +92,8 @@ export const paramDef = { objectStorageS3ForcePathStyle: { type: 'boolean' }, enableIpLogging: { type: 'boolean' }, enableActiveEmailValidation: { type: 'boolean' }, + enableChartsForRemoteUser: { type: 'boolean' }, + enableChartsForFederatedInstances: { type: 'boolean' }, }, required: [], } as const; @@ -114,10 +115,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { set.disableRegistration = ps.disableRegistration; } - if (typeof ps.useStarForReactionFallback === 'boolean') { - set.useStarForReactionFallback = ps.useStarForReactionFallback; - } - if (Array.isArray(ps.pinnedUsers)) { set.pinnedUsers = ps.pinnedUsers.filter(Boolean); } @@ -382,6 +379,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { set.enableActiveEmailValidation = ps.enableActiveEmailValidation; } + if (ps.enableChartsForRemoteUser !== undefined) { + set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser; + } + + if (ps.enableChartsForFederatedInstances !== undefined) { + set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances; + } + await this.metaService.update(set); this.moderationLogService.insertModerationLog(me, 'updateMeta'); }); diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index b57906a688eeea07e62659014739f3a6c1f7f340..b7ce3363a93aeb0414280d6d4e0fead6f2ca24e4 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.keywords.length === 0) { + if ((ps.keywords.length === 0) || ps.keywords[0].every(x => x === '')) { throw new Error('invalid param'); } @@ -103,9 +103,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } } + const now = new Date(); + const antenna = await this.antennasRepository.insert({ id: this.idService.genId(), - createdAt: new Date(), + createdAt: now, + lastUsedAt: now, userId: me.id, name: ps.name, src: ps.src, diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index fbb5acf6172072e5ef5701f0866e86ce663778d2..039ba1115aeb7c5284d16cd9a66621019c8b9e9a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -101,6 +101,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { this.noteReadService.read(me.id, notes); } + this.antennasRepository.update(antenna.id, { + lastUsedAt: new Date(), + }); + return await this.noteEntityService.packMany(notes, me); }); } diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index f6fad50fd9f8c56aec895ec379ff0e9d45d4d69f..4609307774e4b490f1c87464d577b4adf0593ac4 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -31,6 +31,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, + sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] }, }, required: [], } as const; @@ -63,6 +64,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } } + switch (ps.sort) { + case '+createdAt': query.orderBy('file.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('file.createdAt', 'ASC'); break; + case '+name': query.orderBy('file.name', 'DESC'); break; + case '-name': query.orderBy('file.name', 'ASC'); break; + case '+size': query.orderBy('file.size', 'DESC'); break; + case '-size': query.orderBy('file.size', 'ASC'); break; + } + const files = await query.take(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 29f24b045a240aaad03a83c0020ca2b5da7b4c44..ba432c273bfb7dbc42d8df9d98764723ac41d66a 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -48,6 +48,7 @@ export const meta = { message: 'No such user.', code: 'NO_SUCH_USER', id: '4362f8dc-731f-4ad8-a694-be5a88922a24', + httpStatusCode: 404, }, }, } as const; diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 21cf414087c364cf846cffa08a5263929c1bfe6d..b3e193cd34f934734eae1cb80c507284adc72377 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { summaly } from 'summaly'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -9,6 +8,7 @@ import type Logger from '@/logger.js'; import { query } from '@/misc/prelude/url.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import { ApiError } from '@/server/api/error.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -40,9 +40,9 @@ export class UrlPreviewService { @bindThis public async handle( - request: FastifyRequest<{ Querystring: { url: string; lang: string; } }>, + request: FastifyRequest<{ Querystring: { url: string; lang?: string; } }>, reply: FastifyReply, - ) { + ): Promise<object | undefined> { const url = request.query.url; if (typeof url !== 'string') { reply.code(400); @@ -78,7 +78,7 @@ export class UrlPreviewService { this.logger.succ(`Got preview of ${url}: ${summary.title}`); - if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { + if (!(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { throw new Error('unsupported schema included'); } @@ -95,9 +95,15 @@ export class UrlPreviewService { return summary; } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); - reply.code(200); + reply.code(422); reply.header('Cache-Control', 'max-age=86400, immutable'); - return {}; + return { + error: new ApiError({ + message: 'Failed to get preview', + code: 'URL_PREVIEW_FAILED', + id: '09d01cb5-53b9-4856-82e5-38a50c290a3b', + }), + }; } } } diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts new file mode 100644 index 0000000000000000000000000000000000000000..f35aae9dc65da1eb4e5bec9c1686d656a5179f39 --- /dev/null +++ b/packages/backend/test/e2e/clips.ts @@ -0,0 +1,962 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { JTDDataType } from 'ajv/dist/jtd'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js'; +import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js'; +import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js'; +import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js'; +import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js'; +import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js'; +import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js'; +import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js'; +import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js'; +import { + signup, + post, + startServer, + api, + successfulApiCall, + failedApiCall, + ApiRequest, + hiddenNote, +} from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('クリップ', () => { + type User = Packed<'User'>; + type Note = Packed<'Note'>; + type Clip = Packed<'Clip'>; + + let app: INestApplicationContext; + + let alice: User; + let bob: User; + let aliceNote: Note; + let aliceHomeNote: Note; + let aliceFollowersNote: Note; + let aliceSpecifiedNote: Note; + let bobNote: Note; + let bobHomeNote: Note; + let bobFollowersNote: Note; + let bobSpecifiedNote: Note; + + const compareBy = <T extends { id: string }, >(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { + return selector(a).localeCompare(selector(b)); + }; + + type CreateParam = JTDDataType<typeof CreateParamDef>; + const defaultCreate = (): Partial<CreateParam> => ({ + name: 'test', + }); + const create = async (parameters: Partial<CreateParam> = {}, request: Partial<ApiRequest> = {}): Promise<Clip> => { + const clip = await successfulApiCall<Clip>({ + endpoint: '/clips/create', + parameters: { + ...defaultCreate(), + ...parameters, + }, + user: alice, + ...request, + }); + + // 入力ãŒçµæžœã¨ã—ã¦å…¥ã£ã¦ã„ã‚‹ã“㨠+ assert.deepStrictEqual(clip, { + ...clip, + ...defaultCreate(), + ...parameters, + }); + return clip; + }; + + const createMany = async (parameters: Partial<CreateParam>, count = 10, user = alice): Promise<Clip[]> => { + return await Promise.all([...Array(count)].map((_, i) => create({ + name: `test${i}`, + ...parameters, + }, { user }))); + }; + + type UpdateParam = JTDDataType<typeof UpdateParamDef>; + const update = async (parameters: Partial<UpdateParam>, request: Partial<ApiRequest> = {}): Promise<Clip> => { + const clip = await successfulApiCall<Clip>({ + endpoint: '/clips/update', + parameters: { + name: 'updated', + ...parameters, + }, + user: alice, + ...request, + }); + + // 入力ãŒçµæžœã¨ã—ã¦å…¥ã£ã¦ã„ã‚‹ã“ã¨ã€‚clipIdã¯idã«ãªã‚‹ã®ã§æ¶ˆã—ã¦ãŠã + delete (parameters as { clipId?: string }).clipId; + assert.deepStrictEqual(clip, { + ...clip, + ...parameters, + }); + return clip; + }; + + type DeleteParam = JTDDataType<typeof DeleteParamDef>; + const deleteClip = async (parameters: DeleteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return await successfulApiCall<void>({ + endpoint: '/clips/delete', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type ShowParam = JTDDataType<typeof ShowParamDef>; + const show = async (parameters: ShowParam, request: Partial<ApiRequest> = {}): Promise<Clip> => { + return await successfulApiCall<Clip>({ + endpoint: '/clips/show', + parameters, + user: alice, + ...request, + }); + }; + + const list = async (request: Partial<ApiRequest>): Promise<Clip[]> => { + return successfulApiCall<Clip[]>({ + endpoint: '/clips/list', + parameters: {}, + user: alice, + ...request, + }); + }; + + const usersClips = async (request: Partial<ApiRequest>): Promise<Clip[]> => { + return await successfulApiCall<Clip[]>({ + endpoint: '/users/clips', + parameters: {}, + user: alice, + ...request, + }); + }; + + beforeAll(async () => { + app = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + + // FIXME: misskey-jsã®Noteã¯outdatedãªã®ã§ç›´æŽ¥å¤‰æ›ã§ããªã„ + aliceNote = await post(alice, { text: 'test' }) as any; + aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; + aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; + aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; + bobNote = await post(bob, { text: 'test' }) as any; + bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; + bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; + bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + afterEach(async () => { + // テスト間ã§å½±éŸ¿ã—åˆã‚ãªã„よã†ã«æ¯Žå›žå…¨éƒ¨æ¶ˆã™ã€‚ + for (const user of [alice, bob]) { + const list = await api('/clips/list', { limit: 11 }, user); + for (const clip of list.body) { + await api('/clips/delete', { clipId: clip.id }, user); + } + } + }); + + test('ã®ä½œæˆãŒã§ãã‚‹', async () => { + const res = await create(); + // ISO 8601ã§æ—¥ä»˜ãŒè¿”ã£ã¦ãã‚‹ã“㨠+ assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.lastClippedAt, null); + assert.strictEqual(res.name, 'test'); + assert.strictEqual(res.description, null); + assert.strictEqual(res.isPublic, false); + assert.strictEqual(res.favoritedCount, 0); + assert.strictEqual(res.isFavorited, false); + }); + + test('ã®ä½œæˆã¯ãƒãƒªã‚·ãƒ¼ã§å®šã‚られãŸæ•°ä»¥ä¸Šã¯ã§ããªã„。', async () => { + // ãƒãƒªã‚·ãƒ¼ + 1ã¾ã§ä½œã‚Œã‚‹ã¨ã„ã†æ‰€ãŒãƒŸã‚½ + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; + for (let i = 0; i < clipLimit; i++) { + await create(); + } + + await failedApiCall({ + endpoint: '/clips/create', + parameters: defaultCreate(), + user: alice, + }, { + status: 400, + code: 'TOO_MANY_CLIPS', + id: '920f7c2d-6208-4b76-8082-e632020f5883', + }); + }); + + const createClipAllowedPattern = [ + { label: 'nameãŒæœ€å¤§é•·', parameters: { name: 'x'.repeat(100) } }, + { label: 'private', parameters: { isPublic: false } }, + { label: 'public', parameters: { isPublic: true } }, + { label: 'descriptionãŒnull', parameters: { description: null } }, + { label: 'descriptionãŒæœ€å¤§é•·', parameters: { description: 'a'.repeat(2048) } }, + ]; + test.each(createClipAllowedPattern)('ã®ä½œæˆã¯$labelã§ã‚‚ã§ãã‚‹', async ({ parameters }) => await create(parameters)); + + const createClipDenyPattern = [ + { label: 'nameãŒnull', parameters: { name: null } }, + { label: 'nameãŒæœ€å¤§é•·+1', parameters: { name: 'x'.repeat(101) } }, + { label: 'isPublicãŒboolã˜ã‚ƒãªã„', parameters: { isPublic: 'true' } }, + { label: 'descriptionãŒã‚¼ãƒé•·', parameters: { description: '' } }, + { label: 'descriptionãŒæœ€å¤§é•·+1', parameters: { description: 'a'.repeat(2049) } }, + ]; + test.each(createClipDenyPattern)('ã®ä½œæˆã¯$labelãªã‚‰ã§ããªã„', async ({ parameters }) => failedApiCall({ + endpoint: '/clips/create', + parameters: { + ...defaultCreate(), + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + })); + + test('ã®æ›´æ–°ãŒã§ãã‚‹', async () => { + const res = await update({ + clipId: (await create()).id, + name: 'updated', + description: 'new description', + isPublic: true, + }); + + // ISO 8601ã§æ—¥ä»˜ãŒè¿”ã£ã¦ãã‚‹ã“㨠+ assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.lastClippedAt, null); + assert.strictEqual(res.name, 'updated'); + assert.strictEqual(res.description, 'new description'); + assert.strictEqual(res.isPublic, true); + assert.strictEqual(res.favoritedCount, 0); + assert.strictEqual(res.isFavorited, false); + }); + + test.each(createClipAllowedPattern)('ã®æ›´æ–°ã¯$labelã§ã‚‚ã§ãã‚‹', async ({ parameters }) => await update({ + clipId: (await create()).id, + name: 'updated', + ...parameters, + })); + + test.each([ + { label: 'clipIdãŒnull', parameters: { clipId: null } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): User => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', + } }, + ...createClipDenyPattern as any, + ])('ã®æ›´æ–°ã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/update', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + name: 'updated', + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('ã®å‰Šé™¤ãŒã§ãã‚‹', async () => { + await deleteClip({ + clipId: (await create()).id, + }); + assert.deepStrictEqual(await list({}), []); + }); + + test.each([ + { label: 'clipIdãŒnull', parameters: { clipId: null } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '70ca08ba-6865-4630-b6fb-8494759aa754', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): User => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '70ca08ba-6865-4630-b6fb-8494759aa754', + } }, + ])('ã®å‰Šé™¤ã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/delete', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('ã®ID指定å–å¾—ãŒã§ãã‚‹', async () => { + const clip = await create(); + const res = await show({ clipId: clip.id }); + assert.deepStrictEqual(res, clip); + }); + + test('ã®ID指定å–å¾—ã¯ä»–人ã®Privateãªã‚¯ãƒªãƒƒãƒ—ã¯å–å¾—ã§ããªã„', async () => { + const clip = await create({ isPublic: false }, { user: bob } ); + failedApiCall({ + endpoint: '/clips/show', + parameters: { clipId: clip.id }, + user: alice, + }, { + status: 400, + code: 'NO_SUCH_CLIP', + id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', + }); + }); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', + } }, + ])('ã®ID指定å–å¾—ã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, assetion }) => failedApiCall({ + endpoint: '/clips/show', + parameters: { + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('ã®ä¸€è¦§(clips/list)ãŒå–å¾—ã§ãã‚‹(空)', async () => { + const res = await list({}); + assert.deepStrictEqual(res, []); + }); + + test('ã®ä¸€è¦§(clips/list)ãŒå–å¾—ã§ãã‚‹(上é™ã„ã£ã±ã„)', async () => { + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; + const clips = await createMany({}, clipLimit); + const res = await list({ + parameters: { limit: 1 }, // FIXME: 無視ã•ã‚Œã¦11全部返ã£ã¦ãã‚‹ + }); + + // è¿”ã£ã¦ãã‚‹é…列ã«ã¯é †åºä¿éšœãŒãªã„ã®ã§idã§ã‚½ãƒ¼ãƒˆã—ã¦åŽ³å¯†æ¯”較 + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + clips.sort(compareBy(s => s.id)), + ); + }); + + test('ã®ä¸€è¦§ãŒå–å¾—ã§ãã‚‹(空)', async () => { + const res = await usersClips({ + parameters: { + userId: alice.id, + }, + }); + assert.deepStrictEqual(res, []); + }); + + test.each([ + { label: '' }, + { label: '他人アカウントã‹ã‚‰', user: (): User => bob }, + ])('ã®ä¸€è¦§ãŒ$labelå–å¾—ã§ãã‚‹', async () => { + const clips = await createMany({ isPublic: true }); + const res = await usersClips({ + parameters: { + userId: alice.id, + }, + }); + + // è¿”ã£ã¦ãã‚‹é…列ã«ã¯é †åºä¿éšœãŒãªã„ã®ã§idã§ã‚½ãƒ¼ãƒˆã—ã¦åŽ³å¯†æ¯”較 + assert.deepStrictEqual( + res.sort(compareBy<Clip>(s => s.id)), + clips.sort(compareBy(s => s.id))); + + // èªè¨¼çŠ¶æ…‹ã§è¦‹ãŸã¨ãã ã‘isFavoritedãŒå…¥ã£ã¦ã„ã‚‹ + for (const clip of res) { + assert.strictEqual(clip.isFavorited, false); + } + }); + + test.each([ + { label: '未èªè¨¼', user: (): undefined => undefined }, + { label: 'å˜åœ¨ã—ãªã„ユーザーã®ã‚‚ã®', parameters: { userId: 'xxxxxxx' } }, + ])('ã®ä¸€è¦§ã¯$labelã§ã‚‚å–å¾—ã§ãã‚‹', async ({ parameters, user }) => { + const clips = await createMany({ isPublic: true }); + const res = await usersClips({ + parameters: { + userId: alice.id, + limit: clips.length, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }); + + // 未èªè¨¼ã§è¦‹ãŸã¨ãã¯isFavoritedã¯å…¥ã‚‰ãªã„ + for (const clip of res) { + assert.strictEqual('isFavorited' in clip, false); + } + }); + + test('ã®ä¸€è¦§ã¯Privateãªã‚¯ãƒªãƒƒãƒ—ã‚’å«ã¾ãªã„(自分ã®ã‚‚ã®ã§ã‚ã£ã¦ã‚‚。)', async () => { + await create({ isPublic: false }); + const aliceClip = await create({ isPublic: true }); + const res = await usersClips({ + parameters: { + userId: alice.id, + limit: 2, + }, + }); + assert.deepStrictEqual(res, [aliceClip]); + }); + + test('ã®ä¸€è¦§ã¯ID指定ã§ç¯„囲é¸æŠžãŒã§ãã‚‹', async () => { + const clips = await createMany({ isPublic: true }, 7); + clips.sort(compareBy(s => s.id)); + const res = await usersClips({ + parameters: { + userId: alice.id, + sinceId: clips[1].id, + untilId: clips[5].id, + limit: 4, + }, + }); + + // Promise.allã§è¿”ã£ã¦ãã‚‹é…列ã«ã¯é †åºä¿éšœãŒãªã„ã®ã§idã§ã‚½ãƒ¼ãƒˆã—ã¦åŽ³å¯†æ¯”較 + assert.deepStrictEqual( + res.sort(compareBy<Clip>(s => s.id)), + [clips[2], clips[3], clips[4]], // sinceIdã¨untilId自体ã¯çµæžœã«å«ã¾ã‚Œãªã„ + clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id)); + }); + + test.each([ + { label: 'userId未指定', parameters: { userId: undefined } }, + { label: 'limitゼãƒ', parameters: { limit: 0 } }, + { label: 'limit最大+1', parameters: { limit: 101 } }, + ])('ã®ä¸€è¦§ã¯$labelã ã¨å–å¾—ã§ããªã„', async ({ parameters }) => failedApiCall({ + endpoint: '/users/clips', + parameters: { + userId: alice.id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + })); + + test.each([ + { label: '作æˆ', endpoint: '/clips/create' }, + { label: 'æ›´æ–°', endpoint: '/clips/update' }, + { label: '削除', endpoint: '/clips/delete' }, + { label: 'å–å¾—', endpoint: '/clips/list' }, + { label: 'ãŠæ°—ã«å…¥ã‚Šè¨å®š', endpoint: '/clips/favorite' }, + { label: 'ãŠæ°—ã«å…¥ã‚Šè§£é™¤', endpoint: '/clips/unfavorite' }, + { label: 'ãŠæ°—ã«å…¥ã‚Šå–å¾—', endpoint: '/clips/my-favorites' }, + { label: 'ãƒŽãƒ¼ãƒˆè¿½åŠ ', endpoint: '/clips/add-note' }, + { label: 'ノート削除', endpoint: '/clips/remove-note' }, + ])('ã®$labelã¯æœªèªè¨¼ã§ã¯ã§ããªã„', async ({ endpoint }) => await failedApiCall({ + endpoint: endpoint, + parameters: {}, + user: undefined, + }, { + status: 401, + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + })); + + describe('ã®ãŠæ°—ã«å…¥ã‚Š', () => { + let aliceClip: Clip; + + type FavoriteParam = JTDDataType<typeof FavoriteParamDef>; + const favorite = async (parameters: FavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return successfulApiCall<void>({ + endpoint: '/clips/favorite', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type UnfavoriteParam = JTDDataType<typeof UnfavoriteParamDef>; + const unfavorite = async (parameters: UnfavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return successfulApiCall<void>({ + endpoint: '/clips/unfavorite', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + const myFavorites = async (request: Partial<ApiRequest> = {}): Promise<Clip[]> => { + return successfulApiCall<Clip[]>({ + endpoint: '/clips/my-favorites', + parameters: {}, + user: alice, + ...request, + }); + }; + + beforeEach(async () => { + aliceClip = await create(); + }); + + test('ã‚’è¨å®šã§ãる。', async () => { + await favorite({ clipId: aliceClip.id }); + const clip = await show({ clipId: aliceClip.id }); + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + }); + + test('ã¯Publicãªä»–人ã®ã‚¯ãƒªãƒƒãƒ—ã«è¨å®šã§ãる。', async () => { + const publicClip = await create({ isPublic: true }); + await favorite({ clipId: publicClip.id }, { user: bob }); + const clip = await show({ clipId: publicClip.id }, { user: bob }); + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + + // isFavoritedã¯è¦‹ã‚‹äººã«ã‚ˆã£ã¦åˆ‡ã‚Šæ›¿ã‚る。 + const clip2 = await show({ clipId: publicClip.id }); + assert.strictEqual(clip2.favoritedCount, 1); + assert.strictEqual(clip2.isFavorited, false); + }); + + test('ã¯1ã¤ã®ã‚¯ãƒªãƒƒãƒ—ã«å¯¾ã—ã¦è¤‡æ•°äººãŒè¨å®šã§ãる。', async () => { + const publicClip = await create({ isPublic: true }); + await favorite({ clipId: publicClip.id }, { user: bob }); + await favorite({ clipId: publicClip.id }); + const clip = await show({ clipId: publicClip.id }, { user: bob }); + assert.strictEqual(clip.favoritedCount, 2); + assert.strictEqual(clip.isFavorited, true); + + const clip2 = await show({ clipId: publicClip.id }); + assert.strictEqual(clip2.favoritedCount, 2); + assert.strictEqual(clip2.isFavorited, true); + }); + + test('ã¯11を超ãˆã¦è¨å®šã§ãる。', async () => { + const clips = [ + aliceClip, + ...await createMany({}, 10, alice), + ...await createMany({ isPublic: true }, 10, bob), + ]; + for (const clip of clips) { + await favorite({ clipId: clip.id }); + } + + // pagenationã¯ãªã„。全部一気ã«ã¨ã‚Œã‚‹ã€‚ + const favorited = await myFavorites(); + assert.strictEqual(favorited.length, clips.length); + for (const clip of favorited) { + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + } + }); + + test('ã¯åŒã˜ã‚¯ãƒªãƒƒãƒ—ã«å¯¾ã—ã¦äºŒå›žè¨å®šã§ããªã„。', async () => { + await favorite({ clipId: aliceClip.id }); + await failedApiCall({ + endpoint: '/clips/favorite', + parameters: { + clipId: aliceClip.id, + }, + user: alice, + }, { + status: 400, + code: 'ALREADY_FAVORITED', + id: '92658936-c625-4273-8326-2d790129256e', + }); + }); + + test.each([ + { label: 'clipIdãŒnull', parameters: { clipId: null } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): User => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', + } }, + ])('ã®è¨å®šã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/favorite', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('ã‚’è¨å®šè§£é™¤ã§ãる。', async () => { + await favorite({ clipId: aliceClip.id }); + await unfavorite({ clipId: aliceClip.id }); + const clip = await show({ clipId: aliceClip.id }); + assert.strictEqual(clip.favoritedCount, 0); + assert.strictEqual(clip.isFavorited, false); + assert.deepStrictEqual(await myFavorites(), []); + }); + + test.each([ + { label: 'clipIdãŒnull', parameters: { clipId: null } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '2603966e-b865-426c-94a7-af4a01241dc1', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): User => bob, assertion: { + code: 'NOT_FAVORITED', + id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', + } }, + { label: 'ãŠæ°—ã«å…¥ã‚Šã—ã¦ã„ãªã„クリップ', assertion: { + code: 'NOT_FAVORITED', + id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', + } }, + ])('ã®è¨å®šè§£é™¤ã¯$labelãªã‚‰ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/unfavorite', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('ã‚’å–å¾—ã§ãる。', async () => { + await favorite({ clipId: aliceClip.id }); + const favorited = await myFavorites(); + assert.deepStrictEqual(favorited, [await show({ clipId: aliceClip.id })]); + }); + + test('ã‚’å–å¾—ã—ãŸã¨ã他人ã®ãŠæ°—ã«å…¥ã‚Šã¯å«ã¾ãªã„。', async () => { + await favorite({ clipId: aliceClip.id }); + const favorited = await myFavorites({ user: bob }); + assert.deepStrictEqual(favorited, []); + }); + }); + + describe('ã«ç´ã¥ãノート', () => { + let aliceClip: Clip; + + const sampleNotes = (): Note[] => [ + aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, + bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote, + ]; + + type AddNoteParam = JTDDataType<typeof AddNoteParamDef>; + const addNote = async (parameters: AddNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return successfulApiCall<void>({ + endpoint: '/clips/add-note', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type RemoveNoteParam = JTDDataType<typeof RemoveNoteParamDef>; + const removeNote = async (parameters: RemoveNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => { + return successfulApiCall<void>({ + endpoint: '/clips/remove-note', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type NotesParam = JTDDataType<typeof NotesParamDef>; + const notes = async (parameters: Partial<NotesParam>, request: Partial<ApiRequest> = {}): Promise<Note[]> => { + return successfulApiCall<Note[]>({ + endpoint: '/clips/notes', + parameters, + user: alice, + ...request, + }); + }; + + beforeEach(async () => { + aliceClip = await create(); + }); + + test('ã‚’è¿½åŠ ã§ãる。', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + const res = await show({ clipId: aliceClip.id }); + assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString()); + assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), [aliceNote]); + + // 他人ã®éžå…¬é–‹ãƒŽãƒ¼ãƒˆã‚‚çªã£è¾¼ã‚ã‚‹ + await addNote({ clipId: aliceClip.id, noteId: bobHomeNote.id }); + await addNote({ clipId: aliceClip.id, noteId: bobFollowersNote.id }); + await addNote({ clipId: aliceClip.id, noteId: bobSpecifiedNote.id }); + }); + + test('ã¨ã—ã¦åŒã˜ãƒŽãƒ¼ãƒˆã‚’二回ç´ã¥ã‘ã‚‹ã“ã¨ã¯ã§ããªã„', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: alice, + }, { + status: 400, + code: 'ALREADY_CLIPPED', + id: '734806c4-542c-463a-9311-15c512803965', + }); + }); + + // TODO: 17000msãらã„ã‹ã‹ã‚‹... + test('ã‚’ãƒãƒªã‚·ãƒ¼ã§å®šã‚られãŸä¸Šé™ã„ã£ã±ã„(200)を超ãˆã¦è¿½åŠ ã¯ã§ããªã„。', async () => { + const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; + const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { + text: `test ${i}`, + }) as unknown)) as Note[]; + await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); + + await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: alice, + }, { + status: 400, + code: 'TOO_MANY_CLIP_NOTES', + id: 'f0dba960-ff73-4615-8df4-d6ac5d9dc118', + }); + }); + + test('ã¯ä»–人ã®ã‚¯ãƒªãƒƒãƒ—ã¸è¿½åŠ ã§ããªã„。', async () => await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: bob, + }, { + status: 400, + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + })); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + } }, + { label: 'å˜åœ¨ã—ãªã„ノート', parameters: { noteId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_NOTE', + id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b', + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): object => bob, assetion: { + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + } }, + ])('ã®è¿½åŠ ã¯$labelã ã¨ã§ããªã„', async ({ parameters, user, assetion }) => failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('を削除ã§ãる。', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + await removeNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), []); + }); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteã¨ç•°ãªã‚‹ + } }, + { label: 'å˜åœ¨ã—ãªã„ノート', parameters: { noteId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_NOTE', + id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteã¨ç•°ãªã‚‹ + } }, + { label: '他人ã®ã‚¯ãƒªãƒƒãƒ—', user: (): object => bob, assetion: { + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteã¨ç•°ãªã‚‹ + } }, + ])('ã®å‰Šé™¤ã¯$labelã ã¨ã§ããªã„', async ({ parameters, user, assetion }) => failedApiCall({ + endpoint: '/clips/remove-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('ã‚’å–å¾—ã§ãる。', async () => { + const noteList = sampleNotes(); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ clipId: aliceClip.id }); + + // 自分ã®ãƒŽãƒ¼ãƒˆã¯éžå…¬é–‹ã§ã‚‚入れられるã—ã€è¦‹ãˆã‚‹ + // 他人ã®éžå…¬é–‹ãƒŽãƒ¼ãƒˆã¯å…¥ã‚Œã‚‰ã‚Œã‚‹ã‘ã©ã€é™¤å¤–ã•ã‚Œã‚‹ + const expects = [ + aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, + bobNote, bobHomeNote, + ]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test('を始端IDã¨limitã§å–å¾—ã§ãる。', async () => { + const noteList = sampleNotes(); + noteList.sort(compareBy(s => s.id)); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ + clipId: aliceClip.id, + sinceId: noteList[2].id, + limit: 3, + }); + + // Promise.allã§è¿”ã£ã¦ãã‚‹é…列ã¯IDé †ã§ä¸¦ã‚“ã§ãªã„ã®ã§ã‚½ãƒ¼ãƒˆã—ã¦åŽ³å¯†æ¯”較 + const expects = [noteList[3], noteList[4], noteList[5]]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test('ã‚’ID範囲指定ã§å–å¾—ã§ãる。', async () => { + const noteList = sampleNotes(); + noteList.sort(compareBy(s => s.id)); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ + clipId: aliceClip.id, + sinceId: noteList[1].id, + untilId: noteList[4].id, + }); + + // Promise.allã§è¿”ã£ã¦ãã‚‹é…列ã¯IDé †ã§ä¸¦ã‚“ã§ãªã„ã®ã§ã‚½ãƒ¼ãƒˆã—ã¦åŽ³å¯†æ¯”較 + const expects = [noteList[2], noteList[3]]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test.todo('Remoteã®ãƒŽãƒ¼ãƒˆã‚‚クリップã§ãる。ã©ã†ãƒ†ã‚¹ãƒˆã—よã†ï¼Ÿ'); + + test('ã¯ä»–人ã®Publicãªã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰ã‚‚å–å¾—ã§ãる。', async () => { + const bobClip = await create({ isPublic: true }, { user: bob } ); + await addNote({ clipId: bobClip.id, noteId: aliceNote.id }, { user: bob }); + const res = await notes({ clipId: bobClip.id }); + assert.deepStrictEqual(res, [aliceNote]); + }); + + test('ã¯Publicãªã‚¯ãƒªãƒƒãƒ—ãªã‚‰èªè¨¼ãªã—ã§ã‚‚å–å¾—ã§ãる。(éžå…¬é–‹ãƒŽãƒ¼ãƒˆã¯hideã•ã‚Œã¦è¿”ã£ã¦ãã‚‹)', async () => { + const publicClip = await create({ isPublic: true }); + await addNote({ clipId: publicClip.id, noteId: aliceNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceHomeNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceFollowersNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceSpecifiedNote.id }); + + const res = await notes({ clipId: publicClip.id }, { user: undefined }); + const expects = [ + aliceNote, aliceHomeNote, + // èªè¨¼ãªã—ã ã¨éžå…¬é–‹ãƒŽãƒ¼ãƒˆã¯çµæžœã«ã¯å«ã‚€ã‘ã©hideã•ã‚Œã‚‹ã€‚ + hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote), + ]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test.todo('ブãƒãƒƒã‚¯ã€ãƒŸãƒ¥ãƒ¼ãƒˆã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®è¨å®šï¼†å–å¾—etc.'); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'limitゼãƒ', parameters: { limit: 0 } }, + { label: 'limit最大+1', parameters: { limit: 101 } }, + { label: 'å˜åœ¨ã—ãªã„クリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + { label: '他人ã®Privateãªã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰', user: (): object => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + { label: '未èªè¨¼ã§Privateãªã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰', user: (): undefined => undefined, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + ])('ã¯$labelã ã¨å–å¾—ã§ããªã„', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/notes', + parameters: { + clipId: aliceClip.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + }); +}); diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index cbe7b894f4b279d4beeecb6b866fb1d32c239fe7..afb72c84d40e4bdcc89851c8cb9ec3c2f9ade48e 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -162,14 +162,14 @@ describe('Endpoints', () => { const res = await api('/users/show', { userId: '000000000000000000000000', }); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); }); test('é–“é•ã£ãŸIDã§æ€’られる', async () => { const res = await api('/users/show', { userId: 'kyoppie', }); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); }); }); @@ -841,4 +841,12 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].id, carolPost.id); }); }); + + describe('URL preview', () => { + test('Error from summaly becomes HTTP 422', async () => { + const res = await simpleGet('/url?url=https://e:xample.com'); + assert.strictEqual(res.status, 422); + assert.strictEqual(res.body.error.code, 'URL_PREVIEW_FAILED'); + }); + }); }); diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts index 0549800a684c106b1adbce3e388d258aa4d1cba7..4065665579556365f475e6cbb86ceae062c60f95 100644 --- a/packages/backend/test/unit/DriveService.ts +++ b/packages/backend/test/unit/DriveService.ts @@ -1,55 +1,56 @@ process.env.NODE_ENV = 'test'; -import { jest } from '@jest/globals'; import { Test } from '@nestjs/testing'; +import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { DriveService } from '@/core/DriveService.js'; import { CoreModule } from '@/core/CoreModule.js'; -import { S3Service } from '@/core/S3Service'; -import type { Meta } from '@/models'; -import type { DeleteObjectOutput } from 'aws-sdk/clients/s3'; -import type { AWSError } from 'aws-sdk/lib/error'; -import type { PromiseResult, Request } from 'aws-sdk/lib/request'; import type { TestingModule } from '@nestjs/testing'; describe('DriveService', () => { let app: TestingModule; let driveService: DriveService; + const s3Mock = mockClient(S3Client); - beforeEach(async () => { + beforeAll(async () => { app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], - providers: [DriveService, S3Service], + providers: [DriveService], }).compile(); app.enableShutdownHooks(); driveService = app.get<DriveService>(DriveService); + }); - const s3Service = app.get<S3Service>(S3Service); - const s3 = s3Service.getS3({} as Meta); - - // new S3() surprisingly does not return an instance of class S3. - // Let's use getPrototypeOf here to get a real prototype, since spying on S3.prototype doesn't work. - // TODO: Use `aws-sdk-client-mock` package when upgrading to AWS SDK v3. - jest.spyOn(Object.getPrototypeOf(s3), 'deleteObject').mockImplementation(() => { - // Roughly mock AWS request object - return { - async promise(): Promise<PromiseResult<DeleteObjectOutput, AWSError>> { - const err = new Error('mock') as AWSError; - err.code = 'NoSuchKey'; - throw err; - }, - } as Request<DeleteObjectOutput, AWSError>; - }); + beforeEach(async () => { + s3Mock.reset(); + }); + + afterAll(async () => { + await app.close(); }); describe('Object storage', () => { + test('delete a file', async () => { + s3Mock.on(DeleteObjectCommand) + .resolves({} as DeleteObjectCommandOutput); + + await driveService.deleteObjectStorageFile('peace of the world'); + }); + + test('delete a file then unexpected error', async () => { + s3Mock.on(DeleteObjectCommand) + .rejects(new InvalidObjectState({ $metadata: {}, message: '' })); + + await expect(driveService.deleteObjectStorageFile('unexpected')).rejects.toThrowError(Error); + }); + test('delete a file with no valid key', async () => { - try { - await driveService.deleteObjectStorageFile('lol no way'); - } catch (err: any) { - console.log(err.cause); - throw err; - } + // Some S3 implementations returns 404 Not Found on deleting with a non-existent key + s3Mock.on(DeleteObjectCommand) + .rejects(new NoSuchKey({ $metadata: {}, message: 'allowed error.' })); + + await driveService.deleteObjectStorageFile('lol no way'); }); }); }); diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index 6a20a1e08e15361a9f50c9c3ad21e3e70dce7107..38db081ac089ca359bc82199d2daba590895eaef 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -74,19 +74,19 @@ describe('ReactionService', () => { }); test('fallback - undefined', async () => { - assert.strictEqual(await reactionService.toDbReaction(undefined), 'ðŸ‘'); + assert.strictEqual(await reactionService.toDbReaction(undefined), 'â¤'); }); test('fallback - null', async () => { - assert.strictEqual(await reactionService.toDbReaction(null), 'ðŸ‘'); + assert.strictEqual(await reactionService.toDbReaction(null), 'â¤'); }); test('fallback - empty', async () => { - assert.strictEqual(await reactionService.toDbReaction(''), 'ðŸ‘'); + assert.strictEqual(await reactionService.toDbReaction(''), 'â¤'); }); test('fallback - unknown', async () => { - assert.strictEqual(await reactionService.toDbReaction('unknown'), 'ðŸ‘'); + assert.strictEqual(await reactionService.toDbReaction('unknown'), 'â¤'); }); }); }); diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts new file mode 100644 index 0000000000000000000000000000000000000000..1dfa22afd2901a3f729ef28073cd379de7e5d52d --- /dev/null +++ b/packages/backend/test/unit/S3Service.ts @@ -0,0 +1,77 @@ +process.env.NODE_ENV = 'test'; + +import { Test } from '@nestjs/testing'; +import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; +import { GlobalModule } from '@/GlobalModule.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { S3Service } from '@/core/S3Service'; +import { Meta } from '@/models'; +import type { TestingModule } from '@nestjs/testing'; + +describe('S3Service', () => { + let app: TestingModule; + let s3Service: S3Service; + const s3Mock = mockClient(S3Client); + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + providers: [S3Service], + }).compile(); + app.enableShutdownHooks(); + s3Service = app.get<S3Service>(S3Service); + }); + + beforeEach(async () => { + s3Mock.reset(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('upload', () => { + test('upload a file', async () => { + s3Mock.on(PutObjectCommand).resolves({}); + + await s3Service.upload({ objectStorageRegion: 'us-east-1' } as Meta, { + Bucket: 'fake', + Key: 'fake', + Body: 'x', + }); + }); + + test('upload a large file', async () => { + s3Mock.on(CreateMultipartUploadCommand).resolves({ UploadId: '1' }); + s3Mock.on(UploadPartCommand).resolves({ ETag: '1' }); + s3Mock.on(CompleteMultipartUploadCommand).resolves({ Bucket: 'fake', Key: 'fake' }); + + await s3Service.upload({} as Meta, { + Bucket: 'fake', + Key: 'fake', + Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeã«ã—ã¦ã„ã‚‹ 8 * 1024 * 1024 を越ãˆã‚‹ã‚µã‚¤ã‚º + }); + }); + + test('upload a file error', async () => { + s3Mock.on(PutObjectCommand).rejects({ name: 'Fake Error' }); + + await expect(s3Service.upload({ objectStorageRegion: 'us-east-1' } as Meta, { + Bucket: 'fake', + Key: 'fake', + Body: 'x', + })).rejects.toThrowError(Error); + }); + + test('upload a large file error', async () => { + s3Mock.on(UploadPartCommand).rejects(); + + await expect(s3Service.upload({} as Meta, { + Bucket: 'fake', + Key: 'fake', + Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeã«ã—ã¦ã„ã‚‹ 8 * 1024 * 1024 を越ãˆã‚‹ã‚µã‚¤ã‚º + })).rejects.toThrowError(Error); + }); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 4d52c2f06284e4c2031bc78c3dde20c5d96990c3..4f501a8726cd84dde4b1835543af10f1e3beb328 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,5 +1,7 @@ +import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { isAbsolute, basename } from 'node:path'; +import { inspect } from 'node:util'; import WebSocket from 'ws'; import fetch, { Blob, File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; @@ -22,6 +24,36 @@ export const api = async (endpoint: string, params: any, me?: any) => { return await request(`api/${normalized}`, params, me); }; +export type ApiRequest = { + endpoint: string, + parameters: object, + user: object | undefined, +}; + +export const successfulApiCall = async <T, >(request: ApiRequest, assertion: { + status: number, +} = { status: 200 }): Promise<T> => { + const { endpoint, parameters, user } = request; + const { status } = assertion; + const res = await api(endpoint, parameters, user); + assert.strictEqual(res.status, status, inspect(res.body)); + return res.body; +}; + +export const failedApiCall = async <T, >(request: ApiRequest, assertion: { + status: number, + code: string, + id: string +}): Promise<T> => { + const { endpoint, parameters, user } = request; + const { status, code, id } = assertion; + const res = await api(endpoint, parameters, user); + assert.strictEqual(res.status, status, inspect(res.body)); + assert.strictEqual(res.body.error.code, code, inspect(res.body)); + assert.strictEqual(res.body.error.id, id, inspect(res.body)); + return res.body; +}; + const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { const auth = me ? { i: me.token, @@ -69,6 +101,21 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create'] return res.body ? res.body.createdNote : null; }; +// éžå…¬é–‹ãƒŽãƒ¼ãƒˆã‚’API越ã—ã«è¦‹ãŸã¨ãã®ãƒŽãƒ¼ãƒˆ NoteEntityService.ts +export const hiddenNote = (note: any): any => { + const temp = { + ...note, + fileIds: [], + files: [], + text: null, + cw: null, + isHidden: true, + }; + delete temp.visibleUserIds; + delete temp.poll; + return temp; +}; + export const react = async (user: any, note: any, reaction: string): Promise<any> => { await api('notes/reactions/create', { noteId: note.id, diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts new file mode 100644 index 0000000000000000000000000000000000000000..c95da64bbaebd23be9cd8f700ae7adca8be2e69d --- /dev/null +++ b/packages/frontend/src/cache.ts @@ -0,0 +1,6 @@ +import * as misskey from 'misskey-js'; +import { Cache } from '@/scripts/cache'; + +export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity); +export const rolesCache = new Cache(Infinity); +export const userListsCache = new Cache<misskey.entities.UserList[]>(Infinity); diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 8c17c0530adee25cafb4608ae9452721a2cb5fb9..ab408b500867d06fa9e175a9f138045ef84a9a85 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -32,14 +32,14 @@ </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref } from 'vue'; +import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import bytes from '@/filters/bytes'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { $i } from '@/account'; +import { getDriveFileMenu } from '@/scripts/get-drive-file-menu'; const props = withDefaults(defineProps<{ file: Misskey.entities.DriveFile; @@ -60,48 +60,16 @@ const isDragging = ref(false); const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`); -function getMenu() { - return [{ - text: i18n.ts.rename, - icon: 'ti ti-forms', - action: rename, - }, { - text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: props.file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off', - action: toggleSensitive, - }, { - text: i18n.ts.describeFile, - icon: 'ti ti-text-caption', - action: describe, - }, null, { - text: i18n.ts.copyUrl, - icon: 'ti ti-link', - action: copyUrl, - }, { - type: 'a', - href: props.file.url, - target: '_blank', - text: i18n.ts.download, - icon: 'ti ti-download', - download: props.file.name, - }, null, { - text: i18n.ts.delete, - icon: 'ti ti-trash', - danger: true, - action: deleteFile, - }]; -} - function onClick(ev: MouseEvent) { if (props.selectMode) { emit('chosen', props.file); } else { - os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + os.popupMenu(getDriveFileMenu(props.file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); } } function onContextmenu(ev: MouseEvent) { - os.contextMenu(getMenu(), ev); + os.contextMenu(getDriveFileMenu(props.file), ev); } function onDragstart(ev: DragEvent) { @@ -118,62 +86,6 @@ function onDragend() { isDragging.value = false; emit('dragend'); } - -function rename() { - os.inputText({ - title: i18n.ts.renameFile, - placeholder: i18n.ts.inputNewFileName, - default: props.file.name, - }).then(({ canceled, result: name }) => { - if (canceled) return; - os.api('drive/files/update', { - fileId: props.file.id, - name: name, - }); - }); -} - -function describe() { - os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { - default: props.file.comment != null ? props.file.comment : '', - file: props.file, - }, { - done: caption => { - os.api('drive/files/update', { - fileId: props.file.id, - comment: caption.length === 0 ? null : caption, - }); - }, - }, 'closed'); -} - -function toggleSensitive() { - os.api('drive/files/update', { - fileId: props.file.id, - isSensitive: !props.file.isSensitive, - }); -} - -function copyUrl() { - copyToClipboard(props.file.url); - os.success(); -} -/* -function addApp() { - alert('not implemented yet'); -} -*/ -async function deleteFile() { - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }), - }); - - if (canceled) return; - os.api('drive/files/delete', { - fileId: props.file.id, - }); -} </script> <style lang="scss" scoped> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index af81051a54ec68fb49a1c6e9ba6c055e01a3c141..72c6e55df10a934eb6370b23aa0700217d5b74f9 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -109,6 +109,9 @@ <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)"> <i class="ti ti-minus"></i> </button> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> + <i class="ti ti-paperclip"></i> + </button> <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()"> <i class="ti ti-dots"></i> </button> @@ -151,7 +154,7 @@ import { reactionPicker } from '@/scripts/reaction-picker'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { $i } from '@/account'; import { i18n } from '@/i18n'; -import { getNoteMenu } from '@/scripts/get-note-menu'; +import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; @@ -192,6 +195,7 @@ const menuButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>(); +const clipButton = shallowRef<HTMLElement>(); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); @@ -392,6 +396,10 @@ function menu(viaKeyboard = false): void { }).then(focus); } +async function clip() { + os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClipPage }), clipButton.value).then(focus); +} + function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; os.popupMenu([{ diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index ea72e1b517cb418c11f1f5e555abded765167614..715fd3a9a8e3773190384057da67b03c5e57248b 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -114,6 +114,9 @@ <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <i class="ti ti-minus"></i> </button> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="button _button" @mousedown="clip()"> + <i class="ti ti-paperclip"></i> + </button> <button ref="menuButton" class="button _button" @mousedown="menu()"> <i class="ti ti-dots"></i> </button> @@ -156,7 +159,7 @@ import { reactionPicker } from '@/scripts/reaction-picker'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; import { $i } from '@/account'; import { i18n } from '@/i18n'; -import { getNoteMenu } from '@/scripts/get-note-menu'; +import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; @@ -196,6 +199,7 @@ const menuButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>(); +const clipButton = shallowRef<HTMLElement>(); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); @@ -384,6 +388,10 @@ function menu(viaKeyboard = false): void { }).then(focus); } +async function clip() { + os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus); +} + function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; os.popupMenu([{ diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 7fb830d537c8ec3731277280d326820a52cdaffc..814ab53d27197b6064a44238853406c3653e2bef 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -1,25 +1,26 @@ <template> -<span v-if="!link" v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> +<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> <img :class="$style.inner" :src="url" decoding="async"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> - <template v-if="user.isCat"> - <div :class="$style.earLeft"/> - <div :class="$style.earRight"/> - </template> -</span> -<MkA v-else v-user-preview="preview ? user.id : undefined" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" :to="userPage(user)" :target="target"> - <img :class="$style.inner" :src="url" decoding="async"/> - <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> - <template v-if="user.isCat"> - <div :class="$style.earLeft"/> - <div :class="$style.earRight"/> - </template> -</MkA> + <div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]"> + <div :class="$style.earLeft"> + <div v-if="useBlurEffect" :class="$style.layer"> + <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> + </div> + </div> + <div :class="$style.earRight"> + <div v-if="useBlurEffect" :class="$style.layer"> + <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> + </div> + </div> + </div> +</component> </template> <script lang="ts" setup> import { watch } from 'vue'; import * as misskey from 'misskey-js'; +import MkA from './MkA.vue'; import { getStaticImageUrl } from '@/scripts/media-proxy'; import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; import { acct, userPage } from '@/filters/user'; @@ -27,6 +28,7 @@ import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; import { defaultStore } from '@/store'; const squareAvatars = $ref(defaultStore.state.squareAvatars); +const useBlurEffect = $ref(defaultStore.state.useBlurEffect); const props = withDefaults(defineProps<{ user: misskey.entities.User; @@ -45,15 +47,20 @@ const emit = defineEmits<{ (ev: 'click', v: MouseEvent): void; }>(); +const bound = $computed(() => props.link + ? { to: userPage(props.user), target: props.target } + : {}); + const url = $computed(() => defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(props.user.avatarUrl) : props.user.avatarUrl); -function onClick(ev: MouseEvent) { +function onClick(ev: MouseEvent): void { + if (props.link) return; emit('click', ev); } -let color = $ref(); +let color = $ref<string | undefined>(); watch(() => props.user.avatarBlurhash, () => { color = extractAvgColorFromBlurhash(props.user.avatarBlurhash); @@ -120,42 +127,113 @@ watch(() => props.user.avatarBlurhash, () => { } .cat { - > .earLeft, - > .earRight { + > .ears { contain: strict; - display: inline-block; - height: 50%; - width: 50%; - background: currentColor; + position: absolute; + top: -50%; + left: -50%; + width: 100%; + height: 100%; + padding: 50%; + + &.mask { + -webkit-mask: + url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') center / 50% 50%, + linear-gradient(#fff, #fff); + -webkit-mask-composite: destination-out, source-over; + mask: + url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%, + linear-gradient(#fff, #fff); // polyfill of `image(#fff)` + } - &::before { + > .earLeft, + > .earRight { contain: strict; - content: ''; - display: block; - width: 60%; - height: 60%; - margin: 20%; - background: #df548f; + display: inline-block; + height: 50%; + width: 50%; + background: currentColor; + + &::after { + contain: strict; + content: ''; + display: block; + width: 60%; + height: 60%; + margin: 20%; + background: #df548f; + } + + > .layer { + contain: strict; + position: absolute; + top: 0; + width: 280%; + height: 280%; + + > .plot { + contain: strict; + width: 100%; + height: 100%; + clip-path: path('M0 0H1V1H0z'); + transform: scale(32767); + transform-origin: 0 0; + } + } } - } - - > .earLeft { - border-radius: 0 75% 75%; - transform: rotate(37.5deg) skew(30deg); - } - > .earRight { - border-radius: 75% 0 75% 75%; - transform: rotate(-37.5deg) skew(-30deg); - } - - &:hover { > .earLeft { - animation: earwiggleleft 1s infinite; + transform: rotate(37.5deg) skew(30deg); + + &, &::after { + border-radius: 0 75% 75%; + } + + > .layer { + left: 0; + transform: + skew(-30deg) + rotate(-37.5deg) + translate(-2.82842712475%, /* -2 * sqrt(2) */ + -38.5857864376%); /* 40 - 2 * sqrt(2) */ + + > .plot { + background-position: 20% 10%; /* ~= 37.5deg */ + } + } } > .earRight { - animation: earwiggleright 1s infinite; + transform: rotate(-37.5deg) skew(-30deg); + + &, &::after { + border-radius: 75% 0 75% 75%; + } + + > .layer { + right: 0; + transform: + skew(30deg) + rotate(37.5deg) + translate(2.82842712475%, /* 2 * sqrt(2) */ + -38.5857864376%); /* 40 - 2 * sqrt(2) */ + + > .plot { + background-position: 80% 10%; /* ~= 37.5deg */ + } + } + } + } + + &:hover { + > .ears { + > .earLeft { + animation: earwiggleleft 1s infinite; + } + + > .earRight { + animation: earwiggleright 1s infinite; + } } } } diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue index 07729b8cf94badb7e05f715803978a0650f41390..343d2c4c5cf7e0b6d8089d8506011ad844d6b9b2 100644 --- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue +++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue @@ -10,6 +10,8 @@ <option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option> <option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option> <option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option> + <option value="notesLessThanOrEq">{{ i18n.ts._role._condition.notesLessThanOrEq }}</option> + <option value="notesMoreThanOrEq">{{ i18n.ts._role._condition.notesMoreThanOrEq }}</option> <option value="and">{{ i18n.ts._role._condition.and }}</option> <option value="or">{{ i18n.ts._role._condition.or }}</option> <option value="not">{{ i18n.ts._role._condition.not }}</option> @@ -42,7 +44,7 @@ <template #suffix>sec</template> </MkInput> - <MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq'].includes(type)" v-model="v.value" type="number"> + <MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number"> </MkInput> </div> </template> @@ -91,6 +93,8 @@ const type = computed({ if (t === 'followersMoreThanOrEq') v.value.value = 10; if (t === 'followingLessThanOrEq') v.value.value = 10; if (t === 'followingMoreThanOrEq') v.value.value = 10; + if (t === 'notesLessThanOrEq') v.value.value = 10; + if (t === 'notesMoreThanOrEq') v.value.value = 10; v.value.type = t; }, }); diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 550de24bb2cf288ef857ad2e133c34009da77c64..8aae39cba1a6cf0cf057bf6fea89108fbe19e0dd 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -12,7 +12,7 @@ <MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> <MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu> + <MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu> </div> </MkSpacer> </div> @@ -220,6 +220,12 @@ onUnmounted(() => { ro.disconnect(); }); +watch(router.currentRef, (to) => { + if (to.route.path === "/admin" && to.child?.route.name == null && !narrow) { + router.replace('/admin/overview'); + } +}); + provideMetadataReceiver((info) => { if (info == null) { childInfo = null; diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 7c2f04a9abb61e58fddbee694f0f1f16a18c8c1a..ebe1a8ade098cc6847b54249545fee1f3ca1c5b3 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -46,7 +46,7 @@ let sensitiveWords: string = $ref(''); async function init() { const meta = await os.api('admin/meta'); - sensitiveWords = meta.pinnedUsers.join('\n'); + sensitiveWords = meta.sensitiveWords.join('\n'); } function save() { diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue index 80e97fed93834e702261a6569af845629f749479..509d329eb16ced178d5ef5e31e6293355f7c3ed3 100644 --- a/packages/frontend/src/pages/admin/queue.vue +++ b/packages/frontend/src/pages/admin/queue.vue @@ -4,6 +4,8 @@ <MkSpacer :content-max="800"> <XQueue v-if="tab === 'deliver'" domain="deliver"/> <XQueue v-else-if="tab === 'inbox'" domain="inbox"/> + <br> + <MkButton @click="promoteAllQueues"><i class="ti ti-reload"></i> {{ i18n.ts.retryAllQueuesNow }}</MkButton> </MkSpacer> </MkStickyContainer> </template> @@ -15,6 +17,7 @@ import * as os from '@/os'; import * as config from '@/config'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import MkButton from '@/components/MkButton.vue'; let tab = $ref('deliver'); @@ -30,6 +33,18 @@ function clear() { }); } +function promoteAllQueues() { + os.confirm({ + type: 'warning', + title: i18n.ts.retryAllQueuesConfirmTitle, + text: i18n.ts.retryAllQueuesConfirmText, + }).then(({ canceled }) => { + if (canceled) return; + + os.apiWithDialog('admin/queue/promote', { type: tab }); + }); +} + const headerActions = $computed(() => [{ asFullButton: true, icon: 'ti ti-external-link', diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index e6896237f8f9b3723a3674a205edd94f2ad5a972..b1aa03f1f74f23a366a8b03d297f114d4c46cfc1 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -26,6 +26,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { useRouter } from '@/router'; import MkButton from '@/components/MkButton.vue'; +import { rolesCache } from '@/cache'; const router = useRouter(); @@ -61,6 +62,7 @@ if (props.id) { } async function save() { + rolesCache.delete(); if (role) { os.apiWithDialog('admin/roles/update', { roleId: role.id, diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 12f341c01d7064b4857b8549066ee60859edcccd..65e64930d5261792f1f3b03d7db98c1d50c88e72 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -43,6 +43,14 @@ <MkSwitch v-model="emailRequiredForSignup"> <template #label>{{ i18n.ts.emailRequiredForSignup }}</template> </MkSwitch> + + <MkSwitch v-model="enableChartsForRemoteUser"> + <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> + </MkSwitch> + + <MkSwitch v-model="enableChartsForFederatedInstances"> + <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> + </MkSwitch> </div> </FormSection> @@ -175,6 +183,8 @@ let cacheRemoteFiles: boolean = $ref(false); let enableRegistration: boolean = $ref(false); let emailRequiredForSignup: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false); +let enableChartsForRemoteUser: boolean = $ref(false); +let enableChartsForFederatedInstances: boolean = $ref(false); let swPublicKey: any = $ref(null); let swPrivateKey: any = $ref(null); let deeplAuthKey: string = $ref(''); @@ -198,6 +208,8 @@ async function init() { enableRegistration = !meta.disableRegistration; emailRequiredForSignup = meta.emailRequiredForSignup; enableServiceWorker = meta.enableServiceWorker; + enableChartsForRemoteUser = meta.enableChartsForRemoteUser; + enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances; swPublicKey = meta.swPublickey; swPrivateKey = meta.swPrivateKey; deeplAuthKey = meta.deeplAuthKey; @@ -222,6 +234,8 @@ function save() { disableRegistration: !enableRegistration, emailRequiredForSignup, enableServiceWorker, + enableChartsForRemoteUser, + enableChartsForFederatedInstances, swPublicKey, swPrivateKey, deeplAuthKey, diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 7515a9122a53802e4f03f6114f9b8830551f5eb2..2b64de088ab717cd8e4f68a2a8affda095bf8d27 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -30,6 +30,7 @@ import * as os from '@/os'; import { definePageMetadata } from '@/scripts/page-metadata'; import { url } from '@/config'; import MkButton from '@/components/MkButton.vue'; +import { clipsCache } from '@/cache'; const props = defineProps<{ clipId: string, @@ -108,6 +109,8 @@ const headerActions = $computed(() => clip && isOwned ? [{ clipId: clip.id, ...result, }); + + clipsCache.delete(); }, }, ...(clip.isPublic ? [{ icon: 'ti ti-share', @@ -133,6 +136,8 @@ const headerActions = $computed(() => clip && isOwned ? [{ await os.apiWithDialog('clips/delete', { clipId: clip.id, }); + + clipsCache.delete(); }, }] : null); diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index 4c23985f3baff9ede11d306e8454cf3e66fd7e84..aad914d6bbff5c13e0c8000b9b2ea10d1c6e2110 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -28,6 +28,7 @@ import MkClipPreview from '@/components/MkClipPreview.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { clipsCache } from '@/cache'; const pagination = { endpoint: 'clips/list' as const, @@ -65,6 +66,8 @@ async function create() { os.apiWithDialog('clips/create', result); + clipsCache.delete(); + pagingComponent.reload(); } diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 8a96b54881bd4831006e2dd13627f5f042934263..11a2aca8c504e9fc8f256191de8e904778aea38e 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -24,6 +24,7 @@ import MkAvatars from '@/components/MkAvatars.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { userListsCache } from '@/cache'; const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>(); @@ -38,6 +39,7 @@ async function create() { }); if (canceled) return; await os.apiWithDialog('users/lists/create', { name: name }); + userListsCache.delete(); pagingComponent.reload(); } diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 205434971dccbd6a4f3438d2c3c885c0395f2741..768a48746c95c3dd965f7d9472a5a6c347da5e24 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -37,6 +37,7 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; import { userPage } from '@/filters/user'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import { userListsCache } from '@/cache'; const props = defineProps<{ listId: string; @@ -97,6 +98,8 @@ async function renameList() { name: name, }); + userListsCache.delete(); + list.name = name; } @@ -107,10 +110,10 @@ async function deleteList() { }); if (canceled) return; - await os.api('users/lists/delete', { + await os.apiWithDialog('users/lists/delete', { listId: list.id, }); - os.success(); + userListsCache.delete(); mainRouter.push('/my/lists'); } diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue new file mode 100644 index 0000000000000000000000000000000000000000..8178343bbb425dd282b463442a1bbd12608e8152 --- /dev/null +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -0,0 +1,156 @@ +<template> +<div class="_gaps"> + <MkSelect v-model="sortModeSelect"> + <template #label>{{ i18n.ts.sort }}</template> + <option v-for="x in sortOptions" :key="x.value" :value="x.value">{{ x.displayName }}</option> + </MkSelect> + <div v-if="!fetching"> + <MkPagination v-slot="{items}" :pagination="pagination"> + <div class="_gaps"> + <div + v-for="file in items" :key="file.id" + class="_button" + @click="$event => onClick($event, file)" + @contextmenu.stop="$event => onContextMenu($event, file)" + > + <div :class="$style.file"> + <div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div> + <MkDriveFileThumbnail :class="$style.fileThumbnail" :file="file" fit="contain"/> + <div :class="$style.fileBody"> + <div style="margin-bottom: 4px;"> + {{ file.name }} + </div> + <div> + <span style="margin-right: 1em;">{{ file.type }}</span> + <span>{{ bytes(file.size) }}</span> + </div> + <div> + <span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> + </div> + <div v-if="sortModeSelect === 'sizeDesc'"> + <div :class="$style.meter"><div :class="$style.meterValue" :style="genUsageBar(file.size)"></div></div> + </div> + </div> + </div> + </div> + </div> + </MkPagination> + </div> + <div v-else> + <MkLoading/> + </div> +</div> +</template> + +<script setup lang="ts"> +import { computed, ref, watch } from 'vue'; +import tinycolor from 'tinycolor2'; +import * as os from '@/os'; +import MkPagination from '@/components/MkPagination.vue'; +import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; +import { i18n } from '@/i18n'; +import bytes from '@/filters/bytes'; +import { dateString } from '@/filters/date'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import MkSelect from '@/components/MkSelect.vue'; +import { getDriveFileMenu } from '@/scripts/get-drive-file-menu'; + +let sortMode = ref('+size'); +const pagination = { + endpoint: 'drive/files' as const, + limit: 10, + params: computed(() => ({ sort: sortMode.value })), +}; + +const sortOptions = [ + { value: 'sizeDesc', displayName: i18n.ts._drivecleaner.orderBySizeDesc }, + { value: 'createdAtAsc', displayName: i18n.ts._drivecleaner.orderByCreatedAtAsc }, +]; + +const capacity = ref<number>(0); +const usage = ref<number>(0); +const fetching = ref(true); +const sortModeSelect = ref('sizeDesc'); + +fetchDriveInfo(); + +watch(sortModeSelect, () => { + switch (sortModeSelect.value) { + case 'sizeDesc': + sortMode.value = '+size'; + fetchDriveInfo(); + break; + + case 'createdAtAsc': + sortMode.value = '-createdAt'; + fetchDriveInfo(); + break; + } +}); + +function fetchDriveInfo(): void { + fetching.value = true; + os.api('drive').then(info => { + capacity.value = info.capacity; + usage.value = info.usage; + fetching.value = false; + }); +} + +function genUsageBar(fsize: number): object { + return { + width: `${fsize / usage.value * 100}%`, + background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }), + }; +} + +function onClick(ev: MouseEvent, file) { + os.popupMenu(getDriveFileMenu(file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); +} + +function onContextMenu(ev: MouseEvent, file): void { + os.contextMenu(getDriveFileMenu(file), ev); +} + +definePageMetadata({ + title: i18n.ts.drivecleaner, + icon: 'ti ti-trash', +}); +</script> + +<style lang="scss" module> +.file { + display: flex; + width: 100%; + box-sizing: border-box; + text-align: left; + align-items: center; + + &:hover { + color: var(--accent); + } +} + +.fileThumbnail { + width: 100px; + height: 100px; +} + +.fileBody { + margin-left: 0.3em; + padding: 8px; + flex: 1; +} + +.meter { + margin-top: 8px; + height: 12px; + background: rgba(0, 0, 0, 0.1); + overflow: clip; + border-radius: 999px; +} + +.meterValue { + height: 100%; +} +</style> diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index a23bdfe69ec316449fbd5966c4f62e5d20afd74e..d3fb422e01d41bc5c8039b7f46eac4f9a818a8cd 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -32,6 +32,9 @@ <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffixIcon><i class="ti ti-folder"></i></template> </FormLink> + <FormLink to="/settings/drive/cleaner"> + {{ i18n.ts.drivecleaner }} + </FormLink> <MkSwitch v-model="keepOriginalUploading"> <template #label>{{ i18n.ts.keepOriginalUploading }}</template> <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 2e2c456c07603044d44ee15f707958f5e61dbed5..dd62a325303b3b6f6b8c1dc266a615520d403742 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -47,6 +47,7 @@ <div class="_gaps_m"> <div class="_gaps_s"> <MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch> + <MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch> <MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch> <MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch> @@ -143,6 +144,7 @@ async function reloadAsk() { const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind')); const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior')); const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover')); +const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index f1a450e18e644fa3cecd141acd3aa87e85c21b08..ae36466eec4a39e0d72747df75de8b505bfab456 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -7,7 +7,7 @@ <div v-if="!narrow || currentPage?.route.name == null" class="nav"> <div class="baaadecd"> <MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu> + <MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu> </div> </div> <div v-if="!(narrow && currentPage?.route.name == null)" class="main"> @@ -230,6 +230,12 @@ onUnmounted(() => { ro.disconnect(); }); +watch(router.currentRef, (to) => { + if (to.route.name === "settings" && to.child?.route.name == null && !narrow) { + router.replace('/settings/profile'); + } +}); + const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); provideMetadataReceiver((info) => { diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index ead551e7c4fd1a1eb4981de20f1f0973ec629ee2..b3b33b8026d1667a90f7f0f0a32dc428751b2114 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -1,9 +1,34 @@ <template> <div class="_gaps_m"> - <MkTextarea v-model="items" tall manual-save> + <FormSlot> <template #label>{{ i18n.ts.navbar }}</template> - <template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template> - </MkTextarea> + <MkContainer :show-header="false"> + <Sortable + v-model="items" + item-key="id" + :animation="150" + :handle="'.' + $style.itemHandle" + @start="e => e.item.classList.add('active')" + @end="e => e.item.classList.remove('active')" + > + <template #item="{element,index}"> + <div + v-if="element.type === '-' || navbarItemDef[element.type]" + :class="$style.item" + > + <button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button> + <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span> + <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button> + </div> + </template> + </Sortable> + </MkContainer> + </FormSlot> + <div class="_buttons"> + <MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton> + <MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + </div> <MkRadios v-model="menuDisplay"> <template #label>{{ i18n.ts.display }}</template> @@ -12,26 +37,30 @@ <option value="top">{{ i18n.ts._menuDisplay.top }}</option> <!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドãƒãƒ¼ã‚’完全ã«éš ã›ã‚‹ã‚ˆã†ã«ã™ã‚‹ã¨ã€åˆ¥é€”ãƒãƒ³ãƒãƒ¼ã‚¬ãƒ¼ãƒœã‚¿ãƒ³ã®ã‚ˆã†ãªã‚‚ã®ã‚’UIã«è¡¨ç¤ºã™ã‚‹å¿…è¦ãŒã‚ã‚Šé¢å€’ --> </MkRadios> - - <MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> </div> </template> <script lang="ts" setup> -import { computed, ref, watch } from 'vue'; -import MkTextarea from '@/components/MkTextarea.vue'; +import { computed, defineAsyncComponent, ref, watch } from 'vue'; import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; +import FormSlot from '@/components/form/slot.vue'; +import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os'; import { navbarItemDef } from '@/navbar'; import { defaultStore } from '@/store'; import { unisonReload } from '@/scripts/unison-reload'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { deepClone } from '@/scripts/clone'; + +const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); -const items = ref(defaultStore.state.menu.join('\n')); +const items = ref(defaultStore.state.menu.map(x => ({ + id: Math.random().toString(), + type: x, +}))); -const split = computed(() => items.value.trim().split('\n').filter(x => x.trim() !== '')); const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); async function reloadAsk() { @@ -55,23 +84,28 @@ async function addItem() { }], }); if (canceled) return; - items.value = [...split.value, item].join('\n'); + items.value = [...items.value, { + id: Math.random().toString(), + type: item, + }]; +} + +function removeItem(index: number) { + items.value.splice(index, 1); } async function save() { - defaultStore.set('menu', split.value); + defaultStore.set('menu', items.value.map(x => x.type)); await reloadAsk(); } function reset() { - defaultStore.reset('menu'); - items.value = defaultStore.state.menu.join('\n'); + items.value = defaultStore.def.menu.default.map(x => ({ + id: Math.random().toString(), + type: x, + })); } -watch(items, async () => { - await save(); -}); - watch(menuDisplay, async () => { await reloadAsk(); }); @@ -85,3 +119,44 @@ definePageMetadata({ icon: 'ti ti-list', }); </script> + +<style lang="scss" module> +.item { + position: relative; + display: block; + line-height: 2.85rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: var(--navFg); +} + +.itemIcon { + position: relative; + width: 32px; + margin-right: 8px; +} + +.itemText { + position: relative; + font-size: 0.9em; +} + +.itemRemove { + position: absolute; + z-index: 10000; + width: 32px; + height: 32px; + color: #ff2a2a; + right: 8px; + opacity: 0.8; +} + +.itemHandle { + cursor: move; + width: 32px; + height: 32px; + margin: 0 8px; + opacity: 0.5; +} +</style> diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index a01e3f8cee4b74a1f26c0f94e3833509b5608398..3c782973aec6e185d373d336eb4a2301afd322fa 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -1,7 +1,7 @@ <template> <div class="_gaps_m"> <MkInput v-model="name"> - <template #label>Name</template> + <template #label>{{ i18n.ts._webhookSettings.name }}</template> </MkInput> <MkInput v-model="url" type="url"> @@ -10,24 +10,24 @@ <MkInput v-model="secret"> <template #prefix><i class="ti ti-lock"></i></template> - <template #label>Secret</template> + <template #label>{{ i18n.ts._webhookSettings.secret }}</template> </MkInput> <FormSection> - <template #label>Events</template> + <template #label>{{ i18n.ts._webhookSettings.events }}</template> <div class="_gaps_s"> - <MkSwitch v-model="event_follow">Follow</MkSwitch> - <MkSwitch v-model="event_followed">Followed</MkSwitch> - <MkSwitch v-model="event_note">Note</MkSwitch> - <MkSwitch v-model="event_reply">Reply</MkSwitch> - <MkSwitch v-model="event_renote">Renote</MkSwitch> - <MkSwitch v-model="event_reaction">Reaction</MkSwitch> - <MkSwitch v-model="event_mention">Mention</MkSwitch> + <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch> + <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch> + <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> + <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> + <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> + <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> + <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> </div> </FormSection> - <MkSwitch v-model="active">Active</MkSwitch> + <MkSwitch v-model="active">{{ i18n.ts._webhookSettings.active }}</MkSwitch> <div class="_buttons"> <MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue index 45ab5722c3c1c9e950bd9a753860cc8ea89d617c..6eb8a654f55da4d6387c585cf5fae0c0d6e8447f 100644 --- a/packages/frontend/src/pages/settings/webhook.new.vue +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -1,7 +1,7 @@ <template> <div class="_gaps_m"> <MkInput v-model="name"> - <template #label>Name</template> + <template #label>{{ i18n.ts._webhookSettings.name }}</template> </MkInput> <MkInput v-model="url" type="url"> @@ -10,20 +10,20 @@ <MkInput v-model="secret"> <template #prefix><i class="ti ti-lock"></i></template> - <template #label>Secret</template> + <template #label>{{ i18n.ts._webhookSettings.secret }}</template> </MkInput> <FormSection> - <template #label>Events</template> + <template #label>{{ i18n.ts._webhookSettings.events }}</template> <div class="_gaps_s"> - <MkSwitch v-model="event_follow">Follow</MkSwitch> - <MkSwitch v-model="event_followed">Followed</MkSwitch> - <MkSwitch v-model="event_note">Note</MkSwitch> - <MkSwitch v-model="event_reply">Reply</MkSwitch> - <MkSwitch v-model="event_renote">Renote</MkSwitch> - <MkSwitch v-model="event_reaction">Reaction</MkSwitch> - <MkSwitch v-model="event_mention">Mention</MkSwitch> + <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch> + <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch> + <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> + <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> + <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> + <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> + <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> </div> </FormSection> diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue index e10f65b0afbe09ad8f9b3013ad057659d6ae246f..bc729ab871528c5901a08831460b71aaa08e6ec6 100644 --- a/packages/frontend/src/pages/settings/webhook.vue +++ b/packages/frontend/src/pages/settings/webhook.vue @@ -1,7 +1,7 @@ <template> <div class="_gaps_m"> <FormLink :to="`/settings/webhook/new`"> - Create webhook + {{ i18n.ts._webhookSettings.createWebhook }} </FormLink> <FormSection> @@ -31,6 +31,7 @@ import MkPagination from '@/components/MkPagination.vue'; import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { i18n } from '@/i18n'; const pagination = { endpoint: 'i/webhooks/list' as const, diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 590c5765fd508095764b25339d9437c4fcc9b844..c8077edd288ca0cb7ee9c03fde5e1624107e7309 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -65,6 +65,10 @@ export const routes = [{ path: '/drive', name: 'drive', component: page(() => import('./pages/settings/drive.vue')), + }, { + path: '/drive/cleaner', + name: 'drive', + component: page(() => import('./pages/settings/drive-cleaner.vue')), }, { path: '/notifications', name: 'notifications', diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 6b8041d78e38ba71277df0686a8080f326dfe79e..2ca1b164ae14316fd51f57d1b157e8a17d6874ac 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -471,7 +471,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R components.push(component); const instance = values.OBJ(new Map([ ['id', values.STR(_id)], - ['update', values.FN_NATIVE(async ([def], opts) => { + ['update', values.FN_NATIVE(([def], opts) => { utils.assertObject(def); const updates = getOptions(def, call); for (const update of def.value.keys()) { @@ -491,13 +491,13 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R return { 'Ui:root': rootInstance, - 'Ui:patch': values.FN_NATIVE(async ([id, val], opts) => { + 'Ui:patch': values.FN_NATIVE(([id, val], opts) => { utils.assertString(id); utils.assertArray(val); patch(id.value, val.value, opts.call); }), - 'Ui:get': values.FN_NATIVE(async ([id], opts) => { + 'Ui:get': values.FN_NATIVE(([id], opts) => { utils.assertString(id); const instance = instances[id.value]; if (instance) { @@ -508,7 +508,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R }), // Ui:root.update({ children: [...] }) ã®ç³–衣構文 - 'Ui:render': values.FN_NATIVE(async ([children], opts) => { + 'Ui:render': values.FN_NATIVE(([children], opts) => { utils.assertArray(children); rootComponent.value.children = children.value.map(v => { @@ -517,51 +517,51 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R }); }), - 'Ui:C:container': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:container': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('container', def, id, getContainerOptions, opts.call); }), - 'Ui:C:text': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:text': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('text', def, id, getTextOptions, opts.call); }), - 'Ui:C:mfm': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:mfm': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('mfm', def, id, getMfmOptions, opts.call); }), - 'Ui:C:textarea': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:textarea': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('textarea', def, id, getTextareaOptions, opts.call); }), - 'Ui:C:textInput': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:textInput': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('textInput', def, id, getTextInputOptions, opts.call); }), - 'Ui:C:numberInput': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:numberInput': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.call); }), - 'Ui:C:button': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:button': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('button', def, id, getButtonOptions, opts.call); }), - 'Ui:C:buttons': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:buttons': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('buttons', def, id, getButtonsOptions, opts.call); }), - 'Ui:C:switch': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:switch': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('switch', def, id, getSwitchOptions, opts.call); }), - 'Ui:C:select': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:select': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('select', def, id, getSelectOptions, opts.call); }), - 'Ui:C:folder': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:folder': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('folder', def, id, getFolderOptions, opts.call); }), - 'Ui:C:postFormButton': values.FN_NATIVE(async ([def, id], opts) => { + 'Ui:C:postFormButton': values.FN_NATIVE(([def, id], opts) => { return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.call); }), }; diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts new file mode 100644 index 0000000000000000000000000000000000000000..858e5f03bfe5a439c0e709b5ee51c09001770b7b --- /dev/null +++ b/packages/frontend/src/scripts/cache.ts @@ -0,0 +1,80 @@ + +export class Cache<T> { + private cachedAt: number | null = null; + private value: T | undefined; + private lifetime: number; + + constructor(lifetime: Cache<never>['lifetime']) { + this.lifetime = lifetime; + } + + public set(value: T): void { + this.cachedAt = Date.now(); + this.value = value; + } + + public get(): T | undefined { + if (this.cachedAt == null) return undefined; + if ((Date.now() - this.cachedAt) > this.lifetime) { + this.value = undefined; + this.cachedAt = null; + return undefined; + } + return this.value; + } + + public delete() { + this.value = undefined; + this.cachedAt = null; + } + + /** + * ã‚ャッシュãŒã‚ã‚Œã°ãれを返ã—ã€ç„¡ã‘ã‚Œã°fetcherを呼ã³å‡ºã—ã¦çµæžœã‚’ã‚ャッシュ&è¿”ã—ã¾ã™ + * optional: ã‚ャッシュãŒå˜åœ¨ã—ã¦ã‚‚validatorã§falseã‚’è¿”ã™ã¨ã‚ャッシュ無効扱ã„ã«ã—ã¾ã™ + */ + public async fetch(fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> { + const cachedValue = this.get(); + if (cachedValue !== undefined) { + if (validator) { + if (validator(cachedValue)) { + // Cache HIT + return cachedValue; + } + } else { + // Cache HIT + return cachedValue; + } + } + + // Cache MISS + const value = await fetcher(); + this.set(value); + return value; + } + + /** + * ã‚ャッシュãŒã‚ã‚Œã°ãれを返ã—ã€ç„¡ã‘ã‚Œã°fetcherを呼ã³å‡ºã—ã¦çµæžœã‚’ã‚ャッシュ&è¿”ã—ã¾ã™ + * optional: ã‚ャッシュãŒå˜åœ¨ã—ã¦ã‚‚validatorã§falseã‚’è¿”ã™ã¨ã‚ャッシュ無効扱ã„ã«ã—ã¾ã™ + */ + public async fetchMaybe(fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> { + const cachedValue = this.get(); + if (cachedValue !== undefined) { + if (validator) { + if (validator(cachedValue)) { + // Cache HIT + return cachedValue; + } + } else { + // Cache HIT + return cachedValue; + } + } + + // Cache MISS + const value = await fetcher(); + if (value !== undefined) { + this.set(value); + } + return value; + } +} diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts new file mode 100644 index 0000000000000000000000000000000000000000..52e610e437cb74f6c3ac18ccf6ac1de17021575a --- /dev/null +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -0,0 +1,93 @@ +import * as Misskey from 'misskey-js'; +import { defineAsyncComponent } from 'vue'; +import { i18n } from '@/i18n'; +import copyToClipboard from '@/scripts/copy-to-clipboard'; +import * as os from '@/os'; + +function rename(file: Misskey.entities.DriveFile) { + os.inputText({ + title: i18n.ts.renameFile, + placeholder: i18n.ts.inputNewFileName, + default: file.name, + }).then(({ canceled, result: name }) => { + if (canceled) return; + os.api('drive/files/update', { + fileId: file.id, + name: name, + }); + }); +} + +function describe(file: Misskey.entities.DriveFile) { + os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + default: file.comment != null ? file.comment : '', + file: file, + }, { + done: caption => { + os.api('drive/files/update', { + fileId: file.id, + comment: caption.length === 0 ? null : caption, + }); + }, + }, 'closed'); +} + +function toggleSensitive(file: Misskey.entities.DriveFile) { + os.api('drive/files/update', { + fileId: file.id, + isSensitive: !file.isSensitive, + }); +} + +function copyUrl(file: Misskey.entities.DriveFile) { + copyToClipboard(file.url); + os.success(); +} +/* +function addApp() { + alert('not implemented yet'); +} +*/ +async function deleteFile(file: Misskey.entities.DriveFile) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('driveFileDeleteConfirm', { name: file.name }), + }); + + if (canceled) return; + os.api('drive/files/delete', { + fileId: file.id, + }); +} + +export function getDriveFileMenu(file: Misskey.entities.DriveFile) { + return [{ + text: i18n.ts.rename, + icon: 'ti ti-forms', + action: () => rename(file), + }, { + text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, + icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off', + action: () => toggleSensitive(file), + }, { + text: i18n.ts.describeFile, + icon: 'ti ti-text-caption', + action: () => describe(file), + }, null, { + text: i18n.ts.copyUrl, + icon: 'ti ti-link', + action: () => copyUrl(file), + }, { + type: 'a', + href: file.url, + target: '_blank', + text: i18n.ts.download, + icon: 'ti ti-download', + download: file.name, + }, null, { + text: i18n.ts.delete, + icon: 'ti ti-trash', + danger: true, + action: () => deleteFile(file), + }]; +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 9c0ff3d1b2ac42f45968e12c739bcf124828f45e..00f2523bf9ef94e225aeb19d9636bc30533868de 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -10,6 +10,81 @@ import { url } from '@/config'; import { noteActions } from '@/store'; import { miLocalStorage } from '@/local-storage'; import { getUserMenu } from '@/scripts/get-user-menu'; +import { clipsCache } from '@/cache'; + +export async function getNoteClipMenu(props: { + note: misskey.entities.Note; + isDeleted: Ref<boolean>; + currentClipPage?: Ref<misskey.entities.Clip>; +}) { + const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null + ); + + const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note; + + const clips = await clipsCache.fetch(() => os.api('clips/list')); + return [...clips.map(clip => ({ + text: clip.name, + action: () => { + claimAchievement('noteClipped1'); + os.promiseDialog( + os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), + null, + async (err) => { + if (err.id === '734806c4-542c-463a-9311-15c512803965') { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), + }); + if (!confirm.canceled) { + os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); + if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true; + } + } else { + os.alert({ + type: 'error', + text: err.message + '\n' + err.id, + }); + } + }, + ); + }, + })), null, { + icon: 'ti ti-plus', + text: i18n.ts.createNew, + action: async () => { + const { canceled, result } = await os.form(i18n.ts.createNewClip, { + name: { + type: 'string', + label: i18n.ts.name, + }, + description: { + type: 'string', + required: false, + multiline: true, + label: i18n.ts.description, + }, + isPublic: { + type: 'boolean', + label: i18n.ts.public, + default: false, + }, + }); + if (canceled) return; + + const clip = await os.apiWithDialog('clips/create', result); + + clipsCache.delete(); + + claimAchievement('noteClipped1'); + os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); + }, + }]; +} export function getNoteMenu(props: { note: misskey.entities.Note; @@ -208,64 +283,7 @@ export function getNoteMenu(props: { type: 'parent', icon: 'ti ti-paperclip', text: i18n.ts.clip, - children: async () => { - const clips = await os.api('clips/list'); - return [{ - icon: 'ti ti-plus', - text: i18n.ts.createNew, - action: async () => { - const { canceled, result } = await os.form(i18n.ts.createNewClip, { - name: { - type: 'string', - label: i18n.ts.name, - }, - description: { - type: 'string', - required: false, - multiline: true, - label: i18n.ts.description, - }, - isPublic: { - type: 'boolean', - label: i18n.ts.public, - default: false, - }, - }); - if (canceled) return; - - const clip = await os.apiWithDialog('clips/create', result); - - claimAchievement('noteClipped1'); - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); - }, - }, null, ...clips.map(clip => ({ - text: clip.name, - action: () => { - claimAchievement('noteClipped1'); - os.promiseDialog( - os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), - null, - async (err) => { - if (err.id === '734806c4-542c-463a-9311-15c512803965') { - const confirm = await os.confirm({ - type: 'warning', - text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), - }); - if (!confirm.canceled) { - os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); - if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true; - } - } else { - os.alert({ - type: 'error', - text: err.message + '\n' + err.id, - }); - } - }, - ); - }, - }))]; - }, + children: () => getNoteClipMenu(props), }, statePromise.then(state => state.isMutedThread ? { icon: 'ti ti-message-off', diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index d7eb331183a69bebe672dcdbeb3c0cf85a094f29..fe941c77b27fa1fb9735420894a892f1545b4f46 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -8,6 +8,7 @@ import { userActions } from '@/store'; import { $i, iAmModerator } from '@/account'; import { mainRouter } from '@/router'; import { Router } from '@/nirax'; +import { rolesCache, userListsCache } from '@/cache'; export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) { const meId = $i ? $i.id : null; @@ -126,7 +127,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router icon: 'ti ti-list', text: i18n.ts.addToList, children: async () => { - const lists = await os.api('users/lists/list'); + const lists = await userListsCache.fetch(() => os.api('users/lists/list')); return lists.map(list => ({ text: list.name, @@ -147,7 +148,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router icon: 'ti ti-badges', text: i18n.ts.roles, children: async () => { - const roles = await os.api('admin/roles/list'); + const roles = await rolesCache.fetch(() => os.api('admin/roles/list')); return roles.filter(r => r.target === 'manual').map(r => ({ text: r.name, diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 3d87234f417ae41811f55f488b0b022a0be595f8..c3cf48afc4cbf9d5b51cd79dba87ee633025e0c8 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -290,6 +290,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + showClipButtonInNoteFooter: { + where: 'device', + default: false, + }, aiChanMode: { where: 'device', default: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2397b154ec671f65169c7e5038c49ee8ea0429f..4cc42f17882085b2b1ecd3ae4e5c491066aa2098 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: packages/backend: specifiers: + '@aws-sdk/client-s3': ^3.294.0 + '@aws-sdk/lib-storage': ^3.294.0 + '@aws-sdk/node-http-handler': ^3.292.0 '@bull-board/api': 5.0.0 '@bull-board/fastify': 5.0.0 '@bull-board/ui': 5.0.0 @@ -127,7 +130,7 @@ importers: ajv: 8.12.0 archiver: 5.3.1 autwh: 0.1.0 - aws-sdk: 2.1318.0 + aws-sdk-client-mock: ^2.1.1 bcryptjs: 2.4.3 blurhash: 2.0.5 bull: 4.10.4 @@ -219,6 +222,9 @@ importers: ws: 8.12.1 xev: 3.0.2 dependencies: + '@aws-sdk/client-s3': 3.294.0 + '@aws-sdk/lib-storage': 3.294.0_@aws-sdk+client-s3@3.294.0 + '@aws-sdk/node-http-handler': 3.292.0 '@bull-board/api': 5.0.0 '@bull-board/fastify': 5.0.0 '@bull-board/ui': 5.0.0 @@ -241,7 +247,6 @@ importers: ajv: 8.12.0 archiver: 5.3.1 autwh: 0.1.0 - aws-sdk: 2.1318.0 bcryptjs: 2.4.3 blurhash: 2.0.5 bull: 4.10.4 @@ -385,6 +390,7 @@ importers: '@types/ws': 8.5.4 '@typescript-eslint/eslint-plugin': 5.54.1_mlk7dnz565t663n4razh6a6v6i '@typescript-eslint/parser': 5.54.1_ycpbpc6yetojsgtrx3mwntkhsu + aws-sdk-client-mock: 2.1.1 cross-env: 7.0.3 eslint: 8.35.0 eslint-plugin-import: 2.27.5_uyiasnnzcqrxqkfvjklwnmwcha @@ -645,6 +651,987 @@ packages: '@jridgewell/trace-mapping': 0.3.17 dev: true + /@aws-crypto/crc32/3.0.0: + resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.292.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/crc32c/3.0.0: + resolution: {integrity: sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.292.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/ie11-detection/3.0.0: + resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha1-browser/3.0.0: + resolution: {integrity: sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-locate-window': 3.208.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-browser/3.0.0: + resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-locate-window': 3.208.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-js/3.0.0: + resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.292.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/supports-web-crypto/3.0.0: + resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/util/3.0.0: + resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} + dependencies: + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-sdk/abort-controller/3.292.0: + resolution: {integrity: sha512-lf+OPptL01kvryIJy7+dvFux5KbJ6OTwLPPEekVKZ2AfEvwcVtOZWFUhyw3PJCBTVncjKB1Kjl3V/eTS3YuPXQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/chunked-blob-reader-native/3.292.0: + resolution: {integrity: sha512-A34sBrnggm9mXPZeeEie4jDv9zHRMS0LSm85VkfrBLuYYsfsw9DxmW59wJkuo6DIm/RK04oH5+lRMt34koBgrw==} + dependencies: + '@aws-sdk/util-base64': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/chunked-blob-reader/3.292.0: + resolution: {integrity: sha512-ccFPnzBjLbDCmFjTXwhsfD58vtEiAjbor3A9tvnou+3Dj6RrMEGPaTu5tcw3mwWb2zh1K3HFJg6Bmb0no49TRw==} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/client-s3/3.294.0: + resolution: {integrity: sha512-J0rTBpZlmeNWgpYaGM7w55Hdmh8LWfYFmb09Fr0Oee/VGFgi28p3vCCnP+ploo1TlFRdsPlGZJ7zod+m/iPeBg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha1-browser': 3.0.0 + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.294.0 + '@aws-sdk/config-resolver': 3.292.0 + '@aws-sdk/credential-provider-node': 3.294.0 + '@aws-sdk/eventstream-serde-browser': 3.292.0 + '@aws-sdk/eventstream-serde-config-resolver': 3.292.0 + '@aws-sdk/eventstream-serde-node': 3.292.0 + '@aws-sdk/fetch-http-handler': 3.292.0 + '@aws-sdk/hash-blob-browser': 3.292.0 + '@aws-sdk/hash-node': 3.292.0 + '@aws-sdk/hash-stream-node': 3.292.0 + '@aws-sdk/invalid-dependency': 3.292.0 + '@aws-sdk/md5-js': 3.292.0 + '@aws-sdk/middleware-bucket-endpoint': 3.292.0 + '@aws-sdk/middleware-content-length': 3.292.0 + '@aws-sdk/middleware-endpoint': 3.292.0 + '@aws-sdk/middleware-expect-continue': 3.292.0 + '@aws-sdk/middleware-flexible-checksums': 3.292.0 + '@aws-sdk/middleware-host-header': 3.292.0 + '@aws-sdk/middleware-location-constraint': 3.292.0 + '@aws-sdk/middleware-logger': 3.292.0 + '@aws-sdk/middleware-recursion-detection': 3.292.0 + '@aws-sdk/middleware-retry': 3.293.0 + '@aws-sdk/middleware-sdk-s3': 3.292.0 + '@aws-sdk/middleware-serde': 3.292.0 + '@aws-sdk/middleware-signing': 3.292.0 + '@aws-sdk/middleware-ssec': 3.292.0 + '@aws-sdk/middleware-stack': 3.292.0 + '@aws-sdk/middleware-user-agent': 3.293.0 + '@aws-sdk/node-config-provider': 3.292.0 + '@aws-sdk/node-http-handler': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/signature-v4-multi-region': 3.292.0 + '@aws-sdk/smithy-client': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/url-parser': 3.292.0 + '@aws-sdk/util-base64': 3.292.0 + '@aws-sdk/util-body-length-browser': 3.292.0 + '@aws-sdk/util-body-length-node': 3.292.0 + '@aws-sdk/util-defaults-mode-browser': 3.292.0 + '@aws-sdk/util-defaults-mode-node': 3.292.0 + '@aws-sdk/util-endpoints': 3.293.0 + '@aws-sdk/util-retry': 3.292.0 + '@aws-sdk/util-stream-browser': 3.292.0 + '@aws-sdk/util-stream-node': 3.292.0 + '@aws-sdk/util-user-agent-browser': 3.292.0 + '@aws-sdk/util-user-agent-node': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + '@aws-sdk/util-waiter': 3.292.0 + '@aws-sdk/xml-builder': 3.292.0 + fast-xml-parser: 4.1.2 + tslib: 2.5.0 + transitivePeerDependencies: + - '@aws-sdk/signature-v4-crt' + - aws-crt + dev: false + + /@aws-sdk/client-sso-oidc/3.294.0: + resolution: {integrity: sha512-/ZfDud76MdSPJ/TxjV2xLE30XbBQDZwKQ32axwoK1eziPvrAIUBYVgpBwj+m0quhoiQhBKkg3aFl6j39AF2thw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.292.0 + '@aws-sdk/fetch-http-handler': 3.292.0 + '@aws-sdk/hash-node': 3.292.0 + '@aws-sdk/invalid-dependency': 3.292.0 + '@aws-sdk/middleware-content-length': 3.292.0 + '@aws-sdk/middleware-endpoint': 3.292.0 + '@aws-sdk/middleware-host-header': 3.292.0 + '@aws-sdk/middleware-logger': 3.292.0 + '@aws-sdk/middleware-recursion-detection': 3.292.0 + '@aws-sdk/middleware-retry': 3.293.0 + '@aws-sdk/middleware-serde': 3.292.0 + '@aws-sdk/middleware-stack': 3.292.0 + '@aws-sdk/middleware-user-agent': 3.293.0 + '@aws-sdk/node-config-provider': 3.292.0 + '@aws-sdk/node-http-handler': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/smithy-client': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/url-parser': 3.292.0 + '@aws-sdk/util-base64': 3.292.0 + '@aws-sdk/util-body-length-browser': 3.292.0 + '@aws-sdk/util-body-length-node': 3.292.0 + '@aws-sdk/util-defaults-mode-browser': 3.292.0 + '@aws-sdk/util-defaults-mode-node': 3.292.0 + '@aws-sdk/util-endpoints': 3.293.0 + '@aws-sdk/util-retry': 3.292.0 + '@aws-sdk/util-user-agent-browser': 3.292.0 + '@aws-sdk/util-user-agent-node': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso/3.294.0: + resolution: {integrity: sha512-+FuxQTi5WvnaXM5JbNLkBIzQ3An4gA0ox61N1u+3xled+nywKb1yQ7WmRpyMG5bLbkmnj3aqoo5/uskFc4c4EA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.292.0 + '@aws-sdk/fetch-http-handler': 3.292.0 + '@aws-sdk/hash-node': 3.292.0 + '@aws-sdk/invalid-dependency': 3.292.0 + '@aws-sdk/middleware-content-length': 3.292.0 + '@aws-sdk/middleware-endpoint': 3.292.0 + '@aws-sdk/middleware-host-header': 3.292.0 + '@aws-sdk/middleware-logger': 3.292.0 + '@aws-sdk/middleware-recursion-detection': 3.292.0 + '@aws-sdk/middleware-retry': 3.293.0 + '@aws-sdk/middleware-serde': 3.292.0 + '@aws-sdk/middleware-stack': 3.292.0 + '@aws-sdk/middleware-user-agent': 3.293.0 + '@aws-sdk/node-config-provider': 3.292.0 + '@aws-sdk/node-http-handler': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/smithy-client': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/url-parser': 3.292.0 + '@aws-sdk/util-base64': 3.292.0 + '@aws-sdk/util-body-length-browser': 3.292.0 + '@aws-sdk/util-body-length-node': 3.292.0 + '@aws-sdk/util-defaults-mode-browser': 3.292.0 + '@aws-sdk/util-defaults-mode-node': 3.292.0 + '@aws-sdk/util-endpoints': 3.293.0 + '@aws-sdk/util-retry': 3.292.0 + '@aws-sdk/util-user-agent-browser': 3.292.0 + '@aws-sdk/util-user-agent-node': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts/3.294.0: + resolution: {integrity: sha512-AefqwhFjTDzelZuSYhriJbiI+GQwf2yKiKAnCt0gRj6rswewStM63Gtlhfb01sFPp+ZiqPcyQ47LqUaHp1mz/g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.292.0 + '@aws-sdk/credential-provider-node': 3.294.0 + '@aws-sdk/fetch-http-handler': 3.292.0 + '@aws-sdk/hash-node': 3.292.0 + '@aws-sdk/invalid-dependency': 3.292.0 + '@aws-sdk/middleware-content-length': 3.292.0 + '@aws-sdk/middleware-endpoint': 3.292.0 + '@aws-sdk/middleware-host-header': 3.292.0 + '@aws-sdk/middleware-logger': 3.292.0 + '@aws-sdk/middleware-recursion-detection': 3.292.0 + '@aws-sdk/middleware-retry': 3.293.0 + '@aws-sdk/middleware-sdk-sts': 3.292.0 + '@aws-sdk/middleware-serde': 3.292.0 + '@aws-sdk/middleware-signing': 3.292.0 + '@aws-sdk/middleware-stack': 3.292.0 + '@aws-sdk/middleware-user-agent': 3.293.0 + '@aws-sdk/node-config-provider': 3.292.0 + '@aws-sdk/node-http-handler': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/smithy-client': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/url-parser': 3.292.0 + '@aws-sdk/util-base64': 3.292.0 + '@aws-sdk/util-body-length-browser': 3.292.0 + '@aws-sdk/util-body-length-node': 3.292.0 + '@aws-sdk/util-defaults-mode-browser': 3.292.0 + '@aws-sdk/util-defaults-mode-node': 3.292.0 + '@aws-sdk/util-endpoints': 3.293.0 + '@aws-sdk/util-retry': 3.292.0 + '@aws-sdk/util-user-agent-browser': 3.292.0 + '@aws-sdk/util-user-agent-node': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + fast-xml-parser: 4.1.2 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/config-resolver/3.292.0: + resolution: {integrity: sha512-cB3twnNR7vYvlt2jvw8VlA1+iv/tVzl+/S39MKqw2tepU+AbJAM0EHwb/dkf1OKSmlrnANXhshx80MHF9zL4mA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/signature-v4': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-config-provider': 3.292.0 + '@aws-sdk/util-middleware': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/credential-provider-env/3.292.0: + resolution: {integrity: sha512-YbafSG0ZEKE2969CJWVtUhh3hfOeLPecFVoXOtegCyAJgY5Ghtu4TsVhL4DgiGAgOC30ojAmUVQEXzd7xJF5xA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/credential-provider-imds/3.292.0: + resolution: {integrity: sha512-W/peOgDSRYulgzFpUhvgi1pCm6piBz6xrVN17N4QOy+3NHBXRVMVzYk6ct2qpLPgJUSEZkcpP+Gds+bBm8ed1A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/node-config-provider': 3.292.0 + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/url-parser': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/credential-provider-ini/3.294.0: + resolution: {integrity: sha512-pdTPbaAb5bWA+DnuKoL2TpXeNDp6Ejpv/OYt+bw2gdzl9w5r/ZCtUTTbW+Vvejr4WL5s3c1bY96kwdqCn7iLqA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.292.0 + '@aws-sdk/credential-provider-imds': 3.292.0 + '@aws-sdk/credential-provider-process': 3.292.0 + '@aws-sdk/credential-provider-sso': 3.294.0 + '@aws-sdk/credential-provider-web-identity': 3.292.0 + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/shared-ini-file-loader': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node/3.294.0: + resolution: {integrity: sha512-zUL1Qhb4BsQIZCs/TPpG4oIYH/9YsGiS+Se1tasSGjTOLfBy7jhOZ0QIdpEeyAx/EP8blOBredM9xWfEXgiHVA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.292.0 + '@aws-sdk/credential-provider-imds': 3.292.0 + '@aws-sdk/credential-provider-ini': 3.294.0 + '@aws-sdk/credential-provider-process': 3.292.0 + '@aws-sdk/credential-provider-sso': 3.294.0 + '@aws-sdk/credential-provider-web-identity': 3.292.0 + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/shared-ini-file-loader': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-process/3.292.0: + resolution: {integrity: sha512-CFVXuMuUvg/a4tknzRikEDwZBnKlHs1LZCpTXIGjBdUTdosoi4WNzDLzGp93ZRTtcgFz+4wirz2f7P3lC0NrQw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/shared-ini-file-loader': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/credential-provider-sso/3.294.0: + resolution: {integrity: sha512-UxrcAA/0l7j9+3tolYcG5M61D/IE1Bjd/9H87H1i2A2BrwUUBhW1Dp/vvROEDrrywlMDG3CDF3T/7ADtTak+sg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.294.0 + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/shared-ini-file-loader': 3.292.0 + '@aws-sdk/token-providers': 3.294.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-web-identity/3.292.0: + resolution: {integrity: sha512-4DbtIEM9gGVfqYlMdYXg3XY+vBhemjB1zXIequottW8loLYM8Vuz4/uGxxKNze6evVVzowsA0wKrYclE1aj/Rg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-codec/3.292.0: + resolution: {integrity: sha512-P0np4vhCKf/JH6I39Id8DxZR+UZzG+Br+vOrTinerMfOhzTa2229XmL8pwlMpOoxnJLMPmEDtD1KQqLslBEXtw==} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-hex-encoding': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-serde-browser/3.292.0: + resolution: {integrity: sha512-VzRbJqqE444GOuoNTxTJ1dC1IhNhA6jfHjgsI8iDRHraaEukGqsPx1vkc+byxrDEjgxKN5IqOwZ4yJWMIAozBA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-serde-universal': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-serde-config-resolver/3.292.0: + resolution: {integrity: sha512-Ndx+qJyWmBCW9FSm68AGLoO4AZ0AaL/wjpJEgFF2sZBWjYe9O9PB9IGR/yuqCBTElf3YtSiFMsloikQaz2ft6g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-serde-node/3.292.0: + resolution: {integrity: sha512-NFCEiNCetNye7jQfRd5y/7J9dLg9+uL57698wYeXeadlwJ8Cd/Nhsz+t7RIbP05VqshU+anXARMB1avl9oAijQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-serde-universal': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-serde-universal/3.292.0: + resolution: {integrity: sha512-1gqZNx+S1EUpl3Tq6uIesiDx8gnkpXqPsFfCZT7lSWWXBpnHmnUZAh3jbiO9UlQbYuB9SfT0EBKb1iOY9z4j1Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-codec': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/fetch-http-handler/3.292.0: + resolution: {integrity: sha512-zh3bhUJbL8RSa39ZKDcy+AghtUkIP8LwcNlwRIoxMQh3Row4D1s4fCq0KZCx98NJBEXoiTLyTQlZxxI//BOb1Q==} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/querystring-builder': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-base64': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/hash-blob-browser/3.292.0: + resolution: {integrity: sha512-4+Fm4IOkxGqgx8dU0EbExCq6xx30y369ZSXz89h9YDQYdJ2Muw7iNCHAg/4VM+gfp0vo9J8zPOTsSju8LNS5Jg==} + dependencies: + '@aws-sdk/chunked-blob-reader': 3.292.0 + '@aws-sdk/chunked-blob-reader-native': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/hash-node/3.292.0: + resolution: {integrity: sha512-1yLxmIsvE+eK36JXEgEIouTITdykQLVhsA5Oai//Lar6Ddgu1sFpLDbdkMtKbrh4I0jLN9RacNCkeVQjZPTCCQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-buffer-from': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/hash-stream-node/3.292.0: + resolution: {integrity: sha512-p2nj9A5lZKQU45Q4Od3iZDvpziEpojAyuyAI0HPzpIuJIfzFQ0/7pMBKde1li6wq93rpyFLwNufV6FEZnKCYRg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/invalid-dependency/3.292.0: + resolution: {integrity: sha512-39OUV78CD3TmEbjhpt+V+Fk4wAGWhixqHxDSN8+4WL0uB4Fl7k5m3Z9hNY78AttHQSl2twR7WtLztnXPAFsriw==} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/is-array-buffer/3.292.0: + resolution: {integrity: sha512-kW/G5T/fzI0sJH5foZG6XJiNCevXqKLxV50qIT4B1pMuw7regd4ALIy0HwSqj1nnn9mSbRWBfmby0jWCJsMcwg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/lib-storage/3.294.0_@aws-sdk+client-s3@3.294.0: + resolution: {integrity: sha512-5H/1EgGDIt8Ls/YOepfkyyBwkyQ9d668/gmnWGWRvytar+cVMHu/D5G88831luPrlzyZ+jR+Te7Nc2oqYqamTw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@aws-sdk/abort-controller': ^3.0.0 + '@aws-sdk/client-s3': ^3.0.0 + dependencies: + '@aws-sdk/client-s3': 3.294.0 + '@aws-sdk/middleware-endpoint': 3.292.0 + '@aws-sdk/smithy-client': 3.292.0 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/md5-js/3.292.0: + resolution: {integrity: sha512-ngfsKLgQenXW3EbsDf47PVNys1SecTbsq6k88h7+Aa8BU49+9ZOIz4VDpWuPiNyYpeV7jJdl1dfD+ujOYvvgNw==} + dependencies: + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-bucket-endpoint/3.292.0: + resolution: {integrity: sha512-XRy9RSUIRcbxYfH504ywhQllgfdf3wVhk2k0mMPYnUbeEhAFe1/eUog2v/bi07/q5TQ4Hppi+W3nHCVualQEow==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-arn-parser': 3.292.0 + '@aws-sdk/util-config-provider': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-content-length/3.292.0: + resolution: {integrity: sha512-2gMWzQus5mj14menolpPDbYBeaOYcj7KNFZOjTjjI3iQ0KqyetG6XasirNrcJ/8QX1BRmpTol8Xjp2Ue3Gbzwg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-endpoint/3.292.0: + resolution: {integrity: sha512-cPMkiSxpZGG6tYlW4OS+ucS6r43f9ddX9kcUoemJCY10MOuogdPjulCAjE0HTs2PLKSOrrG4CTP4Q4wWDrH4Bw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-serde': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/signature-v4': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/url-parser': 3.292.0 + '@aws-sdk/util-config-provider': 3.292.0 + '@aws-sdk/util-middleware': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-expect-continue/3.292.0: + resolution: {integrity: sha512-bZ2bsBud3E6BebZWGxVcWxBSg09bP0KyX8PT0jI66JM0yTbZSJhoGhlKAqfNG46R9h4K5tCYB2uYgV/3oU/ZpQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-flexible-checksums/3.292.0: + resolution: {integrity: sha512-AxU/Gb+TRdl/0jHmbreYh3QnB0jR25zgjPZ4/JbGBJ2SQI9jm3LCNK9XOrPUmZp/vu9wsvyxtmKQidpQ5+FX5w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@aws-crypto/crc32c': 3.0.0 + '@aws-sdk/is-array-buffer': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-host-header/3.292.0: + resolution: {integrity: sha512-mHuCWe3Yg2S5YZ7mB7sKU6C97XspfqrimWjMW9pfV2usAvLA3R0HrB03jpR5vpZ3P4q7HB6wK3S6CjYMGGRNag==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-location-constraint/3.292.0: + resolution: {integrity: sha512-WTbMyoCckdkmq7Yok0gI4226gTmxP/zM1fbFiC+liZXBJ+H5EvIFmu30tWbX+4m41LL/XQVm65olXJFwhoExGQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-logger/3.292.0: + resolution: {integrity: sha512-yZNY1XYmG3NG+uonET7jzKXNiwu61xm/ZZ6i/l51SusuaYN+qQtTAhOFsieQqTehF9kP4FzbsWgPDwD8ZZX9lw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-recursion-detection/3.292.0: + resolution: {integrity: sha512-kA3VZpPko0Zqd7CYPTKAxhjEv0HJqFu2054L04dde1JLr43ro+2MTdX7vsHzeAFUVRphqatFFofCumvXmU6Mig==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-retry/3.293.0: + resolution: {integrity: sha512-7tiaz2GzRecNHaZ6YnF+Nrtk3au8qF6oiipf11R7MJiqJ0fkMLnz/iRrlakDziS9qF/a9v+3yxb4W4NHK3f4Tw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/service-error-classification': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-middleware': 3.292.0 + '@aws-sdk/util-retry': 3.292.0 + tslib: 2.5.0 + uuid: 8.3.2 + dev: false + + /@aws-sdk/middleware-sdk-s3/3.292.0: + resolution: {integrity: sha512-kEUmh3ZM34H+2bEQfpZhVotJCNYpSbq9Q4YxlWVbnjiO/VS+S9BFEM3Fcj5+EzEgI02tNNi6/qTXj3iS8tT6hA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-arn-parser': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-sdk-sts/3.292.0: + resolution: {integrity: sha512-GN5ZHEqXZqDi+HkVbaXRX9HaW/vA5rikYpWKYsmxTUZ7fB7ijvEO3co3lleJv2C+iGYRtUIHC4wYNB5xgoTCxg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.292.0 + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/signature-v4': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-serde/3.292.0: + resolution: {integrity: sha512-6hN9mTQwSvV8EcGvtXbS/MpK7WMCokUku5Wu7X24UwCNMVkoRHLIkYcxHcvBTwttuOU0d8hph1/lIX4dkLwkQw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-signing/3.292.0: + resolution: {integrity: sha512-GVfoSjDjEQ4TaO6x9MffyP3uRV+2KcS5FtexLCYOM9pJcnE9tqq9FJOrZ1xl1g+YjUVKxo4x8lu3tpEtIb17qg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/signature-v4': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-middleware': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-ssec/3.292.0: + resolution: {integrity: sha512-VfwrTEs9nYU6sCnt/cffhnJ2djGkMyMbBEysMZm2HEbFMloGKBd0Wtvk9y+SWPa6+DDRe2CqqX8jMzrO4JT4Eg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-stack/3.292.0: + resolution: {integrity: sha512-WdQpRkuMysrEwrkByCM1qCn2PPpFGGQ2iXqaFha5RzCdZDlxJni9cVNb6HzWUcgjLEYVTXCmOR9Wxm3CNW44Qg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-user-agent/3.293.0: + resolution: {integrity: sha512-gZ7/e6XwpKk9mvgA78q4Ffc796jTn02TUKx2qMDnkLVbeJXBNN2jnvYEKq8v70+o7fd/ALRudg8gBDmkkhM/Hw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-endpoints': 3.293.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/node-config-provider/3.292.0: + resolution: {integrity: sha512-S3NnC9dQ5GIbJYSDIldZb4zdpCOEua1tM7bjYL3VS5uqCEM93kIi/o/UkIUveMp/eqTS2LJa5HjNIz5Te6je0A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/shared-ini-file-loader': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/node-http-handler/3.292.0: + resolution: {integrity: sha512-L/E3UDSwXLXjt1XWWh0RBD55F+aZI1AEdPwdES9i1PjnZLyuxuDhEDptVibNN56+I9/4Q3SbmuVRVlOD0uzBag==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.292.0 + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/querystring-builder': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/property-provider/3.292.0: + resolution: {integrity: sha512-dHArSvsiqhno/g55N815gXmAMrmN8DP7OeFNqJ4wJG42xsF2PFN3DAsjIuHuXMwu+7A3R1LHqIpvv0hA9KeoJQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/protocol-http/3.292.0: + resolution: {integrity: sha512-NLi4fq3k41aXIh1I97yX0JTy+3p6aW1NdwFwdMa674z86QNfb4SfRQRZBQe9wEnAZ/eWHVnlKIuII+U1URk/Kg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/querystring-builder/3.292.0: + resolution: {integrity: sha512-XElIFJaReIm24eEvBtV2dOtZvcm3gXsGu/ftG8MLJKbKXFKpAP1q+K6En0Bs7/T88voKghKdKpKT+eZUWgTqlg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-uri-escape': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/querystring-parser/3.292.0: + resolution: {integrity: sha512-iTYpYo7a8X9RxiPbjjewIpm6XQPx2EOcF1dWCPRII9EFlmZ4bwnX+PDI36fIo9oVs8TIKXmwNGODU9nsg7CSAw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/service-error-classification/3.292.0: + resolution: {integrity: sha512-X1k3sixCeC45XSNHBe+kRBQBwPDyTFtFITb8O5Qw4dS9XWGhrUJT4CX0qE5aj8qP3F9U5nRizs9c2mBVVP0Caw==} + engines: {node: '>=14.0.0'} + dev: false + + /@aws-sdk/shared-ini-file-loader/3.292.0: + resolution: {integrity: sha512-Av2TTYg1Jig2kbkD56ybiqZJB6vVrYjv1W5UQwY/q3nA/T2mcrgQ20ByCOt5Bv9VvY7FSgC+znj+L4a7RLGmBg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/signature-v4-multi-region/3.292.0: + resolution: {integrity: sha512-MjWEIjbAr7n9vsFeLpoRzNSYFgWOROf1mLj6Db8TfRowaortUBO7PbleLV4n3SPujSnxhaVBzlmnCY2AjatH9g==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@aws-sdk/signature-v4-crt': ^3.118.0 + peerDependenciesMeta: + '@aws-sdk/signature-v4-crt': + optional: true + dependencies: + '@aws-sdk/protocol-http': 3.292.0 + '@aws-sdk/signature-v4': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-arn-parser': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/signature-v4/3.292.0: + resolution: {integrity: sha512-+rw47VY5mvBecn13tDQTl1ipGWg5tE63faWgmZe68HoBL87ZiDzsd7bUKOvjfW21iMgWlwAppkaNNQayYRb2zg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-hex-encoding': 3.292.0 + '@aws-sdk/util-middleware': 3.292.0 + '@aws-sdk/util-uri-escape': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/smithy-client/3.292.0: + resolution: {integrity: sha512-S8PKzjPkZ6SXYZuZiU787dMsvQ0d/LFEhw2OI4Oe2An9Fc2IwJ2FYukyHoQJOV2tV0DiuMebPo7eMyQyjKElvA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-stack': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/token-providers/3.294.0: + resolution: {integrity: sha512-6nwO04LtC5f4AsUvGZXyjaswuEK4Rr2VsuANpMKrPCgunRfI58a8YXLniudOSXN6e7CFJ6M3uo/h5YXqtnzGug==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso-oidc': 3.294.0 + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/shared-ini-file-loader': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/types/3.292.0: + resolution: {integrity: sha512-1teYAY2M73UXZxMAxqZxVS2qwXjQh0OWtt7qyLfha0TtIk/fZ1hRwFgxbDCHUFcdNBSOSbKH/ESor90KROXLCQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/url-parser/3.292.0: + resolution: {integrity: sha512-NZeAuZCk1x6TIiWuRfbOU6wHPBhf0ly2qOHzWut4BCH+b4RrDmFF8EmXcH1auEfGhE7yRyR6XqIN0t3S+hYACA==} + dependencies: + '@aws-sdk/querystring-parser': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-arn-parser/3.292.0: + resolution: {integrity: sha512-xfE4U94TfjMC2WNNDte/kDByf16GrQKaS0BKsm+Fk/PaeHUofEp8suOEz/EVdEoa3Ayy2Uc5QdhrGnlqf8MxeA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-base64/3.292.0: + resolution: {integrity: sha512-zjNCwNdy617yFvEjZorepNWXB2sQCVfsShCwFy/kIQ5iW5tT2jQKaqc0K77diU9atkooxw9p1W9m9sOgrkOFNw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-body-length-browser/3.292.0: + resolution: {integrity: sha512-Wd/BM+JsMiKvKs/bN3z6TredVEHh2pKudGfg3CSjTRpqFpOG903KDfyHBD42yg5PuCHoHoewJvTPKwgn7/vhaw==} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-body-length-node/3.292.0: + resolution: {integrity: sha512-BBgipZ2P6RhogWE/qj0oqpdlyd3iSBYmb+aD/TBXwB2lA/X8A99GxweBd/kp06AmcJRoMS9WIXgbWkiiBlRlSA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-buffer-from/3.292.0: + resolution: {integrity: sha512-RxNZjLoXNxHconH9TYsk5RaEBjSgTtozHeyIdacaHPj5vlQKi4hgL2hIfKeeNiAfQEVjaUFF29lv81xpNMzVMQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-config-provider/3.292.0: + resolution: {integrity: sha512-t3noYll6bPRSxeeNNEkC5czVjAiTPcsq00OwfJ2xyUqmquhLEfLwoJKmrT1uP7DjIEXdUtfoIQ2jWiIVm/oO5A==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-defaults-mode-browser/3.292.0: + resolution: {integrity: sha512-7+zVUlMGfa8/KT++9humHo6IDxTnxMCmWUj5jVNlkpk6h7Ecmppf7aXotviyVIA43lhtz0p2AErs0N0ekEUK+w==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/types': 3.292.0 + bowser: 2.11.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-defaults-mode-node/3.292.0: + resolution: {integrity: sha512-SSIw85eF4BVs0fOJRyshT+R3b/UmBPhiVKCUZm2rq6+lIGkDPiSwQU3d/80AhXtiL5SFT/IzAKKgQd8qMa7q3A==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/config-resolver': 3.292.0 + '@aws-sdk/credential-provider-imds': 3.292.0 + '@aws-sdk/node-config-provider': 3.292.0 + '@aws-sdk/property-provider': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-endpoints/3.293.0: + resolution: {integrity: sha512-R/99aNV49Refpv5guiUjEUrZYlvnfaNBniB+/ZtMO3ixxUopapssCrUivuJrmhccmrYaTCZw7dRzIWjU1jJhKg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-hex-encoding/3.292.0: + resolution: {integrity: sha512-qBd5KFIUywQ3qSSbj814S2srk0vfv8A6QMI+Obs1y2LHZFdQN5zViptI4UhXhKOHe+NnrHWxSuLC/LMH6q3SmA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-locate-window/3.208.0: + resolution: {integrity: sha512-iua1A2+P7JJEDHVgvXrRJSvsnzG7stYSGQnBVphIUlemwl6nN5D+QrgbjECtrbxRz8asYFHSzhdhECqN+tFiBg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-middleware/3.292.0: + resolution: {integrity: sha512-KjhS7flfoBKDxbiBZjLjMvEizXgjfQb7GQEItgzGoI9rfGCmZtvqCcqQQoIlxb8bIzGRggAUHtBGWnlLbpb+GQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-retry/3.292.0: + resolution: {integrity: sha512-JEHyF7MpVeRF5uR4LDYgpOKcFpOPiAj8TqN46SVOQQcL1K+V7cSr7O7N7J6MwJaN9XOzAcBadeIupMm7/BFbgw==} + engines: {node: '>= 14.0.0'} + dependencies: + '@aws-sdk/service-error-classification': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-stream-browser/3.292.0: + resolution: {integrity: sha512-yzwpjq18oefyp/Sv+Z0VWh7ziRPp+qM0pDUrTfuAnXg+mrlxaPDXJOhp5LoY8AVHcDPOEdIbzz0b00G48FabIg==} + dependencies: + '@aws-sdk/fetch-http-handler': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-base64': 3.292.0 + '@aws-sdk/util-hex-encoding': 3.292.0 + '@aws-sdk/util-utf8': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-stream-node/3.292.0: + resolution: {integrity: sha512-p3DHXvWo4Zdka75HwewUnWjpFp/gOT4SYYEOAsv3BwuZGxfmnojK9OVCkUBJ7s6LeHMKTgGqQPwAnVFu7iIZNg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/node-http-handler': 3.292.0 + '@aws-sdk/types': 3.292.0 + '@aws-sdk/util-buffer-from': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-uri-escape/3.292.0: + resolution: {integrity: sha512-hOQtUMQ4VcQ9iwKz50AoCp1XBD5gJ9nly/gJZccAM7zSA5mOO8RRKkbdonqquVHxrO0CnYgiFeCh3V35GFecUw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-user-agent-browser/3.292.0: + resolution: {integrity: sha512-dld+lpC3QdmTQHdBWJ0WFDkXDSrJgfz03q6mQ8+7H+BC12ZhT0I0g9iuvUjolqy7QR00OxOy47Y9FVhq8EC0Gg==} + dependencies: + '@aws-sdk/types': 3.292.0 + bowser: 2.11.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-user-agent-node/3.292.0: + resolution: {integrity: sha512-f+NfIMal5E61MDc5WGhUEoicr7b1eNNhA+GgVdSB/Hg5fYhEZvFK9RZizH5rrtsLjjgcr9nPYSR7/nDKCJLumw==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/node-config-provider': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-utf8-browser/3.259.0: + resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-utf8/3.292.0: + resolution: {integrity: sha512-FPkj+Z59/DQWvoVu2wFaRncc3KVwe/pgK3MfVb0Lx+Ibey5KUx+sNpJmYcVYHUAe/Nv/JeIpOtYuC96IXOnI6w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-waiter/3.292.0: + resolution: {integrity: sha512-+7j+mcWUY4GwU8nTK4MvLWpOzS34SJZL85qLxQ04pysoCSHkInyS51D1ejBVNlJdbUSFvIcU0WHU0y6MDDeJzg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.292.0 + '@aws-sdk/types': 3.292.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/xml-builder/3.292.0: + resolution: {integrity: sha512-0zgnhdwUy30q/1NPXi5ekdzHQqCs3ZJaUeGbvYMO54osi4K5hygAyTsyWtv6oaJggRqZrB0LAZ9xN6hG+sA8/g==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + /@babel/code-frame/7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} engines: {node: '>=6.9.0'} @@ -2317,6 +3304,12 @@ packages: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} + /@sinonjs/commons/1.8.6: + resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} + dependencies: + type-detect: 4.0.8 + dev: true + /@sinonjs/commons/2.0.0: resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} dependencies: @@ -2327,6 +3320,24 @@ packages: dependencies: '@sinonjs/commons': 2.0.0 + /@sinonjs/fake-timers/9.1.2: + resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} + dependencies: + '@sinonjs/commons': 1.8.6 + dev: true + + /@sinonjs/samsam/7.0.1: + resolution: {integrity: sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==} + dependencies: + '@sinonjs/commons': 2.0.0 + lodash.get: 4.4.2 + type-detect: 4.0.8 + dev: true + + /@sinonjs/text-encoding/0.7.2: + resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==} + dev: true + /@sqltools/formatter/1.2.5: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} dev: false @@ -3065,6 +4076,12 @@ packages: '@types/node': 18.15.0 dev: true + /@types/sinon/10.0.13: + resolution: {integrity: sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==} + dependencies: + '@types/sinonjs__fake-timers': 8.1.2 + dev: true + /@types/sinonjs__fake-timers/8.1.1: resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} dev: true @@ -4205,6 +5222,7 @@ packages: /available-typed-arrays/1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + dev: true /avvio/8.2.0: resolution: {integrity: sha512-bbCQdg7bpEv6kGH41RO/3B2/GMMmJSo2iBK+X8AWN9mujtfUipMDfIjsgHCfpnKqoGEQrrmCDKSa5OQ19+fDmg==} @@ -4216,21 +5234,13 @@ packages: - supports-color dev: false - /aws-sdk/2.1318.0: - resolution: {integrity: sha512-xRCKqx4XWXUIpjDCVHmdOSINEVCIC5+yhmgUGR9A6VfxfPs59HbxKyd5LB+CmXhVbwVUM4SRWG5O+agQj+w7Eg==} - engines: {node: '>= 10.0.0'} + /aws-sdk-client-mock/2.1.1: + resolution: {integrity: sha512-UuxXmICU4nmXTRm2BzLZdXmnyI+5NEBb5McRDkObasXVxXChvLm0Ci/PGENh4sCD+Es64SJiz70mtY48JROk0A==} dependencies: - buffer: 4.9.2 - events: 1.1.1 - ieee754: 1.1.13 - jmespath: 0.16.0 - querystring: 0.2.0 - sax: 1.2.1 - url: 0.10.3 - util: 0.12.5 - uuid: 8.0.0 - xml2js: 0.4.19 - dev: false + '@types/sinon': 10.0.13 + sinon: 14.0.2 + tslib: 2.5.0 + dev: true /aws-sign2/0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} @@ -4241,7 +5251,7 @@ packages: /axios/0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} dependencies: - follow-redirects: 1.15.2_debug@4.3.4 + follow-redirects: 1.15.2 transitivePeerDependencies: - debug dev: false @@ -4458,6 +5468,10 @@ packages: /boolbase/1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + /bowser/2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + dev: false + /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -4561,12 +5575,11 @@ packages: engines: {node: '>=4'} dev: false - /buffer/4.9.2: - resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + /buffer/5.6.0: + resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - isarray: 1.0.0 dev: false /buffer/5.7.1: @@ -6897,11 +7910,6 @@ packages: resolution: {integrity: sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg==} dev: false - /events/1.1.1: - resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} - engines: {node: '>=0.4.x'} - dev: false - /events/3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -7163,6 +8171,13 @@ packages: strnum: 1.0.5 dev: false + /fast-xml-parser/4.1.2: + resolution: {integrity: sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + /fastify-plugin/4.5.0: resolution: {integrity: sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==} dev: false @@ -7398,6 +8413,16 @@ packages: readable-stream: 2.3.7 dev: false + /follow-redirects/1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /follow-redirects/1.15.2_debug@4.3.4: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -7408,11 +8433,13 @@ packages: optional: true dependencies: debug: 4.3.4 + dev: true /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 + dev: true /for-in/1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} @@ -7861,6 +8888,7 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.0 + dev: true /got/11.8.5: resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==} @@ -8294,10 +9322,6 @@ packages: safari-14-idb-fix: 3.0.0 dev: false - /ieee754/1.1.13: - resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} - dev: false - /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -8510,6 +9534,7 @@ packages: dependencies: call-bind: 1.0.2 has-tostringtag: 1.0.0 + dev: true /is-array-buffer/3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} @@ -8553,6 +9578,7 @@ packages: /is-callable/1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} + dev: true /is-ci/3.0.1: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} @@ -8653,13 +9679,6 @@ packages: engines: {node: '>=6'} dev: true - /is-generator-function/1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: false - /is-glob/3.1.0: resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} engines: {node: '>=0.10.0'} @@ -8832,6 +9851,7 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 + dev: true /is-typedarray/1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -8881,7 +9901,6 @@ packages: /isarray/0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - dev: false /isarray/1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -9511,11 +10530,6 @@ packages: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} dev: true - /jmespath/0.16.0: - resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} - engines: {node: '>= 0.6.0'} - dev: false - /joi/17.7.0: resolution: {integrity: sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==} dependencies: @@ -9751,6 +10765,10 @@ packages: resolution: {integrity: sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==} dev: false + /just-extend/4.2.1: + resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==} + dev: true + /jwa/2.0.0: resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} dependencies: @@ -10587,6 +11605,16 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: false + /nise/5.1.4: + resolution: {integrity: sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 10.0.2 + '@sinonjs/text-encoding': 0.7.2 + just-extend: 4.2.1 + path-to-regexp: 1.8.0 + dev: true + /node-abi/3.31.0: resolution: {integrity: sha512-eSKV6s+APenqVh8ubJyiu/YhZgxQpGP66ntzUb3lY1xB9ukSRaGnx0AIxI+IM+1+IVYC1oWobgG5L3Lt9ARykQ==} engines: {node: '>=10'} @@ -11219,6 +12247,12 @@ packages: path-root-regex: 0.1.2 dev: false + /path-to-regexp/1.8.0: + resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} + dependencies: + isarray: 0.0.1 + dev: true + /path-to-regexp/3.2.0: resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} dev: false @@ -11985,10 +13019,6 @@ packages: pump: 2.0.1 dev: false - /punycode/1.3.2: - resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} - dev: false - /punycode/2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -12041,11 +13071,6 @@ packages: strict-uri-encode: 1.1.0 dev: false - /querystring/0.2.0: - resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} - engines: {node: '>=0.4.x'} - dev: false - /querystring/0.2.1: resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} engines: {node: '>=0.4.x'} @@ -12649,10 +13674,6 @@ packages: immutable: 4.2.2 source-map-js: 1.0.2 - /sax/1.2.1: - resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} - dev: false - /sax/1.2.4: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} dev: false @@ -12817,6 +13838,17 @@ packages: is-arrayish: 0.3.2 dev: false + /sinon/14.0.2: + resolution: {integrity: sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 9.1.2 + '@sinonjs/samsam': 7.0.1 + diff: 5.1.0 + nise: 5.1.4 + supports-color: 7.2.0 + dev: true + /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -13113,6 +14145,13 @@ packages: internal-slot: 1.0.5 dev: true + /stream-browserify/3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + /stream-combiner/0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} dependencies: @@ -13803,14 +14842,12 @@ packages: /tslib/1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true /tslib/2.4.1: resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} /tslib/2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - dev: false /tsutils/3.21.0_typescript@4.5.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -14175,13 +15212,6 @@ packages: resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==} dev: false - /url/0.10.3: - resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} - dependencies: - punycode: 1.3.2 - querystring: 0.2.0 - dev: false - /urlsafe-base64/1.0.0: resolution: {integrity: sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA==} dev: false @@ -14202,26 +15232,12 @@ packages: /util-deprecate/1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - /util/0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - dependencies: - inherits: 2.0.4 - is-arguments: 1.1.1 - is-generator-function: 1.0.10 - is-typed-array: 1.1.10 - which-typed-array: 1.1.9 - dev: false - /uuid/3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true dev: false - /uuid/8.0.0: - resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} - dev: false - /uuid/8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -14678,6 +15694,7 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 is-typed-array: 1.1.10 + dev: true /which/1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} @@ -14784,13 +15801,6 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - /xml2js/0.4.19: - resolution: {integrity: sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==} - dependencies: - sax: 1.2.4 - xmlbuilder: 9.0.7 - dev: false - /xml2js/0.4.23: resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} engines: {node: '>=4.0.0'} @@ -14804,11 +15814,6 @@ packages: engines: {node: '>=4.0'} dev: false - /xmlbuilder/9.0.7: - resolution: {integrity: sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==} - engines: {node: '>=4.0'} - dev: false - /xmlchars/2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: false