diff options
264 files changed, 15764 insertions, 4908 deletions
diff --git a/.config/docker_example.yml b/.config/docker_example.yml index ce2daf3aec..1e03e902bf 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -38,14 +38,14 @@ # Option 3: If neither of the above applies to you. # (In this case, the source code should be published # on the Misskey interface. IT IS NOT ENOUGH TO -# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY +# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY # E-MAIL OR OTHER MEANS. If you are not satisfied # with this, it is recommended that you read the # license again carefully. Anyway, enabling this # option will automatically generate and publish a # tarball at build time, protecting you from # inadvertent license violations. (There is no legal -# guarantee, of course.) The tarball will generated +# guarantee, of course.) The tarball will be generated # from the root directory of your codebase. So it is # also recommended to check <built/tarball> directory # once after building and before activating the server @@ -171,10 +171,28 @@ redis: # #prefix: example-prefix # #db: 1 -# ┌───────────────────────────┠-#───┘ MeiliSearch configuration └───────────────────────────── +# ┌───────────────────────────────┠+#───┘ Fulltext search configuration └───────────────────────────── -# You can set scope to local (default value) or global +# These are the setting items for the full-text search provider. +fulltextSearch: + # You can select the ID generation method. + # - sqlLike (default) + # Use SQL-like search. + # This is a standard feature of PostgreSQL, so no special extensions are required. + # - sqlPgroonga + # Use pgroonga. + # You need to install pgroonga and configure it as a PostgreSQL extension. + # In addition to the above, you need to create a pgroonga index on the text column of the note table. + # see: https://pgroonga.github.io/tutorial/ + # - meilisearch + # Use Meilisearch. + # You need to install Meilisearch and configure. + provider: sqlLike + +# For Meilisearch settings. +# If you select "meilisearch" for "fulltextSearch.provider", it must be set. +# You can set scope to local or global (default value) # (include notes from remote). #meilisearch: @@ -317,3 +335,13 @@ checkActivityPubGetSignature: false # Permission bits are specified as a base-8 string representing User/Group/Other permissions. # This setting is only useful for custom deployments, such as using a reverse proxy to serve media. #filePermissionBits: '644' + +# Log settings +# logging: +# sql: +# # Outputs query parameters during SQL execution to the log. +# # default: false +# enableQueryParamLogging: false +# # Disable query truncation. If set to true, the full text of the query will be output to the log. +# # default: false +# disableQueryTruncation: false diff --git a/.config/example.yml b/.config/example.yml index f781b72b91..6303e5ecf4 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -196,10 +196,28 @@ redis: # # You can specify more ioredis options... # #username: example-username -# ┌───────────────────────────┠-#───┘ MeiliSearch configuration └───────────────────────────── +# ┌───────────────────────────────┠+#───┘ Fulltext search configuration └───────────────────────────── -# You can set scope to local (default value) or global +# These are the setting items for the full-text search provider. +fulltextSearch: + # You can select the ID generation method. + # - sqlLike (default) + # Use SQL-like search. + # This is a standard feature of PostgreSQL, so no special extensions are required. + # - sqlPgroonga + # Use pgroonga. + # You need to install pgroonga and configure it as a PostgreSQL extension. + # In addition to the above, you need to create a pgroonga index on the text column of the note table. + # see: https://pgroonga.github.io/tutorial/ + # - meilisearch + # Use Meilisearch. + # You need to install Meilisearch and configure. + provider: sqlLike + +# For Meilisearch settings. +# If you select "meilisearch" for "fulltextSearch.provider", it must be set. +# You can set scope to local or global (default value) # (include notes from remote). #meilisearch: @@ -353,3 +371,13 @@ checkActivityPubGetSignature: false # Permission bits are specified as a base-8 string representing User/Group/Other permissions. # This setting is only useful for custom deployments, such as using a reverse proxy to serve media. #filePermissionBits: '644' + +# Log settings +# logging: +# sql: +# # Outputs query parameters during SQL execution to the log. +# # default: false +# enableQueryParamLogging: false +# # Disable query truncation. If set to true, the full text of the query will be output to the log. +# # default: false +# disableQueryTruncation: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dbc457710..f12e24209b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,96 @@ +## 2025.2.0 + +### General +- Fix: Docker ã®ãƒ“ルドã«å¤±æ•—ã™ã‚‹å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/883) + +### Client +- Fix: 一部環境ã§ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãªãƒ•ァイルをå«ã‚€ãƒŽãƒ¼ãƒˆã®éžè¡¨ç¤ºãŒåйã‹ãªã„å•題 +- Fix: データセーãƒãƒ¼æœ‰åŠ¹æ™‚ã«ã‚‚ユーザーページã®ã€Œãƒ•ァイルã€ã‚¿ãƒ–ã§ç”»åƒãŒèªã¿è¾¼ã¾ã‚Œã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ + +### Server +- Fix: 個別ãŠçŸ¥ã‚‰ã›ãƒšãƒ¼ã‚¸ã®metaã‚¿ã‚°å‡ºåŠ›ã®æ¡ä»¶ãŒé–“é•ã£ã¦ã„ãŸã®ã‚’ä¿®æ£ + + +## 2025.1.0 + +### Note +- [é‡è¦] ノート検索プãƒãƒã‚¤ãƒ€ã®è¿½åŠ ã«ä¼´ã„ã€configファイル(default.ymlãªã©ï¼‰ã®æ§‹æˆãŒå°‘ã—変ã‚りã¾ã™. + - æ–°ã—ã„è¨å®šé …ç›®"fulltextSearch.provider"ãŒè¿½åŠ ã•れã¾ã—ãŸ. sqlLike, sqlPgroonga, meilisearchã®ã„ãšã‚Œã‹ã‚’è¨å®šå‡ºæ¥ã¾ã™. + - ã™ã§ã«Meilisearchã‚’ãŠä½¿ã„ã®å ´åˆã€ **"fulltextSearch.provider"ã‚’"meilisearch"ã«è¨å®šã™ã‚‹å¿…è¦** ãŒã‚りã¾ã™. + - 詳細㯠#14730 ãŠã‚ˆã³ `.config/example.yml` ã¾ãŸã¯ `.config/docker_example.yml`ã®'Fulltext search configuration'ã‚’ã”å‚照願ã„ã¾ã™. +- ã€é–‹ç™ºè€…å‘ã‘】従æ¥ã®é–‹ç™ºãƒ¢ãƒ¼ãƒ‰ã§HMRãŒæ©Ÿèƒ½ã—ãªã„å•題ãŒä¿®æ£ã•れãŸãŸã‚ã€ãƒãƒƒã‚¯ã‚¨ãƒ³ãƒ‰ãƒ»ãƒ•ãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰åˆ†é›¢åž‹ã®é–‹ç™ºãƒ¢ãƒ¼ãƒ‰ãŒå‰Šé™¤ã•れã¾ã—ãŸã€‚開発環境ã«ãŠã„ã¦configã®å¤‰æ›´ãŒå¿…è¦ã¨ãªã‚‹å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚ + +### General +- Feat: カスタム絵文å—管ç†ç”»é¢ã‚’リニューアル #10996 + * β版ã¨ã—ã¦å…¬é–‹ã®ãŸã‚ã€æ—§ç”»é¢ã‚‚引ãç¶šã利用å¯èƒ½ã§ã™ + +### Client +- Enhance: PCç”»é¢ã§ãƒãƒ£ãƒ³ãƒãƒ«ãŒè¤‡æ•°åˆ—ã§è¡¨ç¤ºã•れるよã†ã« + (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13) +- Enhance: 照会ã«å¤±æ•—ã—ãŸå ´åˆã€ãã®ç†ç”±ã‚’表示ã™ã‚‹ã‚ˆã†ã« +- Enhance: ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆã§æ¤œçŸ¥ã•れãŸãƒ¯ãƒ¼ãƒ‰ã‚’表示ã§ãるよã†ã« +- Enhance: リモートã®ãƒŽãƒ¼ãƒˆã®ãƒªãƒ³ã‚¯ã‚’コピーã§ãるよã†ã« +- Enhance: 連åˆãŒãƒ›ãƒ¯ã‚¤ãƒˆãƒªã‚¹ãƒˆåŒ–・無効化ã•れã¦ã„るサーãƒãƒ¼å‘ã‘ã®ãƒ‡ã‚¶ã‚¤ãƒ³ä¿®æ£ +- Enhance: AiScriptã®ã‚»ãƒ¼ãƒ–データを明示的ã«å‰Šé™¤ã™ã‚‹é–¢æ•°`Mk:remove`ã‚’è¿½åŠ +- Enhance: ãƒŽãƒ¼ãƒˆã®æ·»ä»˜ãƒ•ァイルを一覧ã§é¡ã‚Œã‚‹ã€Œãƒ•ァイルã€ã‚¿ãƒ–ã‚’è¿½åŠ + (Based on https://github.com/Otaku-Social/maniakey/pull/14) +- Enhance: AiScriptã®æ‹¡å¼µAPI関数ã«ãŠã„ã¦å¼•æ•°ã®åž‹ãƒã‚§ãƒƒã‚¯ã‚’ã‚ˆã‚ŠåŽ³æ ¼ã« +- Enhance: クエリパラメータã§uiを一時的ã«å¤‰æ›´ã§ãるよã†ã« #15240 +- Enhance: リモート絵文å—ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆæ™‚ã«è©³ç´°ã‚’確èªã§ãるよã†ã« #15336 +- Fix: ç”»é¢ã‚µã‚¤ã‚ºãŒå¤‰ã‚ã£ãŸéš›ã«ãƒŠãƒ“ゲーションãƒãƒ¼ãŒè‡ªå‹•ã§æŠ˜ã‚ŠãŸãŸã¾ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: サーãƒãƒ¼æƒ…å ±ãƒ¡ãƒ‹ãƒ¥ãƒ¼ã«åŒºåˆ‡ã‚Šç·šãŒä¸è¶³ã—ã¦ã„ãŸã®ã‚’ä¿®æ£ +- Fix: ノートãŒãƒã‚°ã‚¤ãƒ³ã—ã¦ã„るユーザーã—ã‹è¦‹ã‚Œãªã„å ´åˆã«ãƒã‚°ã‚¤ãƒ³ãƒ€ã‚¤ã‚¢ãƒã‚°ã‚’é–‰ã˜ã‚‹ã¨ãã®å¾Œã®å‹•ç·šãŒãªããªã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: 公開範囲ãŒãƒ›ãƒ¼ãƒ ã®ãƒŽãƒ¼ãƒˆã®åŸ‹ã‚è¾¼ã¿ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆãŒèªã¿è¾¼ã¾ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803) +- Fix: 絵文å—管ç†ç”»é¢ã§ä¸€éƒ¨ã®çµµæ–‡å—ãŒè¡¨ç¤ºã•れãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: プラグイン `register_note_view_interruptor` ã§ãƒŽãƒ¼ãƒˆã®ã‚µãƒ¼ãƒãƒ¼æƒ…å ±ã®æ›¸ãæ›ãˆãŒã§ããªã„å•é¡Œã‚’ä¿®æ£ +- Fix: Botプãƒãƒ†ã‚¯ã‚·ãƒ§ãƒ³ã®è¨å®šå¤‰æ›´æ™‚ã¯å®Ÿéš›ã«æ¤œè¨¼ã‚’通éŽã—ãªã„ã¨ä¿å˜ã§ããªã„よã†ã«( #15137 ) +- Fix: ノート検索ãŒä½¿ç”¨ã§ããªã„å ´åˆã§ã‚‚ãƒãƒ£ãƒ³ãƒãƒ«ã®ãƒŽãƒ¼ãƒˆæ¤œç´¢æ¬„ãŒã§ã¦ã„ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: `Ui:C:select`ã§å€¤ã®å¤‰æ›´ãŒç”»é¢ã«åæ˜ ã•れãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: MiAuthèªå¯ç”»é¢ã§ã€èªå¯å‡¦ç†ã«å¤±æ•—ã—ãŸå ´åˆã§ã‚‚コールãƒãƒƒã‚¯URLã«é·ç§»ã—ã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) +- Fix: ノート作æˆç”»é¢ã§ãƒ•ã‚¡ã‚¤ãƒ«ã®æ·»ä»˜å¯èƒ½å€‹æ•°ã‚’è¶…ãˆã¦ã‚‚ãƒŽãƒ¼ãƒˆãƒœã‚¿ãƒ³ãŒæŠ¼ã›ã¦ã„ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: 「アカウントを管ç†ã€ç”»é¢ã§ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼æƒ…å ±ã®å–å¾—ã«å¤±æ•—ã—ãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆï¼ˆå‰Šé™¤ã•れãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆãªã©ï¼‰ãŒè¡¨ç¤ºã•れãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: MacOSã§Chrome系ブラウザを使用ã—ã¦ã„ã‚‹å ´åˆã«ã€Misskeyã‚’é–‰ã˜ãŸéš›ã«ä»–ã®ã‚¿ãƒ–ã®ã‚ªãƒ¼ãƒ‡ã‚£ã‚ªæ©Ÿèƒ½ã¨å¹²æ¸‰ã™ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: 言語データã®ã‚ャッシュ状æ³ã«ã‚ˆã£ã¦ã¯ã€åŸ‹ã‚è¾¼ã¿ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆãŒæ£ã—ãèµ·å‹•ã—ãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: 「削除ã—ã¦ç·¨é›†ã€ã§ãƒŽãƒ¼ãƒˆã®å¼•用を解除出æ¥ãªã‹ã£ãŸå•題を修æ£( #14476 ) +- Fix: RSSã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆãŒæ£ã—ã表示ã•れãªã„å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/857) +- Fix: ワードミュートã®ä¿å˜å¤±æ•—時ã«APIã‚¨ãƒ©ãƒ¼ãŒæ¡ã‚Šã¤ã¶ã•れる事ãŒã‚ã‚‹ã®ã‚’ä¿®æ£ +- Fix: アンケートã§ãƒªãƒ¢ãƒ¼ãƒˆã®çµµæ–‡å—ãŒæ£ã—ãæç”»ã§ããªã„å•題ã®ä¿®æ£ + (Cherry-picked from https://github.com/yojo-art/cherrypick/pull/153) +- Fix: éžãƒã‚°ã‚¤ãƒ³æ™‚ã®ã‚µãƒ¼ãƒãƒ¼æ¦‚è¦ç”»é¢ã®ãƒ¡ãƒ‹ãƒ¥ãƒ¼ãƒœã‚¿ãƒ³ãŒæŠ¼ã›ãªã„ã“ã¨ãŒã‚ã‚‹ã®ã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/656) +- Fix: URLã«ã¯ã˜ã‚ã‹ã‚‰`#pswp`ãŒå«ã¾ã‚Œã¦ã„ã‚‹å ´åˆã«ç”»åƒãƒ“ューワーãŒãƒ–ãƒ©ã‚¦ã‚¶ã®æˆ»ã‚‹ãƒœã‚¿ãƒ³ã§é–‰ã˜ã‚‰ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: ãƒãƒ¼ãƒ«ä½œæˆç”»é¢ã§è¨å®šã§ãã‚‹ã‚¢ã‚¤ã‚³ãƒ³ãƒ‡ã‚³ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã®æœ€å¤§å–付個数を16ã«åˆ¶é™ +- Fix: Firefox Nightlyãªã©ã§ã‚¢ã‚¤ã‚³ãƒ³ãŒèªã¿è¾¼ã‚ãªã„å•é¡Œã‚’ä¿®æ£ + +### Server +- Enhance: pg_bigmãŒåˆ©ç”¨ã§ãるよã†ã€ãƒŽãƒ¼ãƒˆã®æ¤œç´¢ã‚’ILIKE演算åã§ãªãLIKE演算åã§LOWER()ã‚’ã‹ã‘ãŸãƒ†ã‚ストã«å¯¾ã—ã¦è¡Œã†ã‚ˆã†ã« +- Enhance: ノート検索ã®é¸æŠžè‚¢ã¨ã—ã¦pgroongaã«å¯¾å¿œ ( #14730 ) +- Enhance: ãƒãƒ£ãƒ¼ãƒˆæ›´æ–°æ™‚ã«DBã«åŒæ™‚接続ã—ãªã„よã†ã« + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830) +- Enhance: config(default.yml)ã‹ã‚‰SQLãƒã‚°å…¨æ–‡ã‚’出力ã™ã‚‹ã‹å¦ã‹ã‚’è¨å®šå¯èƒ½ã« ( #15266 ) +- Fix: ユーザーã®ãƒ—ãƒãƒ•ィール画é¢ã‚’アドレス入力ãªã©ã§ç›´æŽ¥è¡¨ç¤ºã—ãŸéš›ã«æ¦‚è¦ã‚¿ãƒ–ã®æç”»ã«å¤±æ•—ã™ã‚‹å•題ã®ä¿®æ£( #15032 ) +- Fix: èµ·å‹•å‰ã®ç–Žé€šãƒã‚§ãƒƒã‚¯ãŒæ©Ÿèƒ½ã—ãªããªã£ã¦ã„ãŸå•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) +- Fix: ノートã®é–²è¦§ã«ãƒã‚°ã‚¤ãƒ³å¿…é ˆã«ã—ã¦ã‚‚Feedã§ãƒŽãƒ¼ãƒˆãŒè¡¨ç¤ºã•れã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ +- Fix: 絵文å—ã®é€£åˆã§ãƒ©ã‚¤ã‚»ãƒ³ã‚¹æ¬„を相互ã«ã‚„りå–りã™ã‚‹ã‚ˆã†ã« ( #10859, #14109 ) +- Fix: ãƒãƒƒã‚¯ãƒ€ã‚¦ãƒ³ã•ã‚ŒãŸæœŸé–“指定ã®ãƒŽãƒ¼ãƒˆãŒStreaming経由ã§LTLã«å‡ºç¾ã™ã‚‹ã®ã‚’ä¿®æ£ ( #15200 ) +- Fix: disableClusteringè¨å®šæ™‚ã®åˆæœŸåŒ–ãƒã‚¸ãƒƒã‚¯ã‚’調整( #15223 ) +- Fix: URLã¨URIãŒç•°ãªã‚‹ã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ã®ç…§ä¼šã«å¤±æ•—ã™ã‚‹å•題を修æ£( #15039 ) +- Fix: ActivityPubリクエストã‹ã©ã†ã‹ã®åˆ¤å®šãŒæ£ã—ããªã„å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869) +- Fix: `/api/pages/update`ã«ã¦`name`を指定ã›ãšã«ãƒªã‚¯ã‚¨ã‚¹ãƒˆã™ã‚‹ã¨ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã™ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: AIセンシティブ判定㌠arm64 環境ã§å‹•作ã—ãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: éžMisskeyç³»ã®ã‚½ãƒ•トウェアã‹ã‚‰HTML`<ruby>`ã‚¿ã‚°ã‚’å«ã‚€ãƒŽãƒ¼ãƒˆã‚’å—ä¿¡ã—ãŸå ´åˆã€MFMã®èªã¿ä»®å(ルビ)文法ã«å¤‰æ›ã—ã¦è¡¨ç¤º +- Fix: 連åˆOFFã§æŠ•ç¨¿ã•れãŸãƒŽãƒ¼ãƒˆã«å¯¾ã™ã‚‹å†—é•·ãªå‡¦ç†ã‚’æŠ‘æ¢ ( #15018 ) +- Fix: `/api.json`ã®ãƒ¬ã‚¹ãƒãƒ³ã‚¹ãŒ2回目ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆä»¥é™ãŠã‹ã—ããªã‚‹å•é¡Œã‚’ä¿®æ£ + +### Misskey.js +- Feat: allow setting `binaryType` of WebSocket connection + ## 2024.11.0 ### Note @@ -37,7 +130,7 @@ - Fix: デッã‚ã®ã‚¿ã‚¤ãƒ ラインカラムã§ã€Œã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãªãƒ•ァイルをå«ã‚€ãƒŽãƒ¼ãƒˆã‚’表示ã€è¨å®šãŒä½¿ç”¨ã§ããªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ - Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used - Fix: ãƒªãƒ³ã‚¯åˆ‡ã‚Œã‚’ä¿®æ£ -= Fix: ノート投稿ボタンã«ãƒ›ãƒãƒ¼æ™‚ã®ã‚¹ã‚¿ã‚¤ãƒ«ãŒé©ç”¨ã•れã¦ã„ãªã„ã®ã‚’ä¿®æ£ +- Fix: ノート投稿ボタンã«ãƒ›ãƒãƒ¼æ™‚ã®ã‚¹ã‚¿ã‚¤ãƒ«ãŒé©ç”¨ã•れã¦ã„ãªã„ã®ã‚’ä¿®æ£ (Cherry-picked from https://github.com/taiyme/misskey/pull/305) - Fix: メールアドレス登録有効化時ã®ã€Œå®Œäº†ã€ãƒ€ã‚¤ã‚¢ãƒã‚°ãƒœãƒƒã‚¯ã‚¹ã®è¡¨ç¤ºæ¡ä»¶ã‚’ä¿®æ£ - Fix: ç”»é¢å¹…ãŒç‹ã„環境ã§ãƒ‡ã‚¶ã‚¤ãƒ³ãŒå´©ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b01135d11..e7cd453f72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -204,25 +204,10 @@ pnpm dev command. - Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). -- Vite HMR (just the `vite` command) is available. The behavior may be different from production. - Service Worker is watched by esbuild. -- The front end can be viewed by accessing `http://localhost:5173`. -- The backend listens on the port configured with `port` in .config/default.yml. -If you have not changed it from the default, it will be "http://localhost:3000". -If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts. - -### `MK_DEV_PREFER=backend pnpm dev` -pnpm dev has another mode with `MK_DEV_PREFER=backend`. - -``` -MK_DEV_PREFER=backend pnpm dev -``` - -- This mode is closer to the production environment than the default mode. -- Vite runs behind the backend (the backend will proxy Vite at /vite). +- Vite HMR (just the `vite` command) is available. The behavior may be different from production. +- Vite runs behind the backend (the backend will proxy Vite at /vite and /embed_vite except for websocket used for HMR). - You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml). -- To change the port of Vite, specify with `VITE_PORT` environment variable. -- HMR may not work in some environments such as Windows. ## Testing @@ -499,6 +484,11 @@ describe('test', () => { コード上ã§Misskeyã®ãƒ‰ãƒ¡ã‚¤ãƒ³å›ºæœ‰ã®æ¦‚念ã«ã¯`Mi`ã‚’prefixã™ã‚‹ã“ã¨ã§ã€ä»–ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã®åŒæ§˜ã®æ¦‚念ã¨åŒºåˆ¥ã§ãã‚‹ã»ã‹ã€åå‰ã®è¡çªã‚’防ã。 ãŸã ã—ã€æ–‡è„ˆä¸ŠMisskeyã®ã‚‚ã®ã‚’指ã™ã“ã¨ãŒæ˜Žã‚‰ã‹ã§ã‚りã€åå‰ã®è¡çªã®æã‚ŒãŒãªã„å ´åˆã¯ã€ä¸€æ™‚çš„ãªãƒãƒ¼ã‚«ãƒ«å¤‰æ•°ã«é™ã£ã¦`Mi`ã‚’çœç•¥ã—ã¦ã‚‚よã„。 +### Misskey.jsã®åž‹ç”Ÿæˆ +```bash +pnpm build-misskey-js-with-types +``` + ### How to resolve conflictions occurred at pnpm-lock.yaml? Just execute `pnpm` to fix it. @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2024 syuilo and contributors +Copyright © 2014-2025 syuilo and contributors And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. diff --git a/Dockerfile b/Dockerfile index aff4074079..5c610c3caf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,15 @@ FROM node:${NODE_VERSION} as build RUN apk add git linux-headers build-base +ENV COREPACK_DEFAULT_TO_LATEST=0 + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean \ + ; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ + && apt-get update \ + && apt-get install -yqq --no-install-recommends \ + build-essential ENV PYTHONUNBUFFERED=1 ENV COREPACK_DEFAULT_TO_LATEST=0 RUN apk add --update python3 && ln -sf python3 /usr/bin/python diff --git a/SECURITY.md b/SECURITY.md index 8d3d44db41..499fa2c1fa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,6 +8,11 @@ bug report to the GitLab repository. Thanks for helping make Sharkey safe for everyone. +> [!note] +> CNA [requires](https://www.cve.org/ResourcesSupport/AllResources/CNARules#section_5-2_Description) that CVEs include a description in English for inclusion in the CVE Catalog. +> +> When creating a security advisory, all content must be written in English (it is acceptable to include a non-English description along with the English one). + ## When create a patch If you can also create a patch to fix the vulnerability, please create a PR on the private fork. diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 2f1b391b53..1f885c66a2 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1584,3 +1584,6 @@ _reversi: _offlineScreen: title: "غير متصل - يتعذر الاتصال بالخادم" header: "يتعذر الاتصال بالخادم" +_remoteLookupErrors: + _noSuchObject: + title: "غير موجود" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 6cd577b4a9..9c8566c5b7 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1348,3 +1348,6 @@ _moderationLogTypes: resetPassword: "পাসওয়ারà§à¦¡ রিসেট করà§à¦¨" _reversi: total: "মোট" +_remoteLookupErrors: + _noSuchObject: + title: "পাওয়া যায়নি" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 1aca3390e6..9954b2a747 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -5,6 +5,7 @@ introMisskey: "Benvingut! Misskey és un servei de microblogging descentralitzat poweredByMisskeyDescription: "{name} És un dels serveis (anomenats instà ncies de Misskey) que utilitzen la plataforma de codi obert <b>Misskey</b>." monthAndDay: "{day}/{month}" search: "Cercar" +reset: "Reiniciar" notifications: "Notificacions" username: "Nom d'usuari" password: "Contrasenya" @@ -43,11 +44,12 @@ favorites: "Favorits" unfavorite: "Eliminar dels preferits" favorited: "Afegit als preferits." alreadyFavorited: "Ja s'ha afegit als preferits." -cantFavorite: "No s'ha pogut afegir als preferits." -pin: "Fixar al perfil" +cantFavorite: "No es pot afegir als preferits." +pin: "Fixa al perfil" unpin: "Para de fixar del perfil" -copyContent: "Copiar el contingut" -copyLink: "Copiar l'enllaç" +copyContent: "Copia el contingut" +copyLink: "Copia l'enllaç" +copyRemoteLink: "Copiar l'enllaç remot" copyLinkRenote: "Copiar l'enllaç de la renota" delete: "Elimina" deleteAndEdit: "Eliminar i editar" @@ -103,9 +105,9 @@ enterListName: "Introdueix un nom per a la llista" privacy: "Privadesa" makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació" defaultNoteVisibility: "Visibilitat per defecte" -follow: "Seguint" +follow: "Segueix" followRequest: "Enviar sol·licitud de seguiment" -followRequests: "Sol·licituds de seguiment" +followRequests: "Peticions de seguiment" unfollow: "Deixar de seguir" followRequestPending: "Sol·licituds de seguiment pendents" enterEmoji: "Introduir un emoji" @@ -195,7 +197,7 @@ setWallpaper: "Defineix el fons de pantalla" removeWallpaper: "Elimina el fons de pantalla" searchWith: "Cerca: {q}" youHaveNoLists: "No tens cap llista" -followConfirm: "Està s segur que vols deixar de seguir {name}?" +followConfirm: "Segur que vols seguir a {name}?" proxyAccount: "Compte de proxy" proxyAccountDescription: "Un compte proxy és un compte que actua com a seguidor remot per als usuaris en determinades condicions. Per exemple, quan un usuari afegeix un usuari remot a la llista, l'activitat de l'usuari remot no es lliurarà al servidor si cap usuari local segueix aquest usuari, de manera que el compte proxy el seguirà ." host: "Amfitrió" @@ -222,7 +224,7 @@ version: "Versió" metadata: "Metadades" withNFiles: "{n} fitxer(s)" monitor: "Monitor" -jobQueue: "Cua de tasques" +jobQueue: "Cua de feines" cpuAndMemory: "CPU i memòria" network: "Xarxa" disk: "Disc" @@ -325,7 +327,7 @@ dark: "Fosc" lightThemes: "Temes clars" darkThemes: "Temes foscos" syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu" -drive: "Unitat" +drive: "Disc" fileName: "Nom del Fitxer" selectFile: "Selecciona un fitxer" selectFiles: "Selecciona fitxers" @@ -340,11 +342,11 @@ deleteFolder: "Elimina la carpeta" folder: "Carpeta " addFile: "Afegeix un fitxer" showFile: "Mostrar fitxer" -emptyDrive: "La teva unitat és buida" +emptyDrive: "El teu Disc és buit" emptyFolder: "La carpeta està buida" unableToDelete: "No es pot eliminar" inputNewFileName: "Introduïu el nom de fitxer nou" -inputNewDescription: "Inserta una nova llegenda" +inputNewDescription: "Escriu el peu de foto." inputNewFolderName: "Introduïu el nom de la carpeta nova" circularReferenceFolder: "La carpeta destinatà ria és una subcarpeta de la carpeta a la qual la desitges moure" hasChildFilesOrFolders: "No és possible esborrar aquesta carpeta ja que no és buida" @@ -537,7 +539,7 @@ mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única limitTo: "Limita a {x}" noFollowRequests: "No tens sol·licituds de seguiment" openImageInNewTab: "Obre imatges a una nova pestanya" -dashboard: "Panell de control" +dashboard: "Taulell de control" local: "Local" remote: "Remot" total: "Total" @@ -573,7 +575,7 @@ serverLogs: "Registres del servidor" deleteAll: "Elimina-ho tot" showFixedPostForm: "Mostrar el formulari per escriure a l'inici de la lÃnia de temps" showFixedPostFormInChannel: "Mostrar el formulari d'escriptura al principi de la lÃnia de temps (Canals)" -withRepliesByDefaultForNewlyFollowed: "Inclou les respostes d'usuaris nous seguits a la lÃnia de temps per defecte." +withRepliesByDefaultForNewlyFollowed: "Inclou les respostes d'usuaris nous que segueixes a la lÃnia de temps per defecte." newNoteRecived: "Hi ha publicacions noves" sounds: "Sons" sound: "So" @@ -614,7 +616,7 @@ unsetUserBanner: "Desactiva el bà ner " unsetUserBannerConfirm: "Segur que vols desactivar el bà ner?" deleteAllFiles: "Esborra tots els arxius" deleteAllFilesConfirm: "Segur que vols esborrar tots els arxius?" -removeAllFollowing: "Deixa de seguir tots els usuaris seguits" +removeAllFollowing: "Deixa de seguir tots els usuaris que segueixes" removeAllFollowingDescription: "El fet d'executar això, et farà deixar de seguir a tots els usuaris de {host}. Si us plau, executa això si l'amfitrió, per exemple, ja no existeix." userSuspended: "Aquest usuari ha sigut suspès" userSilenced: "Aquest usuari està sent silenciat" @@ -645,7 +647,7 @@ expandTweet: "Expandir post" themeEditor: "Editor de temes" description: "Descripció" describeFile: "Afegir subtitulació" -enterFileDescription: "Afegeix un tÃtol" +enterFileDescription: "Escriu un peu de foto" author: "Autor" leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?" manage: "Administració" @@ -684,11 +686,15 @@ smtpSecure: "Fes servir SSL/TLS per connexions SMTP" smtpSecureInfo: "Desactiva això quan facis servir connexions STARTTLS" testEmail: "Prova l'enviament de correu " wordMute: "Silenciar paraules " +wordMuteDescription: "Minimitza les notes que contenen la paraula o frase especificada. Les notes minimitzades poden visualitzar-se fent clic sobre elles." hardWordMute: "Silenciar paraules fortes" +showMutedWord: "Mostrar paraules silenciades" +hardWordMuteDescription: "Oculta les notes que contenen la paraula o frase especificada. A diferència de Silenciar paraula, la nota quedarà completament oculta a la vista." regexpError: "Error de l'expressió regular " regexpErrorDescription: "S'ha produït un error a l'expressió regular a la lÃnia {line} de les paraules silenciades {tab}:" instanceMute: "Silenciar servidor" userSaysSomething: "{name} n'ha dit alguna cosa" +userSaysSomethingAbout: "{name} està parlant sobre \"{word}\"" makeActive: "Activar" display: "Veure" copy: "Copiar" @@ -729,7 +735,7 @@ instanceTicker: "Informació de notes de la instà ncia " waitingFor: "Esperant {x}" random: "Aleatori " system: "Sistema" -switchUi: "Canviar interfÃcie d'usuari " +switchUi: "Canviar la interfÃcie" desktop: "Escriptori" clip: "Retalls" createNew: "Crear" @@ -747,7 +753,7 @@ repliesCount: "Nombre de respostes" renotesCount: "Impulsos fets" repliedCount: "Nombre de respostes rebudes" renotedCount: "Impulsos rebuts" -followingCount: "Nombre de comptes seguits" +followingCount: "Nombre de comptes que segueixes" followersCount: "Nombre de seguidors" sentReactionsCount: "Nombre de reaccions enviades" receivedReactionsCount: "Nombre de reaccions rebudes" @@ -779,7 +785,7 @@ thisIsExperimentalFeature: "Aquesta és una caracterÃstica experimental. La sev developer: "Programador" makeExplorable: "Fes que el compte sigui visible a la secció \"Explorar\"" makeExplorableDescription: "Si desactives aquesta opció, el teu compte no sortirà a la secció \"Explorar\"" -showGapBetweenNotesInTimeline: "Mostra una separació entre els articles a la lÃnia de temps" +showGapBetweenNotesInTimeline: "Notes separades a la lÃnia de temps" duplicate: "Duplicat" left: "Esquerra" center: "Centre" @@ -910,7 +916,7 @@ off: "Desactivar" emailRequiredForSignup: "Demanar correu electrònic per registrar-se " unread: "Sense llegir" filter: "Filtrar" -controlPanel: "Panel de control" +controlPanel: "Taulell de control" manageAccounts: "Gestionar comptes" makeReactionsPublic: "Reaccions públiques " makeReactionsPublicDescription: "Això fa que totes les teves reaccions siguin visibles públicament " @@ -929,7 +935,7 @@ useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calai welcomeBackWithName: "Benvingut de nou, {name}" clickToFinishEmailVerification: "Si us plau, fes clic a [{ok}] per completar la verificació per correu electrònic " overridedDeviceKind: "Tipus de dispositiu" -smartphone: "Telèfon intel·ligent" +smartphone: "Mòbil " tablet: "Tauleta" auto: "Automà tic " themeColor: "Color del tema" @@ -1010,7 +1016,7 @@ sendPushNotificationReadMessageCaption: "Això pot fer que el teu dispositiu con windowMaximize: "Maximitzar " windowMinimize: "Minimitzar" windowRestore: "Restaurar" -caption: "Llegenda" +caption: "Peu de foto" loggedInAsBot: "Identificat com a bot" tools: "Eines" cannotLoad: "No es pot carregar" @@ -1133,7 +1139,7 @@ channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista d thisChannelArchived: "Aquest Canal ha sigut arxivat." displayOfNote: "Mostrar notes" initialAccountSetting: "Configuració del perfil" -youFollowing: "Seguit" +youFollowing: "Seguint" preventAiLearning: "Descartar l'ús d'aprenentatge automà tic (IA Generativa)" preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automà tic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta." options: "Opcions" @@ -1199,7 +1205,7 @@ showRenotes: "Mostrar impulsos" edited: "Editat" notificationRecieveConfig: "Parà metres de notificacions" mutualFollow: "Seguidor mutu" -followingOrFollower: "Seguit o seguidor" +followingOrFollower: "Seguint o seguidor" fileAttachedOnly: "Només notes amb adjunts" showRepliesToOthersInTimeline: "Mostrar les respostes a altres a la lÃnia de temps" hideRepliesToOthersInTimeline: "Amagar les respostes a altres a la lÃnia de temps" @@ -1301,6 +1307,8 @@ lockdown: "Bloquejat" pleaseSelectAccount: "Seleccionar un compte" availableRoles: "Roles disponibles " acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perills." +federationSpecified: "Aquest servidor treballa amb una federació de llistes blanques. No pot interactuar amb altres servidors que no siguin els especificats per l'administrador." +federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors." _accountSettings: requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut" requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació." @@ -1473,7 +1481,7 @@ _accountMigration: startMigration: "Migrar" migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podrà s tornar a fer servir aquest compte mai més." movedAndCannotBeUndone: "Aquest compte ha migrat.\nLes migracions no es poden desfer." - postMigrationNote: "Aquest compte deixarà de seguir tots els comptes que segueix 24 hores després de germinar la migració.\nEl nombre de seguidors i seguits passarà a ser de zero. Per evitar que els teus seguidors no puguin veure les publicacions marcades com a només seguidors continuaren seguint aquest compte." + postMigrationNote: "Aquest compte deixarà de seguir tots els comptes que segueix 24 hores després de terminar la migració.\nEl nombre de seguidors i seguits passarà a ser de zero. Per evitar que els teus seguidors no puguin veure les publicacions marcades com a només seguidors continuaren seguint aquest compte." movedTo: "Nou compte:" _achievements: earnedAt: "Desbloquejat el" @@ -1832,7 +1840,7 @@ _emailUnavailable: smtp: "Aquest servidor de correu electrònic no respon" banned: "No pots registrar-te amb aquesta adreça de correu electrònic " _ffVisibility: - public: "Publicar" + public: "Públic " followers: "Visible només per a seguidors " private: "Privat" _signup: @@ -1866,7 +1874,7 @@ _gallery: unlike: "Ja no m'agrada" _email: _follow: - title: "t'ha seguit" + title: "Tens un nou seguidor" _receiveFollowRequest: title: "Has rebut una sol·licitud de seguiment" _plugin: @@ -1900,7 +1908,7 @@ _registry: domain: "Domini" createKey: "Crear una clau" _aboutMisskey: - about: "Misskey és un programa de codi obert desenvolupar per syuilo des de 2014" + about: "Misskey és un programa de codi obert desenvolupat des del 2014 per syuilo" contributors: "Col·laboradors principals" allContributors: "Tots els col·laboradors " source: "Codi font" @@ -2219,7 +2227,7 @@ _widgets: slideshow: "Presentació" button: "Botó " onlineUsers: "Usuaris actius" - jobQueue: "Cua de tasques" + jobQueue: "Cua de feines" serverMetric: "Mètriques del servidor" aiscript: "Consola AiScript" aiscriptApp: "Aplicació AiScript" @@ -2299,7 +2307,7 @@ _exportOrImport: allNotes: "Totes les publicacions" favoritedNotes: "Notes preferides" clips: "Retalls" - followingList: "Seguint" + followingList: "Seguint " muteList: "Silencia" blockingList: "Bloqueja" userLists: "Llistes" @@ -2438,7 +2446,7 @@ _notification: _types: all: "Tots" note: "Notes noves" - follow: "Seguint" + follow: "Segueix-me" mention: "Menció" reply: "Respostes" renote: "Renotar" @@ -2454,7 +2462,7 @@ _notification: test: "Prova la notificació" app: "Notificacions d'aplicacions" _actions: - followBack: "t'ha seguit també" + followBack: "També et segueix" reply: "Respondre" renote: "Renotar" _deck: @@ -2721,6 +2729,66 @@ _contextMenu: app: "Aplicació " appWithShift: "Aplicació amb la tecla shift" native: "InterfÃcie del navegador" +_gridComponent: + _error: + requiredValue: "Aquest camp és obligatori" + columnTypeNotSupport: "La validació d'expressions regulars només s'admet per columnes de tipus text." + patternNotMatch: "Aquest valor no coincideix amb {pattern}" + notUnique: "Aquest valor ha de ser únic " +_roleSelectDialog: + notSelected: "No seleccionat" +_customEmojisManager: + _gridCommon: + copySelectionRows: "Copiar lÃnies seleccionades " + copySelectionRanges: "Copiar selecció " + deleteSelectionRows: "Esborrar lÃnies seleccionades" + deleteSelectionRanges: "Esborrar files de la selecció " + searchSettings: "Configuració del cercador" + searchSettingCaption: "Defineix criteris de cerca detallats." + searchLimit: "Nombre de pantalles" + sortOrder: "Ordenar" + registrationLogs: "Registres d'inscripcions " + registrationLogsCaption: "Quan s'actualitzin o s'esborrin emojis es mostrarà un registre. Desapareixeran quan s'actualitzin, s'esborrin, visitis una nova pà gina o la recarreguis." + alertEmojisRegisterFailedDescription: "No s'ha pogut actualitzar o esborrar l'emoji. Si us plau, dona una ullada al registre per més detalls." + _logs: + showSuccessLogSwitch: "Mostrar el registre d'èxit " + failureLogNothing: "No hi ha registres de fallades." + logNothing: "No hi ha registres." + _remote: + selectionRowDetail: "Detall de la lÃnia seleccionada" + importSelectionRows: "Importar les files seleccionades" + importSelectionRangesRows: "Importar les files de la selecció " + importEmojisButton: "Importar els Emojis marcats" + confirmImportEmojisTitle: "Importar Emojis" + confirmImportEmojisDescription: "Importar {count} Emojis d'una adreça remota. Tingues cura de les llicències dels Emojis. Vols importar-los?" + _local: + tabTitleList: "Llistar els Emojis registrats" + tabTitleRegister: "Registre d'Emojis" + _list: + emojisNothing: "No hi ha Emojis registrats" + markAsDeleteTargetRows: "Files seleccionades que s'han d'esborrar " + markAsDeleteTargetRanges: "Selecció de files per la seva eliminació " + alertUpdateEmojisNothingDescription: "No hi ha Emojis actualitzats." + alertDeleteEmojisNothingDescription: "No hi ha Emoji per esborrar." + confirmMovePage: "Vols canviar de pà gina?" + confirmChangeView: "Vols canviar la pantalla?" + confirmUpdateEmojisDescription: "Actualitzar {count} Emojis. Vols executar-ho?" + confirmDeleteEmojisDescription: "Esborrar {count} Emojis marcats. Vols continuar?" + confirmResetDescription: "Es restabliran tots els canvis fets fins ara." + confirmMovePageDesciption: "S'han fet canvis als Emojis d'aquesta pà gina. Si continues navegant sense guardar els canvis, es perdran tots els canvis fets en aquesta pà gina." + dialogSelectRoleTitle: "Buscar Emojis per rol" + _register: + uploadSettingTitle: "Actualitza la configuració " + uploadSettingDescription: "En aquesta pantalla pots configurar el que s'ha de fer quan es puja un Emoji." + directoryToCategoryLabel: "Escriu el nom del directori al camp de \"categoria\"" + directoryToCategoryCaption: "Quan arrossegues un directori, escriu el nom del directori al camp categoria." + emojiInputAreaCaption: "Selecciona els Emojis que vols registrar gent servir un dels mètodes." + emojiInputAreaList1: "Arrossega i deixar anar fitxers o directoris dintre del quadrat." + emojiInputAreaList2: "Clica l'enllaç per seleccionar un fitxer des del teu ordinador." + emojiInputAreaList3: "Clica aquest enllaç per seleccionar del Disc" + confirmRegisterEmojisDescription: "Registrar els Emojis de la llista com a nous Emojis personalitzats. Vols continuar? (Per evitar una sobrecà rrega només {count} Emojis es poden registrar d'una sola vegada)" + confirmClearEmojisDescription: "Descartar els canvis i esborrar els Emojis de la llista. Vols continuar?" + confirmUploadEmojisDescription: "Pujar els {count} fitxers que has arrossegat al disc. Vols continuar?" _embedCodeGen: title: "Personalitza el codi per incrustar" header: "Mostrar la capçalera" @@ -2744,3 +2812,34 @@ _selfXssPrevention: _followRequest: recieved: "Sol·licituds rebudes" sent: "Sol·licituds enviades" +_remoteLookupErrors: + _federationNotAllowed: + title: "No es pot establir connexió amb aquest servidor" + description: "És possible que s'hagi desactivat la comunicació amb aquest servidor o que hagi estat bloquejat.\nPosa't en contacte amb l'administrador del servidor." + _uriInvalid: + title: "L'adreça és incorrecte" + description: "Hi ha un problema amb l'adreça introduïda; comprova que no hagis escrit carà cters que no es puguin fer servir." + _requestFailed: + title: "La sol·licitud a fallat" + description: "La comunicació amb aquest servidor a fallat. És possible que l'altre servidor no funcioni. Comprova també que no has posat una adreça no và lida o inexistent." + _responseInvalid: + title: "La resposta no és correcta " + description: "Hem pogut comunicar-nos amb aquest servidor, però les dades rebudes no són correctes." + _responseInvalidIdHostNotMatch: + description: "El domini de l'adreça introduïda no és el mateix que el domini de l'adreça final obtinguda. Si està s consultant continguts remots mitjançant servidors tercers, torna a fer la consulta fent servir l'adreça que es pot obtenir en el servidor origen." + _noSuchObject: + title: "No s'ha trobat" + description: "No es pot trobar el recurs sol·licitat, si us plau comprova l'adreça una altra vegada." +_captcha: + verify: "Passar pel CAPTCHA" + testSiteKeyMessage: "Pots comprovar una vista prèvia introduïnt valors de prova per la clau del lloc i la clau secreta. Si vols més informació consulteu la següent pà gina." + _error: + _requestFailed: + title: "Ha fallat la sol·licitud del CAPTCHA" + text: "Si us plau, torna a intentar-ho d'aquà una estona o comprova els ajustos de nou." + _verificationFailed: + title: "Ha fallat la validació CAPTCHA" + text: "Comprova que els ajustos són els correctes." + _unknown: + title: "Error CAPTCHA" + text: "S'ha produït un error inesperat." diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 504ba1f8c8..20bea96b7f 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -2024,3 +2024,6 @@ _moderationLogTypes: createInvitation: "Vygenerovat pozvánku" _reversi: total: "Celkem" +_remoteLookupErrors: + _noSuchObject: + title: "Nenalezeno" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index d85c930b73..e99a32a364 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -5,6 +5,7 @@ introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microbl poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform <b>Misskey</b> betriebenen Dienste." monthAndDay: "{day}.{month}." search: "Suchen" +reset: "Zurücksetzen" notifications: "Benachrichtigungen" username: "Benutzername" password: "Passwort" @@ -48,6 +49,7 @@ pin: "An dein Profil anheften" unpin: "Von deinem Profil lösen" copyContent: "Inhalt kopieren" copyLink: "Link kopieren" +copyRemoteLink: "Renote-Link kopieren" copyLinkRenote: "Renote-Link kopieren" delete: "Löschen" deleteAndEdit: "Löschen und Bearbeiten" @@ -185,7 +187,9 @@ addAccount: "Benutzerkonto hinzufügen" reloadAccountsList: "Benutzerkontoliste aktualisieren" loginFailed: "Anmeldung fehlgeschlagen" showOnRemote: "Auf Ursprungsinstanz ansehen" +continueOnRemote: "Weiter auf Remote-Server" chooseServerOnMisskeyHub: "Wähle einen Server aus dem Misskey Hub" +specifyServerHost: "Server-Host auswählen" inputHostName: "Gib die Domain an" general: "Allgemein" wallpaper: "Hintergrund" @@ -237,6 +241,8 @@ silencedInstances: "Stummgeschaltete Instanzen" silencedInstancesDescription: "Gib die Hostnamen der Instanzen, welche stummgeschaltet werden sollen, durch Zeilenumbrüche getrennt an. Alle Konten dieser Instanzen werden als stummgeschaltet behandelt, können nur noch Follow-Anfragen stellen und wenn nicht gefolgt keine lokalen Konten erwähnen. Blockierte Instanzen sind davon nicht betroffen." mediaSilencedInstances: "Medien-stummgeschaltete Server" mediaSilencedInstancesDescription: "Gib pro Zeile die Hostnamen der Server ein, dessen Medien du stummschalten möchtest. Alle Benutzerkonten der aufgeführten Server werden als sensibel behandelt und können keine benutzerdefinierten Emojis verwenden. Gesperrte Server sind davon nicht betroffen." +federationAllowedHosts: "Föderierte Instanzen" +federationAllowedHostsDescription: "Trage die Hostnamen ein mit den du eine Föderation eingehen möchtest. Trenne mit Zeilenumbruch." muteAndBlock: "Stummschaltungen und Blockierungen" mutedUsers: "Stummgeschaltete Benutzer" blockedUsers: "Blockierte Benutzer" @@ -449,6 +455,7 @@ totpDescription: "Logge dich via Authentifizierungs-App mit Einmalpasswort ein" moderator: "Moderator" moderation: "Moderation" moderationNote: "Moderationsnotiz" +moderationNoteDescription: "Trage hier Notizen ein. Diese sind nur für die Moderatoren sichtbar." addModerationNote: "Moderationsnotiz hinzufügen" moderationLogs: "Moderationsprotokolle" nUsersMentioned: "Von {n} Benutzern erwähnt" @@ -488,6 +495,7 @@ noMessagesYet: "Noch keine Nachrichten vorhanden" newMessageExists: "Du hast eine neue Nachricht" onlyOneFileCanBeAttached: "Es kann pro Nachricht nur eine Datei angehängt werden" signinRequired: "Bitte registriere oder melde dich an, um fortzufahren" +signinOrContinueOnRemote: "Um fortzufahren, gehe zu deiner Instanz oder registriere bzw. melde dich an dieser Instanz an. " invitations: "Einladungen" invitationCode: "Einladungscode" checking: "Wird überprüft …" @@ -511,6 +519,7 @@ emojiStyle: "Emoji-Stil" native: "Nativ" menuStyle: "Menü Stil" style: "Stil" +drawer: "App-Übersicht" popup: "Pop-up" showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen" showReactionsCount: "Zeige die Anzahl der Reaktionen auf Notizen an" @@ -579,6 +588,7 @@ masterVolume: "Gesamtlautstärke" notUseSound: "Gebe kein Ton aus" useSoundOnlyWhenActive: "Gebe nur Ton aus, wenn Misskey aktiv ist" details: "Details" +renoteDetails: "Renote Details" chooseEmoji: "Emoji auswählen" unableToProcess: "Der Vorgang konnte nicht abgeschlossen werden" recentUsed: "Vor kurzem verwendet" @@ -595,6 +605,7 @@ descendingOrder: "Absteigende Reihenfolge" scratchpad: "Testumgebung" scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Misskey überprüfen." uiInspector: "UI-Inspektor" +uiInspectorDescription: "Die Liste der UI-Komponenten-Server können im Zwischenspeicher angesehen werden. Die UI-Komponente wird von der Funktion Ui:C: generiert." output: "Ausgabe" script: "Skript" disablePagesScript: "AiScript auf Seiten deaktivieren" @@ -675,11 +686,15 @@ smtpSecure: "Für SMTP-Verbindungen implizit SSL/TLS verwenden" smtpSecureInfo: "Schalte dies aus, falls du STARTTLS verwendest." testEmail: "Emailversand testen" wordMute: "Wortstummschaltung" -hardWordMute: "Harte Wort-Stummschaltung" +wordMuteDescription: "Minimiert Notizen, die das angegebene Wort oder den angegebenen Ausdruck enthalten. Minimierte Notizen können angezeigt werden, indem du auf sie klickst." +hardWordMute: "Harte Wortstummschaltung" +showMutedWord: "Stummgeschaltete Wörter anzeigen" +hardWordMuteDescription: "Blendet Notizen aus, die das angegebene Wort oder die angegebene Phrase enthalten. Im Gegensatz zur Wortstummschaltung wird die Notiz vollständig ausgeblendet." regexpError: "Fehler in einem regulären Ausdruck" regexpErrorDescription: "Im regulären Ausdruck deiner in Zeile {line} von {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:" instanceMute: "Instanzstummschaltungen" userSaysSomething: "{name} hat etwas gesagt" +userSaysSomethingAbout: "{name} sagt etwas über '{word}'" makeActive: "Aktivieren" display: "Anzeigeart" copy: "Kopieren" @@ -848,6 +863,7 @@ administration: "Verwaltung" accounts: "Benutzerkonten" switch: "Wechseln" noMaintainerInformationWarning: "Betreiberinformationen sind nicht konfiguriert." +noInquiryUrlWarning: "Keine gültige URL." noBotProtectionWarning: "Schutz vor Bots ist nicht konfiguriert." configure: "Konfigurieren" postToGallery: "Neuen Galeriebeitrag erstellen" @@ -1080,12 +1096,15 @@ retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?" retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen." enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen" enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen" +enableStatsForFederatedInstances: "Abruf von Informationen über förderierte Server" showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen" reactionsDisplaySize: "Reaktionsanzeigegröße" limitWidthOfReaction: "Begrenze die Breite der Reaktion und zeige sie verkleinert an" noteIdOrUrl: "Notiz-ID oder URL" video: "Video" videos: "Videos" +audio: "Audio" +audioFiles: "Audio" dataSaver: "Datensparmodus" accountMigration: "Kontomigration" accountMoved: "Dieser Benutzer ist zu einem neuen Konto migriert:" @@ -1125,6 +1144,9 @@ preventAiLearning: "Verwendung in machinellem Lernen (Generative bzw. Prediktive preventAiLearningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (Generative bzw. Prediktive AI/KI) zu verwenden. Dies wird durch das Hinzufügen einer \"noai\"-Flag in der HTML-Antwort des jeweiligen Inhalts erreicht. Da diese Flag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich." options: "Optionen" specifyUser: "Spezifischer Benutzer" +lookupConfirm: "Zustimmen?" +openTagPageConfirm: "Hashtag Seite wirklich öffnen?" +specifyHost: "Host" failedToPreviewUrl: "Vorschau nicht anzeigbar" update: "Aktualisieren" rolesThatCanBeUsedThisEmojiAsReaction: "Rollen, die dieses Emoji als Reaktion verwenden können" @@ -1183,6 +1205,7 @@ showRenotes: "Renotes anzeigen" edited: "Bearbeitet" notificationRecieveConfig: "Benachrichtigungseinstellungen" mutualFollow: "Gegenseitig gefolgt" +followingOrFollower: "Follow oder Follower" fileAttachedOnly: "Nur Notizen mit Dateien" showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen" hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen" @@ -1194,7 +1217,10 @@ externalServices: "Externe Dienste" sourceCode: "Quellcode" sourceCodeIsNotYetProvided: "Der Quellcode ist noch nicht verfügbar. Kontaktiere den Administrator, um das Problem zu lösen." repositoryUrl: "Repository URL" +repositoryUrlDescription: "Solltest du Misskey so wie es ist verwenden (im unveränderten Quellcode), gebe Folgendes an:\nhttps://github.com/misskey-dev/misskey" repositoryUrlOrTarballRequired: "Wenn du kein Repository veröffentlicht hast, musst du stattdessen einen Tarball bereitstellen. Siehe .config/example.yml für weitere Informationen." +feedback: "Feedback" +feedbackUrl: "Feedback-Website" impressum: "Impressum" impressumUrl: "Impressums-URL" impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend." @@ -1204,6 +1230,7 @@ tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung" avatarDecorations: "Profilbilddekoration" attach: "Anbringen" detach: "Entfernen" +detachAll: "Alles Entfernen" angle: "Winkel" flip: "Umdrehen" showAvatarDecorations: "Profilbilddekoration anzeigen" @@ -1216,18 +1243,27 @@ signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen. cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden." doReaction: "Reagieren" code: "Code" +reloadRequiredToApplySettings: "Eine Aktualisierung ist erforderlich, um die Einstellungen zu übernehmen." remainingN: "Verbleibend: {n}" overwriteContentConfirm: "Bist du sicher, dass du den aktuellen Inhalt überschreiben willst?" seasonalScreenEffect: "Saisonaler Bildschirmeffekt" decorate: "Dekorieren" addMfmFunction: "MFM hinzufügen" enableQuickAddMfmFunction: "Erweiterte MFM-Auswahl anzeigen" +bubbleGame: "Bubble Game" sfx: "Soundeffekte" soundWillBePlayed: "Es wird Ton wiedergegeben" showReplay: "Wiederholung anzeigen" +replay: "Aufzeichnen" +replaying: "Aufzeichnung" +endReplay: "Aufzeichnung verlassen" +copyReplayData: "Aufzeichnung kopieren" ranking: "Rangliste" lastNDays: "Letzten {n} Tage" backToTitle: "Zurück zum Startbildschirm" +hemisphere: "Hemisphäre" +withSensitive: "Zeige \"sensitive Inhalte\" an" +userSaysSomethingSensitive: "{name} sagt etwas mit sensiblem Inhalt." enableHorizontalSwipe: "Wischen, um zwischen Tabs zu wechseln" loading: "Laden" surrender: "Abbrechen" @@ -1240,6 +1276,8 @@ useNativeUIForVideoAudioPlayer: "Browser-Benutzeroberfläche für die Video- und keepOriginalFilename: "Ursprünglichen Dateinamen beibehalten" keepOriginalFilenameDescription: "Wenn diese Einstellung deaktiviert ist, wird der Dateiname beim Hochladen automatisch durch eine zufällige Zeichenfolge ersetzt." noDescription: "Keine Beschreibung vorhanden" +alwaysConfirmFollow: "Folgen immer bestätigen" +inquiry: "Kontakt" tryAgain: "Bitte später erneut versuchen" confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen" sensitiveMediaRevealConfirm: "Es könnte sich um sensible Medien handeln. Möchtest du sie anzeigen?" @@ -1249,29 +1287,41 @@ fromX: "Von {x}" genEmbedCode: "Einbettungscode generieren" noteOfThisUser: "Notizen dieses Benutzers" clipNoteLimitExceeded: "Zu diesem Clip können keine weiteren Notizen hinzugefügt werden." +performance: "Leistung" +modified: "Bearbeitet" discard: "Verwerfen" thereAreNChanges: "Es gibt {n} Änderung(en)" signinWithPasskey: "Mit Passkey anmelden" +unknownWebAuthnKey: "Unbekannter Passkey" passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen." passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert." messageToFollower: "Nachricht an die Follower" +target: "Speicherort" testCaptchaWarning: "Diese Funktion ist für CAPTCHA-Testzwecke gedacht.\n<strong>Nicht in einer Produktivumgebung verwenden.</strong>" prohibitedWordsForNameOfUser: "Verbotene Begriffe für Benutzernamen" prohibitedWordsForNameOfUserDescription: "Wenn eine Zeichenfolge aus dieser Liste im Namen eines Benutzers enthalten ist, wird der Benutzername abgelehnt. Benutzer mit Moderatorenrechten sind von dieser Einschränkung nicht betroffen." yourNameContainsProhibitedWords: "Dein Name enthält einen verbotenen Begriff" yourNameContainsProhibitedWordsDescription: "Der Name enthält eine verbotene Zeichenfolge. Wende dich an deinen Serveradministrator, wenn du diesen Namen verwenden möchtest." +thisContentsAreMarkedAsSigninRequiredByAuthor: "Logge dich ein, um weitere Inhalte von diesem Nutzer zu sehen." +lockdown: "Sperren" pleaseSelectAccount: "Bitte Konto auswählen" availableRoles: "Verfügbare Rollen" +federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren." +federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren." _accountSettings: requireSigninToViewContents: "Anmeldung erfordern, um Inhalte anzuzeigen" requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln." + requireSigninToViewContentsDescription2: "Der Inhalt wird nicht in URL-Vorschauen (OGP), eingebettet in Webseiten oder auf Servern, die keine Zitate unterstützen, angezeigt." requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern." makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar" makeNotesHiddenBefore: "Frühere Notizen privat machen" + makeNotesHiddenBeforeDescription: "" mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden." + notesOlderThanSpecifiedDateAndTime: "Notizen vor einem bestimmtem Datum und Uhrzeit" _abuseUserReport: forward: "Weiterleiten" forwardDescription: "Leite die Meldung an einen entfernten Server als anonymes Systemkonto weiter." + resolve: "lösen" accept: "Akzeptieren" reject: "Ablehnen" _delivery: @@ -1381,6 +1431,7 @@ _serverSettings: fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden." fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen" fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. " + openRegistrationWarning: "Das Aktivieren von Registrierungen ist riskant. Es wird empfohlen, sie nur dann zu aktivieren, wenn der Server ständig überwacht wird und im Falle eines Problems sofort reagiert werden kann." thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern." _accountMigration: moveFrom: "Von einem anderen Konto zu diesem migrieren" @@ -1842,6 +1893,7 @@ _channel: notesCount: "{n} Notizen" nameAndDescription: "Name und Beschreibung" nameOnly: "Nur Name" + allowRenoteToExternal: "Renotes und Zitierungen außerhalb des Kanals erlauben" _menuDisplay: sideFull: "Seitlich" sideIcon: "Seitlich (Icons)" @@ -1930,6 +1982,7 @@ _sfx: note: "Notizen" noteMy: "Meine Notizen" notification: "Benachrichtigungen" + reaction: "Auswählen einer Reaktion" _soundSettings: driveFile: "Audiodatei aus dem Drive verwenden" driveFileWarn: "Wähle eine Audiodatei aus dem Drive" @@ -2028,12 +2081,22 @@ _permissions: "read:admin:server-info": "Serverinformationen anzeigen" "read:admin:show-moderation-log": "Moderationsprotokoll einsehen" "read:admin:show-user": "Private Benutzerinformationen einsehen" + "write:admin:roles": "Rollen verwalten" + "read:admin:roles": "Rollen anzeigen" + "write:admin:relays": "Relays verwalten" + "read:admin:relays": "Relays anzeigen" "write:admin:invite-codes": "Einladungscodes verwalten" "read:admin:invite-codes": "Einladungscodes anzeigen" "write:admin:announcements": "Ankündigungen verwalten" "read:admin:announcements": "Ankündigungen einsehen" "write:admin:avatar-decorations": "Kann Avatar-Dekorationen verwalten" "read:admin:avatar-decorations": "Avatar-Dekorationen ansehen" + "write:admin:account": "Benutzerkonten verwalten" + "read:admin:account": "Benutzerkonten anzeigen" + "write:admin:emoji": "Emojis verwalten" + "read:admin:emoji": "Emojis anzeigen" + "write:admin:queue": "Job-Warteschlange verwalten" + "read:admin:queue": "Job-Warteschlange anzeigen" _auth: shareAccessTitle: "Verteilung von App-Berechtigungen" shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?" @@ -2151,6 +2214,8 @@ _profile: changeAvatar: "Profilbild ändern" changeBanner: "Banner ändern" verifiedLinkDescription: "Gibst du hier eine URL ein, die einen Link zu deinem Profile enthält, wird neben diesem Feld ein Icon zur Besitzbestätigung angezeigt." + avatarDecorationMax: "Du kannst bis zu {max} Dekorationen hinzufügen." + followedMessageDescription: "Du kannst eine kurze Nachricht festlegen, die dem Empfänger angezeigt wird, wenn er dir folgt." _exportOrImport: allNotes: "Alle Notizen" favoritedNotes: "Als Favorit markierte Notizen" @@ -2283,6 +2348,7 @@ _notification: reactedBySomeUsers: "{n} Benutzer haben eine Reaktion geschickt" renotedBySomeUsers: "Renote von {n} Benutzern" followedBySomeUsers: "Von {n} Benutzern gefolgt" + login: "Neue Anmeldung erfolgt" _types: all: "Alle" note: "Neue Notizen" @@ -2295,6 +2361,7 @@ _notification: pollEnded: "Ende von Umfragen" receiveFollowRequest: "Erhaltene Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen" + roleAssigned: "Rolle zugewiesen" achievementEarned: "Errungenschaft freigeschaltet" login: "Anmelden" app: "Benachrichtigungen von Apps" @@ -2346,6 +2413,7 @@ _webhookSettings: createWebhook: "Webhook erstellen" name: "Name" secret: "Secret" + trigger: "Auslöser" active: "Aktiviert" _events: follow: "Wenn du jemandem folgst" @@ -2357,8 +2425,10 @@ _webhookSettings: mention: "Wenn du erwähnt wirst" _abuseReport: _notificationRecipient: + createRecipient: "Meldungsempfänger hinzufügen" _recipientType: mail: "Email" + keywords: "Schlüsselwort" _moderationLogTypes: createRole: "Rolle erstellt" deleteRole: "Rolle gelöscht" @@ -2393,6 +2463,13 @@ _moderationLogTypes: createAvatarDecoration: "Profilbilddekoration erstellt" updateAvatarDecoration: "Profilbilddekoration aktualisiert" deleteAvatarDecoration: "Profilbilddekoration gelöscht" + unsetUserAvatar: "Profilbild zurückgesetzt" + unsetUserBanner: "Profilbanner zurückgesetzt" + createSystemWebhook: "System-Webhook erstellt" + updateSystemWebhook: "System-Webhook aktualisiert" + deleteSystemWebhook: "System-Webhook gelöscht" + deletePage: "Seite gelöscht" + deleteGalleryPost: "Galeriebeitrag gelöscht" _fileViewer: title: "Dateiinformationen" type: "Dateityp" @@ -2442,6 +2519,10 @@ _externalResourceInstaller: _themeInstallFailed: title: "Das Farbschema konnte nicht installiert werden" description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden." +_hemisphere: + N: "Nördliche Erdhalbkugel" + S: "Südliche Erdhalbkugel" + caption: "Wird in einigen Client-Einstellungen zur Bestimmung der Jahreszeit verwendet." _reversi: blackOrWhite: "Schwarz/Weiß" rules: "Regeln" @@ -2449,17 +2530,26 @@ _reversi: white: "Weiß" total: "Gesamt" _offlineScreen: + title: "Offline - keine Verbindung zum Server möglich" header: "Verbindung zum Server nicht möglich" _urlPreviewSetting: title: "Einstellungen der URL-Vorschau" enable: "URL-Vorschau aktivieren" timeout: "Zeitüberschreitung beim Abrufen der Vorschau (ms)" + timeoutDescription: "Übersteigt die für die Vorschau benötigte Zeit diesen Wert, wird keine Vorschau generiert." maximumContentLength: "Maximale Content-Length (Bytes)" + maximumContentLengthDescription: "Wenn die Content-Length diesen Wert überschreitet, wird keine Vorschau erzeugt." + requireContentLength: "Vorschau nur generieren, wenn Content-Length verfügbar ist" + requireContentLengthDescription: "Wenn der Server keine Content-Length zurückgibt, wird keine Vorschau erzeugt." + userAgent: "User-Agent" _mediaControls: playbackRate: "Wiedergabegeschwindigkeit" _contextMenu: title: "Kontextmenü" app: "Anwendung" +_gridComponent: + _error: + requiredValue: "Dieser Wert ist ein Pflichtfeld" _embedCodeGen: title: "Einbettungscode anpassen" header: "Kopfzeile anzeigen" @@ -2476,3 +2566,13 @@ _selfXssPrevention: title: "„Füge in diesen Bereich etwas ein“ ist eine Betrugsmasche." description1: "Wenn du hier etwas einfügst, könnte ein böswilliger Benutzer dein Konto übernehmen oder deine persönlichen Daten stehlen." description3: "Weitere Informationen findest du hier. {link}" +_remoteLookupErrors: + _federationNotAllowed: + title: "Kommunikation mit diesem Server nicht möglich" + description: "Möglicherweise wurde die Kommunikation mit diesem Server deaktiviert oder dieser Server ist blockiert.\nWende dich bitte an den Serveradministrator." + _uriInvalid: + title: "URI ist fehlerhaft" + description: "Es gibt ein Problem mit der von dir eingegebenen URI. Bitte prüfe, ob du Zeichen eingegeben hast, die in der URI nicht verwendet werden können." + _noSuchObject: + title: "Nicht gefunden" + description: "Die angeforderte Ressource konnte nicht gefunden werden, bitte überprüfe die URI erneut." diff --git a/locales/en-US.yml b/locales/en-US.yml index 2f2d10c6b5..d4803310c6 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -5,6 +5,7 @@ introMisskey: "Welcome! Misskey is an open source, decentralized microblogging s poweredByMisskeyDescription: "{name} is one of the services powered by the open source platform <b>Misskey</b> (referred to as a \"Misskey instance\")." monthAndDay: "{month}/{day}" search: "Search" +reset: "Reset" notifications: "Notifications" username: "Username" password: "Password" @@ -48,6 +49,7 @@ pin: "Pin to profile" unpin: "Unpin from profile" copyContent: "Copy contents" copyLink: "Copy link" +copyRemoteLink: "Copy remote link" copyLinkRenote: "Copy renote link" delete: "Delete" deleteAndEdit: "Delete and edit" @@ -684,11 +686,15 @@ smtpSecure: "Use implicit SSL/TLS for SMTP connections" smtpSecureInfo: "Turn this off when using STARTTLS" testEmail: "Test email delivery" wordMute: "Word mute" +wordMuteDescription: "Minimize notes that contain the specified word or phrase. Minimized notes can be displayed by clicking on them." hardWordMute: "Hard word mute" +showMutedWord: "Show muted words" +hardWordMuteDescription: "Hide notes that contain the specified word or phrase. Unlike word mute, the note will be completely hidden from view." regexpError: "Regular Expression error" regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:" instanceMute: "Instance Mutes" userSaysSomething: "{name} said something" +userSaysSomethingAbout: "{name} said something about \"{word}\"" makeActive: "Activate" display: "Display" copy: "Copy" @@ -1300,6 +1306,9 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require log lockdown: "Lockdown" pleaseSelectAccount: "Select an account" availableRoles: "Available roles" +acknowledgeNotesAndEnable: "Turn on after understanding the precautions." +federationSpecified: "This server is operated in an allowlist federation. Interacting with servers other than those designated by the administrator is not allowed." +federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers." _accountSettings: requireSigninToViewContents: "Require sign-in to view contents" requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information." @@ -1456,9 +1465,9 @@ _serverSettings: reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase." inquiryUrl: "Inquiry URL" inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." + openRegistration: "Make the account creation open" + openRegistrationWarning: "Opening registration carries risks. It is recommended to only enable it if you have a system in place to continuously monitor the server and respond immediately in case of any issues." thisSettingWillAutomaticallyOffWhenModeratorsInactive: "If no moderator activity is detected for a while, this setting will be automatically turned off to prevent spam." - openRegistration: "Open account creation" - openRegistrationWarning: "Opening up registration is risky, so we recommend turning it on only if you are able to constantly monitor your server and respond immediately if any problems arise." _accountMigration: moveFrom: "Migrate another account to this one" moveFromSub: "Create alias to another account" @@ -2720,6 +2729,65 @@ _contextMenu: app: "Application" appWithShift: "Application with shift key" native: "Native" +_gridComponent: + _error: + requiredValue: "This value is required" + columnTypeNotSupport: "Validation with regular expression is supported only for type:text columns." + patternNotMatch: "This value doesn't match the pattern in {pattern}" + notUnique: "This value must be unique" +_roleSelectDialog: + notSelected: "Not selected" +_customEmojisManager: + _gridCommon: + copySelectionRows: "Copy selected rows" + copySelectionRanges: "Copy selected ranges" + deleteSelectionRows: "Delete selected rows" + deleteSelectionRanges: "Delete rows in the selection" + searchSettings: "Search settings" + searchSettingCaption: "Set detailed search criteria." + searchLimit: "" + sortOrder: "Sort order" + registrationLogs: "Registration log" + registrationLogsCaption: "Logs will be displayed when updating or deleting Emojis. They will disappear after updating or deleting them, moving to a new page, or reloading." + alertEmojisRegisterFailedDescription: "Failed to update or delete Emojis. Please check the registration log for details." + _logs: + showSuccessLogSwitch: "Show success log" + failureLogNothing: "There is no failure log." + logNothing: "There is no log." + _remote: + selectionRowDetail: "Selected row's detail" + importSelectionRows: "Import selected rows" + importSelectionRangesRows: "Import rows in the selection" + importEmojisButton: "Import checked Emojis" + confirmImportEmojisTitle: "Import Emojis" + confirmImportEmojisDescription: "Import {count} Emoji(s) received from the remote server. Please pay close attention to the license of the Emoji. Are you sure to continue?" + _local: + tabTitleList: "List of registered Emojis" + tabTitleRegister: "Emoji registration" + _list: + emojisNothing: "There are no registered Emojis." + markAsDeleteTargetRows: "Mark selected rows as a target to delete" + markAsDeleteTargetRanges: "Mark rows in the selection as a target to delete" + alertUpdateEmojisNothingDescription: "There are no updated Emojis." + alertDeleteEmojisNothingDescription: "There are no Emojis to be deleted." + confirmMovePage: "" + confirmChangeView: "" + confirmUpdateEmojisDescription: "Update {count} Emoji(s). Are you sure to continue?" + confirmDeleteEmojisDescription: "Delete checked {count} Emoji(s). Are you sure to continue?" + confirmResetDescription: "" + dialogSelectRoleTitle: "Search by roll set in Emojis" + _register: + uploadSettingTitle: "Upload settings" + uploadSettingDescription: "On this screen, you can configure the behavior when uploading Emojis." + directoryToCategoryLabel: "Enter the directory name in the \"category\" field" + directoryToCategoryCaption: "When you drag and drop a directory, enter the directory name in the \"category\" field." + emojiInputAreaCaption: "Select the Emojis you wish to register using one of the methods." + emojiInputAreaList1: "Drag and drop image files or a directory into this frame" + emojiInputAreaList2: "Click this link to select from your computer" + emojiInputAreaList3: "Click this link to select from the drive" + confirmRegisterEmojisDescription: "Register the Emojis from the list as new custom Emojis. Are you sure to continue? (To avoid overload, only {count} Emoji(s) can be registered in a single operation)" + confirmClearEmojisDescription: "Discard the edits and clear the Emojis from the list. Are you sure to continue?" + confirmUploadEmojisDescription: "Upload the dragged and dropped {count} file(s) to the drive. Are you sure to continue?" _embedCodeGen: title: "Customize embed code" header: "Show header" @@ -2743,4 +2811,34 @@ _selfXssPrevention: _followRequest: recieved: "Received" sent: "Sent" -acknowledgeNotesAndEnable: "Be sure you have understood the warnings before turning this on" +_remoteLookupErrors: + _federationNotAllowed: + title: "Unable to communicate with this server" + description: "Communication with this server may have been disabled or this server may be blocked.\nPlease contact the server administrator." + _uriInvalid: + title: "URI is invalid" + description: "There is a problem with the URI you entered. Please check if you entered characters that cannot be used in the URI." + _requestFailed: + title: "Request failed" + description: "Communication with this server failed. The server may be down. Also, please make sure that you have not entered an invalid or nonexistent URI." + _responseInvalid: + title: "Response is invalid" + description: "It could communicate with this server, but the data obtained was incorrect." + _responseInvalidIdHostNotMatch: + description: "The domain of the entered URI differs from the domain of the final obtained URI. If you are looking up remote content through a third-party server, please look up again using a URI that can be obtained from the origin server." + _noSuchObject: + title: "Not found" + description: "The requested resource was not found, please recheck the URI." +_captcha: + verify: "Please verify the CAPTCHA" + testSiteKeyMessage: "You can check the preview by entering the test values for the site and secret keys.\nPlease see the following page for details." + _error: + _requestFailed: + title: "Failed to request CAPTCHA" + text: "Please run it after a while or check the settings again." + _verificationFailed: + title: "Failed to validate CAPTCHA" + text: "Please check again if the settings are correct." + _unknown: + title: "CAPTCHA error" + text: "An unexpected error occurred." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index a4ec114b15..28cfba1c20 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -5,11 +5,13 @@ introMisskey: "¡Bienvenido/a! Misskey es un servicio de microblogging descentra poweredByMisskeyDescription: "{name} es uno de los servicios (también llamado instancia) que usa la plataforma de código abierto <b>Misskey</b>" monthAndDay: "{day}/{month}" search: "Buscar" +reset: "Reiniciar" notifications: "Notificaciones" username: "Nombre de usuario" password: "Contraseña" initialPasswordForSetup: "Contraseña para iniciar la inicialización" initialPasswordIsIncorrect: "La contraseña para iniciar la configuración inicial es incorrecta." +initialPasswordForSetupDescription: "Si ha instalado Misskey usted mismo, utilice la contraseña introducida en el archivo de configuración.\nSi utiliza un servicio de alojamiento de Misskey o similar, utilice la contraseña proporcionada.\nSi no ha establecido una contraseña, déjela en blanco para continuar." forgotPassword: "Olvidé mi contraseña" fetchingAsApObject: "Buscando en el fediverso" ok: "OK" @@ -47,6 +49,7 @@ pin: "Fijar al perfil" unpin: "Desfijar" copyContent: "Copiar contenido" copyLink: "Copiar enlace" +copyRemoteLink: "Copiar enlace remoto" copyLinkRenote: "Copiar enlace de renota" delete: "Borrar" deleteAndEdit: "Borrar y editar" @@ -198,6 +201,7 @@ followConfirm: "¿Desea seguir a {name}?" proxyAccount: "Cuenta proxy" proxyAccountDescription: "Una cuenta proxy es una cuenta que actúa como un seguidor remoto de un usuario bajo ciertas condiciones. Por ejemplo, cuando un usuario añade un usuario remoto a una lista, si ningún usuario local sigue al usuario agregado a la lista, la instancia no puede obtener su actividad. Asà que la cuenta proxy sigue al usuario añadido a la lista" host: "Host" +selectSelf: "ElÃgete a ti mismo" selectUser: "Elegir usuario" recipient: "Recipiente" annotation: "Anotación" @@ -213,6 +217,7 @@ perDay: "por dÃa" stopActivityDelivery: "Dejar de enviar actividades" blockThisInstance: "Bloquear instancia" silenceThisInstance: "Silenciar esta instancia" +mediaSilenceThisInstance: "Silencia la Multimedia(Imágenes,videos...) para este servidor" operations: "Operaciones" software: "Software" version: "Versión" @@ -234,6 +239,10 @@ blockedInstances: "Instancias bloqueadas" blockedInstancesDescription: "Seleccione los hosts de las instancias que desea bloquear, separadas por una linea nueva. Las instancias bloqueadas no podrán comunicarse con esta instancia." silencedInstances: "Instancias silenciadas" silencedInstancesDescription: "Listar los hostname de las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas." +mediaSilencedInstances: "Servidores silenciados (Multimedia)" +mediaSilencedInstancesDescription: "Listar las instancias que quieres silenciar. Todas las cuentas de las instancias listadas serán tratadas como silenciadas, solo podrán hacer peticiones de seguimiento, y no podrán mencionar cuentas locales si no las siguen. Esto no afecta a las instancias bloqueadas." +federationAllowedHosts: "Servidores federados" +federationAllowedHostsDescription: "Establezca los nombres de los servidores que pueden federarse, separados por una nueva lÃnea." muteAndBlock: "Silenciar y bloquear" mutedUsers: "Usuarios silenciados" blockedUsers: "Usuarios bloqueados" @@ -324,6 +333,7 @@ selectFile: "Elegir archivo" selectFiles: "Elegir archivos" selectFolder: "Seleccione una carpeta" selectFolders: "Seleccione carpetas" +fileNotSelected: "Archivo no seleccionado." renameFile: "Renombrar archivo" folderName: "Nombre de la carpeta" createFolder: "Crear carpeta" @@ -331,6 +341,7 @@ renameFolder: "Renombrar carpeta" deleteFolder: "Borrar carpeta" folder: "Carpeta" addFile: "Agregar archivo" +showFile: "Examinar archivos" emptyDrive: "El drive está vacÃo" emptyFolder: "La carpeta está vacÃa" unableToDelete: "No se puede borrar" @@ -444,6 +455,7 @@ totpDescription: "Ingresa una contaseña de un sólo uso usando la aplicación a moderator: "Moderador" moderation: "Moderación" moderationNote: "Nota de moderación" +moderationNoteDescription: "Puedes rellenar notas que solo se comparten entre moderadores." addModerationNote: "Añadir nota de moderación" moderationLogs: "Log de moderación" nUsersMentioned: "{n} usuarios mencionados" @@ -478,10 +490,12 @@ retype: "Ingrese de nuevo" noteOf: "Notas de {user}" quoteAttached: "Cita añadida" quoteQuestion: "¿Quiere añadir una cita?" +attachAsFileQuestion: "El texto del portapapeles es demasiado grande ¿Desea adjuntarlo como archivo de texto?" noMessagesYet: "Aún no hay chat" newMessageExists: "Tienes un mensaje nuevo" onlyOneFileCanBeAttached: "Solo se puede añadir un archivo al mensaje" signinRequired: "Iniciar sesión" +signinOrContinueOnRemote: "Para continuar, tendrá que ir a su servidor o registrarse e iniciar sesión en este servidor" invitations: "Invitar" invitationCode: "Código de invitación" checking: "Comprobando" @@ -505,6 +519,8 @@ emojiStyle: "Estilo de emoji" native: "Nativo" menuStyle: "Diseño del menú" style: "Diseño" +drawer: "Cajón de Aplicaciones" +popup: "Ventana emergente" showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor" showReactionsCount: "Mostrar el número de reacciones en las notas" noHistory: "No hay datos en el historial" @@ -572,6 +588,7 @@ masterVolume: "Volumen principal" notUseSound: "Sin sonido" useSoundOnlyWhenActive: "Sonar solo cuando Misskey esté activo" details: "Detalles" +renoteDetails: "Detalles(Renota)" chooseEmoji: "Elije un emoji" unableToProcess: "La operación no se puede llevar a cabo" recentUsed: "Usado recientemente" @@ -587,6 +604,7 @@ ascendingOrder: "Ascendente" descendingOrder: "Descendente" scratchpad: "Scratch pad" scratchpadDescription: "Scratchpad proporciona un entorno experimental para AiScript. Puede escribir, ejecutar y verificar los resultados que interactúan con Misskey." +uiInspector: "Inspector de UI" output: "Salida" script: "Script" disablePagesScript: "Deshabilitar AiScript en Páginas" @@ -667,7 +685,10 @@ smtpSecure: "Usar SSL/TLS implÃcito en la conexión SMTP" smtpSecureInfo: "Apagar cuando se use STARTTLS" testEmail: "Prueba de envÃo" wordMute: "Silenciar palabras" +wordMuteDescription: "Minimiza las notas que contienen la palabra o frase especificada. Las notas minimizadas pueden visualizarse haciendo clic sobre ellas." hardWordMute: "Filtro de palabra fuerte" +showMutedWord: "Mostrar palabras silenciadas." +hardWordMuteDescription: "Oculta las notas que contienen la palabra o frase especificada. A diferencia de Silenciar palabra, la nota quedará completamente oculta a la vista." regexpError: "Error de la expresión regular" regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line} de las palabras muteadas {tab}" instanceMute: "Instancias silenciadas" @@ -1117,6 +1138,8 @@ preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generati preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podrÃa lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada." options: "Opción" specifyUser: "Especificar usuario" +lookupConfirm: "¿Quiere informarse?" +specifyHost: "Especificar Host" failedToPreviewUrl: "No se pudo generar la vista previa" update: "Actualizar" rolesThatCanBeUsedThisEmojiAsReaction: "Roles que pueden usar este emoji como reacción" @@ -1251,6 +1274,11 @@ tryAgain: "Por favor , inténtalo de nuevo" performance: "Rendimiento" unknownWebAuthnKey: "Esto no se ha registrado llave maestra." messageToFollower: "Mensaje a seguidores" +federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador." +federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores" +_accountSettings: + requireSigninToViewContents: "Se requiere iniciar sesión para ver el contenido" + requireSigninToViewContentsDescription1: "Requiere iniciar sesión para ver todas las notas y otros contenidos que hayas creado. Se espera que esto evite que los rastreadores recopilen información." _abuseUserReport: accept: "Acepte" reject: "repudio" @@ -2535,3 +2563,6 @@ _mediaControls: pip: "Picture in Picture" playbackRate: "Velocidad de reproducción" loop: "Reproducción en bucle" +_remoteLookupErrors: + _noSuchObject: + title: "No se encuentra" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index b105a86b5e..473774114e 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -2364,3 +2364,6 @@ _mediaControls: _embedCodeGen: title: "Personnaliser le code d'intégration" generateCode: "Générer le code d'intégration" +_remoteLookupErrors: + _noSuchObject: + title: "Non trouvé" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index fe3f207618..9a28cee275 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -2610,3 +2610,6 @@ _mediaControls: pip: "Gambar dalam Gambar" playbackRate: "Kecepatan Pemutaran" loop: "Ulangi Pemutaran" +_remoteLookupErrors: + _noSuchObject: + title: "Tidak dapat ditemukan" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 66ca935b1b..d2942c389c 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -5,6 +5,7 @@ introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>." monthAndDay: "{day}/{month}" search: "Cerca" +reset: "Ripristinare" notifications: "Notifiche" username: "Nome utente" password: "Password" @@ -48,6 +49,7 @@ pin: "Fissa sul profilo" unpin: "Non fissare sul profilo" copyContent: "Copia il contenuto" copyLink: "Copia il link" +copyRemoteLink: "Copia link remoto" copyLinkRenote: "Copia collegamento alla Rinota" delete: "Elimina" deleteAndEdit: "Elimina e modifica" @@ -56,7 +58,7 @@ addToList: "Aggiungi alla lista" addToAntenna: "Aggiungi all'antenna" sendMessage: "Invia messaggio" copyRSS: "Copia RSS" -copyUsername: "Copia nome utente" +copyUsername: "Copia indirizzo del profilo" copyUserId: "Copia ID del profilo" copyNoteId: "Copia ID della Nota" copyFileId: "Copia ID del file" @@ -105,7 +107,7 @@ makeFollowManuallyApprove: "Approva i follower manualmente" defaultNoteVisibility: "Privacy predefinita delle note" follow: "Segui" followRequest: "Richiesta di follow" -followRequests: "Richieste di follow" +followRequests: "Relazioni" unfollow: "Togli Following" followRequestPending: "Richiesta in approvazione" enterEmoji: "Inserisci emoji" @@ -440,7 +442,7 @@ recentlyRegisteredUsers: "Profili iscritti di recente" recentlyDiscoveredUsers: "Profili scoperti di recente" exploreUsersCount: "Ci sono {count} profili" exploreFediverse: "Esplora il Fediverso" -popularTags: "Tag di tendenza" +popularTags: "Hashtag popolari" userList: "Liste" about: "Informazioni" aboutMisskey: "Informazioni di Misskey" @@ -535,7 +537,7 @@ regenerate: "Generare di nuovo" fontSize: "Dimensione carattere" mediaListWithOneImageAppearance: "Altezza dell'elenco media con una sola immagine " limitTo: "Limita a {x}" -noFollowRequests: "Non hai alcuna richiesta di follow" +noFollowRequests: "Non ci sono richieste di relazione" openImageInNewTab: "Apri le immagini in un nuovo tab" dashboard: "Pannello di controllo" local: "Locale" @@ -551,8 +553,8 @@ promote: "Pubblicizza" numberOfDays: "Numero di giorni" hideThisNote: "Nasconda la nota" showFeaturedNotesInTimeline: "Mostrare le note di tendenza nella tua timeline" -objectStorage: "Stoccaggio oggetti" -useObjectStorage: "Utilizza stoccaggio oggetti" +objectStorage: "Storage S3" +useObjectStorage: "Utilizza lo storage S3 in cloud" objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "URL di riferimento. In caso di utilizzo di proxy o CDN l'URL è 'https://<bucket>.s3.amazonaws.com' per S3, 'https://storage.googleapis.com/<bucket>' per GCS eccetera. " objectStorageBucket: "Bucket" @@ -586,6 +588,7 @@ masterVolume: "Volume principale" notUseSound: "Non emettere suoni" useSoundOnlyWhenActive: "Emetti suoni solo quando Misskey è in attività " details: "Dettagli" +renoteDetails: "Dettagli della Rinota" chooseEmoji: "Scegli emoji" unableToProcess: "Impossibile compiere l'operazione" recentUsed: "Usato di recente" @@ -683,11 +686,15 @@ smtpSecure: "Usare SSL/TLS implicito per le connessioni SMTP" smtpSecureInfo: "Disabilitare quando è attivo STARTTLS." testEmail: "Verifica il funzionamento" wordMute: "Filtri parole" +wordMuteDescription: "Contrae le Note con la parola o la frase specificata. Permette di espandere le Note, cliccandole." hardWordMute: "Filtro parole forte" +showMutedWord: "Elenca le parole silenziate" +hardWordMuteDescription: "Nasconde le Note con la parola o la frase specificata. A differenza delle parole silenziate, la Nota non verrà federata." regexpError: "errore regex" regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:" instanceMute: "Silenziare l'istanza" userSaysSomething: "{name} ha detto qualcosa" +userSaysSomethingAbout: "{name} ha Notato a riguardo di \"{word}\"" makeActive: "Attiva" display: "Visualizza" copy: "Copia" @@ -699,7 +706,7 @@ database: "Base dati" channel: "Canale" create: "Crea" notificationSetting: "Impostazioni notifiche" -notificationSettingDesc: "Seleziona il tipo di notifiche da visualizzare." +notificationSettingDesc: "Scegli quali notifiche mostrare." useGlobalSetting: "Usa impostazioni generali" useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifiche del profilo. Altrimenti si possono segliere impostazioni personalizzate." other: "Eccetera" @@ -906,7 +913,7 @@ itsOn: "Abilitato" itsOff: "Disabilitato" on: "Acceso" off: "Spento" -emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi" +emailRequiredForSignup: "L'indirizzo e-mail è obbligatorio per registrarsi" unread: "Non lette" filter: "Filtri" controlPanel: "Pannello di controllo" @@ -969,7 +976,7 @@ requireAdminForView: "Per visualizzarli, è necessario aver effettuato l'accesso isSystemAccount: "Questi profili vengono creati e gestiti automaticamente dal sistema" typeToConfirm: "Digita {x} per continuare" deleteAccount: "Eliminazione profilo" -document: "Documento" +document: "Documentazione" numberOfPageCache: "Numero di pagine cache" numberOfPageCacheDescription: "Aumenta l'usabilità , ma aumenta anche il carico e l'utilizzo della memoria." logoutConfirm: "Vuoi davvero uscire da Misskey? " @@ -1105,7 +1112,7 @@ accountMovedShort: "Questo profilo è stato migrato" operationForbidden: "Operazione non consentita" forceShowAds: "Mostra sempre i banner" addMemo: "Aggiungi Memo" -editMemo: "Modifica Memo" +editMemo: "Modifica il promemoria" reactionsList: "Chi ha reagito?" renotesList: "Chi ha Rinotato?" notificationDisplay: "Stile delle notifiche" @@ -1139,7 +1146,7 @@ options: "Opzioni del ruolo" specifyUser: "Profilo specifico" lookupConfirm: "Vuoi davvero richiedere informazioni?" openTagPageConfirm: "Vuoi davvero aprire la pagina dell'hashtag?" -specifyHost: "Specifica l'host" +specifyHost: "Host specifici" failedToPreviewUrl: "Anteprima non disponibile" update: "Aggiorna" rolesThatCanBeUsedThisEmojiAsReaction: "Ruoli che possono usare questa emoji come reazione" @@ -1239,7 +1246,7 @@ code: "Codice" reloadRequiredToApplySettings: "Per applicare le impostazioni, occorre ricaricare." remainingN: "Rimangono: {n}" overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?" -seasonalScreenEffect: "Schermate in base alla stagione" +seasonalScreenEffect: "Abilita gli effetti speciali stagionali" decorate: "Decora" addMfmFunction: "Aggiungi decorazioni" enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM" @@ -1298,6 +1305,10 @@ yourNameContainsProhibitedWordsDescription: "Se desideri comunque utilizzare que thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autore richiede di iscriversi per vedere il contenuto" lockdown: "Isolamento" pleaseSelectAccount: "Per favore, seleziona un profilo" +availableRoles: "Ruoli disponibili" +acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento." +federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione." +federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server." _accountSettings: requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione" requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler." @@ -1454,6 +1465,8 @@ _serverSettings: reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis." inquiryUrl: "URL di contatto" inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione." + openRegistration: "Registrazioni aperte" + openRegistrationWarning: "L’apertura della registrazione comporta dei rischi. Ti consigliamo di attivarla solo se hai predisposto il monitoraggio continuo del tuo server e puoi rispondere immediatamente se si verifica un problema." thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo." _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" @@ -1920,10 +1933,10 @@ _serverDisconnectedBehavior: quiet: "Visualizza avviso in modo discreto" _channel: create: "Nuovo canale" - edit: "Gerisci canale" + edit: "Modifica il canale" setBanner: "Scegli intestazione" removeBanner: "Rimuovi intestazione" - featured: "Di tendenza" + featured: "Popolari nel canale" owned: "I miei canali" following: "Following" usersCount: "{n} partecipanti" @@ -1948,7 +1961,7 @@ _instanceMute: _theme: explore: "Esplora temi" install: "Installa un tema" - manage: "Gestione temi" + manage: "Gestione dei temi" code: "Codice tema" description: "Descrizione" installed: "{name} è installato" @@ -2095,12 +2108,12 @@ _permissions: "read:messaging": "Visualizzare la chat" "write:messaging": "Gestire la chat" "read:mutes": "Vedi i profili silenziati" - "write:mutes": "Gestisci i profili silenziati" + "write:mutes": "Gestione dei profili silenziati" "write:notes": "Creare / Eliminare note" "read:notifications": "Visualizzare notifiche" - "write:notifications": "Gestire notifiche" + "write:notifications": "Gestione delle notifiche" "read:reactions": "Vedi reazioni" - "write:reactions": "Gerisci reazioni" + "write:reactions": "Gestione delle reazioni" "write:votes": "Votare" "read:pages": "Visualizzare pagine" "write:pages": "Gestire pagine" @@ -2109,7 +2122,7 @@ _permissions: "read:user-groups": "Vedere i gruppi di utenti" "write:user-groups": "Gestire i gruppi di utenti" "read:channels": "Visualizza canali" - "write:channels": "Gerisci canali" + "write:channels": "Gestione dei canali" "read:gallery": "Visualizza la galleria." "write:gallery": "Gestione della galleria" "read:gallery-likes": "Visualizza i contenuti della galleria." @@ -2200,7 +2213,7 @@ _widgets: notifications: "Notifiche" timeline: "Timeline" calendar: "Calendario" - trends: "Di tendenza" + trends: "Hashtag popolari" clock: "Orologio" rss: "Lettura RSS" rssTicker: "Nastro RSS" @@ -2440,13 +2453,13 @@ _notification: quote: "Cita" reaction: "Reazioni" pollEnded: "Sondaggio chiuso." - receiveFollowRequest: "Richiesta di follow ricevuta" - followRequestAccepted: "Richiesta di follow accettata" + receiveFollowRequest: "Richieste di follow in arrivo" + followRequestAccepted: "Richieste di follow accettate" roleAssigned: "Ruolo concesso" achievementEarned: "Risultato raggiunto" exportCompleted: "Esportazione completata" - login: "Accedi" - test: "Prova la notifica" + login: "Accessi" + test: "Notifiche di test" app: "Notifiche da applicazioni" _actions: followBack: "Following ricambiato" @@ -2716,6 +2729,66 @@ _contextMenu: app: "Applicazione" appWithShift: "Applicazione Shift+Tasto" native: "Interfaccia utente del browser" +_gridComponent: + _error: + requiredValue: "Campo obbligatorio" + columnTypeNotSupport: "Solo le colonne type:text permettono la convalida delle Espresioni Regolari" + patternNotMatch: "Il valore non coincide con {pattern}" + notUnique: "Il valore deve essere univoco" +_roleSelectDialog: + notSelected: "Niente selezioato" +_customEmojisManager: + _gridCommon: + copySelectionRows: "Copia le righe selezionate" + copySelectionRanges: "Copia l'intervallo selezionato" + deleteSelectionRows: "Elimina le righe selezionate" + deleteSelectionRanges: "Elimina le righe nell'intervallo selezionato" + searchSettings: "Impostazioni di ricerca" + searchSettingCaption: "Imposta condizioni di ricerca dettagliate." + searchLimit: "Risultati visualizzati" + sortOrder: "Ordine" + registrationLogs: "Storico della registrazione" + registrationLogsCaption: "Lo storico verrà visualizzato in base alla attività sulle emoji. Scompare quando si esegue un'operazione di aggiornamento/eliminazione o si modifica/ricarica la pagina." + alertEmojisRegisterFailedDescription: "Attenzione, è impossibile modificare la emoji. Si prega di controllare lo storico per ulteriori dettagli." + _logs: + showSuccessLogSwitch: "Mostra le azioni a buon fine" + failureLogNothing: "Non ci sono errori nello storico delle emoji" + logNothing: "Lo storico è vuoto." + _remote: + selectionRowDetail: "Dettagli della riga selezionata" + importSelectionRows: "Importa le righe selezionate" + importSelectionRangesRows: "Importa le righe nell'intervallo selezionato" + importEmojisButton: "Importa le emoji selezionate" + confirmImportEmojisTitle: "Importazione emoji" + confirmImportEmojisDescription: "Importazione di {count} emoji ricevute da remoto. Si prega di prestare molta attenzione al tipo di licenza delle emoji. Vuoi confermare?" + _local: + tabTitleList: "Elenco delle emoji registrate" + tabTitleRegister: "Registrazione emoji" + _list: + emojisNothing: "Non ci sono emoji registrate." + markAsDeleteTargetRows: "Selezionare le righe come eliminabili" + markAsDeleteTargetRanges: "Selezionare le righe nell'intervallo come eliminabili" + alertUpdateEmojisNothingDescription: "Non ci sono emoji aggiornate." + alertDeleteEmojisNothingDescription: "Non ci sono emoji da eliminare." + confirmMovePage: "Vuoi davvero spostare la pagina?" + confirmChangeView: "Vuoi davvero cambiare la vista?" + confirmUpdateEmojisDescription: "Aggiornamento di {count} emoji. Vuoi davvero continuare?" + confirmDeleteEmojisDescription: "Eliminazione delle {count} emoji selezionate. Vuoi davvero continuare?" + confirmResetDescription: "Verranno ripristinate tutte le modifiche apportate finora." + confirmMovePageDesciption: "Sono state modificate le emoji in questa pagina.\nUscendo senza salvare, tutte le modifiche verranno ignorate." + dialogSelectRoleTitle: "Cerca emoji per ruolo" + _register: + uploadSettingTitle: "Caricamento impostazioni" + uploadSettingDescription: "Questa schermata ti permette di scegliere il comportamento durante il caricamento delle emoji." + directoryToCategoryLabel: "Inseriscile in una cartella omonima alla categoria" + directoryToCategoryCaption: "Crea il campo categoria in base alla cartella." + emojiInputAreaCaption: "Seleziona l'emoji da registrare utilizzando uno dei metodi." + emojiInputAreaList1: "Trascina una immagine o una cartella in quest'area" + emojiInputAreaList2: "Clicca per scegliere file dal tuo dispositivo" + emojiInputAreaList3: "Clicca per selezionare dal Drive" + confirmRegisterEmojisDescription: "Registrazione delle emoji elencate come nuove emoji personalizzate. Vuoi davvero procedere? (Per evitare sovraccarichi, puoi registrare al massimo {count} emoji per volta)" + confirmClearEmojisDescription: "Annullare le modifiche e cancella le emoji nell'elenco. Confermi?" + confirmUploadEmojisDescription: "Caricamento sul Drive di {count} file locali. Vuoi davvero procedere?" _embedCodeGen: title: "Personalizza il codice di incorporamento" header: "Mostra la testata" @@ -2736,3 +2809,37 @@ _selfXssPrevention: description1: "Incollando qualcosa qui, malintenzionati potrebbero prendere il controllo del tuo profilo o rubare i tuoi dati personali." description2: "Se non sai esattamente cosa stai facendo, %c smetti subito e chiudi questa finestra." description3: "Per favore, controlla questo collegamento per avere maggiori dettagli. {link}" +_followRequest: + recieved: "Ricezione richiesta di Follow" + sent: "Richiesta di Follow, inviata" +_remoteLookupErrors: + _federationNotAllowed: + title: "Server irraggiungibile" + description: "La comunicazione con questo server potrebbe essere disattivata. Hai bloccato il server? Oppure potrebbero averlo bloccato gli amministratori. Contattali per ulteriori informazioni." + _uriInvalid: + title: "URL non valido" + description: "Controlla che l'indirizzo sia valido e sia privo di caratteri non validi." + _requestFailed: + title: "Richiesta fallita" + description: "La comunicazione col server non è riuscita. Potrebbe essere inattivo. Assicurati anche che la URL sia valida." + _responseInvalid: + title: "Risposta non valida" + description: "La comunicazione col server è andata a buon fine, ma abbiamo ricevuto dati non validi." + _responseInvalidIdHostNotMatch: + description: "L'indirizzo immesso non coincide con la URL finale. Interrogando i server per un contenuto remoto, assicurarsi di utilizzare la URL finale e non quella di un server intermedio." + _noSuchObject: + title: "Non trovato" + description: "La risorsa richiesta non è stata trovata. Verificare nuovamente la URL." +_captcha: + verify: "Per favore, controlla la verifica CAPTCHA" + testSiteKeyMessage: "Puoi provare l'anteprima inserendo valori di test, sia per la chiave del sito che per la chiave segreta.\nSi prega di controllare la pagina qui sotto per i dettagli." + _error: + _requestFailed: + title: "Errore durante la richiesta del CAPTCHA" + text: "Riprova più tardi o controlla nuovamente le impostazioni." + _verificationFailed: + title: "Convalida CAPTCHA non riuscita" + text: "Si prega di verificare nuovamente se le impostazioni sono corrette." + _unknown: + title: "Errore CAPTCHA" + text: "Si è verificato un errore imprevisto." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1b59708d85..a578704434 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -5,6 +5,7 @@ introMisskey: "よã†ã“ãï¼Misskeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Misskey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ã§ã™ã€‚" monthAndDay: "{month}月 {day}æ—¥" search: "検索" +reset: "リセット" notifications: "通知" username: "ユーザーå" password: "パスワード" @@ -48,6 +49,7 @@ pin: "ピン留ã‚" unpin: "ピン留ã‚解除" copyContent: "内容をコピー" copyLink: "リンクをコピー" +copyRemoteLink: "リモートã®ãƒªãƒ³ã‚¯ã‚’コピー" copyLinkRenote: "リノートã®ãƒªãƒ³ã‚¯ã‚’コピー" delete: "削除" deleteAndEdit: "削除ã—ã¦ç·¨é›†" @@ -684,11 +686,15 @@ smtpSecure: "SMTP æŽ¥ç¶šã«æš—黙的ãªSSL/TLSを使用ã™ã‚‹" smtpSecureInfo: "STARTTLS使用時ã¯ã‚ªãƒ•ã«ã—ã¾ã™ã€‚" testEmail: "é…信テスト" wordMute: "ワードミュート" +wordMuteDescription: "指定ã—ãŸèªžå¥ã‚’å«ã‚€ãƒŽãƒ¼ãƒˆã‚’最å°åŒ–ã—ã¾ã™ã€‚最å°åŒ–ã•れãŸãƒŽãƒ¼ãƒˆã‚’クリックã™ã‚‹ã“ã¨ã§è¡¨ç¤ºã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" hardWordMute: "ãƒãƒ¼ãƒ‰ãƒ¯ãƒ¼ãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆ" +showMutedWord: "ミュートã•れãŸãƒ¯ãƒ¼ãƒ‰ã‚’表示" +hardWordMuteDescription: "指定ã—ãŸèªžå¥ã‚’å«ã‚€ãƒŽãƒ¼ãƒˆã‚’éš ã—ã¾ã™ã€‚ワードミュートã¨ã¯ç•°ãªã‚Šã€ãƒŽãƒ¼ãƒˆã¯å®Œå…¨ã«è¡¨ç¤ºã•れãªããªã‚Šã¾ã™ã€‚" regexpError: "æ£è¦è¡¨ç¾ã‚¨ãƒ©ãƒ¼" regexpErrorDescription: "{tab}ワードミュートã®{line}è¡Œç›®ã®æ£è¦è¡¨ç¾ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ:" instanceMute: "サーãƒãƒ¼ãƒŸãƒ¥ãƒ¼ãƒˆ" userSaysSomething: "{name}ãŒä½•ã‹ã‚’言ã„ã¾ã—ãŸ" +userSaysSomethingAbout: "{name}ãŒã€Œ{word}ã€ã«ã¤ã„ã¦ä½•ã‹ã‚’言ã„ã¾ã—ãŸ" makeActive: "アクティブã«ã™ã‚‹" display: "表示" copy: "コピー" @@ -1301,6 +1307,8 @@ lockdown: "ãƒãƒƒã‚¯ãƒ€ã‚¦ãƒ³" pleaseSelectAccount: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’é¸æŠžã—ã¦ãã ã•ã„" availableRoles: "利用å¯èƒ½ãªãƒãƒ¼ãƒ«" acknowledgeNotesAndEnable: "注æ„äº‹é …ã‚’ç†è§£ã—ãŸä¸Šã§ã‚ªãƒ³ã«ã—ã¾ã™ã€‚" +federationSpecified: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯ãƒ›ãƒ¯ã‚¤ãƒˆãƒªã‚¹ãƒˆé€£åˆã§é‹ç”¨ã•れã¦ã„ã¾ã™ã€‚管ç†è€…ãŒæŒ‡å®šã—ãŸã‚µãƒ¼ãƒãƒ¼ä»¥å¤–ã¨ã‚„りå–りã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。" +federationDisabled: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯é€£åˆãŒç„¡åŠ¹åŒ–ã•れã¦ã„ã¾ã™ã€‚ä»–ã®ã‚µãƒ¼ãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨ã‚„りå–りã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。" _accountSettings: requireSigninToViewContents: "コンテンツã®è¡¨ç¤ºã«ãƒã‚°ã‚¤ãƒ³ã‚’å¿…é ˆã«ã™ã‚‹" @@ -2801,6 +2809,69 @@ _contextMenu: appWithShift: "Shiftã‚ーã§ã‚¢ãƒ—リケーション" native: "ブラウザã®UI" +_gridComponent: + _error: + requiredValue: "ã“ã®å€¤ã¯å¿…é ˆé …ç›®ã§ã™" + columnTypeNotSupport: "æ£è¦è¡¨ç¾ã«ã‚ˆã‚‹ãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³ã¯type:textã®ã‚«ãƒ©ãƒ ã®ã¿ã‚µãƒãƒ¼ãƒˆã—ã¾ã™ã€‚" + patternNotMatch: "ã“ã®å€¤ã¯{pattern}ã®ãƒ‘ターンã«ä¸€è‡´ã—ã¾ã›ã‚“" + notUnique: "ã“ã®å€¤ã¯ä¸€æ„ã§ã‚ã‚‹å¿…è¦ãŒã‚りã¾ã™" + +_roleSelectDialog: + notSelected: "é¸æŠžã•れã¦ã„ã¾ã›ã‚“" + +_customEmojisManager: + _gridCommon: + copySelectionRows: "é¸æŠžè¡Œã‚’ã‚³ãƒ”ãƒ¼" + copySelectionRanges: "é¸æŠžç¯„å›²ã‚’ã‚³ãƒ”ãƒ¼" + deleteSelectionRows: "é¸æŠžè¡Œã‚’å‰Šé™¤" + deleteSelectionRanges: "é¸æŠžç¯„å›²ã®å€¤ã‚’クリア" + searchSettings: "検索è¨å®š" + searchSettingCaption: "検索æ¡ä»¶ã‚’詳細ã«è¨å®šã—ã¾ã™ã€‚" + searchLimit: "表示件数" + sortOrder: "並ã³é †" + registrationLogs: "登録ãƒã‚°" + registrationLogsCaption: "çµµæ–‡å—æ›´æ–°ãƒ»å‰Šé™¤æ™‚ã®ãƒã‚°ãŒè¡¨ç¤ºã•れã¾ã™ã€‚更新・削除æ“作を行ã£ãŸã‚Šã€ãƒšãƒ¼ã‚¸ã‚’é·ç§»ãƒ»ãƒªãƒãƒ¼ãƒ‰ã™ã‚‹ã¨æ¶ˆãˆã¾ã™ã€‚" + alertEmojisRegisterFailedDescription: "絵文å—ã®æ›´æ–°ãƒ»å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—ãŸã€‚詳細ã¯ç™»éŒ²ãƒã‚°ã‚’ã”確èªãã ã•ã„。" + _logs: + showSuccessLogSwitch: "æˆåŠŸãƒã‚°ã‚’表示" + failureLogNothing: "失敗ãƒã‚°ã¯ã‚りã¾ã›ã‚“。" + logNothing: "ãƒã‚°ã¯ã‚りã¾ã›ã‚“。" + _remote: + selectionRowDetail: "é¸æŠžè¡Œã®è©³ç´°" + importSelectionRows: "é¸æŠžè¡Œã‚’ã‚¤ãƒ³ãƒãƒ¼ãƒˆ" + importSelectionRangesRows: "é¸æŠžç¯„å›²ã®è¡Œã‚’インãƒãƒ¼ãƒˆ" + importEmojisButton: "ãƒã‚§ãƒƒã‚¯ã•れãŸçµµæ–‡å—をインãƒãƒ¼ãƒˆ" + confirmImportEmojisTitle: "絵文å—ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆ" + confirmImportEmojisDescription: "リモートã‹ã‚‰å—ä¿¡ã—ãŸ{count}個ã®çµµæ–‡å—ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’行ã„ã¾ã™ã€‚絵文å—ã®ãƒ©ã‚¤ã‚»ãƒ³ã‚¹ã«ååˆ†ãªæ³¨æ„を払ã£ã¦ãã ã•ã„。実行ã—ã¾ã™ã‹ï¼Ÿ" + _local: + tabTitleList: "登録済ã¿çµµæ–‡å—一覧" + tabTitleRegister: "絵文å—ã®ç™»éŒ²" + _list: + emojisNothing: "登録ã•れãŸçµµæ–‡å—ã¯ã‚りã¾ã›ã‚“。" + markAsDeleteTargetRows: "é¸æŠžè¡Œã‚’å‰Šé™¤å¯¾è±¡ã«ã™ã‚‹" + markAsDeleteTargetRanges: "é¸æŠžç¯„å›²ã®è¡Œã‚’削除対象ã«ã™ã‚‹" + alertUpdateEmojisNothingDescription: "変更ã•れãŸçµµæ–‡å—ã¯ã‚りã¾ã›ã‚“。" + alertDeleteEmojisNothingDescription: "削除対象ã®çµµæ–‡å—ã¯ã‚りã¾ã›ã‚“。" + confirmMovePage: "ページを移動ã—ã¾ã™ã‹ï¼Ÿ" + confirmChangeView: "表示を変更ã—ã¾ã™ã‹ï¼Ÿ" + confirmUpdateEmojisDescription: "{count}個ã®çµµæ–‡å—ã‚’æ›´æ–°ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ" + confirmDeleteEmojisDescription: "ãƒã‚§ãƒƒã‚¯ãŒã¤ã‘られãŸ{count}個ã®çµµæ–‡å—を削除ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ" + confirmResetDescription: "今ã¾ã§ã«åŠ ãˆãŸå¤‰æ›´ãŒã™ã¹ã¦ãƒªã‚»ãƒƒãƒˆã•れã¾ã™ã€‚" + confirmMovePageDesciption: "ã“ã®ãƒšãƒ¼ã‚¸ã®çµµæ–‡å—ã«å¤‰æ›´ãŒåŠ ãˆã‚‰ã‚Œã¦ã„ã¾ã™ã€‚\nä¿å˜ã›ãšã«ã“ã®ã¾ã¾ãƒšãƒ¼ã‚¸ã‚’移動ã™ã‚‹ã¨ã€ã“ã®ãƒšãƒ¼ã‚¸ã§åŠ ãˆãŸå¤‰æ›´ã¯ã™ã¹ã¦ç ´æ£„ã•れã¾ã™ã€‚" + dialogSelectRoleTitle: "絵文å—ã«è¨å®šã•れãŸãƒãƒ¼ãƒ«ã§æ¤œç´¢" + _register: + uploadSettingTitle: "アップãƒãƒ¼ãƒ‰è¨å®š" + uploadSettingDescription: "ã“ã®ç”»é¢ã§çµµæ–‡å—アップãƒãƒ¼ãƒ‰ã‚’行ã†éš›ã®å‹•作をè¨å®šã§ãã¾ã™ã€‚" + directoryToCategoryLabel: "ディレクトリåã‚’\"category\"ã«å…¥åŠ›ã™ã‚‹" + directoryToCategoryCaption: "ディレクトリをドラッグ・ドãƒãƒƒãƒ—ã—ãŸæ™‚ã«ã€ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªåã‚’\"category\"ã«å…¥åŠ›ã—ã¾ã™ã€‚" + emojiInputAreaCaption: "ã„ãšã‚Œã‹ã®æ–¹æ³•ã§ç™»éŒ²ã™ã‚‹çµµæ–‡å—ã‚’é¸æŠžã—ã¦ãã ã•ã„。" + emojiInputAreaList1: "ã“ã®æž ã«ç”»åƒãƒ•ァイルã¾ãŸã¯ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’ドラッグ&ドãƒãƒƒãƒ—" + emojiInputAreaList2: "ã“ã®ãƒªãƒ³ã‚¯ã‚’クリックã—ã¦PCã‹ã‚‰é¸æŠžã™ã‚‹" + emojiInputAreaList3: "ã“ã®ãƒªãƒ³ã‚¯ã‚’クリックã—ã¦ãƒ‰ãƒ©ã‚¤ãƒ–ã‹ã‚‰é¸æŠžã™ã‚‹" + confirmRegisterEmojisDescription: "リストã«è¡¨ç¤ºã•れã¦ã„る絵文å—ã‚’æ–°ãŸãªã‚«ã‚¹ã‚¿ãƒ 絵文å—ã¨ã—ã¦ç™»éŒ²ã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿï¼ˆè² è·ã‚’é¿ã‘ã‚‹ãŸã‚ã€ä¸€åº¦ã®æ“作ã§ç™»éŒ²å¯èƒ½ãªçµµæ–‡å—ã¯{count}ä»¶ã¾ã§ã§ã™ï¼‰" + confirmClearEmojisDescription: "ç·¨é›†å†…å®¹ã‚’ç ´æ£„ã—ã€ãƒªã‚¹ãƒˆã«è¡¨ç¤ºã•れã¦ã„る絵文å—をクリアã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" + confirmUploadEmojisDescription: "ドラッグ&ドãƒãƒƒãƒ—ã•れãŸ{count}個ã®ãƒ•ァイルをドライブã«ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ" + _embedCodeGen: title: "埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’カスタマイズ" header: "ヘッダーを表示" @@ -2826,3 +2897,36 @@ _selfXssPrevention: _followRequest: recieved: "å—ã‘å–ã£ãŸç”³è«‹" sent: "é€ã£ãŸç”³è«‹" + +_remoteLookupErrors: + _federationNotAllowed: + title: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¨ã¯é€šä¿¡ã§ãã¾ã›ã‚“" + description: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¨ã®é€šä¿¡ãŒç„¡åŠ¹åŒ–ã•れã¦ã„ã‚‹ã‹ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’ブãƒãƒƒã‚¯ã—ã¦ã„る・ブãƒãƒƒã‚¯ã•れã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚\nサーãƒãƒ¼ç®¡ç†è€…ã«ãŠå•ã„åˆã‚ã›ãã ã•ã„。" + _uriInvalid: + title: "URIãŒä¸æ£ã§ã™" + description: "入力ã•れãŸURIã«å•題ãŒã‚りã¾ã™ã€‚URIã«ä½¿ç”¨ã§ããªã„æ–‡å—を入力ã—ã¦ã„ãªã„ã‹ç¢ºèªã—ã¦ãã ã•ã„。" + _requestFailed: + title: "リクエストã«å¤±æ•—ã—ã¾ã—ãŸ" + description: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¨ã®é€šä¿¡ã«å¤±æ•—ã—ã¾ã—ãŸã€‚相手サーãƒãƒ¼ãŒãƒ€ã‚¦ãƒ³ã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚ã¾ãŸã€ä¸æ£ãªURIã‚„å˜åœ¨ã—ãªã„URIを入力ã—ã¦ã„ãªã„ã‹ç¢ºèªã—ã¦ãã ã•ã„。" + _responseInvalid: + title: "レスãƒãƒ³ã‚¹ãŒä¸æ£ã§ã™" + description: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¨é€šä¿¡ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã—ãŸãŒã€å¾—られãŸãƒ‡ãƒ¼ã‚¿ãŒä¸æ£ãªã‚‚ã®ã§ã—ãŸã€‚" + _responseInvalidIdHostNotMatch: + description: "入力ã•れãŸURIã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã¨æœ€çµ‚çš„ã«å¾—られãŸURIã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã¨ãŒç•°ãªã‚Šã¾ã™ã€‚第三者ã®ã‚µãƒ¼ãƒãƒ¼ã‚’介ã—ã¦ãƒªãƒ¢ãƒ¼ãƒˆã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を照会ã—ã¦ã„ã‚‹å ´åˆã¯ã€ç™ºä¿¡å…ƒã®ã‚µãƒ¼ãƒãƒ¼ã§å–å¾—ã§ãã‚‹URIを使用ã—ã¦ç…§ä¼šã—ç›´ã—ã¦ãã ã•ã„。" + _noSuchObject: + title: "見ã¤ã‹ã‚Šã¾ã›ã‚“" + description: "è¦æ±‚ã•れãŸãƒªã‚½ãƒ¼ã‚¹ã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚URIã‚’ã‚‚ã†ä¸€åº¦ãŠç¢ºã‹ã‚ãã ã•ã„。" + +_captcha: + verify: "CAPTCHAを通éŽã—ã¦ãã ã•ã„" + testSiteKeyMessage: "サイトã‚ーã¨ã‚·ãƒ¼ã‚¯ãƒ¬ãƒƒãƒˆã‚ーã«ãƒ†ã‚¹ãƒˆç”¨ã®å€¤ã‚’入力ã™ã‚‹ã“ã¨ã§ãƒ—レビューを確èªã§ãã¾ã™ã€‚\n詳細ã¯ä¸‹è¨˜ãƒšãƒ¼ã‚¸ã‚’ã”確èªãã ã•ã„。" + _error: + _requestFailed: + title: "CAPTCHAã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¤±æ•—ã—ã¾ã—ãŸ" + text: "ã—ã°ã‚‰ã後ã«å®Ÿè¡Œã™ã‚‹ã‹ã€è¨å®šã‚’ã‚‚ã†ä¸€åº¦ã”確èªãã ã•ã„。" + _verificationFailed: + title: "CAPTCHAã®æ¤œè¨¼ã«å¤±æ•—ã—ã¾ã—ãŸ" + text: "è¨å®šãŒæ£ã—ã„ã‹ã©ã†ã‹ã‚‚ã†ä¸€åº¦ç¢ºèªãã ã•ã„。" + _unknown: + title: "CAPTCHAエラー" + text: "想定外ã®ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index c3e0096926..2dd2220791 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -15,7 +15,7 @@ forgotPassword: "パスワード忘れãŸã‚“?" fetchingAsApObject: "今ã¡ã¨é€£åˆã«ç…§ä¼šã—ã¨ã‚‹ã§" ok: "ãˆãˆã§" gotIt: "ã»ã„" -cancel: "ã‚„ã‚ã¨ã" +cancel: "ã‚„ã‚ã‚‹" noThankYou: "ã‚„ã‚ã¨ã" enterUsername: "ユーザーåを入れã¦ã‚„" renotedBy: "{user}ãŒãƒªãƒŽãƒ¼ãƒˆã—ãŸã§" @@ -26,7 +26,7 @@ settings: "è¨å®š" notificationSettings: "通知ã®è¨å®š" basicSettings: "基本è¨å®š" otherSettings: "ã»ã‹ã®è¨å®š" -openInWindow: "ウィンドウã§é–‹ãã§" +openInWindow: "ウィンドウã§é–‹ã" profile: "プãƒãƒ•ィール" timeline: "タイムライン" noAccountDescription: "自己紹介食ã£ã¦ã‚‚ãŸ" @@ -45,7 +45,7 @@ favorited: "ãŠæ°—ã«å…¥ã‚Šã«å…¥ã‚ŒãŸã§ã€‚" alreadyFavorited: "ã‚‚ã†ãŠæ°—ã«å…¥ã‚Šã«å…¥ã‚Œã¨ã‚‹ãŒãªã€‚" cantFavorite: "アカンã€ãŠæ°—ã«å…¥ã‚Šã«å…¥ã‚Œã‚Œã‚“ã‹ã£ãŸã‚。" pin: "ピン留ã‚ã—ã¨ã" -unpin: "ã‚„ã£ã±ãƒ”ン留ã‚ã›ã‚“" +unpin: "ピン留ã‚ã‚„ã‚ã‚‹" copyContent: "内容をコピー" copyLink: "リンクをコピー" copyLinkRenote: "リノートã®ãƒªãƒ³ã‚¯ã‚’コピーã™ã‚‹ã§ï¼Ÿ" @@ -63,7 +63,7 @@ copyFileId: "ファイルIDをコピー" copyFolderId: "フォルダーIDをコピー" copyProfileUrl: "プãƒãƒ•ィールURLをコピー" searchUser: "ユーザーを探ã™" -searchThisUsersNotes: "ユーザーã®ãƒŽãƒ¼ãƒˆã‚’検索" +searchThisUsersNotes: "ユーザーã®ãƒŽãƒ¼ãƒˆã‚’探ã™" reply: "返事" loadMore: "ã¾ã ã¾ã ã‚ã‚‹ã§ï¼" showMore: "ã¾ã ã¾ã ã‚ã‚‹ã§ï¼" @@ -138,8 +138,8 @@ reactionSettingDescription2: "ドラッグã§ä¸¦ã³æ›¿ãˆã€ã‚¯ãƒªãƒƒã‚¯ã§å‰Šé™ rememberNoteVisibility: "公開範囲覚ãˆã¨ã„ã¦" attachCancel: "ã®ã£ã‘ã‚‹ã®ã‚„ã‚ã‚‹" deleteFile: "ファイルをã»ã‹ã™" -markAsSensitive: "ã¡ã‚‡ã£ã¨ã“れã¯ã‚¢ã‚«ãƒ³" -unmarkAsSensitive: "ãã“ã¾ã§ã‚¢ã‚«ãƒ³ã“ã¨ãªã„ã‚„ã‚" +markAsSensitive: "ã¡ã‚‡ã£ã¨è¦‹ã›ã‚‰ã‚Œã¸ã‚“ã‚" +unmarkAsSensitive: "別ã«ãˆãˆã‚“ã˜ã‚ƒã?" enterFileName: "ファイルåを入れã¦ã‚„" mute: "ミュート" unmute: "ミュートやã‚ãŸã‚‹" @@ -152,13 +152,13 @@ unsuspend: "溶ã‹ã™" blockConfirm: "ブãƒãƒƒã‚¯ã—ã¦ã‚‚ãˆãˆã‚“ã‹ï¼Ÿ" unblockConfirm: "ブãƒãƒƒã‚¯ã‚„ã‚ãŸã‚‹ã£ã¦ã»ã‚“ã¾ã‹ï¼Ÿ" suspendConfirm: "å‡çµã—ã¦ã—ã‚‚ã†ã¦ãˆãˆã‹ï¼Ÿ" -unsuspendConfirm: "è§£å‡ã™ã‚‹ã‘ã©ãˆãˆã‹ï¼Ÿ" +unsuspendConfirm: "溶ã‹ã—ãŸã‚‹ã‘ã©ãˆãˆã‹ï¼Ÿ" selectList: "リストをé¸ã¶" editList: "リストã„ã˜ã‚‹" selectChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã‚’é¸ã¶" selectAntenna: "アンテナをé¸ã¶" editAntenna: "アンテナã„ã˜ã‚‹" -createAntenna: "アンテナを作æˆ" +createAntenna: "アンテナを作る" selectWidget: "ウィジェットをé¸ã¶" editWidgets: "ウィジェットをã„ã˜ã‚‹" editWidgetsExit: "ã„ã˜ã‚‹ã®ã‚’ã‚„ã‚ã‚‹" @@ -172,12 +172,12 @@ settingGuide: "ãˆãˆæ„Ÿã˜ã®è¨å®š" cacheRemoteFiles: "リモートã®ãƒ•ァイルをã‚ャッシュã™ã‚‹" cacheRemoteFilesDescription: "ã“ã®è¨å®šã‚’入れã¨ã£ãŸã‚‰ã€ãƒªãƒ¢ãƒ¼ãƒˆã®ãƒ•ァイルを端ã‹ã‚‰ç«¯ã¾ã§ã“ã®ã‚µãƒ¼ãƒãƒ¼ã®ã‚ャッシュんä¸çªã£è¾¼ã‚€ã‚ˆã†ã«ãªã‚‹ã§ã€‚ç”»åƒæ˜ ã—出ã™ã‚“ãŒã‚ã£ã¡ã‚ƒé€Ÿã†ãªã‚‹ã‘ã©ã€ã‚µãƒ¼ãƒãƒ¼ã®å®¹é‡ã‚’ã‚„ãŸã‚‰ã¨é£Ÿã†ã‚ˆã†ã«ãªã‚‹ã§ã€‚リモートã®äººãŒã©ã‚“ã ã‘é•·ãã‚ャッシュをæŒã£ã¨ãã‹ã¯ãƒ‰ãƒ©ã‚¤ãƒ–容é‡ã®åˆ¶é™ã§æ±ºã‚ã¨ãã§ã€‚制é™ã‚’è¶…ãˆãŸã‚‰å¤ã„ã®ã‹ã‚‰é †ã€…ã«æ¶ˆã—ã¦ã£ã¦ã€ã‹ã‚りã«ãƒªãƒ³ã‚¯ã«ãªã‚‹ã§ã€‚ã“ã®è¨å®šã‚’切ã£ãŸã‚‰ã€ãƒªãƒ¢ãƒ¼ãƒˆã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯æœ€åˆã£ã‹ã‚‰ãƒªãƒ³ã‚¯ã¨ã—ã¦æ‰±ã†ã“ã¨ã«ã™ã‚‹ã‘ã©ã€ç”»åƒã®ã‚µãƒ ãƒä½œã‚‹ã®ã¨ã‹ã¿ã‚“ãªã®ãƒ—ライãƒã‚·ãƒ¼å®ˆã‚‹ãŸã‚ã«ã€default.ymlã®proxyRemoteFilesã‚’trueã«ã—ã¨ã„ãŸã»ã†ãŒãˆãˆã‚ˆã€‚" youCanCleanRemoteFilesCache: "ファイル管ç†ã«ã‚る🗑ï¸ãƒœã‚¿ãƒ³ã§ã‚ャッシュ全部ã»ã‹ã™ã§ã€‚" -cacheRemoteSensitiveFiles: "リモートã®ãã‚ã©ã„ファイルをã‚ャッシュã«çªã£è¾¼ã‚€" +cacheRemoteSensitiveFiles: "リモートã®ãã‚ã©ã„ファイルをã‚ャッシュã™ã‚‹" cacheRemoteSensitiveFilesDescription: "ã“ã®è¨å®šã‚’切るã¨ã€ãƒªãƒ¢ãƒ¼ãƒˆã®ãã‚ã©ã„ファイルã¯ã‚ャッシュã›ãšç›´ã§ãƒªãƒ³ã‚¯ã™ã‚‹ã‚ˆã†ã«ãªã‚‹ã§ã€‚" flagAsBot: "Botã«ã™ã‚‹ã§" flagAsBotDescription: "ã‚‚ã—ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’プãƒã‚°ãƒ©ãƒ 使ã†ã¦é‹ç”¨ã™ã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ã“ã®ãƒ•ラグをオンã«ã—ã¦ã‚„。オンã«ã™ã‚Œã°ã€å応ãŒãƒãƒ¼ãƒƒã¦é€£éŽ–ã›ã‚“よã†ã«é–‹ç™ºè€…ãŒä½¿ã†ãŸã‚Šã€Misskeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã‚“ã«ãªã‚‹ã‹ã‚‰ãªã€‚" flagAsCat: "猫や。ã‹ã‚ãˆãˆãªã€‚" -flagAsCatDescription: "ãƒã‚³ã«ãªã‚ŠãŸã„ã‚“ãªã‚‰ã“れã¤ã‘ã¨ã。" +flagAsCatDescription: "猫ã«ãªã‚ŠãŸã„ã‚“ãªã‚‰ã“れã¤ã‘ã¨ã。" flagShowTimelineReplies: "タイムラインã«ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã™ã‚‹ã§" flagShowTimelineRepliesDescription: "オンã«ã—ãŸã‚‰ã€ã‚¿ã‚¤ãƒ ラインã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆã®ä»–ã«ã‚‚ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ä»–ã®ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã™ã‚‹ã§ã€‚" autoAcceptFollowed: "フォãƒãƒ¼ã—ã¨ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’勿‰‹ã«è¨±å¯ã—ã¨ã" @@ -186,9 +186,9 @@ reloadAccountsList: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãƒªã‚¹ãƒˆã®æƒ…å ±ã‚’æ›´æ–°" loginFailed: "ãƒã‚°ã‚¤ãƒ³ã«å¤±æ•—ã—ã¦ã‚‚ã†ãŸâ€¦" showOnRemote: "リモートã§è¦‹ã‚‹" continueOnRemote: "リモートã§ç¶šè¡Œ" -chooseServerOnMisskeyHub: "Misskey Hubã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼ã‚’é¸æŠž" +chooseServerOnMisskeyHub: "Misskey Hubã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼ã‚’é¸ã¶" specifyServerHost: "サーãƒãƒ¼ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã‚’直接指定" -inputHostName: "ドメインを入力ã›ãˆã‚„" +inputHostName: "ドメインを入力ã—ã¦ã‚„" general: "全般" wallpaper: "å£ç´™" setWallpaper: "å£ç´™ã‚’è¨å®š" @@ -586,6 +586,7 @@ masterVolume: "全体ã®ã‚„ã‹ã¾ã—ã•" notUseSound: "音出ã•ã¸ã‚“" useSoundOnlyWhenActive: "MisskeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã¨ãã ã‘音出ã™" details: "ã‚‚ã£ã¨" +renoteDetails: "リノートã®è©³ç´°" chooseEmoji: "絵文å—ã‚’é¸ã¶" unableToProcess: "ãªã‚“ã‹å¥¥ã®æ–¹ã§è©°ã¾ã£ã¦ã‚‚ã†ãŸ" recentUsed: "最近使ã£ãŸã‚„ã¤" @@ -946,6 +947,9 @@ oneHour: "1時間" oneDay: "1æ—¥" oneWeek: "1週間" oneMonth: "1ヶ月" +threeMonths: "3ヶ月" +oneYear: "1å¹´" +threeDays: "3æ—¥" reflectMayTakeTime: "åæ˜ ã•れるã¾ã§æ™‚é–“ãŒã‹ã‹ã‚‹ã“ã¨ãŒã‚ã‚‹ã§" failedToFetchAccountInformation: "アカウントã®å–å¾—ã«å¤±æ•—ã—ãŸã¿ãŸã„や…" rateLimitExceeded: "レート制é™ãŒè¶…ãˆãŸã¿ãŸã„ã‚„ã§" @@ -1292,6 +1296,23 @@ prohibitedWordsForNameOfUser: "ç¦æ¢ãƒ¯ãƒ¼ãƒ‰ï¼ˆãƒ¦ãƒ¼ã‚¶ãƒ¼å)" prohibitedWordsForNameOfUserDescription: "ã“ã®ãƒªã‚¹ãƒˆã®ä¸ã«ã‚ã‚‹æ–‡å—列ãŒãƒ¦ãƒ¼ã‚¶ãƒ¼åã«å…¥ã£ã¨ã£ãŸã‚‰ã€ãã®åå‰ã«å¤‰æ›´ã§ãã²ã‚“よã†ã«ãªã‚‹ã§ã€‚モデレーター権é™ãŒã‚るユーザーã¯é™¤å¤–や。" yourNameContainsProhibitedWords: "ãã®åå‰ã¯ç¦æ¢ã—ãŸæ–‡å—列ãŒå«ã¾ã‚Œã¨ã‚‹ã§" yourNameContainsProhibitedWordsDescription: "ãã®åå‰ã¯ç¦æ¢ã—ãŸæ–‡å—列ãŒå«ã¾ã‚Œã¨ã‚‹ã‚。ã©ã†ã—ã¦ã‚‚ã£ã¦è¨€ã†ãªã‚‰ã€ã‚µãƒ¼ãƒãƒ¼ç®¡ç†è€…ã«è¨€ã†ã—ã‹ãªã„ã§ã€‚" +thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者ãŒã€è¡¨ç¤ºã«ãƒã‚°ã‚¤ãƒ³ãŒè¦ã‚‹ã£ã¦è¨å®šã—ã¦ã‚‹ã§" +lockdown: "ãƒãƒƒã‚¯ãƒ€ã‚¦ãƒ³" +pleaseSelectAccount: "アカウントé¸ã‚“ã§ã‚„" +availableRoles: "使ãˆã‚‹ãƒãƒ¼ãƒ«" +acknowledgeNotesAndEnable: "注æ„äº‹é …ã‚’ã‚ã‹ã£ãŸä¸Šã§ã‚ªãƒ³ã«ã™ã‚‹ã€‚" +_accountSettings: + requireSigninToViewContents: "ãƒã‚°ã‚¤ãƒ³ã—ã¦ã‚‚らã£ã¦ã‹ã‚‰ã‚³ãƒ³ãƒ†ãƒ³ãƒ„見ã¦ã‚‚らã†" + requireSigninToViewContentsDescription1: "ã‚ãªãŸãŒä½œæˆã—ãŸå…¨éƒ¨ã®ãƒŽãƒ¼ãƒˆã¨ã‹ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を見れるよã†ã«ã™ã‚‹ã®ã«ãƒã‚°ã‚¤ãƒ³ãŒã„るよã†ã«ã™ã‚‹ã§ã€‚クãƒãƒ¼ãƒ©ãƒ¼ã«ã„ã‚ã„ã‚åŽé›†ã•れるんを防ã’ã‚‹ã‹ã‚‚ã—れん。" + requireSigninToViewContentsDescription2: "URLプレビュー(OGP)ã€Webページã¸ã®åŸ‹ã‚è¾¼ã¿ã€ãƒŽãƒ¼ãƒˆã®å¼•用ã«å¯¾å¿œã—ã¦ãªã„サーãƒãƒ¼ã‹ã‚‰ã®è¡¨ç¤ºãŒã§ãã‚“ããªã‚‹ã§ã€‚" + requireSigninToViewContentsDescription3: "リモートサーãƒãƒ¼ã«é€£åˆã•れãŸã‚³ãƒ³ãƒ†ãƒ³ãƒ„ã¯ã€ã“れらã®åˆ¶é™ãŒé©ç”¨ã•れんã‹ã‚‚ã—れんã§ã€‚" + makeNotesFollowersOnlyBefore: "昔ã®ãƒŽãƒ¼ãƒˆã‚’フォãƒãƒ¯ãƒ¼ã ã‘ã«è¦‹ã¦ã‚‚らã†" + makeNotesFollowersOnlyBeforeDescription: "ã“ã®æ©Ÿèƒ½ãŒæœ‰åйã«ãªã£ã¦ã‚‹é–“ã¯ã€è¨å®šã•ã‚ŒãŸæ—¥æ™‚よりå‰ã€ãれã‹è¨å®šã•ã‚ŒãŸæ™‚é–“ãŒçµŒã£ãŸãƒŽãƒ¼ãƒˆãŒãƒ•ã‚©ãƒãƒ¯ãƒ¼ã®ã¿è¦‹ã‚Œã‚‹ã‚ˆã†ã«ãªã‚‹ã§ã€‚ç„¡åŠ¹ã«æˆ»ã™ã¨ã€ãƒŽãƒ¼ãƒˆã®å…¬é–‹çŠ¶æ…‹ã‚‚æˆ»ã‚‹ã§ã€‚" + makeNotesHiddenBefore: "昔ã®ãƒŽãƒ¼ãƒˆã‚’見れんよã†ã«ã™ã‚‹" + makeNotesHiddenBeforeDescription: "ã“ã®æ©Ÿèƒ½ãŒæœ‰åйã«ãªã£ã¦ã‚‹é–“ã¯ã€è¨å®šã•ã‚ŒãŸæ—¥æ™‚よりå‰ã€ãれã‹è¨å®šã•ã‚ŒãŸæ™‚é–“ãŒçµŒã£ãŸãƒŽãƒ¼ãƒˆãŒãƒ•ã‚©ãƒãƒ¯ãƒ¼ã®ã¿è¦‹ã‚Œã‚‹ã‚ˆã†ã«ãªã‚‹ã§ã€‚ç„¡åŠ¹ã«æˆ»ã™ã¨ã€ãƒŽãƒ¼ãƒˆã®å…¬é–‹çŠ¶æ…‹ã‚‚æˆ»ã‚‹ã§ã€‚" + mayNotEffectForFederatedNotes: "リモートサーãƒãƒ¼ã«é€£åˆã•れãŸãƒŽãƒ¼ãƒˆã«ã¯åŠ¹æžœãŒåŠã°ã‚“ã‹ã‚‚ã—れん。" + notesHavePassedSpecifiedPeriod: "決ã‚ãŸæ™‚é–“ãŒçµŒã£ãŸãƒŽãƒ¼ãƒˆ" + notesOlderThanSpecifiedDateAndTime: "決ã‚ãŸæ—¥æ™‚よりå‰ã®ãƒŽãƒ¼ãƒˆ" _abuseUserReport: forward: "転é€" forwardDescription: "匿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã£ã¦ã“ã¨ã«ã—ã¦ã€ãƒªãƒ¢ãƒ¼ãƒˆã‚µãƒ¼ãƒãƒ¼ã«é€šå ±ã‚’転é€ã™ã‚‹ã§ã€‚" @@ -1436,6 +1457,8 @@ _serverSettings: reactionsBufferingDescription: "有効ã«ã—ãŸã‚‰ã€ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ä½œã‚‹ã¨ãã®ãƒ‘フォーマンスãŒã™ã£ã”ã„上ãŒã£ã¦ã€ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã¸ã®è² è·ãŒæ¸›ã‚‹ã§ã€‚代ã‚りã«ã€Redisã®ãƒ¡ãƒ¢ãƒªä½¿ç”¨ã¯å¢—ãˆã‚‹ã§ã€‚" inquiryUrl: "å•ã„åˆã‚ã›å…ˆURL" inquiryUrlDescription: "サーãƒãƒ¼é‹å–¶è€…ã¸ã®ãŠå•ã„åˆã‚ã›ãƒ•ォームã®URLã‚„ã€é‹å–¶è€…ã®é€£çµ¡å…ˆç‰ãŒè¨˜è¼‰ã•れãŸWebページã®URLを指定ã™ã‚‹ã§ã€‚" + openRegistration: "アカウントã®ä½œæˆã‚’オープンã«ã™ã‚‹" + openRegistrationWarning: "登録を解放ã™ã‚‹ã®ã¯ãƒªã‚¹ã‚¯ãŒä¼´ã†ã§ã€‚サーãƒãƒ¼ã‚’ã„ã£ã¤ã‚‚監視ã—ã¦ã€ãªã‚“ã‹èµ·ããŸã‚‰ã™ãã«å¯¾å¿œã§ãã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ã‚ªãƒ³ã«ã—ã¦ã‚‚ãˆãˆã¨æ€ã†ã€‚" thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターãŒãŠã‚‰ã‚“ã‹ã£ãŸã‚‰ã€ã‚¹ãƒ‘ムを防ããŸã‚ã«ã“ã®è¨å®šã¯å‹æ‰‹ã«åˆ‡ã‚‰ã‚Œã‚‹ã§ã€‚" _accountMigration: moveFrom: "別ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«å¼•ã£è¶Šã™" @@ -2156,8 +2179,11 @@ _auth: permissionAsk: "ã“ã®ã‚¢ãƒ—ãƒªã¯æ¬¡ã®æ¨©é™ã‚’è¦æ±‚ã—ã¨ã‚‹ã§" pleaseGoBack: "ã‚¢ãƒ—ãƒªã‚±ãƒ¼ã‚·ãƒ§ãƒ³ã«æˆ»ã£ã¦ãˆãˆã‚ˆ" callback: "ã‚¢ãƒ—ãƒªã‚±ãƒ¼ã‚·ãƒ§ãƒ³ã«æˆ»ã£ã¨ã‚‹ã§" + accepted: "アクセスを許å¯ã—ãŸã§" denied: "アクセスを拒å¦ã£ãŸã§" + scopeUser: "以下ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨ã—ã¦ã„ã˜ã£ã¦ã‚‹ã§" pleaseLogin: "アプリã«ã‚¢ã‚¯ã‚»ã‚¹ã•ã›ã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ãƒã‚°ã‚¤ãƒ³ã—ã¦ã‚„。" + byClickingYouWillBeRedirectedToThisUrl: "アクセスを許ã—ãŸã‚‰ã€è‡ªå‹•ã§ä¸‹ã®URLã«é·ç§»ã™ã‚‹ã§" _antennaSources: all: "ã¿ã‚“ãªã®ãƒŽãƒ¼ãƒˆ" homeTimeline: "フォãƒãƒ¼ã—ã¨ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆ" @@ -2709,3 +2735,30 @@ _embedCodeGen: generateCode: "埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ä½œã‚‹" codeGenerated: "コード作ã£ãŸã§" codeGeneratedDescription: "作ã£ãŸã‚³ãƒ¼ãƒ‰ã¯ã‚¦ã‚§ãƒ–サイトã«è²¼ã£ã¤ã‘ã¦ä½¿ã£ã¦ã‚„。" +_selfXssPrevention: + warning: "è¦å‘Š" + title: "「ã“ã®ç”»é¢ã«ãªã‚“ã‹è²¼ã‚Šä»˜ã‘ã‚ã€ã¯å…¨éƒ¨è©æ¬ºã‚„ã§ã€‚" + description1: "ã“ã“ã«ãªã‚“ã‹ã¯ã¤ã£ã¤ã‘ã‚‹ã¨ã€æ‚ªã„ユーザーã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆä¹—ã£å–られãŸã‚Šã€å€‹äººæƒ…å ±ç›—ã¾ã‚ŒãŸã‚Šã™ã‚‹ã‹ã‚‚ã‚„ã§" + description2: "ã¯ã£ã¤ã‘よã†ã¨ã—ã¦ã‚‹ã‚‚ã®ãŒãªã‚“ãªã‚“ã‹ã‚ã‹ã‚‰ã‚“ã®ã‚„ã£ãŸã‚‰ã€%c今ã™ã作æ¥ã‚„ã‚ã¦ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã‚’é–‰ã˜ã¦ã€‚" + description3: "詳ã—ãã¯ã“れを見ã¦ã€‚{link}" +_followRequest: + recieved: "もらã£ãŸç”³è«‹" + sent: "é€ã£ãŸç”³è«‹" +_remoteLookupErrors: + _federationNotAllowed: + title: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¨é€šä¿¡ã§ãã‚“" + description: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¨ã®é€šä¿¡ã¯ç„¡åŠ¹åŒ–ã•れã¦ã‚‹ã‹ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’ブãƒãƒƒã‚¯ã—ã¦ã‚‹ã‚“ã‹ã€ãƒ–ãƒãƒƒã‚¯ã•れã¦ã‚‹ã‹ã‚‚ã—れん。\nサーãƒãƒ¼ç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ã‚„。" + _uriInvalid: + title: "URIãŒãŠã‹ã—ã„ã§" + description: "入力ã•れãŸURIã«å•題ãŒã‚ã‚‹ã§ã€‚URIã«ä½¿ãˆã‚“æ–‡å—を入れã¦ãªã„ã‹ã‚‰ç¢ºã‹ã‚ã¦ã€‚" + _requestFailed: + title: "リクエスト失敗ã—ã¦ã‚‚ã†ãŸã§" + description: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¨ã®é€šä¿¡ã«å¤±æ•—ã—ã¦ã‚‚ã†ãŸã‚。相手サーãƒãƒ¼ãŒãƒ€ã‚¦ãƒ³ã—ã¦ã‚‹ã‹ã‚‚ã—れん。ã‚ã¨ã€ãŠã‹ã—ã„URIã¨ã‹ã€ã‚りãˆã‚“URIを入れã¦ãªã„ã‹ç¢ºã‹ã‚ã¦ã€‚" + _responseInvalid: + title: "レスãƒãƒ³ã‚¹ãŒãŠã‹ã—ã„ã§" + description: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¨é€šä¿¡ã™ã‚‹ã“ã¨ã¯ã§ããŸã‘ã©ã€ã‚‚らã£ãŸãƒ‡ãƒ¼ã‚¿ãŒãŠã‹ã—ã‹ã£ãŸã§ã€‚" + _responseInvalidIdHostNotMatch: + description: "入力ã•れãŸURIã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã¨æœ€çµ‚çš„ã«å¾—られãŸURIã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã¨ãŒé•ã†ã§ã€‚第三者ã®ã‚µãƒ¼ãƒãƒ¼ã‚’介ã—ã¦ãƒªãƒ¢ãƒ¼ãƒˆã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を照会ã—ã¦ã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ç™ºä¿¡å…ƒã®ã‚µãƒ¼ãƒãƒ¼ã§å–å¾—ã§ãã‚‹URIを使ã£ã¦ç…§ä¼šã—ç›´ã—ã¦ã€‚" + _noSuchObject: + title: "見ã¤ã‹ã‚‰ã¸ã‚“ã" + description: "求ã‚られãŸãƒªã‚½ãƒ¼ã‚¹ãŒè¦‹ã¤ã‹ã‚‰ã‚“ã‹ã£ãŸã§ã€‚URIã‚’ã‚‚ã£ã‹ã„確ã‹ã‚ã¦ã‚„。" diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index 60b82d5db9..4b9650b636 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -840,3 +840,6 @@ _reversi: black: "꺼ë©" white: "í—ˆì˜" total: "합게" +_remoteLookupErrors: + _noSuchObject: + title: "몬 찾앗ì‹ë‹ˆë‹¤" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index d694d2dbae..45d7f26075 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -5,6 +5,7 @@ introMisskey: "환ì˜í•©ë‹ˆë‹¤! Misskey는 오픈 소스 분산형 마ì´í¬ë¡œ poweredByMisskeyDescription: "{name} 서버는 오픈소스 í”Œëž«í¼ <b>Misskey</b>ì˜ ì„œë²„ ê°€ìš´ë° í•˜ë‚˜ìž…ë‹ˆë‹¤." monthAndDay: "{month}ì›” {day}ì¼" search: "검색" +reset: "초기화" notifications: "알림" username: "ìœ ì €ëª…" password: "비밀번호" @@ -48,6 +49,7 @@ pin: "í”„ë¡œí•„ì— ê³ ì •" unpin: "프로필ì—서 ê³ ì • í•´ì œ" copyContent: "ë‚´ìš© 복사" copyLink: "ë§í¬ 복사" +copyRemoteLink: "리모트 ì„œë²„ì˜ ë§í¬ë¡œ 복사하기" copyLinkRenote: "리노트 ë§í¬ 복사" delete: "ì‚ì œ" deleteAndEdit: "ì‚ì œ 후 편집" @@ -684,11 +686,15 @@ smtpSecure: "SMTP ì—°ê²°ì— Implicit SSL/TTS 사용" smtpSecureInfo: "STARTTLS 사용 시ì—는 í•´ì œí•©ë‹ˆë‹¤." testEmail: "ì´ë©”ì¼ ì „ì†¡ 테스트" wordMute: "단어 뮤트" +wordMuteDescription: "ì •í•´ì§„ 단어가 í¬í•¨ëœ 노트를 최소화 한 ìƒíƒœë¡œ 표시합니다. 최소화 ëœ ë…¸íŠ¸ëŠ” í´ë¦í•´ì„œ í‘œì‹œí• ìˆ˜ 있습니다." hardWordMute: "하드 단어 뮤트" +showMutedWord: "뮤트한 단어를 표시하기" +hardWordMuteDescription: "ì •í•œ 단어가 들어간 노트를 숨ê¹ë‹ˆë‹¤. 단어 뮤트와 ì°¨ì´ì ì€ ë…¸íŠ¸ê°€ 아예 ë³´ì´ì§€ 않습니다." regexpError: "ì •ê·œ í‘œí˜„ì‹ ì˜¤ë¥˜" regexpErrorDescription: "{tab}단어 뮤트 {line}í–‰ì˜ ì •ê·œ 표현ì‹ì— 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤:" instanceMute: "서버 뮤트" userSaysSomething: "{name}ë‹˜ì´ ë¬´ì–¸ê°€ë¥¼ ë§í–ˆìŠµë‹ˆë‹¤" +userSaysSomethingAbout: "{name}ë‹˜ì´ \"{word}\"를 언급했습니다." makeActive: "활성화" display: "보기" copy: "복사" @@ -1277,7 +1283,7 @@ confirmWhenRevealingSensitiveMedia: "민ê°í•œ 미디어를 ì—´ 때 ë‘ ë²ˆ 확ì sensitiveMediaRevealConfirm: "민ê°í•œ 미디어입니다. í‘œì‹œí• ê¹Œìš”?" createdLists: "ë§Œë“ ë¦¬ìŠ¤íŠ¸" createdAntennas: "ë§Œë“ ì•ˆí…Œë‚˜" -fromX: "{x}부터" +fromX: "{x}ì—서" genEmbedCode: "ìž„ë² ë””ë“œ 코드 만들기" noteOfThisUser: "ì´ ìœ ì €ì˜ ë…¸íŠ¸ 목ë¡" clipNoteLimitExceeded: "ë” ì´ìƒ ì´ í´ë¦½ì— 노트를 추가 í• ìˆ˜ 없습니다." @@ -1301,13 +1307,15 @@ lockdown: "ìž ê¸ˆ" pleaseSelectAccount: "ê³„ì •ì„ ì„ íƒí•´ì£¼ì„¸ìš”." availableRoles: "사용 가능한 ì—í• " acknowledgeNotesAndEnable: "활성화 하기 ì „ì— ì£¼ì˜ ì‚¬í•ì„ í™•ì¸í–ˆìŠµë‹ˆë‹¤." +federationSpecified: "ì´ ì„œë²„ëŠ” í™”ì´íЏ 리스트 ì œë„로 ìš´ì˜ ì¤‘ 입니다. ì •í•´ì§„ 리모트 서버가 아닌 경우 ì—°í•©ë˜ì§€ 않습니다." +federationDisabled: "ì´ ì„œë²„ëŠ” ì—°í•©ì„ í•˜ì§€ ì•Šê³ ìžˆìŠµë‹ˆë‹¤. 리모트 서버 ìœ ì €ì™€ í†µì‹ ì„ í• ìˆ˜ 없습니다." _accountSettings: - requireSigninToViewContents: "콘í…ì¸ ì—´ëžŒì„ ìœ„í•´ 로그ì¸ìœ¼ 필수로 ì„¤ì •í•˜ê¸°" + requireSigninToViewContents: "콘í…ì¸ ì—´ëžŒì„ ìœ„í•´ 로그ì¸ì„ 필수로 ì„¤ì •í•˜ê¸°" requireSigninToViewContentsDescription1: "ìžì‹ ì´ ìž‘ì„±í•œ ëª¨ë“ ë…¸íŠ¸ ë“±ì˜ ì½˜í…ì¸ ë¥¼ 보기 위해 로그ì¸ì„ 필수로 ì„¤ì •í•©ë‹ˆë‹¤. í¬ë¡¤ëŸ¬ê°€ ì •ë³´ 수집하는 ê²ƒì„ ë°©ì§€í•˜ëŠ” 효과를 ê¸°ëŒ€í• ìˆ˜ 있습니다." requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페ì´ì§€ì— 삽입, 노트 ì¸ìš©ì„ ì§€ì›í•˜ì§€ 않는 서버ì—서 ë³¼ 수 없게 ë©ë‹ˆë‹¤." requireSigninToViewContentsDescription3: "ì›ê²© ì„œë²„ì— ì—°í•©ëœ ì½˜í…ì¸ ì—는 ì´ëŸ¬í•œ ì œí•œì´ ì ìš©ë˜ì§€ ì•Šì„ ìˆ˜ 있습니다." makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 ë³¼ 수 있ë„ë¡ ì„¤ì •í•˜ê¸°" - makeNotesFollowersOnlyBeforeDescription: "ì´ ê¸°ëŠ¥ì´ í™œì„±í™”ë˜ì–´ 있는 ë™ì•ˆ, ì„¤ì •ëœ ë‚ ì§œ ë° ì‹œê°„ë³´ë‹¤ 과거 ë˜ëŠ” ì„¤ì •ëœ ì‹œê°„ì´ ì§€ë‚œ 노트는 팔로워만 ë³¼ 수 있게 ë©ë‹ˆë‹¤.비활성화하면 ë…¸íŠ¸ì˜ ê³µê°œ ìƒíƒœë„ ì›ëž˜ëŒ€ë¡œ ëŒì•„갑니다." + makeNotesFollowersOnlyBeforeDescription: "ì´ ê¸°ëŠ¥ì´ í™œì„±í™”ë˜ì–´ 있는 ë™ì•ˆ, ì„¤ì •ëœ ë‚ ì§œ ë° ì‹œê°„ë³´ë‹¤ 과거 ë˜ëŠ” ì„¤ì •ëœ ì‹œê°„ì´ ì§€ë‚œ 노트는 팔로워만 ë³¼ 수 있게 ë©ë‹ˆë‹¤. 비활성화하면 ë…¸íŠ¸ì˜ ê³µê°œ ìƒíƒœë„ ì›ëž˜ëŒ€ë¡œ ëŒì•„갑니다." makeNotesHiddenBefore: "과거 노트 비공개로 ì „í™˜í•˜ê¸°" makeNotesHiddenBeforeDescription: "ì´ ê¸°ëŠ¥ì´ í™œì„±í™”ë˜ì–´ 있는 ë™ì•ˆ ì„¤ì •í•œ ë‚ ì§œ ë° ì‹œê°„ë³´ë‹¤ 과거 ë˜ëŠ” ì„¤ì •í•œ ì‹œê°„ì´ ì§€ë‚œ 노트는 본ì¸ë§Œ ë³¼ 수 있게(비공개로 ì „í™˜) ë©ë‹ˆë‹¤. 비활성화하면 ë…¸íŠ¸ì˜ ê³µê°œ ìƒíƒœë„ ì›ëž˜ëŒ€ë¡œ ëŒì•„갑니다." mayNotEffectForFederatedNotes: "ì›ê²© ì„œë²„ì— ì—°í•©ëœ ë…¸íŠ¸ì—는 효과가 ì—†ì„ ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." @@ -2514,7 +2522,7 @@ _webhookSettings: reaction: "누군가 ë‚´ ë…¸íŠ¸ì— ë¦¬ì•¡ì…˜í–ˆì„ ë•Œ" mention: "누군가 나를 ë©˜ì…˜í–ˆì„ ë•Œ" _systemEvents: - abuseReport: "ìœ ì €ë¡" + abuseReport: "ìœ ì €ë¡œë¶€í„° ì‹ ê³ ë¥¼ ë°›ì•˜ì„ ë•Œ" abuseReportResolved: "ë°›ì€ ì‹ ê³ ë¥¼ ì²˜ë¦¬í–ˆì„ ë•Œ" userCreated: "ìœ ì €ê°€ ìƒì„±ë˜ì—ˆì„ 때" inactiveModeratorsWarning: "모ë”ë ˆì´í„°ê°€ ì¼ì • 기간ë™ì•ˆ 활ë™í•˜ì§€ ì•Šì€ ê²½ìš°" @@ -2721,6 +2729,66 @@ _contextMenu: app: "ì• í”Œë¦¬ì¼€ì´ì…˜" appWithShift: "Shift 키로 ì• í”Œë¦¬ì¼€ì´ì…˜" native: "브ë¼ìš°ì €ì˜ UI" +_gridComponent: + _error: + requiredValue: "ì´ ê°’ì€ í•„ìˆ˜ í•목입니다." + columnTypeNotSupport: "ì •ê·œí‘œí˜„ ê·œì¹™ì´ type:textì¸ ì¹¼ëŸ¼ë§Œ ì§€ì›í•©ë‹ˆë‹¤." + patternNotMatch: "ì´ ê°’ì€ {pattern} 패턴과 ì¼ì¹˜í•˜ì§€ 않습니다." + notUnique: "ì´ ê°’ì€ ë‹¤ë¥¸ ê°’ê³¼ 중복ë˜ì§€ 않아야 합니다." +_roleSelectDialog: + notSelected: "ì„ íƒí•˜ì§€ 않았습니다." +_customEmojisManager: + _gridCommon: + copySelectionRows: "ì„ íƒí•œ í–‰ì„ ë³µì‚¬í•˜ê¸°" + copySelectionRanges: "ì„ íƒë²”위를 복사하기" + deleteSelectionRows: "ì„ íƒí•œ í–‰ì„ ì‚ì œ" + deleteSelectionRanges: "ì„ íƒí•œ í–‰ì„ ì‚ì œ" + searchSettings: "검색 ì„¤ì •" + searchSettingCaption: "ê³ ê¸‰ ê²€ìƒ‰ì„ ì„¤ì •í•©ë‹ˆë‹¤." + searchLimit: "표시 건수" + sortOrder: "ì •ë ¬ 순서" + registrationLogs: "ë“±ë¡ ë¡œê·¸" + registrationLogsCaption: "ì´ëª¨ì§€ë¥¼ ê°±ì‹ í•˜ê±°ë‚˜ ì‚ì œí• ë•Œ 로그가 표시ë©ë‹ˆë‹¤. ê°±ì‹ ë˜ëŠ” ì‚ì œí•˜ê±°ë‚˜, 페ì´ì§€ ì´ë™, 새로 ê³ ì¹¨í•˜ë©´ ì‚ì œë©ë‹ˆë‹¤." + alertEmojisRegisterFailedDescription: "ì´ëª¨ì§€ë¥¼ ê°±ì‹ ë˜ëŠ” ì‚ì œí•˜ì§€ 못했습니다. ìžì„¸í•œ ë‚´ìš©ì€ ë“±ë¡ ë¡œê·¸ë¥¼ 확ì¸í•´ì£¼ì„¸ìš”." + _logs: + showSuccessLogSwitch: "성공 로그를 표시" + failureLogNothing: "실패 로그가 없습니다." + logNothing: "로그가 없습니다." + _remote: + selectionRowDetail: "ì„ íƒ í–‰ (ìƒì„¸)" + importSelectionRows: "ì„ íƒ í–‰ì„ ê°€ì ¸ì˜¤ê¸°" + importSelectionRangesRows: "ì„ íƒí•œ 범위 ì•ˆì˜ í–‰ì„ ê°€ì ¸ì˜¤ê¸°" + importEmojisButton: "ì„ íƒí•œ ì´ëª¨ì§€ë¥¼ ê°€ì ¸ì˜¤ê¸°" + confirmImportEmojisTitle: "ì´ëª¨ì§€ ê°€ì ¸ì˜¤ê¸°" + confirmImportEmojisDescription: "리모트 서버ì—서 받아온 ì´ëª¨ì§€ {count}개를 ì´ ì„œë²„ë¡œ ê°€ì ¸ì˜µë‹ˆë‹¤. ì´ëª¨ì§€ì˜ ì €ìž‘ê¶Œ, ë¼ì´ì„ 스를 확실히 확ì¸í•˜ì…¨ë‹¤ë©´ 실행해주세요." + _local: + tabTitleList: "등ë¡í•œ ì´ëª¨ì§€ 리스트" + tabTitleRegister: "ì´ëª¨ì§€ 등ë¡" + _list: + emojisNothing: "등ë¡í•œ ì´ëª¨ì§€ê°€ 없습니다." + markAsDeleteTargetRows: "ì„ íƒí•œ í–‰ì„ ì‚ì œí• ëŒ€ìƒìœ¼ë¡œ 하기" + markAsDeleteTargetRanges: "ì„ íƒí•œ ë²”ìœ„ì˜ í–‰ì„ ì‚ì œ 대ìƒìœ¼ë¡œ 하기" + alertUpdateEmojisNothingDescription: "ë³€ê²½í• ì´ëª¨ì§€ê°€ 없습니다." + alertDeleteEmojisNothingDescription: "ì‚ì œ 대ìƒì˜ ì´ëª¨ì§€ëŠ” 없습니다." + confirmMovePage: "페ì´ì§€ë¥¼ ì´ë™í• 까요?" + confirmChangeView: "표시를 바꿀까요?" + confirmUpdateEmojisDescription: "{count}ê°œì˜ ì´ëª¨ì§€ë¥¼ ê°±ì‹ í•©ë‹ˆë‹¤. ì‹¤í–‰í• ê¹Œìš”?" + confirmDeleteEmojisDescription: "ì„ íƒí•œ ì´ëª¨ì§€ {count}개를 ì‚ì œí•©ë‹ˆë‹¤. ì‹¤í–‰í• ê¹Œìš”?" + confirmResetDescription: "지금까지 í–ˆë˜ ë³€ê²½ ë‚´ìš©ì´ ëª¨ë‘ ì´ˆê¸°í™”ë©ë‹ˆë‹¤." + confirmMovePageDesciption: "ì´ íŽ˜ì´ì§€ì˜ ì´ëª¨ì§€ì— ë³€ê²½ì´ ìžˆìŠµë‹ˆë‹¤.\nì €ìž¥í•˜ì§€ ì•Šì€ ìƒíƒœë¡œ 페ì´ì§€ë¥¼ ì´ë™í•˜ë©´, ì´ íŽ˜ì´ì§€ì—서 바꾼 변경 ë‚´ìš©ì´ ëª¨ë‘ ì§€ì›Œì§‘ë‹ˆë‹¤." + dialogSelectRoleTitle: "ì´ëª¨ì§€ì— ì„¤ì •ëœ ì—í• ì„ ê²€ìƒ‰" + _register: + uploadSettingTitle: "업로드 ì„¤ì •" + uploadSettingDescription: "여기서 ì´ëª¨ì§€ë¥¼ 업로드 í• ë•Œì˜ ë™ìž‘ì„ ì„¤ì •í• ìˆ˜ 있습니다." + directoryToCategoryLabel: "ë””ë ‰í† ë¦¬ ì´ë¦„ì„ \"category\"로 ìž…ë ¥í•˜ê¸°" + directoryToCategoryCaption: "ë””ë ‰í† ë¦¬ë¥¼ 드래그 앤 드ë¡í•œ 경우, ë””ë ‰í† ë¦¬ ì´ë¦„ì„ \"category\"로 ìž…ë ¥í•©ë‹ˆë‹¤." + emojiInputAreaCaption: "ì´ëª¨ì§€ë¥¼ 등ë¡í• ë°©ë²•ì„ ì„ íƒí•´ì£¼ì„¸ìš”." + emojiInputAreaList1: "ì´ í‹€ ì•ˆì— ì´ë¯¸ì§€ íŒŒì¼ ë˜ëŠ” ë””ë ‰í† ë¦¬ë¥¼ ëŒì–´ì„œ ê°€ì ¸ì˜¤ê¸°" + emojiInputAreaList2: "ì´ ë§í¬ë¥¼ í´ë¦í•´ì„œ PCì—서 ì„ íƒí•˜ê¸°" + emojiInputAreaList3: "ì´ ë§í¬ë¥¼ í´ë¦í•´ì„œ 드ë¼ì´ë¸Œì—서 ì„ íƒí•˜ê¸°" + confirmRegisterEmojisDescription: "ë¦¬ìŠ¤íŠ¸ì— í‘œì‹œë˜ì–´ì§„ ì´ëª¨ì§€ë¥¼ 새로운 커스텀 ì´ëª¨ì§€ë¡œ 등ë¡í•©ë‹ˆë‹¤. ì‹¤í–‰í• ê¹Œìš”? (부하를 피하기 위해, 한 ë²ˆì— ë“±ë¡í• 수 있는 ì´ëª¨ì§€ëŠ” {count}건까지 입니다.)" + confirmClearEmojisDescription: "편집 ë‚´ìš©ì„ ì§€ìš°ê³ , 목ë¡ì— 표시ë˜ì–´ì§„ ì´ëª¨ì§€ë¥¼ ì§€ì›ë‹ˆë‹¤. ì‹¤í–‰í• ê¹Œìš”?" + confirmUploadEmojisDescription: "드래그 앤 드ë¡í•œ {count}ê°œì˜ íŒŒì¼ì„ 드ë¼ì´ë¸Œì— 업로드 합니다. ì‹¤í–‰í• ê¹Œìš”?" _embedCodeGen: title: "ìž„ë² ë””ë“œ 코드를 커스터마ì´ì¦ˆ" header: "í•´ë”를 표시" @@ -2744,3 +2812,34 @@ _selfXssPrevention: _followRequest: recieved: "ë°›ì€ ì‹ ì²" sent: "보낸 ì‹ ì²" +_remoteLookupErrors: + _federationNotAllowed: + title: "ì´ ì„œë²„ì™€ í†µì‹ í• ìˆ˜ ì—†ìŒ" + description: "ì´ ì„œë²„ì™€ì˜ í†µì‹ ì´ ë¹„í™œì„±í™” ë˜ì—ˆê±°ë‚˜, ì´ ì„œë²„ë¥¼ 차단 중ì´ê±°ë‚˜ 서버ì—게 차단ë˜ì—ˆì„ 수 있습니다.\n서버 관리ìžì—게 문ì˜í•˜ì„¸ìš”." + _uriInvalid: + title: "URIê°€ 잘못ë˜ì—ˆìŠµë‹ˆë‹¤." + description: "ìž…ë ¥í•œ URIì— ë¬¸ì œê°€ 있습니다. URIì— ì“¸ 수 없는 문ìžë¥¼ 넣었는지 확ì¸í•´ë³´ì„¸ìš”." + _requestFailed: + title: "ìš”ì²ì„ 실패했습니다." + description: "해당 서버와 í†µì‹ ì„ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. ìƒëŒ€ë°© ì„œë²„ì— ì ‘ì† ë¶ˆê°€ëŠ¥í•œ ìƒíƒœì¼ ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. ë˜ëŠ” ìž˜ëª»ëœ URI ë˜ëŠ” 없는 URI를 ìž…ë ¥í–ˆëŠ”ì§€ 확ì¸í•´ë³´ì„¸ìš”." + _responseInvalid: + title: "ìœ íš¨í•˜ì§€ ì•Šì€ ë°˜ì‘입니다." + description: "ì´ ì„œë²„ì™€ í†µì‹ í• ìˆ˜ 있지만, ë°ì´í„°ê°€ 올바르지 않습니다." + _responseInvalidIdHostNotMatch: + description: "ìž…ë ¥ëœ URIê³¼ ì‹¤ì œ URIê°€ 다릅니다. ì œ 3ìž ì„œë²„ë¥¼ 통한 리모트 컨í…ì¸ ë¥¼ 조회하는 경우, ì›ëž˜ 서버 측ì—서 받아올 수 있는 URI를 사용하여 조회하시길 ë°”ëžë‹ˆë‹¤." + _noSuchObject: + title: "ì°¾ì„ ìˆ˜ 없습니다" + description: "ìš”êµ¬ëœ ë¦¬ì†ŒìŠ¤ë¥¼ ì°¾ì„ ìˆ˜ 없습니다. URI를 다시 한 번 확ì¸í•´ë³´ì„¸ìš”." +_captcha: + verify: "CAPTCHA를 ë¨¼ì € 해결하세요." + testSiteKeyMessage: "사ì´íЏ 키와 비밀 í‚¤ì— í…ŒìŠ¤íŠ¸ìš© ê°’ì„ ìž…ë ¥í•˜ì—¬ 미리보기를 확ì¸í• 수 있습니다.\nìžì„¸í•œ ë‚´ìš©ì€ ì•„ëž˜ 페ì´ì§€ë¥¼ 확ì¸í•´ë³´ì„¸ìš”." + _error: + _requestFailed: + title: "CAPTCHA ìš”êµ¬ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤." + text: "ìž ì‹œ í›„ì— ë‹¤ì‹œ 실행하거나, ì„¤ì •ì„ ë‹¤ì‹œ 한 번 확ì¸í•´ë³´ì„¸ìš”." + _verificationFailed: + title: "CAPTCHA ê²€ì¦ì„ 실패했습니다." + text: "ì„¤ì •ì´ ì˜¬ë°”ë¥¸ì§€ 다시 한 번 확ì¸í•´ë³´ì„¸ìš”." + _unknown: + title: "CAPTCHA ì—러" + text: "알 수 없는 ì—러가 ë°œìƒí–ˆìŠµë‹ˆë‹¤." diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 38965119fe..2d55c289aa 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -474,3 +474,6 @@ _abuseReport: mail: "àºàºµà»€àº¡àº§" _moderationLogTypes: suspend: "ລະງັບ" +_remoteLookupErrors: + _noSuchObject: + title: "ບà»à»ˆàºžàº»àºš" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 7e5e9cbbfb..685094b4a5 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -8,6 +8,9 @@ search: "Zoeken" notifications: "Meldingen" username: "Gebruikersnaam" password: "Wachtwoord" +initialPasswordForSetup: "Initiële wachtwoord voor configuratie" +initialPasswordIsIncorrect: "Initiële wachtwoord voor configuratie is onjuist" +initialPasswordForSetupDescription: "Gebruik het initiële wachtwoord uit de configuratie, als je Misskey zelf hebt geïnstalleerd.\nAls je een Misskey hosting provider gebruikt, gebruik dan het gegeven wachtwoord.\nAls je geen wachtwoord hebt gezet, laat het dan leeg om verder te gaan." forgotPassword: "Wachtwoord vergeten" fetchingAsApObject: "Ophalen vanuit de Fediverse" ok: "Ok" @@ -108,9 +111,12 @@ enterEmoji: "Voer een emoji in" renote: "Herdelen" unrenote: "Stop herdelen" renoted: "Herdeeld" +renotedToX: "Renoted naar {name}" cantRenote: "Dit bericht kan niet worden herdeeld" cantReRenote: "Een herdeling kan niet worden herdeeld" quote: "Quote" +renoteToChannel: "Renote naar kanaal" +renoteToOtherChannel: "Renote naar ander kanaal" pinnedNote: "Vastgemaakte notitie" pinned: "Vastmaken aan profielpagina" you: "Jij" @@ -119,6 +125,10 @@ sensitive: "NSFW" add: "Toevoegen" reaction: "Reacties" reactions: "Reacties" +emojiPicker: "Emoji kiezer" +pinnedEmojisForReactionSettingDescription: "Kies de emojis die als eerste getoond worden tijdens het reageren" +pinnedEmojisSettingDescription: "Kies de emojis die als eerste getoond worden tijdens het reageren" +emojiPickerDisplay: "Emoji kiezer weergave" reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen" rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen" attachCancel: "Verwijder bijlage" @@ -140,7 +150,7 @@ selectAntenna: "Kies een antenne" selectWidget: "Kies een widget" editWidgets: "Bewerk widgets" editWidgetsExit: "Klaar" -customEmojis: "Maatwerk emoji" +customEmojis: "Eigen emoji" emoji: "Emoji" emojis: "Emoji" emojiName: "Naam emoji" @@ -403,7 +413,31 @@ help: "Help" inputMessageHere: "Voer hier je bericht in" close: "Sluiten" invites: "Uitnodigen" +members: "Leden" +transfer: "Overdracht" +title: "Titel" +text: "Tekst" +enable: "Inschakelen" +next: "Volgende" +retype: "Opnieuw invoeren" +noteOf: "Notitie van {user}" +quoteAttached: "Citaat" +quoteQuestion: "Toevoegen als citaat?" invitations: "Uitnodigen" +dashboard: "Overzicht" +local: "Lokaal" +remote: "Remote" +total: "Totaal" +weekOverWeekChanges: "Wijzigingen sinds vorige week" +dayOverDayChanges: "Dagelijkse wijzigingen" +appearance: "Weergave" +clientSettings: "Clientinstellingen" +accountSettings: "Accountinstellingen" +promotion: "Promotie" +promote: "Promoot" +numberOfDays: "Aantal dagen" +hideThisNote: "Verberg deze notitie" +showFeaturedNotesInTimeline: "Laat featured notities in tijdlijn zien" sound: "Geluid" smtpHost: "Server" smtpUser: "Gebruikersnaam" @@ -501,3 +535,8 @@ _webhookSettings: _moderationLogTypes: suspend: "Opschorten" resetPassword: "Wachtwoord terugzetten" +_reversi: + total: "Totaal" +_remoteLookupErrors: + _noSuchObject: + title: "Niet gevonden" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 87ea01764d..474e05ba67 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -727,3 +727,6 @@ _abuseReport: mail: "E-post" _moderationLogTypes: suspend: "Suspender" +_remoteLookupErrors: + _noSuchObject: + title: "Ikke funnet" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 203f44b334..98465ea82b 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1583,3 +1583,6 @@ _moderationLogTypes: resetPassword: "Zresetuj hasÅ‚o" _reversi: total: "ÅÄ…cznie" +_remoteLookupErrors: + _noSuchObject: + title: "Nie znaleziono" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 7ef9e3a946..aae63805c3 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -8,6 +8,9 @@ search: "Pesquisar" notifications: "Notificações" username: "Nome de usuário" password: "Senha" +initialPasswordForSetup: "Senha para a configuração inicial" +initialPasswordIsIncorrect: "Senha para configuração inicial está incorreta" +initialPasswordForSetupDescription: "Use a senha configurada no arquivo de configuração se você instalou o Misskey manualmente.\nSe você estiver utilizando um serviço de hospedagem, utilize a senha fornecida.\nSe uma senha não foi configurada, deixe em branco e continue." forgotPassword: "Esqueci-me da senha" fetchingAsApObject: "Buscando no Fediverso..." ok: "OK" @@ -196,7 +199,7 @@ followConfirm: "Tem certeza que quer seguir {name}?" proxyAccount: "Conta proxy" proxyAccountDescription: "Uma conta de proxy é uma conta que assume o acompanhamento remoto de um usuário sob certas condições especÃficas. Por exemplo, quando um usuário inclui um usuário remoto em uma lista, mas ninguém na lista está seguindo o usuário remoto, a atividade não é entregue ao servidor. Nesse caso, a conta de proxy entra em ação para seguir o usuário remoto em vez disso." host: "Host" -selectSelf: "Escolher manualmente" +selectSelf: "Selecionar a mim" selectUser: "Selecionar usuário" recipient: "Destinatário" annotation: "Anotação" @@ -236,6 +239,8 @@ silencedInstances: "Instâncias silenciadas" silencedInstancesDescription: "Liste o nome de hospedagem dos servidores que você deseja silenciar, separados por linha. Todas as contas desses servidores serão silenciada e poderão enviar solicitações para seguir, mas não poderão mencionar usuários locais sem segui-los. Isso não afetará servidores bloqueados." mediaSilencedInstances: "Instâncias com mÃdia silenciadas" mediaSilencedInstancesDescription: "Liste o nome de hospedagem dos servidores cuja mÃdia você deseja silenciar, separados por linha. Todas as contas desses servidores serão consideradas sensÃveis e não poderão utilizar emojis personalizados. Isso não afetará servidores bloqueados." +federationAllowedHosts: "Servidores com federação permitida" +federationAllowedHostsDescription: "Especifique o endereço dos servidores em que deseja permitir a federação separados por linha." muteAndBlock: "Silenciar e bloquear" mutedUsers: "Usuários silenciados" blockedUsers: "Usuários bloqueados" @@ -334,6 +339,7 @@ renameFolder: "Renomear Pasta" deleteFolder: "Excluir pasta" folder: "Pasta" addFile: "Adicionar arquivo" +showFile: "Mostrar arquivos" emptyDrive: "O drive está vazio" emptyFolder: "A pasta está vazia" unableToDelete: "Não é possÃvel excluir" @@ -447,6 +453,7 @@ totpDescription: "Digite a senha de uso único informado pelo aplicativo autenti moderator: "Moderador" moderation: "Moderação" moderationNote: "Nota de moderação" +moderationNoteDescription: "Você pode preencher notas que serão compartilhadas apenas com moderadores." addModerationNote: "Adicionar nota de moderação" moderationLogs: "Logs de moderação" nUsersMentioned: "Postado por {n} pessoas" @@ -508,6 +515,10 @@ uiLanguage: "Idioma de exibição da interface " aboutX: "Sobre {x}" emojiStyle: "Estilo de emojis" native: "Nativo" +menuStyle: "Estilo do menu" +style: "Estilo" +drawer: "Gaveta" +popup: "Pop-up" showNoteActionsOnlyHover: "Exibir as ações da nota somente ao passar o cursor sobre ela" showReactionsCount: "Ver o número de reações nas notas" noHistory: "Ainda não há histórico" @@ -575,6 +586,7 @@ masterVolume: "volume principal" notUseSound: "Desabilitar som" useSoundOnlyWhenActive: "Apenas reproduzir sons quando Misskey estiver aberto." details: "Detalhes" +renoteDetails: "Detalhes da repostagem" chooseEmoji: "Selecione um emoji" unableToProcess: "Não é possÃvel concluir a operação" recentUsed: "Usado recentemente" @@ -590,6 +602,8 @@ ascendingOrder: "Ascendente" descendingOrder: "Descendente" scratchpad: "Bloco de rascunho" scratchpadDescription: "O Bloco de rascunho fornece um ambiente experimental para AiScript. Permite escrever, executar e verificar os resultados do código para interagir com o Misskey." +uiInspector: "Inspecionador de interface" +uiInspectorDescription: "Você pode ver a lista de servidores de componentes de interface na memória. Componentes da interface serão gerados pela função Ui:C:." output: "Resultado" script: "Script" disablePagesScript: "Desabilitar scripts nas páginas" @@ -670,7 +684,7 @@ smtpSecure: "Use SSL/TLS implÃcito para conexões SMTP" smtpSecureInfo: "Desative esta opção ao utilizar STARTTLS." testEmail: "Testar envio de e-mail" wordMute: "Silenciar palavras" -hardWordMute: "SIlenciamento pesado de palavra" +hardWordMute: "Silenciar palavras (esconder posts)" regexpError: "Erro na expressão regular" regexpErrorDescription: "Ocorreu um erro na expressão regular na linha {line} da palavra mutada {tab}:" instanceMute: "Instâncias silenciadas" @@ -908,6 +922,7 @@ followersVisibility: "Visibilidade dos seguidores" continueThread: "Ver mais desta conversa" deleteAccountConfirm: "Deseja realmente excluir a conta?" incorrectPassword: "Senha inválida." +incorrectTotp: "A senha de uso único está incorreta ou expirou." voteConfirm: "Deseja confirmar o seu voto em \"{choice}\"?" hide: "Ocultar" useDrawerReactionPickerForMobile: "Mostrar em formato de gaveta" @@ -932,6 +947,9 @@ oneHour: "1 hora" oneDay: "1 dia" oneWeek: "1 semana" oneMonth: "1 mês" +threeMonths: "3 meses" +oneYear: "1 ano" +threeDays: "3 dias" reflectMayTakeTime: "As mudanças podem demorar a aparecer." failedToFetchAccountInformation: "Não foi possÃvel obter informações da conta" rateLimitExceeded: "Taxa limite excedido" @@ -1072,6 +1090,7 @@ retryAllQueuesConfirmTitle: "Gostaria de tentar novamente agora?" retryAllQueuesConfirmText: "Isso irá temporariamente aumentar a carga do servidor." enableChartsForRemoteUser: "Gerar gráficos estatÃsticos de usuários remotos" enableChartsForFederatedInstances: "Gerar gráficos estatÃsticos de instâncias remotas" +enableStatsForFederatedInstances: "Receber estatÃsticas de servidores remotos" showClipButtonInNoteFooter: "Adicionar \"Clip\" ao menu de ação de notas" reactionsDisplaySize: "Tamanho de exibição das reações" limitWidthOfReaction: "Limita o comprimento máximo de reações e as exibe em tamanho reduzido" @@ -1258,7 +1277,49 @@ confirmWhenRevealingSensitiveMedia: "Confirmar ao revelar mÃdia sensÃvel" sensitiveMediaRevealConfirm: "Essa mÃdia pode ser sensÃvel. Deseja revelá-la?" createdLists: "Listas criadas" createdAntennas: "Antenas criadas" +fromX: "De {x}" +genEmbedCode: "Gerar código de embed" +noteOfThisUser: "Notas por este usuário" clipNoteLimitExceeded: "Não é possÃvel adicionar mais notas ao clipe." +performance: "Desempenho" +modified: "Modificado" +discard: "Descartar" +thereAreNChanges: "Há {n} mudança(s)" +signinWithPasskey: "Entrar com Passkey" +unknownWebAuthnKey: "Passkey desconhecida" +passkeyVerificationFailed: "A verificação com Passkey falhou." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "A verificação com Passkey teve êxito, mas a entrada sem senha está desabilitada." +messageToFollower: "Mensagem aos seguidores" +target: "Alvo" +testCaptchaWarning: "Essa função é utilizada apenas para testar CAPTCHA. <strong>Não a use num ambiente de produção.</strong>" +prohibitedWordsForNameOfUser: "Palavras proibidas para nomes de usuário" +prohibitedWordsForNameOfUserDescription: "Se quaisquer palavras dessa lista forem incluÃdas no nome de usuário, seu uso será negado. Usuários com privilégios de moderador não serão afetados pela restrição." +yourNameContainsProhibitedWords: "O seu nome possui palavras proibidas" +yourNameContainsProhibitedWordsDescription: "Se você deseja utilizar esse nome, entre em contato com o administrador do servidor." +thisContentsAreMarkedAsSigninRequiredByAuthor: "O autor exige que você esteja cadastrado para ver" +lockdown: "Lockdown" +pleaseSelectAccount: "Selecione uma conta" +availableRoles: "Cargos disponÃveis" +acknowledgeNotesAndEnable: "Ative após compreender as precauções." +_accountSettings: + requireSigninToViewContents: "Exigir cadastro para ver o conteúdo" + requireSigninToViewContentsDescription1: "Exigir cadastro para ver todas as notas e outro conteúdo que você criou. Isso previne 'crawlers' de coletar os seus dados." + requireSigninToViewContentsDescription2: "Conteúdo não será exibido nas prévias de URL (OGP), incorporado em outras páginas web ou em servidores que não têm suporte a citações." + requireSigninToViewContentsDescription3: "Essas restrições podem não ser aplicadas a conteúdo federado de outros servidores." + makeNotesFollowersOnlyBefore: "Tornar notas passadas visÃveis apenas para seguidores." + makeNotesFollowersOnlyBeforeDescription: "Com essa função ativada, apenas seguidores podem ver as notas anteriores à data e hora marcadas. Se isso for desativado, o status de publicação da nota será reestabelecido." + makeNotesHiddenBefore: "Tornar notas passadas privadas" + makeNotesHiddenBeforeDescription: "Com essa função ativada, apenas você poderá ver as notas anteriores à data e hora marcadas. Se isso for desativado, o status de publicação da nota será reestabelecido." + mayNotEffectForFederatedNotes: "Notas federadas a servidores remotos podem não ser afetadas." + notesHavePassedSpecifiedPeriod: "Notas que duraram um tempo especÃfico." + notesOlderThanSpecifiedDateAndTime: "Notas antes do tempo especÃfico." +_abuseUserReport: + forward: "Encaminhar" + forwardDescription: "Encaminhar a denúncia ao servidor remoto como uma conta anônima do sistema." + resolve: "Resolver" + accept: "Aceitar" + reject: "Rejeitar" + resolveTutorial: "Se a denúncia for legÃtima em conteúdo, selecione \"Aceitar\" para marcar o caso como resolvido afirmativamente.\nSe a denúncia for ilegÃtima em conteúdo, selecione \"Rejeitar\" para marcar o caso como resolvido negativamente." _delivery: status: "Estado de entrega" stop: "Suspenso" @@ -1393,8 +1454,12 @@ _serverSettings: fanoutTimelineDescription: "Melhora significativamente a performance do retorno da linha do tempo e reduz o impacto no banco de dados quando habilitado. Em contrapartida, o uso de memória do Redis aumentará. Considere desabilitar em casos de baixa disponibilidade de memória ou instabilidade do servidor." fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados" fanoutTimelineDbFallbackDescription: "Quando habilitado, a linha do tempo irá recuar ao banco de dados caso consultas adicionais sejam feitas e ela não estiver em cache. Quando desabilitado, o impacto no servidor será reduzido ao eliminar o recuo, mas limita a quantidade de linhas do tempo que podem ser recebidas." + reactionsBufferingDescription: "Quando ativado, o desempenho durante a criação de uma reação será melhorado substancialmente, reduzindo a carga do banco de dados. Porém, a o uso de memória do Redis irá aumentar." inquiryUrl: "URL de inquérito" inquiryUrlDescription: "Especifique um URL para um formulário de inquérito para a administração ou uma página web com informações de contato." + openRegistration: "Abrir a criação de contas" + openRegistrationWarning: "Abrir cadastros contém riscos. É recomendado apenas habilitá-los se houver um sistema de monitoramento contÃnuo e resolução imediata de problemas." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Se nenhuma atividade da moderação for detectada por um tempo, essa configuração será desativada para prevenir spam." _accountMigration: moveFrom: "Migrar outra conta para essa" moveFromSub: "Criar um 'alias' a outra conta" @@ -1726,6 +1791,11 @@ _role: canSearchNotes: "Permitir a busca de notas" canUseTranslator: "Uso do tradutor" avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas" + canImportAntennas: "Permitir importação de antenas" + canImportBlocking: "Permitir importação de bloqueios" + canImportFollowing: "Permitir importação de usuários seguidos" + canImportMuting: "Permitir importação de silenciamentos" + canImportUserLists: "Permitir importação de listas" _condition: roleAssignedTo: "AtribuÃdo a cargos manuais" isLocal: "Usuário local" @@ -2109,8 +2179,11 @@ _auth: permissionAsk: "O aplicativo solicita as seguintes permissões" pleaseGoBack: "Por favor, volte ao aplicativo" callback: "Retornando ao aplicativo" + accepted: "Acesso permitido" denied: "Acesso negado" + scopeUser: "Operar como o usuário a seguir" pleaseLogin: "Por favor, entre para autorizar aplicativos." + byClickingYouWillBeRedirectedToThisUrl: "Quando o acesso for permitido, você será redirecionado para o seguinte endereço" _antennaSources: all: "Todas as notas" homeTimeline: "Notas de usuários seguidos" @@ -2219,6 +2292,9 @@ _profile: changeBanner: "Mudar banner" verifiedLinkDescription: "Ao inserir um URL que contém um link para essa conta, um Ãcone de verificação será exibido ao lado do campo" avatarDecorationMax: "Você pode adicionar até {max} decorações." + followedMessage: "Mensagem exibida quando alguém segue você" + followedMessageDescription: "Você pode definir uma curta mensagem que será exibida aos usuários que seguirem você." + followedMessageDescriptionForLockedAccount: "Se você aceita pedidos de seguidor manualmente, isso será exibido quando você aceitá-los." _exportOrImport: allNotes: "Todas as notas" favoritedNotes: "Notas nos favoritos" @@ -2357,6 +2433,8 @@ _notification: renotedBySomeUsers: "{n} usuários repostaram a nota" followedBySomeUsers: "{n} usuários te seguiram" flushNotification: "Limpar notificações" + exportOfXCompleted: "Exportação de {x} foi concluÃda" + login: "Alguém entrou na conta" _types: all: "Todas" note: "Novas notas" @@ -2371,7 +2449,9 @@ _notification: followRequestAccepted: "Aceitou pedidos de seguidor" roleAssigned: "Cargo dado" achievementEarned: "Conquista desbloqueada" + exportCompleted: "A exportação foi concluÃda" login: "Iniciar sessão" + test: "Notificação teste" app: "Notificações de aplicativos conectados" _actions: followBack: "te seguiu de volta" @@ -2437,7 +2517,10 @@ _webhookSettings: abuseReport: "Quando receber um relatório de abuso" abuseReportResolved: "Quando relatórios de abuso forem resolvidos " userCreated: "Quando um usuário é criado" + inactiveModeratorsWarning: "Quando moderadores estiverem inativos por um tempo" + inactiveModeratorsInvitationOnlyChanged: "Quando um moderador está inativo por um tempo e os cadastros passam a exigir convites" deleteConfirm: "Você tem certeza de que deseja excluir o Webhook?" + testRemarks: "Clique no botão à direita do interruptor para enviar um Webhook de teste com dados fictÃcios." _abuseReport: _notificationRecipient: createRecipient: "Adicionar destinatário para relatórios de abuso" @@ -2481,6 +2564,8 @@ _moderationLogTypes: markSensitiveDriveFile: "Arquivo marcado como sensÃvel" unmarkSensitiveDriveFile: "Arquivo desmarcado como sensÃvel" resolveAbuseReport: "Relatório resolvido" + forwardAbuseReport: "Denúncia encaminhada" + updateAbuseReportNote: "Nota de moderação da denúncia atualizada" createInvitation: "Convite gerado" createAd: "Propaganda criada" deleteAd: "Propaganda excluÃda" @@ -2636,3 +2721,44 @@ _contextMenu: app: "Aplicativo" appWithShift: "Aplicativo com a tecla shift" native: "Nativo" +_embedCodeGen: + title: "Personalizar código do embed" + header: "Exibir cabeçalho" + autoload: "Carregar mais automaticamente (obsoleto)" + maxHeight: "Altura máxima" + maxHeightDescription: "Colocar em 0 desabilita a altura máxima. Especifique um valor para prevenir uma expansão vertical contÃnua." + maxHeightWarn: "O limite de altura máxima está desabilitado (0). Se isso não for intencional, insira um valor para a altura máxima." + previewIsNotActual: "A exibição difere do embed original porque ela excede o tamanho da tela de prévia." + rounded: "Tornar arredondado" + border: "Adicionar uma borda ao quadro externo" + applyToPreview: "Aplicar para a prévia" + generateCode: "Gerar código de embed" + codeGenerated: "O código foi gerado" + codeGeneratedDescription: "Coloque o código no seu website para incorporar o conteúdo." +_selfXssPrevention: + warning: "AVISO" + title: "\"Cole algo nessa tela\" é uma fraude" + description1: "Se você colar algo aqui, um usuário malicioso pode sabotar a sua conta ou roubar informações pessoais." + description2: "Se você não entender exatamente o que está colando, %cpare agora e feche essa janela." + description3: "Para mais informação, clique no link. {link}" +_followRequest: + recieved: "Aplicação recebida" + sent: "Aplicação enviada" +_remoteLookupErrors: + _federationNotAllowed: + title: "Não foi possÃvel se comunicar com o servidor" + description: "Comunicação com esse servidor pode ter sido desabilitada ou o servidor pode ter sido bloqueado.\nPor favor, entre em contato com o administrador do servidor." + _uriInvalid: + title: "Endereço inválido" + description: "Há um problema com o endereço inserido. Por favor, confira se você não inseriu caracteres inválidos." + _requestFailed: + title: "Solicitação falhou" + description: "Comunicação com esse servidor falhou. O servidor pode estar inativo. Além disso, confira se você não inseriu um endereço inválido ou inexistente." + _responseInvalid: + title: "Resposta inválida" + description: "Foi possÃvel comunicar com o servidor, porém os dados obtidos foram incorretos." + _responseInvalidIdHostNotMatch: + description: "O domÃnio do endereço inserido difere do domÃnio do endereço final. Se você estiver pesquisando por um servidor de terceiros, tente buscar novamente com um endereço que pode ser obtido através do servidor original." + _noSuchObject: + title: "Não encontrado" + description: "O recurso solicitado não foi encontrado, confira o endereço." diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 71dc1dc94c..07f4c98d96 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -733,3 +733,6 @@ _moderationLogTypes: resetPassword: "Resetează parola" _reversi: total: "Total" +_remoteLookupErrors: + _noSuchObject: + title: "Nu a fost găsit" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 537e99036c..bc1b12895c 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -18,7 +18,7 @@ gotIt: "ЯÑно!" cancel: "Отмена" noThankYou: "Ðет, ÑпаÑибо" enterUsername: "Введите Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ" -renotedBy: "{user} репоÑтнул(а)" +renotedBy: "{user} делает репоÑÑ‚" noNotes: "Ðет ни одной заметки" noNotifications: "Ðет уведомлений" instance: "ÐкземплÑÑ€" @@ -1063,7 +1063,7 @@ hiddenTags: "Скрытые хештеги" notesSearchNotAvailable: "ПоиÑк заметок недоÑтупен" license: "ЛицензиÑ" unfavoriteConfirm: "Удалить избранное?" -myClips: "Мои клипы" +myClips: "Мои подборки" drivecleaner: "ОчиÑтитель диÑков" retryAllQueuesNow: "Повторить вÑе очереди ÑейчаÑ" retryAllQueuesConfirmTitle: "Хотите попробовать ещё раз?" @@ -1105,16 +1105,18 @@ preservedUsernames: "Зарезервированные имена пользоРpreservedUsernamesDescription: "ПеречиÑлите зарезервированные имена пользователей, отделÑÑ Ð¸Ñ… Ñтроками. Они Ñтанут недоÑтупны при Ñоздании учётной запиÑи. Ðто ограничение не применÑетÑÑ Ð¿Ñ€Ð¸ Ñоздании учётной запиÑи админиÑтраторами. Также, уже ÑущеÑтвующие учётные запиÑи оÑтанутÑÑ Ð±ÐµÐ· изменений." createNoteFromTheFile: "Создать заметку из Ñтого файла" archive: "Ðрхив" +unarchive: "Разархивировать" channelArchiveConfirmTitle: "ПеремеÑтить {name} в архив?" channelArchiveConfirmDescription: "Ðрхивированные каналы переÑтанут отображатьÑÑ Ð² ÑпиÑке каналов или результатах поиÑка. Ð’ них также Ð½ÐµÐ»ÑŒÐ·Ñ Ð±ÑƒÐ´ÐµÑ‚ добавлÑть новые запиÑи." thisChannelArchived: "Ðтот канал находитÑÑ Ð² архиве." displayOfNote: "Отображение заметок" initialAccountSetting: "ÐаÑтройка профилÑ" -youFollowing: "ПодпиÑки" +youFollowing: "Ð’Ñ‹ подпиÑаны" preventAiLearning: "ОтказатьÑÑ Ð¾Ñ‚ иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð² машинном обучении (Генеративный ИИ)" preventAiLearningDescription: "ЗапроÑить краулеров не иÑпользовать опубликованный текÑÑ‚ или Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð¸ Ñ‚.д. Ð´Ð»Ñ Ð¼Ð°ÑˆÐ¸Ð½Ð½Ð¾Ð³Ð¾ Ð¾Ð±ÑƒÑ‡ÐµÐ½Ð¸Ñ (Прогнозирующий / Генеративный ИИ) датаÑетов. Ðто доÑтигаетÑÑ Ð¿ÑƒÑ‚Ñ‘Ð¼ Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ \"noai\" HTTP-заголовка в ответ на ÑоответÑтвующий контент. Полного Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ñ‡ÐµÑ€ÐµÐ· Ñтот заголовок не избежать, так как он может быть проÑто проигнорирован." options: "ÐаÑтройки ролей" specifyUser: "Указанный пользователь" +lookupConfirm: "Хотите узнать?" openTagPageConfirm: "Открыть Ñтраницу Ñтого хештега?" specifyHost: "Указать Ñайт" failedToPreviewUrl: "Предварительный проÑмотр недоÑтупен" @@ -1178,6 +1180,7 @@ keepOriginalFilename: "СохранÑть иÑходное Ð¸Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°" keepOriginalFilenameDescription: "ЕÑли вы выключите данную наÑтройку, имена файлов будут автоматичеÑки заменены Ñлучайной Ñтрокой при загрузке." alwaysConfirmFollow: "Ð’Ñегда подтверждать подпиÑку" inquiry: "СвÑзатьÑÑ" +messageToFollower: "Сообщение подпиÑчикам" _delivery: stop: "Заморожено" _type: @@ -1504,6 +1507,7 @@ _role: rateLimitFactor: "Ограничение активноÑти" descriptionOfRateLimitFactor: "Меньшее значение — Ñлабые ограничениÑ, большее — Ñильные" canHideAds: "Может Ñкрыть рекламу" + canImportFollowing: "Можно импортировать подпиÑчиков" _condition: isLocal: "МеÑтный" isRemote: "ÐемеÑтный" @@ -2143,3 +2147,6 @@ _hemisphere: caption: "ИÑпользуетÑÑ Ð´Ð»Ñ Ð½ÐµÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… наÑтроек клиента Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ Ñезона." _reversi: total: "Ð’Ñего" +_remoteLookupErrors: + _noSuchObject: + title: "Ðе найдено" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index f3f43ee6a6..715ff4c847 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1449,3 +1449,6 @@ _moderationLogTypes: resetPassword: "ResetovaÅ¥ heslo" _reversi: total: "Celkom" +_remoteLookupErrors: + _noSuchObject: + title: "Nenájdené" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index c725282d50..8e68d6cf49 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -2709,3 +2709,6 @@ _embedCodeGen: generateCode: "สร้างโค้ดสำหรับà¸à¸²à¸£à¸à¸±à¸‡" codeGenerated: "รหัสถูà¸à¸ªà¸£à¹‰à¸²à¸‡à¸‚ึ้นà¹à¸¥à¹‰à¸§" codeGeneratedDescription: "นำโค้ดที่สร้างà¹à¸¥à¹‰à¸§à¹„ปวางในเว็บไซต์ขà¸à¸‡à¸„ุณเพื่à¸à¸à¸±à¸‡à¹€à¸™à¸·à¹‰à¸à¸«à¸²" +_remoteLookupErrors: + _noSuchObject: + title: "ไม่พบหน้าที่ต้à¸à¸‡à¸à¸²à¸£" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 69892fedc8..2c63f15aa2 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -8,6 +8,7 @@ search: "Arama" notifications: "Bildirim" username: "Kullanıcı Adı" password: "Åžifre" +initialPasswordForSetup: "" forgotPassword: "ÅŸifremi unuttum" fetchingAsApObject: "從è¯é‚¦å®‡å®™å–å¾—ä¸..." ok: "TAMAM" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 1b21854650..6e3e0bb9da 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1624,3 +1624,6 @@ _moderationLogTypes: resetPassword: "Скинути пароль" _reversi: total: "Ð’Ñього" +_remoteLookupErrors: + _noSuchObject: + title: "Ðе знайдено" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 051a4ae6c5..2116d2b86f 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -1094,3 +1094,6 @@ _moderationLogTypes: resetPassword: "Parolni tiklash" _reversi: total: "Jami" +_remoteLookupErrors: + _noSuchObject: + title: "Topilmadi" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 24faa4b94c..cded29fdba 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1930,3 +1930,6 @@ _moderationLogTypes: createInvitation: "Tạo lá»i má»i" _reversi: total: "Tổng cá»™ng" +_remoteLookupErrors: + _noSuchObject: + title: "Không tìm thấy" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index e6232070d7..1a14f0bf76 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -5,6 +5,7 @@ introMisskey: "欢迎ï¼Misskey是一个开æºçš„ã€åŽ»ä¸å¿ƒåŒ–的“微åšå®¢â poweredByMisskeyDescription: "{name} 是开æºå¹³å° <b>Misskey</b> çš„æœåŠ¡å™¨ä¹‹ä¸€ã€‚" monthAndDay: "{month}月 {day}æ—¥" search: "æœç´¢" +reset: "é‡ç½®" notifications: "通知" username: "用户å" password: "密ç " @@ -48,6 +49,7 @@ pin: "置顶" unpin: "å–æ¶ˆç½®é¡¶" copyContent: "å¤åˆ¶å†…容" copyLink: "å¤åˆ¶é“¾æŽ¥" +copyRemoteLink: "å¤åˆ¶è¿œç¨‹é“¾æŽ¥" copyLinkRenote: "å¤åˆ¶è½¬å¸–链接" delete: "åˆ é™¤" deleteAndEdit: "åˆ é™¤å¹¶ç¼–è¾‘" @@ -142,15 +144,15 @@ markAsSensitive: "æ ‡è®°ä¸ºæ•æ„Ÿå†…容" unmarkAsSensitive: "å–æ¶ˆæ ‡è®°ä¸ºæ•感内容" enterFileName: "输入文件å" mute: "å±è”½" -unmute: "解除é™éŸ³" +unmute: "å–æ¶ˆéšè—" renoteMute: "éšè—转帖" renoteUnmute: "解除éšè—转帖" -block: "拉黑" -unblock: "å–æ¶ˆæ‹‰é»‘" +block: "å±è”½" +unblock: "å–æ¶ˆå±è”½" suspend: "冻结" unsuspend: "解除冻结" -blockConfirm: "ç¡®å®šè¦æ‹‰é»‘å—?" -unblockConfirm: "确定è¦è§£é™¤æ‹‰é»‘å—?" +blockConfirm: "确定è¦å±è”½å—?" +unblockConfirm: "确定è¦å–消å±è”½å—?" suspendConfirm: "è¦å†»ç»“å—?" unsuspendConfirm: "è¦è§£é™¤å†»ç»“å—?" selectList: "选择列表" @@ -195,7 +197,7 @@ setWallpaper: "设置å£çº¸" removeWallpaper: "移除å£çº¸" searchWith: "æœç´¢:{q}" youHaveNoLists: "列表为空" -followConfirm: "ä½ ç¡®å®šè¦å…³æ³¨ {name} å—?" +followConfirm: "确定è¦å…³æ³¨ {name} å—?" proxyAccount: "代ç†è´¦æˆ·" proxyAccountDescription: "代ç†è´¦æˆ·æ˜¯åœ¨æŸäº›æƒ…况下替代用户进行远程关注用的账户。 例如说,当用户将一ä½è¿œç¨‹ç”¨æˆ·æ”¾å…¥ä¸€ä¸ªåˆ—è¡¨ä¸æ—¶ï¼Œå¦‚果本地æœåŠ¡å™¨ä¸Šæ²¡æœ‰ä»»ä½•äººå…³æ³¨è¿™ä½è¿œç¨‹ç”¨æˆ·ï¼Œåˆ™è¿™ä½è¿œç¨‹ç”¨æˆ·çš„账户活动将ä¸ä¼šè¢«é€åˆ°æœ¬åœ°æœåŠ¡å™¨ä¸Šã€‚ä½œä¸ºæ›¿ä»£ï¼Œæ¤æ—¶å°†ä½¿ç”¨ä»£ç†è´¦æˆ·è¿›è¡Œå…³æ³¨ã€‚" host: "主机å" @@ -229,10 +231,10 @@ disk: "å˜å‚¨" instanceInfo: "æœåŠ¡å™¨ä¿¡æ¯" statistics: "统计" clearQueue: "清除队列" -clearQueueConfirmTitle: "确定清除队列?" +clearQueueConfirmTitle: "ç¡®å®šè¦æ¸…除队列å—?" clearQueueConfirmText: "未é€è¾¾çš„帖åå°†ä¸ä¼šè¢«æŠ•递。 é€šå¸¸æ— éœ€æ‰§è¡Œæ¤æ“作。" clearCachedFiles: "清除缓å˜" -clearCachedFilesConfirm: "ç¡®å®šè¦æ¸…除所有缓å˜çš„远程文件?" +clearCachedFilesConfirm: "ç¡®å®šè¦æ¸…除所有缓å˜çš„远程文件å—?" blockedInstances: "被å±è”½çš„æœåŠ¡å™¨" blockedInstancesDescription: "设定è¦å±è”½çš„æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œåˆ†éš”。被å±è”½çš„æœåŠ¡å™¨å°†æ— æ³•ä¸Žæœ¬æœåŠ¡å™¨è¿›è¡Œäº¤æ¢é€šè®¯ã€‚å域åä¹ŸåŒæ ·ä¼šè¢«å±è”½ã€‚" silencedInstances: "被é™éŸ³çš„æœåŠ¡å™¨" @@ -246,7 +248,7 @@ mutedUsers: "å·²éšè—用户" blockedUsers: "å·²å±è”½çš„用户" noUsers: "æ— ç”¨æˆ·" editProfile: "编辑资料" -noteDeleteConfirm: "è¦åˆ 除该帖åå—?" +noteDeleteConfirm: "确定è¦åˆ 除该帖åå—?" pinLimitExceeded: "æ— æ³•ç½®é¡¶æ›´å¤šäº†" intro: "Misskey 的部署结æŸå•¦ï¼åˆ›å»ºç®¡ç†å‘˜è´¦å·å§ï¼" done: "完æˆ" @@ -257,7 +259,7 @@ defaultValueIs: "默认值: {value}" noCustomEmojis: "没有自定义表情符å·" noJobs: "没有任务" federating: "è”åˆä¸" -blocked: "已拉黑" +blocked: "å·²å±è”½" suspended: "åœæ¢æŠ•递" all: "全部" subscribing: "已订阅" @@ -566,7 +568,7 @@ objectStorageRegionDesc: "指定一个å¯ç”¨åŒºï¼Œä¾‹å¦‚“xx-east-1â€ã€‚ å¦‚æž objectStorageUseSSL: "使用 SSL" objectStorageUseSSLDesc: "如果ä¸ä½¿ç”¨ https 进行 API 连接,请关é—。" objectStorageUseProxy: "使用代ç†" -objectStorageUseProxyDesc: "如果您ä¸ä½¿ç”¨ä»£ç†è¿›è¡Œ API 连接,请将其关é—。" +objectStorageUseProxyDesc: "如果ä¸ä½¿ç”¨ä»£ç†è¿›è¡Œ API 连接,请关é—。" objectStorageSetPublicRead: "ä¸Šä¼ æ—¶è®¾ç½®ä¸º public-read" s3ForcePathStyleDesc: "å¯ç”¨ s3ForcePathStyle 会强制将å˜å‚¨æ¡¶å称指定为 URL ä¸è·¯å¾„çš„ä¸€éƒ¨åˆ†ï¼Œè€Œä¸æ˜¯ä¸»æœºå。使用自托管 Minio ç‰æ—¶å¯èƒ½éœ€è¦å¯ç”¨ã€‚" serverLogs: "æœåŠ¡å™¨æ—¥å¿—" @@ -683,12 +685,16 @@ emptyToDisableSmtpAuth: "用户å和密ç 留空å¯ä»¥ç¦ç”¨ SMTP 验è¯" smtpSecure: "在 SMTP 连接ä¸ä½¿ç”¨éšå¼ SSL / TLS" smtpSecureInfo: "使用 STARTTLS æ—¶å…³é—。" testEmail: "邮件å‘逿µ‹è¯•" -wordMute: "éšè—æ–‡å—" -hardWordMute: "å±è”½å…³é”®è¯" +wordMute: "éšè—关键è¯" +wordMuteDescription: "折å åŒ…å«æŒ‡å®šå…³é”®è¯çš„帖å。被折å 的帖åå¯å•击展开。" +hardWordMute: "éšè—硬关键è¯" +showMutedWord: "显示已éšè—的关键è¯" +hardWordMuteDescription: "éšè—åŒ…å«æŒ‡å®šå…³é”®è¯çš„帖å。与éšè—关键è¯ä¸åŒï¼Œå¸–å将完全ä¸ä¼šæ˜¾ç¤ºã€‚" regexpError: "æ£åˆ™è¡¨è¾¾å¼é”™è¯¯" -regexpErrorDescription: "{tab} å±è”½æ–‡å—的第 {line} 行的æ£åˆ™è¡¨è¾¾å¼æœ‰é”™è¯¯ï¼š" +regexpErrorDescription: "{tab} éšè—æ–‡å—的第 {line} 行的æ£åˆ™è¡¨è¾¾å¼æœ‰é”™è¯¯ï¼š" instanceMute: "å·²éšè—çš„æœåС噍" userSaysSomething: "{name} 说了什么,但是被å±è”½è¯è¿‡æ»¤äº†" +userSaysSomethingAbout: "{name} 说了关于「{word}ã€çš„什么" makeActive: "å¯ç”¨" display: "显示" copy: "å¤åˆ¶" @@ -759,7 +765,7 @@ driveFilesCount: "网盘的文件数" driveUsage: "网盘的空间用é‡" noCrawle: "è¦æ±‚æœç´¢å¼•擎ä¸ç´¢å¼•该用户" noCrawleDescription: "è¦æ±‚æœç´¢å¼•擎ä¸è¦æ”¶å½•(索引)您的用户页é¢ï¼Œå¸–å,页é¢ç‰ã€‚" -lockedAccountInfo: "å³ä½¿å¯ç”¨è¯¥åŠŸèƒ½ï¼Œåªè¦æ‚¨ä¸å°†å¸–åå¯è§èŒƒå›´è®¾ç½®ä¸ºâ€œä»…关注者â€ï¼Œä»»ä½•人都还是å¯ä»¥çœ‹åˆ°æ‚¨çš„帖å。" +lockedAccountInfo: "å³ä½¿å¯ç”¨è¯¥åŠŸèƒ½ï¼Œåªè¦å¸–åå¯è§èŒƒå›´ä¸æ˜¯ã€Œä»…关注者ã€ï¼Œä»»ä½•人都å¯ä»¥çœ‹åˆ°æ‚¨çš„帖å。" alwaysMarkSensitive: "é»˜è®¤å°†åª’ä½“æ–‡ä»¶æ ‡è®°ä¸ºæ•æ„Ÿå†…容" loadRawImages: "æ·»åŠ é™„ä»¶å›¾åƒçš„缩略图时使用原始图åƒè´¨é‡" disableShowingAnimatedImages: "䏿’放动画" @@ -846,7 +852,7 @@ active: "活动" offline: "离线" notRecommended: "䏿ލè" botProtection: "Bot防御" -instanceBlocking: "被阻拦的æœåС噍" +instanceBlocking: "å±è”½/é™éŸ³çš„æœåŠ¡å™¨" selectAccount: "选择账户" switchAccount: "切æ¢è´¦æˆ·" enabled: "å·²å¯ç”¨" @@ -1301,6 +1307,8 @@ lockdown: "é”定" pleaseSelectAccount: "è¯·é€‰æ‹©å¸æˆ·" availableRoles: "å¯ç”¨è§’色" acknowledgeNotesAndEnable: "ç†è§£æ³¨æ„事项åŽå†å¼€å¯ã€‚" +federationSpecified: "æ¤æœåС噍已开å¯è”åˆç™½åå•。åªèƒ½ä¸Žç®¡ç†å‘˜æŒ‡å®šçš„æœåŠ¡å™¨é€šä¿¡ã€‚" +federationDisabled: "æ¤æœåС噍已ç¦ç”¨è”åˆã€‚æ— æ³•ä¸Žå…¶å®ƒæœåŠ¡å™¨ä¸Šçš„ç”¨æˆ·é€šä¿¡ã€‚" _accountSettings: requireSigninToViewContents: "需è¦ç™»å½•æ‰èƒ½æ˜¾ç¤ºå†…容" requireSigninToViewContentsDescription1: "您å‘布的所有帖åå°†å˜æˆéœ€è¦ç™»å…¥åŽæ‰ä¼šæ˜¾ç¤ºã€‚有望防æ¢çˆ¬è™«æ”¶é›†å„ç§ä¿¡æ¯ã€‚" @@ -1319,7 +1327,7 @@ _abuseUserReport: resolve: "解决" accept: "确认" reject: "æ‹’ç»" - resolveTutorial: "如果举报内容有ç†ä¸”已解决,选择「确认ã€å°†æ¡ˆä»¶ä»¥è‚¯å®šçš„æ€åº¦æ ‡è®°ä¸ºå·²è§£å†³ã€‚\n如果举报内容站ä¸ä½è„šï¼Œé€‰æ‹©ã€Œæ‹’ç»ã€å°†æ¡ˆä»¶ä»¥å¦å®šçš„æ€åº¦æ ‡è®°ä¸ºå·²è§£å†³ã€‚" + resolveTutorial: "如果认å¯ä¸¾æŠ¥å¹¶å·²è§£å†³ï¼Œé€‰æ‹©ã€Œç¡®è®¤ã€å°†æ¡ˆä»¶ä»¥è‚¯å®šçš„æ€åº¦æ ‡è®°ä¸ºå·²è§£å†³ã€‚\n如果ä¸è®¤å¯ä¸¾æŠ¥ï¼Œé€‰æ‹©ã€Œæ‹’ç»ã€å°†æ¡ˆä»¶ä»¥å¦å®šçš„æ€åº¦æ ‡è®°ä¸ºå·²è§£å†³ã€‚" _delivery: status: "投递状æ€" stop: "åœæ¢æŠ•递" @@ -1468,7 +1476,7 @@ _accountMigration: moveTo: "把这个账户è¿ç§»åˆ°æ–°çš„账户" moveToLabel: "è¿ç§»åŽçš„账户" moveCannotBeUndone: "一旦è¿ç§»è´¦æˆ·ï¼Œå°±æ— 法撤销。" - moveAccountDescription: "\nè¿ç§»åˆ°æ–°å¸æˆ·ã€‚\nã€€ãƒ»çŽ°æœ‰çš„å…³æ³¨è€…è‡ªåŠ¨å…³æ³¨æ–°å¸æˆ·\n ・æ¤å¸æˆ·çš„æ‰€æœ‰å…³æ³¨è€…éƒ½å°†è¢«åˆ é™¤\nã€€ãƒ»æ‚¨å°†æ— æ³•å†ä½¿ç”¨æ¤å¸æˆ·å‘帖。\n关注者è¿ç§»æ˜¯è‡ªåŠ¨çš„ï¼Œä½†å…³æ³¨ä¸è¿ç§»å¿…须手动完æˆã€‚请在è¿ç§»å‰åœ¨æ¤å¸æˆ·ä¸Šå¯¼å‡ºå…³æ³¨åˆ—表,并在è¿ç§»åŽç«‹å³åœ¨ç›®æ ‡å¸æˆ·ä¸Šæ‰§è¡Œå¯¼å…¥ã€‚\nå±è”½åˆ—表也是如æ¤ï¼Œå› æ¤æ‚¨å¿…须手动è¿ç§»å®ƒã€‚\nï¼ˆæ¤æè¿°é€‚ç”¨äºŽè¯¥æœåŠ¡å™¨ï¼ˆMisskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为å¯èƒ½æœ‰æ‰€ä¸åŒã€‚)" + moveAccountDescription: "\nè¿ç§»åˆ°æ–°å¸æˆ·ã€‚\nã€€ãƒ»çŽ°æœ‰çš„å…³æ³¨è€…è‡ªåŠ¨å…³æ³¨æ–°å¸æˆ·\n ・æ¤å¸æˆ·çš„æ‰€æœ‰å…³æ³¨è€…éƒ½å°†è¢«åˆ é™¤\nã€€ãƒ»æ‚¨å°†æ— æ³•å†ä½¿ç”¨æ¤å¸æˆ·å‘帖。\n关注者è¿ç§»æ˜¯è‡ªåŠ¨çš„ï¼Œä½†å…³æ³¨ä¸è¿ç§»å¿…须手动完æˆã€‚请在è¿ç§»å‰åœ¨æ¤å¸æˆ·ä¸Šå¯¼å‡ºå…³æ³¨åˆ—表,并在è¿ç§»åŽç«‹å³åœ¨ç›®æ ‡å¸æˆ·ä¸Šæ‰§è¡Œå¯¼å…¥ã€‚\n列表ã€éšè—ã€å±è”½ä¹Ÿæ˜¯å¦‚æ¤ï¼Œå› æ¤æ‚¨å¿…须手动è¿ç§»å®ƒã€‚\nï¼ˆæ¤æè¿°é€‚ç”¨äºŽè¯¥æœåŠ¡å™¨ï¼ˆMisskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为å¯èƒ½æœ‰æ‰€ä¸åŒã€‚)" moveAccountHowTo: "è¦è¿›è¡Œè´¦æˆ·è¿ç§»ï¼Œè¯·çŽ°åœ¨ç›®æ ‡è´¦æˆ·ä¸ä¸ºæ¤è´¦æˆ·å»ºç«‹ä¸€ä¸ªåˆ«å。\n建立别ååŽï¼Œè¯·åƒè¿™æ ·è¾“å…¥ç›®æ ‡è´¦æˆ·ï¼š@username@server.example.com" startMigration: "è¿ç§»" migrationConfirm: "ç¡®å®šè¦æŠŠæ¤è´¦æˆ·è¿ç§»åˆ° {account} å—?一旦确定åŽï¼Œæ¤æ“ä½œæ— æ³•å–æ¶ˆï¼Œæ¤è´¦æˆ·ä¹Ÿæ— 法以原æ¥çš„状æ€ä½¿ç”¨ã€‚\nåŒæ—¶ï¼Œè¯·ç¡®è®¤è¿ç§»åŽçš„è´¦æˆ·ï¼Œå·²åˆ›é€ åˆ«å。" @@ -1688,7 +1696,7 @@ _achievements: title: "è¶…é«˜æ ¡çº§çš„å¹¸è¿" description: "æ¯ 10 秒有 0.005% 的概率自动获得" _setNameToSyuilo: - title: "åƒç¥žä¸€æ ·å‘" + title: "ä¸Šå¸æƒ…结" description: "å°†å称设定为 syuilo" _passedSinceAccountCreated1: title: "一周年" @@ -1794,7 +1802,7 @@ _role: canImportAntennas: "å…许导入天线" canImportBlocking: "å…许导入å±è”½åˆ—表" canImportFollowing: "å…许导入关注列表" - canImportMuting: "å…许导入å±è”½åˆ—表" + canImportMuting: "å…许导入éšè—列表" canImportUserLists: "å…许导入用户列表" _condition: roleAssignedTo: "已分é…给手动角色" @@ -1948,7 +1956,7 @@ _wordMute: _instanceMute: instanceMuteDescription: "éšè—æœåС噍ä¸çš„æ‰€æœ‰å¸–å和转帖,包括这些æœåŠ¡å™¨ä¸Šçš„ç”¨æˆ·å›žå¤ã€‚" instanceMuteDescription2: "一行一个" - title: "éšè—æœåŠ¡å™¨å·²è®¾ç½®çš„å¸–å。" + title: "下é¢å®žä¾‹ä¸çš„帖å将被éšè—。" heading: "å·²éšè—çš„æœåС噍" _theme: explore: "寻找主题" @@ -2069,12 +2077,12 @@ _2fa: step4: "从现在开始,任何登录æ“ä½œéƒ½å°†è¦æ±‚您æä¾›åЍæ€å£ä»¤ã€‚" securityKeyNotSupported: "您的æµè§ˆå™¨ä¸æ”¯æŒå®‰å…¨å¯†é’¥ã€‚" registerTOTPBeforeKey: "è¦æ³¨å†Œå®‰å…¨å¯†é’¥æˆ– Passkey,请先设置验è¯å™¨ã€‚" - securityKeyInfo: "注册兼容 WebAuthn çš„å¯†é’¥ï¼Œä¾‹å¦‚æ”¯æŒ FIDO2 的硬件安全密钥ã€è®¾å¤‡ä¸Šçš„生物识别功能ã€PIN ç ä»¥åŠ Passkey ç‰ã€‚" + securityKeyInfo: "注册兼容 WebAuthn çš„å¯†é’¥ï¼Œä¾‹å¦‚æ”¯æŒ FIDO2 的硬件安全密钥ã€è®¾å¤‡ä¸Šçš„生物识别功能ã€PIN ä»¥åŠ Passkey ç‰ã€‚" registerSecurityKey: "注册安全密钥或 Passkey" securityKeyName: "输入密钥åç§°" tapSecurityKey: "请按照æµè§ˆå™¨è¯´æ˜Žæ“ä½œæ¥æ³¨å†Œå®‰å…¨å¯†é’¥æˆ– Passkey。" removeKey: "åˆ é™¤å®‰å…¨å¯†é’¥" - removeKeyConfirm: "您确定è¦åˆ 除 {name} å—?" + removeKeyConfirm: "确定è¦åˆ 除 {name} å—?" whyTOTPOnlyRenew: "å½“æ³¨å†Œäº†å®‰å…¨å¯†é’¥æ—¶ï¼Œæ— æ³•å–æ¶ˆä½¿ç”¨éªŒè¯å™¨ã€‚" renewTOTP: "é‡ç½®éªŒè¯å™¨" renewTOTPConfirm: "当å‰éªŒè¯å™¨çš„验è¯ç åŠå¤‡ç”¨ä»£ç 已失效" @@ -2282,7 +2290,7 @@ _profile: name: "昵称" username: "用户å" description: "个人简介" - youCanIncludeHashtags: "ä½ å¯ä»¥åœ¨ä¸ªäººç®€ä»‹ä¸åŒ…å«ä¸€äº›#æ ‡ç¾ã€‚" + youCanIncludeHashtags: "å¯ä»¥åœ¨ä¸ªäººç®€ä»‹ä¸åŒ…å« #æ ‡ç¾ã€‚" metadata: "é™„åŠ ä¿¡æ¯" metadataEdit: "é™„åŠ ä¿¡æ¯ç¼–辑" metadataDescription: "最多å¯ä»¥åœ¨ä¸ªäººèµ„æ–™ä¸ä»¥è¡¨æ ¼å½¢å¼æ˜¾ç¤ºå››æ¡å…¶ä»–ä¿¡æ¯ã€‚" @@ -2386,7 +2394,7 @@ _pages: fontSansSerif: "æ— è¡¬çº¿å—体" eyeCatchingImageSet: "设置å°é¢å›¾ç‰‡" eyeCatchingImageRemove: "åˆ é™¤å°é¢å›¾ç‰‡" - chooseBlock: "æ·»åŠ å—" + chooseBlock: "æ·»åŠ å†…å®¹å—" enterSectionTitle: "è¾“å…¥ä¼šè¯æ ‡é¢˜" selectType: "选择类型" contentBlocks: "内容" @@ -2398,8 +2406,8 @@ _pages: section: "ç« èŠ‚" image: "图片" button: "按钮" - dynamic: "动æ€åŒºå—" - dynamicDescription: "这个区å—å·²ç»åºŸå¼ƒã€‚以åŽè¯·ä½¿ç”¨{play}。" + dynamic: "动æ€å†…容å—" + dynamicDescription: "这个内容å—å·²ç»åºŸå¼ƒã€‚以åŽè¯·ä½¿ç”¨{play}。" note: "嵌入的帖å" _note: id: "帖å ID" @@ -2433,7 +2441,7 @@ _notification: renotedBySomeUsers: "{n} 人转å‘了" followedBySomeUsers: "被 {n} 人关注" flushNotification: "é‡ç½®é€šçŸ¥åކå²" - exportOfXCompleted: "å·²å®Œæˆ {x} 个导出" + exportOfXCompleted: "å·²å®Œæˆ {x} 的导出" login: "有新的登录" _types: all: "全部" @@ -2721,6 +2729,66 @@ _contextMenu: app: "应用" appWithShift: "Shift 键应用" native: "æµè§ˆå™¨çš„用户界é¢" +_gridComponent: + _error: + requiredValue: "æ¤å€¼ä¸ºå¿…填项" + columnTypeNotSupport: "æ£åˆ™è¡¨è¾¾å¼éªŒè¯ä»…æ”¯æŒ type:text 列。" + patternNotMatch: "æ¤å€¼ä¸Ž {pattern} 的模å¼ä¸ä¸€è‡´" + notUnique: "æ¤å€¼å¿…须唯一" +_roleSelectDialog: + notSelected: "未选ä¸" +_customEmojisManager: + _gridCommon: + copySelectionRows: "å¤åˆ¶æ‰€é€‰è¡Œ" + copySelectionRanges: "å¤åˆ¶æ‰€é€‰èŒƒå›´" + deleteSelectionRows: "åˆ é™¤æ‰€é€‰è¡Œ" + deleteSelectionRanges: "åˆ é™¤æ‰€é€‰èŒƒå›´çš„è¡Œ" + searchSettings: "æœç´¢è®¾ç½®" + searchSettingCaption: "设置详细的æœç´¢æ¡ä»¶ã€‚" + searchLimit: "显示项目数" + sortOrder: "æŽ’åºæ–¹å¼" + registrationLogs: "注册日志" + registrationLogsCaption: "å°†æ˜¾ç¤ºæ›´æ–°å’Œåˆ é™¤è¡¨æƒ…ç¬¦å·çš„æ—¥å¿—ã€‚æ‰§è¡Œæ›´æ–°æˆ–åˆ é™¤æ“ä½œï¼Œåˆæˆ–è€…æ›´æ”¹æˆ–é‡æ–°åŠ è½½é¡µé¢æ—¶ä¼šæ¶ˆå¤±ã€‚" + alertEmojisRegisterFailedDescription: "æ›´æ–°æˆ–åˆ é™¤è¡¨æƒ…ç¬¦å·å¤±è´¥ã€‚详情请确认注册日志。" + _logs: + showSuccessLogSwitch: "显示æˆåŠŸæ—¥å¿—" + failureLogNothing: "没有失败日志。" + logNothing: "没有日志" + _remote: + selectionRowDetail: "所选行的详细信æ¯" + importSelectionRows: "导入所选行" + importSelectionRangesRows: "导入所选范围的行" + importEmojisButton: "导入已选择的表情符å·" + confirmImportEmojisTitle: "导入表情符å·" + confirmImportEmojisDescription: "是å¦å¯¼å…¥ä»Žè¿œç¨‹æœåŠ¡å™¨æŽ¥æ”¶çš„ {count} 个表情符å·ï¼Ÿè¯·å¯†åˆ‡å…³æ³¨è¡¨æƒ…符å·çš„许å¯å议。" + _local: + tabTitleList: "已注册的表情符å·åˆ—表" + tabTitleRegister: "注册表情符å·" + _list: + emojisNothing: "没有已注册的表情符å·ã€‚" + markAsDeleteTargetRows: "å°†æ‰€é€‰è¡Œæ ‡è®°ä¸ºåˆ é™¤å¯¹è±¡" + markAsDeleteTargetRanges: "å°†æ‰€é€‰èŒƒå›´çš„è¡Œæ ‡è®°ä¸ºåˆ é™¤å¯¹è±¡" + alertUpdateEmojisNothingDescription: "没有已更改的表情符å·ã€‚" + alertDeleteEmojisNothingDescription: "æ²¡æœ‰è¢«æ ‡è®°ä¸ºåˆ é™¤å¯¹è±¡çš„è¡¨æƒ…ç¬¦å·ã€‚" + confirmMovePage: "è¦ç¦»å¼€æ¤é¡µå—?" + confirmChangeView: "è¦æ›´æ”¹æ˜¾ç¤ºå—?" + confirmUpdateEmojisDescription: "è¦æ›´æ–° {count} 个表情符å·å—?" + confirmDeleteEmojisDescription: "è¦åˆ 除已选择的 {count} 个表情符å·å—?" + confirmResetDescription: "è‡³ä»Šä¸ºæ¢æ‰€åšçš„æ‰€æœ‰ä¿®æ”¹éƒ½å°†è¢«é‡ç½®ã€‚" + confirmMovePageDesciption: "æ¤é¡µé¢ä¸Šçš„表情符å·å·²æ›´æ”¹ã€‚\nè‹¥ä¸ä¿å˜å°±ç¦»å¼€æ¤é¡µï¼Œæ¤é¡µé¢ä¸Šæ‰€æœ‰çš„æ›´æ”¹éƒ½å°†ä¸¢å¤±ã€‚" + dialogSelectRoleTitle: "按角色æœç´¢è¡¨æƒ…符å·" + _register: + uploadSettingTitle: "ä¸Šä¼ è®¾ç½®" + uploadSettingDescription: "å¯ä»¥åœ¨æ¤é¡µé¢è®¾ç½®ä¸Šä¼ è¡¨æƒ…ç¬¦å·æ—¶çš„行为。" + directoryToCategoryLabel: "目录å请输入「categoryã€" + directoryToCategoryCaption: "拖放目录时,目录å请输入「categoryã€" + emojiInputAreaCaption: "请使用其ä¸ä¸€ç§æ–¹æ³•é€‰æ‹©è¦æ³¨å†Œçš„表情符å·ã€‚" + emojiInputAreaList1: "在æ¤åŒºåŸŸå†…æ‹–æ”¾å›¾åƒæ–‡ä»¶æˆ–者目录" + emojiInputAreaList2: "å•击æ¤é“¾æŽ¥ä»¥ä»Žç”µè„‘ä¸é€‰æ‹©" + emojiInputAreaList3: "å•击æ¤é“¾æŽ¥ä»¥ä»Žç½‘盘ä¸é€‰æ‹©" + confirmRegisterEmojisDescription: "è¦å°†åˆ—è¡¨å†…æ˜¾ç¤ºçš„è¡¨æƒ…ç¬¦å·æ›¿æ¢ä¸ºæ–°çš„自定义表情符å·å—?(为é™ä½ŽæœåŠ¡å™¨è´Ÿè½½ï¼Œä¸€æ¬¡æ“作最多åªèƒ½æ³¨å†Œ {count} 个表情符å·ï¼‰" + confirmClearEmojisDescription: "è¦æ”¾å¼ƒç¼–è¾‘å¹¶å°†åˆ—è¡¨å†…è¡¨ç¤ºçš„è¡¨æƒ…ç¬¦å·æ¸…空å—?" + confirmUploadEmojisDescription: "è¦å°†æ‹–放的 {count} ä¸ªæ–‡ä»¶ä¸Šä¼ åˆ°ç½‘ç›˜ä¸Šå—?" _embedCodeGen: title: "自定义嵌入代ç " header: "æ˜¾ç¤ºæ ‡é¢˜" @@ -2744,3 +2812,34 @@ _selfXssPrevention: _followRequest: recieved: "已收到申请" sent: "å·²å‘é€ç”³è¯·" +_remoteLookupErrors: + _federationNotAllowed: + title: "æ— æ³•ä¸Žæ¤æœåŠ¡å™¨é€šä¿¡" + description: "ä¸Žæ¤æœåŠ¡å™¨çš„é€šä¿¡å¯èƒ½è¢«ç¦ç”¨ï¼Œåˆæˆ–者是å±è”½äº†æ¤æœåŠ¡å™¨æˆ–è¢«æ¤æœåС噍å±è”½äº†ã€‚\n请è”ç³»æœåŠ¡å™¨çš„ç®¡ç†è€…。" + _uriInvalid: + title: "URI 有误" + description: "输入的 URI 有问题。请确认是å¦è¾“入了 URI 䏿— 法使用的å—符。" + _requestFailed: + title: "请求失败" + description: "与该æœåŠ¡å™¨çš„é€šä¿¡å¤±è´¥ã€‚å¯¹é¢æœåС噍å¯èƒ½ä¸å¯ç”¨ã€‚å¦å¤–,请确认是å¦è¾“å…¥äº†æ— æ•ˆæˆ–ä¸å˜åœ¨çš„ URI。" + _responseInvalid: + title: "å“åº”æ— æ•ˆ" + description: "æˆåŠŸä¸Žæ¤æœåŠ¡å™¨é€šä¿¡ï¼Œä½†è¿”å›žçš„æ•°æ®æ— 效。" + _responseInvalidIdHostNotMatch: + description: "输入 URI 的域å和最终å–å¾—çš„ URI 的域åä¸åŒã€‚如果是通过第三方æœåŠ¡å™¨èŽ·å–远程内容,请使用å¯ä»¥ä»ŽåŽŸå§‹æœåŠ¡å™¨èŽ·å–内容的 URI å†è¯•一次。" + _noSuchObject: + title: "未找到" + description: "未找到请求的资æºã€‚è¯·å†æ¬¡æ£€æŸ¥ URI。" +_captcha: + verify: "请通过 CAPTCHA 验è¯" + testSiteKeyMessage: "输入测试用的网站密钥åŠç§å¯†å¯†é’¥åŽå¯ä»¥ç”Ÿæˆé¢„览并检查,\n详情请看以下页é¢ã€‚" + _error: + _requestFailed: + title: "请求 CAPTCHA 失败" + text: "请ç¨åŽå†è¯•ï¼Œåˆæˆ–è€…å†æ£€æŸ¥ä¸€æ¬¡è®¾ç½®ã€‚" + _verificationFailed: + title: "éªŒè¯ CAPTCHA 失败" + text: "è¯·å†æ¬¡ç¡®è®¤è®¾ç½®æ˜¯å¦æ£ç¡®ã€‚" + _unknown: + title: "CAPTCHA 错误" + text: "å‘生æ„外错误。" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index d4ffb28c76..159ede1356 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -5,6 +5,7 @@ introMisskey: "æ¡è¿Žï¼Misskey 是一個開放原始碼且去ä¸å¿ƒåŒ–的社群 poweredByMisskeyDescription: "{name}是開放原始碼平臺 <b>Misskey</b> 的伺æœå™¨ä¹‹ä¸€ã€‚" monthAndDay: "{month} 月 {day} æ—¥" search: "æœå°‹" +reset: "é‡è¨" notifications: "通知" username: "使用者å稱" password: "密碼" @@ -48,6 +49,7 @@ pin: "ç½®é ‚" unpin: "å–æ¶ˆç½®é ‚" copyContent: "複製內容" copyLink: "複製連çµ" +copyRemoteLink: "複製é 端的連çµ" copyLinkRenote: "複製轉發的連çµ" delete: "刪除" deleteAndEdit: "刪除並編輯" @@ -230,7 +232,7 @@ instanceInfo: "伺æœå™¨è³‡è¨Š" statistics: "統計" clearQueue: "清除佇列" clearQueueConfirmTitle: "ç¢ºå®šè¦æ¸…除佇列嗎?" -clearQueueConfirmText: "æœªç™¼ä½ˆçš„è²¼æ–‡å°‡ä¸æœƒç™¼ä½ˆã€‚您通常ä¸éœ€è¦ç¢ºèªã€‚" +clearQueueConfirmText: "未æˆåŠŸç™¼ä½ˆçš„è²¼æ–‡å°‡ä¸æœƒå†å˜—試發佈。通常ä¸éœ€è¦é€²è¡Œé€™é …æ“作。" clearCachedFiles: "清除快å–資料" clearCachedFilesConfirm: "ç¢ºå®šè¦æ¸…除所有é 端暫å˜è³‡æ–™å—Žï¼Ÿ" blockedInstances: "å·²å°éŽ–çš„ä¼ºæœå™¨" @@ -291,8 +293,8 @@ messaging: "èŠå¤©" upload: "上傳" keepOriginalUploading: "ä¿ç•™åŽŸåœ–" keepOriginalUploadingDescription: "上傳圖片時ä¿ç•™åŽŸå§‹åœ–ç‰‡ã€‚é—œé–‰æ™‚ï¼Œç€è¦½å™¨æœƒåœ¨ä¸Šå‚³æ™‚生æˆé©ç”¨æ–¼ç¶²è·¯å‚³é€çš„版本。" -fromDrive: "從雲端空間" -fromUrl: "從 URL" +fromDrive: "從雲端空間ä¸é¸æ“‡" +fromUrl: "從 URL 上傳" uploadFromUrl: "從網å€ä¸Šå‚³" uploadFromUrlDescription: "您è¦ä¸Šå‚³çš„æª”案網å€" uploadFromUrlRequested: "已請求上傳" @@ -324,7 +326,7 @@ light: "淺色" dark: "深色" lightThemes: "淺色佈景主題" darkThemes: "深色佈景主題" -syncDeviceDarkMode: "與è¨å‚™çš„æ·±è‰²æ¨¡å¼åŒæ¥" +syncDeviceDarkMode: "與è£ç½®çš„æ·±è‰²æ¨¡å¼åŒæ¥" drive: "雲端硬碟" fileName: "檔案å稱" selectFile: "鏿“‡æª”案" @@ -684,11 +686,15 @@ smtpSecure: "在 SMTP 連接ä¸ä½¿ç”¨éš±å¼ SSL/TLS" smtpSecureInfo: "使用 STARTTLS 時關閉。" testEmail: "測試郵件發é€" wordMute: "被éœéŸ³çš„æ–‡å—" +wordMuteDescription: "å°‡åŒ…å«æŒ‡å®šèªžå¥çš„貼文最å°åŒ–。 點擊最å°åŒ–的貼文å³å¯é¡¯ç¤ºã€‚" hardWordMute: "硬文å—éœéŸ³" +showMutedWord: "顯示éœéŸ³å—" +hardWordMuteDescription: "éš±è—嫿œ‰æŒ‡å®šèªžå¥çš„貼文。 與詞彙éœéŸ³ä¸åŒçš„æ˜¯ï¼Œè²¼æ–‡å°‡å®Œå…¨éš±è—ä¸è¦‹ã€‚" regexpError: "æ£è¦è¡¨é”å¼éŒ¯èª¤" regexpErrorDescription: "{tab} éœéŸ³æ–‡å—的第 {line} 行的æ£è¦è¡¨é”弿œ‰éŒ¯èª¤ï¼š" instanceMute: "被éœéŸ³çš„實例" userSaysSomething: "{name}說了什麼" +userSaysSomethingAbout: "{name} 說了一些關於「{word}ã€çš„話" makeActive: "啟用" display: "檢視" copy: "複製" @@ -764,7 +770,7 @@ alwaysMarkSensitive: "é è¨æ¨™è¨˜æª”æ¡ˆç‚ºæ•æ„Ÿå…§å®¹" loadRawImages: "以原始圖檔顯示附件圖檔的縮圖" disableShowingAnimatedImages: "䏿’放動態圖檔" highlightSensitiveMedia: "å¼·èª¿æ•æ„Ÿæ¨™è¨˜" -verificationEmailSent: "已發é€é©—è‰é›»å郵件。請點擊進入電å郵件ä¸çš„éˆæŽ¥å®Œæˆé©—è‰ã€‚" +verificationEmailSent: "已發é€é©—è‰é›»å郵件。請點擊進入電å郵件ä¸çš„連çµä»¥å®Œæˆé©—è‰ã€‚" notSet: "未è¨å®š" emailVerified: "å·²æˆåŠŸé©—è‰æ‚¨çš„é›»å郵件地å€" noteFavoritesCount: "我的最愛貼文的數目" @@ -821,7 +827,7 @@ apply: "套用" receiveAnnouncementFromInstance: "接收來自伺æœå™¨çš„通知" emailNotification: "郵件通知" publish: "發布" -inChannelSearch: "é »é“内æœå°‹" +inChannelSearch: "é »é“å…§æœå°‹" useReactionPickerForContextMenu: "點擊å³éµé–‹å•Ÿåæ‡‰é¸æ“‡å™¨" typingUsers: "{users}輸入ä¸" jumpToSpecifiedDate: "跳轉到特定日期" @@ -925,7 +931,7 @@ incorrectPassword: "密碼錯誤。" incorrectTotp: "ä¸€æ¬¡æ€§å¯†ç¢¼éŒ¯èª¤ï¼Œæˆ–è€…å·²éŽæœŸã€‚" voteConfirm: "確定投給「{choice}ã€ï¼Ÿ" hide: "éš±è—" -useDrawerReactionPickerForMobile: "在移動è¨å‚™ä¸Šä½¿ç”¨æŠ½å±œé¡¯ç¤º" +useDrawerReactionPickerForMobile: "在行動è£ç½®ä¸Šä½¿ç”¨æŠ½å±œé¡¯ç¤º" welcomeBackWithName: "æ¡è¿Žå›žä¾†ï¼Œ{name}" clickToFinishEmailVerification: "點擊 [{ok}] 完æˆé›»å郵件地å€èªè‰ã€‚" overridedDeviceKind: "è£ç½®é¡žåž‹" @@ -1006,7 +1012,7 @@ unsubscribePushNotification: "åœç”¨æŽ¨æ’通知" pushNotificationAlreadySubscribed: "推æ’通知啟用ä¸" pushNotificationNotSupported: "ç€è¦½å™¨æˆ–伺æœå™¨ä¸æ”¯æ´æŽ¨æ’通知" sendPushNotificationReadMessage: "如果已閱讀通知與訊æ¯ï¼Œå°±åˆªé™¤æŽ¨æ’通知" -sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}ã€é€šçŸ¥å°‡ç«‹åˆ»é¡¯ç¤ºã€‚å¯èƒ½æœƒæ›´æ¶ˆè€—è£ç½®é›»æ± 。" +sendPushNotificationReadMessageCaption: "å¯èƒ½æœƒå°Žè‡´è£ç½®çš„é›»æ± æ¶ˆè€—é‡å¢žåŠ ã€‚" windowMaximize: "最大化" windowMinimize: "最å°åŒ–" windowRestore: "復原" @@ -1175,20 +1181,20 @@ used: "已使用" expired: "éŽæœŸ" doYouAgree: "ä½ åŒæ„嗎?" beSureToReadThisAsItIsImportant: "é‡è¦ï¼Œè«‹å‹™å¿…閱讀。" -iHaveReadXCarefullyAndAgree: "æˆ‘å·²ä»”ç´°é–±è®€ä¸¦åŒæ„「{x}ã€çš„内容。" +iHaveReadXCarefullyAndAgree: "æˆ‘å·²ä»”ç´°é–±è®€ä¸¦åŒæ„「{x}ã€çš„內容。" dialog: "å°è©±æ–¹å¡Š" icon: "圖示" forYou: "給您" currentAnnouncements: "最新公告" pastAnnouncements: "æ·å²å…¬å‘Š" youHaveUnreadAnnouncements: "有未讀的公告。" -useSecurityKey: "請按照ç€è¦½å™¨æˆ–è¨å‚™ä¸Šçš„說明使用安全金鑰或 Passkey。" +useSecurityKey: "請按照ç€è¦½å™¨æˆ–è£ç½®ä¸Šçš„說明來使用安全金鑰或 Passkey。" replies: "回覆" renotes: "轉發" loadReplies: "閱覽回覆" loadConversation: "閱覽å°è©±" pinnedList: "å·²ç½®é ‚çš„æ¸…å–®" -keepScreenOn: "ä¿æŒè¨å‚™èž¢å¹•開啟" +keepScreenOn: "ä¿æŒè£ç½®èž¢å¹•開啟" verifiedLink: "已驗è‰é€£çµ" notifyNotes: "開啟貼文通知" unnotifyNotes: "關閉貼文通知" @@ -1243,7 +1249,7 @@ overwriteContentConfirm: "確定è¦è¦†è“‹ç›®å‰çš„內容嗎?" seasonalScreenEffect: "隨å£ç¯€è®Šæ›ç•«é¢çš„呈ç¾" decorate: "è¨ç½®é åƒè£é£¾" addMfmFunction: "æ’å…¥ MFM 功能語法" -enableQuickAddMfmFunction: "顯示高級 MFM 鏿“‡å™¨" +enableQuickAddMfmFunction: "顯示進階 MFM 鏿“‡å™¨" bubbleGame: "æ°£æ³¡éŠæˆ²" sfx: "音效" soundWillBePlayed: "å°‡æ’æ”¾éŸ³æ•ˆ" @@ -1270,7 +1276,7 @@ useNativeUIForVideoAudioPlayer: "使用ç€è¦½å™¨çš„ UI æ’æ”¾å½±ç‰‡èˆ‡éŸ³è¨Š" keepOriginalFilename: "ä¿ç•™åŽŸå§‹æª”å" keepOriginalFilenameDescription: "如果關閉æ¤è¨ç½®ï¼Œä¸Šå‚³æ™‚檔案å稱會自動替æ›ç‚ºéš¨æ©Ÿå—串。" noDescription: "沒有說明文å—" -alwaysConfirmFollow: "跟隨時總是確èª" +alwaysConfirmFollow: "追隨時總是確èª" inquiry: "è¯çµ¡æˆ‘們" tryAgain: "è«‹å†è©¦ä¸€æ¬¡ã€‚" confirmWhenRevealingSensitiveMedia: "è¦é¡¯ç¤ºæ•感媒體時需確èª" @@ -1301,6 +1307,8 @@ lockdown: "鎖定" pleaseSelectAccount: "è«‹é¸æ“‡å¸³æˆ¶" availableRoles: "å¯ç”¨è§’色" acknowledgeNotesAndEnable: "了解注æ„äº‹é …å¾Œå†é–‹å•Ÿã€‚" +federationSpecified: "æ¤ä¼ºæœå™¨ä»¥ç™½åå–®è¯é‚¦çš„æ–¹å¼é‹ä½œã€‚除了管ç†å“¡æŒ‡å®šçš„伺æœå™¨å¤–,它無法與其他伺æœå™¨äº’動。" +federationDisabled: "æ¤ä¼ºæœå™¨æœªé–‹å•Ÿç«™å°è¯é‚¦ã€‚無法與其他伺æœå™¨ä¸Šçš„使用者互動。" _accountSettings: requireSigninToViewContents: "é ˆç™»å…¥ä»¥é¡¯ç¤ºå…§å®¹" requireSigninToViewContentsDescription1: "å¿…é ˆç™»å…¥æ‰æœƒé¡¯ç¤ºæ‚¨å»ºç«‹çš„貼文ç‰å…§å®¹ã€‚坿œ›æœ‰æ•ˆé˜²æ¢è³‡è¨Šè¢«çˆ¬èŸ²è’集。" @@ -1366,7 +1374,7 @@ _initialAccountSetting: theseSettingsCanEditLater: "這裡的è¨å®šå¯ä»¥åœ¨ä¹‹å¾Œè®Šæ›´ã€‚" youCanEditMoreSettingsInSettingsPageLater: "除æ¤ä¹‹å¤–,還å¯ä»¥åœ¨ã€Œè¨å®šã€é é¢é€²è¡Œå„種è¨å®šã€‚之後請確èªçœ‹çœ‹ã€‚" followUsers: "為了構築時間軸,試著追隨您感興趣的使用者å§ã€‚" - pushNotificationDescription: "啟用推é€é€šçŸ¥ï¼Œå°±å¯ä»¥åœ¨è¨å‚™ä¸ŠæŽ¥æ”¶{name}的通知。" + pushNotificationDescription: "啟用推é€é€šçŸ¥å¾Œï¼Œå°±å¯ä»¥åœ¨è£ç½®ä¸ŠæŽ¥æ”¶ä¾†è‡ª{name}的通知了。" initialAccountSettingCompleted: "åˆå§‹è¨å®šå®Œæˆäº†ï¼" haveFun: "盡情享å—{name}å§ï¼" youCanContinueTutorial: "您å¯ä»¥ç¹¼çºŒå¸ç¿’如何使用{name}(Misskey),也å¯ä»¥å°±æ¤æ‰“ä½ï¼Œç«‹å³é–‹å§‹ä½¿ç”¨ã€‚" @@ -1386,12 +1394,12 @@ _initialTutorial: description: "在Misskey上發布的內容稱為「貼文ã€ã€‚è²¼æ–‡åœ¨æ™‚é–“è»¸ä¸ŠæŒ‰æ™‚é–“é †åºæŽ’åˆ—ï¼Œä¸¦å³æ™‚更新。" reply: "您å¯ä»¥å›žè¦†è²¼æ–‡ï¼Œä¸¦åƒè¨Žè«–串一樣繼續å°è©±ã€‚" renote: "您å¯ä»¥å°‡æ¤è²¼æ–‡åˆ†äº«åˆ°è‡ªå·±çš„æ™‚間軸。您也å¯ä»¥åœ¨å¼•ç”¨æ™‚æ·»åŠ æ–‡å—。" - reaction: "您å¯ä»¥æ·»åР忇‰ã€‚詳細資訊將在下一é 進行說明。" + reaction: "您å¯ä»¥åР入忇‰ã€‚詳細資訊將在下一é 進行說明。" menu: "å¯åŸ·è¡Œå„種æ“作,如查看貼文詳細資訊和複製連çµã€‚" _reaction: title: "ä»€éº¼æ˜¯åæ‡‰ï¼Ÿ" - description: "您å¯ä»¥åœ¨è²¼æ–‡ä¸æ·»åŠ ã€Œåæ‡‰ã€ã€‚您å¯ä»¥ä½¿ç”¨å應輕鬆隨æ„地表é”「最愛/å¤§å¿ƒã€æ‰€ç„¡æ³•傳é”的細微差別。" - letsTryReacting: "å¯ä»¥é€éŽé»žæ“Šè²¼æ–‡ä¸Šçš„「+ã€æŒ‰éˆ•ä¾†æ·»åŠ åæ‡‰ã€‚請嘗試在æ¤ç¯„ä¾‹è²¼æ–‡æ·»åŠ åæ‡‰ï¼" + description: "您å¯ä»¥åœ¨è²¼æ–‡ä¸åŠ ä¸Šã€Œåæ‡‰ã€ã€‚有些用「最愛/大心ã€ç„¡æ³•傳é”的感想,å¯ä»¥ç”¨å應輕鬆地表é”出來。" + letsTryReacting: "按一下貼文上的「+ã€æŒ‰éˆ•å³å¯åР入忇‰ã€‚è©¦è‘—å°æ¤ç¯„ä¾‹è²¼æ–‡åŠ ä¸Šåæ‡‰ï¼" reactToContinue: "æ·»åŠ åæ‡‰ä»¥ç¹¼çºŒæ•™å¸èª²ç¨‹ã€‚" reactNotification: "ç•¶æœ‰äººå°æ‚¨çš„貼文åšå‡ºåæ‡‰æ™‚æœƒå³æ™‚接收到通知。" reactDone: "按下「-ã€æŒ‰éˆ•å¯ä»¥å–æ¶ˆåæ‡‰ã€‚" @@ -1473,7 +1481,7 @@ _accountMigration: startMigration: "é·ç§»" migrationConfirm: "確定è¦å°‡é€™å€‹å¸³æˆ¶é·ç§»è‡³ {account} 嗎?一旦é·ç§»å°±ç„¡æ³•撤銷,也就無法以原來的狀態使用這個帳戶。\nå¦å¤–,請確èªåœ¨è¦é·ç§»åˆ°çš„帳戶已經建立了一個別å。" movedAndCannotBeUndone: "帳戶已é·ç§»ã€‚\né·ç§»ç„¡æ³•撤消。" - postMigrationNote: "å–æ¶ˆè¿½è¹¤æ¤å¸³æˆ¶å°‡åœ¨é·ç§»æ“作後 24 å°æ™‚執行。\n æ¤å¸³æˆ¶æœ‰ 0 個關注者/關注者。 您的關注者ä»ç„¶å¯ä»¥çœ‹åˆ°æ¤å¸³æˆ¶çš„關注者帖åï¼Œå› ç‚ºæ‚¨ä¸æœƒè¢«å–消關注。" + postMigrationNote: "將在完æˆé·ç§»å¾Œçš„ 24 å°æ™‚å–æ¶ˆè¿½éš¨æ‰€æœ‰å¸³è™Ÿã€‚\næ¤å¸³æˆ¶çš„追隨ä¸/追隨者人數將æ¸é›¶ã€‚ç”±æ–¼ä¸æœƒè§£é™¤ç²‰çµ²å°æ‚¨çš„è¿½éš¨ï¼Œå› æ¤ä»–們ä»ç„¶å¯ä»¥ç¹¼çºŒé–±è¦½æ¤å¸³æˆ¶åƒ…å°è¿½éš¨è€…公開的貼文。" movedTo: "è¦é·ç§»åˆ°çš„帳戶:" _achievements: earnedAt: "ç²å¾—日期" @@ -1793,7 +1801,7 @@ _role: avatarDecorationLimit: "é åƒè£é£¾çš„æœ€å¤§è¨ç½®é‡" canImportAntennas: "å…許匯入天線" canImportBlocking: "å…許匯入å°éŽ–åå–®" - canImportFollowing: "å…許匯入跟隨åå–®" + canImportFollowing: "å…許匯入追隨åå–®" canImportMuting: "å…許匯入éœéŸ³åå–®" canImportUserLists: "å…許匯入清單" _condition: @@ -2069,7 +2077,7 @@ _2fa: step4: "從ç¾åœ¨é–‹å§‹ï¼Œä»»ä½•登入æ“ä½œéƒ½å°‡è¦æ±‚您æä¾›æ¬Šæ–。" securityKeyNotSupported: "您的ç€è¦½å™¨ä¸æ”¯æ´å®‰å…¨é‡‘鑰。" registerTOTPBeforeKey: "如è¦è¨»å†Šå®‰å…¨é‡‘鑰或 Passkey,請先è¨å®šé©—è‰æ‡‰ç”¨ç¨‹å¼ã€‚" - securityKeyInfo: "您å¯ä»¥è¨å®šä½¿ç”¨æ”¯æ´ FIDO2 的硬體安全鎖ã€çµ‚端è¨å‚™çš„æŒ‡ç´‹èªè‰ï¼Œæˆ–者 PIN 碼來登入。" + securityKeyInfo: "您å¯ä»¥è¨å®šä½¿ç”¨æ”¯æ´ FIDO2 的硬體安全金鑰,以åŠè£ç½®ä¸Šçš„生物辨è˜ã€PIN 碼和密碼ç‰ä¾†ç™»å…¥ã€‚" registerSecurityKey: "註冊安全金鑰或 Passkey" securityKeyName: "輸入金鑰å稱" tapSecurityKey: "按照ç€è¦½å™¨çš„說明註冊安全金鑰或 Passkey。" @@ -2287,7 +2295,7 @@ _profile: metadataEdit: "ç·¨è¼¯é™„åŠ è³‡è¨Š" metadataDescription: "å¯ä»¥åœ¨å€‹äººè³‡æ–™ä¸ä»¥è¡¨æ ¼å½¢å¼é¡¯ç¤ºå…¶ä»–資訊。" metadataLabel: "標籤" - metadataContent: "内容" + metadataContent: "內容" changeAvatar: "æ›´æ›å¤§é è²¼" changeBanner: "變更橫幅圖åƒ" verifiedLinkDescription: "å¦‚æžœè¼¸å…¥åŒ…å«æ‚¨å€‹äººè³‡æ–™çš„網站 URLï¼Œæ¬„ä½æ—邊將出ç¾é©—è‰åœ–示。" @@ -2627,7 +2635,7 @@ _externalResourceInstaller: description: "å·²å–å¾—è³‡æ–™ä½†è§£æž AiScript 時發生錯誤,導致無法載入。請è¯çµ¡å¤–掛作者。請檢查 Javascript 控制å°ä»¥å–得錯誤詳細資訊。" _pluginInstallFailed: title: "外掛安è£å¤±æ•—" - description: "å®‰è£æ’件時出ç¾å•題。請å†è©¦ä¸€æ¬¡ã€‚è«‹åƒé–± Javascript 控制å°ä»¥å–得錯誤詳細資訊。" + description: "安è£å¤–掛時出ç¾å•題。請å†è©¦ä¸€æ¬¡ã€‚å¯åƒé–± Javascript 控制å°ä»¥å–得錯誤詳細資訊。" _themeParseFailed: title: "佈景主題解æžéŒ¯èª¤" description: "å·²å–得資料但解æžä½ˆæ™¯ä¸»é¡Œæ™‚發生錯誤,導致無法載入。請è¯çµ¡ä½ˆæ™¯ä¸»é¡Œçš„作者。請檢查 Javascript 控制å°ä»¥å–得錯誤詳細資訊。" @@ -2646,7 +2654,7 @@ _dataSaver: description: "å°‡ä¸å†è‡ªå‹•載入網å€é 覽縮圖。" _code: title: "程å¼ç¢¼çªå‡ºé¡¯ç¤º" - description: "如果使用了 MFM 的程å¼ç¢¼çªé¡¯æ¨™è¨˜ï¼Œå‰‡åœ¨é»žæ“Šä¹‹å‰ä¸æœƒè¼‰å…¥ã€‚程å¼ç¢¼çªé¡¯è¦æ±‚åŠ è¼‰æ¯ç¨®ç¨‹å¼èªžè¨€çš„çªé¡¯å®šç¾©æª”案,但由於這些檔案ä¸å†è‡ªå‹•è¼‰å…¥ï¼Œå› æ¤æœ‰æœ›æ¸›å°‘資料æµé‡ã€‚" + description: "如果使用了程å¼ç¢¼çªé¡¯èªžæ³•(如 MFM),則在點擊之å‰ä¸æœƒè¢«è¼‰å…¥ã€‚由於需è¦ç‚ºå°æ‡‰çš„程å¼èªžè¨€ä¸‹è¼‰çªé¡¯å®šç¾©æª”æ¡ˆï¼Œå› æ¤é—œé–‰è‡ªå‹•載入有助於減少資料æµé‡ã€‚" _hemisphere: N: "北åŠçƒ" S: "å—åŠçƒ" @@ -2721,6 +2729,66 @@ _contextMenu: app: "應用程å¼" appWithShift: "Shift 鵿‡‰ç”¨ç¨‹å¼" native: "ç€è¦½å™¨çš„使用者介é¢" +_gridComponent: + _error: + requiredValue: "æ¤å€¼ç‚ºå¿…填欄ä½" + columnTypeNotSupport: "æ£è¦è¡¨é”å¼é©—è‰åƒ…æ”¯æ´ type:text 的欄ä½ã€‚" + patternNotMatch: "æ¤å€¼ä¸ç¬¦åˆ {pattern} ä¸çš„æ¨£å¼ã€‚" + notUnique: "æ¤å€¼å¿…é ˆæ˜¯å”¯ä¸€çš„" +_roleSelectDialog: + notSelected: "æœªé¸æ“‡" +_customEmojisManager: + _gridCommon: + copySelectionRows: "複製é¸å–的行" + copySelectionRanges: "複製é¸å–的範åœ" + deleteSelectionRows: "刪除所é¸çš„行" + deleteSelectionRanges: "刪除é¸å–範åœçš„行" + searchSettings: "æœå°‹è¨å®š" + searchSettingCaption: "詳細è¨å®šæœå°‹æ¢ä»¶ã€‚" + searchLimit: "顯示的數é‡" + sortOrder: "排åº" + registrationLogs: "登錄日誌" + registrationLogsCaption: "會顯示更新或刪除表情符號時的日誌。進行更新或刪除æ“作,或切æ›é é¢ã€é‡æ–°è¼‰å…¥å¾Œï¼Œæ—¥èªŒå°‡æœƒæ¶ˆå¤±ã€‚" + alertEmojisRegisterFailedDescription: "更新或刪除表情符號失敗。詳情請查看登錄日誌。" + _logs: + showSuccessLogSwitch: "顯示æˆåŠŸæ—¥èªŒ" + failureLogNothing: "沒有失敗的日誌。" + logNothing: "沒有日誌。" + _remote: + selectionRowDetail: "é¸å–行的詳細資訊" + importSelectionRows: "匯入é¸å–的行" + importSelectionRangesRows: "匯入é¸å–範åœçš„行" + importEmojisButton: "匯入勾é¸çš„表情符號" + confirmImportEmojisTitle: "匯入表情符號" + confirmImportEmojisDescription: "將從é 端接收的{count}個表情符號進行匯入。請務必注æ„表情符號的授權。是å¦åŸ·è¡Œæ¤æ“作?" + _local: + tabTitleList: "已登錄的表情符號列表" + tabTitleRegister: "登錄表情符號" + _list: + emojisNothing: "沒有登錄的表情符號。" + markAsDeleteTargetRows: "å°‡é¸å–的行è¨ç‚ºåˆªé™¤å°è±¡" + markAsDeleteTargetRanges: "å°‡é¸å–範åœçš„行è¨ç‚ºåˆªé™¤å°è±¡\n" + alertUpdateEmojisNothingDescription: "沒有é¸å–需è¦è®Šæ›´çš„表情符號。" + alertDeleteEmojisNothingDescription: "沒有é¸å–需è¦åˆªé™¤çš„表情符號。" + confirmMovePage: "è¦ç§»å‹•到其他é é¢å—Žï¼Ÿ" + confirmChangeView: "è¦æ›´æ”¹é¡¯ç¤ºæ–¹å¼å—Žï¼Ÿ" + confirmUpdateEmojisDescription: "將更新{count}個表情符號。是å¦åŸ·è¡Œæ¤æ“作?" + confirmDeleteEmojisDescription: "將刪除勾é¸çš„{count}個表情符號。是å¦åŸ·è¡Œæ¤æ“作?" + confirmResetDescription: "ç›®å‰æ‰€åšçš„æ‰€æœ‰è®Šæ›´éƒ½æœƒé‡è¨ã€‚" + confirmMovePageDesciption: "æ¤é é¢çš„表情符號已被更改。 \n若未儲å˜å°±ç›´æŽ¥é›¢é–‹æ¤é é¢ï¼Œå‰‡åœ¨æ¤é é¢é€²è¡Œçš„æ‰€æœ‰æ›´æ”¹å°‡æœƒè¢«æ¨æ£„。" + dialogSelectRoleTitle: "æ ¹æ“šè¡¨æƒ…ç¬¦è™Ÿè¨å®šçš„角色進行æœå°‹" + _register: + uploadSettingTitle: "上傳è¨å®š" + uploadSettingDescription: "您å¯ä»¥åœ¨æ¤ç•«é¢è¨å®šè¡¨æƒ…符號上傳時的æ“作。" + directoryToCategoryLabel: "åœ¨ã€Œé¡žåˆ¥ã€æ¬„ä½ä¸è¼¸å…¥ç›®éŒ„å稱" + directoryToCategoryCaption: "æ‹–æ”¾ç›®éŒ„æ™‚ï¼Œè«‹åœ¨ã€Œé¡žåˆ¥ã€æ¬„ä½ä¸è¼¸å…¥ç›®éŒ„å稱。" + emojiInputAreaCaption: "以下列其ä¸ä¸€ç¨®æ–¹å¼é¸æ“‡æ‚¨æƒ³è¦è¨»å†Šçš„表情符號" + emojiInputAreaList1: "å°‡åœ–ç‰‡æª”æ¡ˆæˆ–ç›®éŒ„æ‹–æ”¾åˆ°æ¤æ¡†ä¸" + emojiInputAreaList2: "點擊æ¤é€£çµå¾žé›»è…¦ä¸é¸æ“‡" + emojiInputAreaList3: "點擊æ¤é€£çµå¾žé›²ç«¯ç¡¬ç¢Ÿä¸é¸æ“‡" + confirmRegisterEmojisDescription: "將列表ä¸é¡¯ç¤ºçš„表情符號登錄為新的自定表情符號。是å¦ç¢ºå®šï¼Ÿï¼ˆç‚ºé¿å…éŽé«˜è² è·ï¼Œæ¯æ¬¡æ“作最多å¯ç™»éŒ„{count}個表情符號)" + confirmClearEmojisDescription: "放棄編輯內容並清除列表ä¸é¡¯ç¤ºçš„表情符號。是å¦ç¢ºå®šï¼Ÿ" + confirmUploadEmojisDescription: "將拖放的{count}個檔案上傳到雲端硬碟。是å¦åŸ·è¡Œæ¤æ“作?" _embedCodeGen: title: "自訂嵌入程å¼ç¢¼" header: "檢視標é " @@ -2744,3 +2812,34 @@ _selfXssPrevention: _followRequest: recieved: "收到的請求" sent: "é€å‡ºçš„請求" +_remoteLookupErrors: + _federationNotAllowed: + title: "無法與這個伺æœå™¨é€šè¨Š" + description: "與æ¤ä¼ºæœå™¨çš„通訊å¯èƒ½è¢«åœç”¨ã€æˆ–å°éŽ–äº†è©²ä¼ºæœå™¨ï¼Œæˆ–被該伺æœå™¨å°éŽ–ã€‚\nè«‹è¯ç¹«æ‚¨çš„伺æœå™¨ç®¡ç†å“¡ã€‚" + _uriInvalid: + title: "URI 䏿£ç¢º" + description: "輸入的 URI 有å•題。請檢查是å¦è¼¸å…¥äº† URI ä¸ä¸èƒ½ä½¿ç”¨çš„å—元。" + _requestFailed: + title: "請求失敗" + description: "與æ¤ä¼ºæœå™¨çš„通訊失敗。å¯èƒ½æ˜¯å°æ–¹ä¼ºæœå™¨æ–·ç·šã€‚ æ¤å¤–,請檢查是å¦è¼¸å…¥äº†ä¸æ£ç¢ºæˆ–ä¸å˜åœ¨çš„ URI。" + _responseInvalid: + title: "å›žæ‡‰ä¸æ£ç¢º" + description: "é›–ç„¶èƒ½å¤ èˆ‡é€™å€‹ä¼ºæœå™¨é€šè¨Šï¼Œä½†æ˜¯å–å¾—çš„è³‡æ–™ä¸æ£ç¢ºã€‚" + _responseInvalidIdHostNotMatch: + description: "輸入的 URI 的網域與最終å–å¾—çš„ URI 的網域ä¸åŒã€‚ 如果您是é€éŽç¬¬ä¸‰æ–¹ä¼ºæœå™¨æŸ¥è©¢é 端內容,請使用å¯åœ¨åŽŸå§‹ä¼ºæœå™¨ä¸Šå–å¾—çš„ URI 冿¬¡æŸ¥è©¢ã€‚" + _noSuchObject: + title: "æŸ¥ç„¡é …ç›®" + description: "ç„¡æ³•æ‰¾åˆ°æ‰€è¦æ±‚的資æºï¼Œè«‹å†æ¬¡æª¢æŸ¥ URI。" +_captcha: + verify: "è«‹é€šéŽ CAPTCHA é©—è‰" + testSiteKeyMessage: "å¯ä»¥è¼¸å…¥ç¶²ç«™é‡‘鑰和秘密金鑰的測試值來檢查é 覽。\n詳細資訊請åƒé–±ä»¥ä¸‹é é¢ã€‚" + _error: + _requestFailed: + title: "CAPTCHA 請求失敗" + text: "è«‹éŽä¸€æ®µæ™‚間後å†åŸ·è¡Œï¼Œæˆ–冿¬¡æª¢æŸ¥è¨å®šã€‚" + _verificationFailed: + title: "CAPTCHA é©—è‰å¤±æ•—" + text: "è«‹å†æ¬¡æª¢æŸ¥è¨å®šæ˜¯å¦æ£ç¢ºã€‚" + _unknown: + title: "CAPTCHA 錯誤" + text: "發生了æ„外的錯誤。" diff --git a/package.json b/package.json index 0f39078491..68ca6fd876 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2025.1.0-dev", + "version": "2025.2.0-dev", "codename": "shonk", "repository": { "type": "git", diff --git a/packages/backend/migration/1709126576000-optimize-emoji-index.js b/packages/backend/migration/1709126576000-optimize-emoji-index.js new file mode 100644 index 0000000000..e4184895d0 --- /dev/null +++ b/packages/backend/migration/1709126576000-optimize-emoji-index.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class OptimizeEmojiIndex1709126576000 { + name = 'OptimizeEmojiIndex1709126576000' + + async up(queryRunner) { + await queryRunner.query(`CREATE INDEX "IDX_EMOJI_ROLE_IDS" ON "emoji" using gin ("roleIdsThatCanBeUsedThisEmojiAsReaction")`) + await queryRunner.query(`CREATE INDEX "IDX_EMOJI_CATEGORY" ON "emoji" ("category")`) + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "IDX_EMOJI_CATEGORY"`) + await queryRunner.query(`DROP INDEX "IDX_EMOJI_ROLE_IDS"`) + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index bace8c1f96..a8566e30d9 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -164,7 +164,6 @@ "proxy-addr": "^2.0.7", "psl": "^1.13.0", "pug": "3.0.3", - "punycode": "2.3.1", "qrcode": "1.5.4", "random-seed": "0.3.0", "ratelimiter": "3.4.1", @@ -223,7 +222,6 @@ "@types/pg": "8.11.10", "@types/proxy-addr": "^2.0.3", "@types/pug": "2.0.10", - "@types/punycode": "2.1.4", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 6ae8ccfbb3..ace7f7841c 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -7,14 +7,14 @@ import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; import { MeiliSearch } from 'meilisearch'; +import { MiMeta } from '@/models/Meta.js'; import { DI } from './di-symbols.js'; import { Config, loadConfig } from './config.js'; import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; import { allSettled } from './misc/promise-tracker.js'; -import type { Provider, OnApplicationShutdown } from '@nestjs/common'; -import { MiMeta } from '@/models/Meta.js'; import { GlobalEvents } from './core/GlobalEventService.js'; +import type { Provider, OnApplicationShutdown } from '@nestjs/common'; const $config: Provider = { provide: DI.config, @@ -33,7 +33,11 @@ const $db: Provider = { const $meilisearch: Provider = { provide: DI.meilisearch, useFactory: (config: Config) => { - if (config.meilisearch) { + if (config.fulltextSearch?.provider === 'meilisearch') { + if (!config.meilisearch) { + throw new Error('MeiliSearch is enabled but no configuration is provided'); + } + return new MeiliSearch({ host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`, apiKey: config.meilisearch.apiKey, diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index 56128a7ab9..735a0f4666 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -70,14 +70,22 @@ async function main() { }); //#endregion - if (cluster.isPrimary || envOption.disableClustering) { - await masterMain(); + if (!envOption.disableClustering) { if (cluster.isPrimary) { + logger.info(`Start main process... pid: ${process.pid}`); + await masterMain(); ev.mount(); + } else if (cluster.isWorker) { + logger.info(`Start worker process... pid: ${process.pid}`); + await workerMain(); + } else { + throw new Error('Unknown process type'); } - } - if (cluster.isWorker) { - await workerMain(); + } else { + // éžclusterã®å ´åˆã¯Masterã®ã¿ãŒèµ·å‹•ã™ã‚‹ãŸã‚ã€Workerã®å‡¦ç†ã¯è¡Œã‚ãªã„(cluster.isWorker === trueã®çŠ¶æ…‹ã§ã“ã®ãƒ–ãƒãƒƒã‚¯ã«æ¥ã‚‹ã“ã¨ã¯ãªã„) + logger.info(`Start main process... pid: ${process.pid}`); + await masterMain(); + ev.mount(); } readyRef.value = true; diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 355e095c12..7b3f3395e6 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -97,22 +97,22 @@ export async function masterMain() { }); } - if (envOption.disableClustering) { + bootLogger.info( + `mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`, + ); + + if (!envOption.disableClustering) { + // clusterモジュール有効時 + if (envOption.onlyServer) { - await server(); + // onlyServer ã‹ã¤ enableCluster ãªå ´åˆã€ãƒ¡ã‚¤ãƒ³ãƒ—ãƒã‚»ã‚¹ã¯forkã®ã¿ã«åˆ¶é™ã™ã‚‹(listenã—ãªã„)。 + // ワーカープãƒã‚»ã‚¹å´ã§listenã™ã‚‹ã¨ã€ãƒ¡ã‚¤ãƒ³ãƒ—ãƒã‚»ã‚¹ã§ãƒãƒ¼ãƒˆã¸ã®ç€ä¿¡ã‚’å—ã‘入れã¦ãƒ¯ãƒ¼ã‚«ãƒ¼ãƒ—ãƒã‚»ã‚¹ã¸ã®åˆ†é…を行ã†å‹•作をã™ã‚‹ã€‚ + // ãã®ãŸã‚ã€ãƒ¡ã‚¤ãƒ³ãƒ—ãƒã‚»ã‚¹ã§ã‚‚直接listenã™ã‚‹ã¨ãƒãƒ¼ãƒˆã®ç«¶åˆãŒç™ºç”Ÿã—ã¦èµ·å‹•ã«å¤±æ•—ã—ã¦ã—ã¾ã†ã€‚ + // see: https://nodejs.org/api/cluster.html#cluster } else if (envOption.onlyQueue) { await jobQueue(); } else { await server(); - await jobQueue(); - } - } else { - if (envOption.onlyServer) { - // nop - } else if (envOption.onlyQueue) { - // nop - } else { - await server(); } if (config.clusterLimit === 0) { @@ -121,6 +121,17 @@ export async function masterMain() { } await spawnWorkers(config.clusterLimit); + } else { + // clusterモジュール無効時 + + if (envOption.onlyServer) { + await server(); + } else if (envOption.onlyQueue) { + await jobQueue(); + } else { + await server(); + await jobQueue(); + } } if (envOption.onlyQueue) { diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 4af1140f36..e980b40224 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -52,6 +52,9 @@ type Source = { redisForJobQueue?: RedisOptionsSource; redisForTimelines?: RedisOptionsSource; redisForReactions?: RedisOptionsSource; + fulltextSearch?: { + provider?: FulltextSearchProvider; + }; meilisearch?: { host: string; port: string; @@ -118,6 +121,13 @@ type Source = { pidFile: string; filePermissionBits?: string; + + logging?: { + sql?: { + disableQueryTruncation? : boolean, + enableQueryParamLogging? : boolean, + } + } }; export type Config = { @@ -143,6 +153,9 @@ export type Config = { user: string; pass: string; }[] | undefined; + fulltextSearch?: { + provider?: FulltextSearchProvider; + }; meilisearch: { host: string; port: string; @@ -179,6 +192,12 @@ export type Config = { signToActivityPubGet: boolean; attachLdSignatureForRelays: boolean; checkActivityPubGetSignature: boolean | undefined; + logging?: { + sql?: { + disableQueryTruncation? : boolean, + enableQueryParamLogging? : boolean, + } + } version: string; publishTarballInsteadOfProvideRepositoryUrl: boolean; @@ -219,6 +238,8 @@ export type Config = { filePermissionBits?: string; }; +export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch'; + const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -302,6 +323,7 @@ export function loadConfig(): Config { db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass }, dbReplications: config.dbReplications, dbSlaves: config.dbSlaves, + fulltextSearch: config.fulltextSearch, meilisearch: config.meilisearch, redis, redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, @@ -354,6 +376,7 @@ export function loadConfig(): Config { import: config.import, pidFile: config.pidFile, filePermissionBits: config.filePermissionBits, + logging: config.logging, }; } diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index adb0a63ad7..e2c492ff80 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -8,6 +8,18 @@ export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; +export const FILE_TYPE_IMAGE = [ + 'image/png', + 'image/gif', + 'image/jpeg', + 'image/webp', + 'image/avif', + 'image/apng', + 'image/bmp', + 'image/tiff', + 'image/x-icon', +]; + // ブラウザã§ç›´æŽ¥è¡¨ç¤ºã™ã‚‹ã“ã¨ã‚’許å¯ã™ã‚‹ãƒ•ァイルã®ç¨®é¡žã®ãƒªã‚¹ãƒˆ // ã“ã“ã«å«ã¾ã‚Œãªã„ã‚‚ã®ã¯ application/octet-stream ã¨ã—ã¦ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã•れる // SVGã¯XSSを生むã®ã§è¨±å¯ã—ãªã„ diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts index 742e2621fd..9bca795479 100644 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ b/packages/backend/src/core/AbuseReportNotificationService.ts @@ -160,22 +160,22 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { }; }); - const recipientWebhookIds = await this.fetchWebhookRecipients() - .then(it => it - .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook') - .map(it => it.systemWebhookId) - .filter(x => x != null)); - for (const webhookId of recipientWebhookIds) { - await Promise.all( - convertedReports.map(it => { - return this.systemWebhookService.enqueueSystemWebhook( - webhookId, - type, - it, - ); - }), - ); - } + const inactiveRecipients = await this.fetchWebhookRecipients() + .then(it => it.filter(it => !it.isActive)); + const withoutWebhookIds = inactiveRecipients + .map(it => it.systemWebhookId) + .filter(x => x != null); + return Promise.all( + convertedReports.map(it => { + return this.systemWebhookService.enqueueSystemWebhook( + type, + it, + { + excludes: withoutWebhookIds, + }, + ); + }), + ); } /** diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/packages/backend/src/core/AiService.ts diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 5b1ab00cfe..79aa722fe5 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -6,6 +6,65 @@ import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; +import { MiMeta } from '@/models/Meta.js'; +import Logger from '@/logger.js'; +import { LoggerService } from './LoggerService.js'; + +export const supportedCaptchaProviders = ['none', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'testcaptcha'] as const; +export type CaptchaProvider = typeof supportedCaptchaProviders[number]; + +export const captchaErrorCodes = { + invalidProvider: Symbol('invalidProvider'), + invalidParameters: Symbol('invalidParameters'), + noResponseProvided: Symbol('noResponseProvided'), + requestFailed: Symbol('requestFailed'), + verificationFailed: Symbol('verificationFailed'), + unknown: Symbol('unknown'), +} as const; +export type CaptchaErrorCode = typeof captchaErrorCodes[keyof typeof captchaErrorCodes]; + +export type CaptchaSetting = { + provider: CaptchaProvider; + hcaptcha: { + siteKey: string | null; + secretKey: string | null; + } + mcaptcha: { + siteKey: string | null; + secretKey: string | null; + instanceUrl: string | null; + } + recaptcha: { + siteKey: string | null; + secretKey: string | null; + } + turnstile: { + siteKey: string | null; + secretKey: string | null; + } +} + +export class CaptchaError extends Error { + public readonly code: CaptchaErrorCode; + public readonly cause?: unknown; + + constructor(code: CaptchaErrorCode, message: string, cause?: unknown) { + super(message); + this.code = code; + this.cause = cause; + this.name = 'CaptchaError'; + } +} + +export type CaptchaSaveSuccess = { + success: true; +} +export type CaptchaSaveFailure = { + success: false; + error: CaptchaError; +} +export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure; type CaptchaResponse = { success: boolean; @@ -15,9 +74,14 @@ type CaptchaResponse = { @Injectable() export class CaptchaService { + private readonly logger: Logger; + constructor( private httpRequestService: HttpRequestService, + private metaService: MetaService, + loggerService: LoggerService, ) { + this.logger = loggerService.getLogger('captcha'); } @bindThis @@ -45,32 +109,32 @@ export class CaptchaService { @bindThis public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> { if (response == null) { - throw new Error('recaptcha-failed: no response provided'); + throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'recaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => { - throw new Error(`recaptcha-request-failed: ${err}`); + throw new CaptchaError(captchaErrorCodes.requestFailed, `recaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new Error(`recaptcha-failed: ${errorCodes}`); + throw new CaptchaError(captchaErrorCodes.verificationFailed, `recaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> { if (response == null) { - throw new Error('hcaptcha-failed: no response provided'); + throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'hcaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => { - throw new Error(`hcaptcha-request-failed: ${err}`); + throw new CaptchaError(captchaErrorCodes.requestFailed, `hcaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new Error(`hcaptcha-failed: ${errorCodes}`); + throw new CaptchaError(captchaErrorCodes.verificationFailed, `hcaptcha-failed: ${errorCodes}`); } } @@ -107,7 +171,7 @@ export class CaptchaService { @bindThis public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> { if (response == null) { - throw new Error('mcaptcha-failed: no response provided'); + throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'mcaptcha-failed: no response provided'); } const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost); @@ -121,46 +185,251 @@ export class CaptchaService { headers: { 'Content-Type': 'application/json', }, - }); + }, { throwErrorWhenResponseNotOk: false }); if (result.status !== 200) { - throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK'); + throw new CaptchaError(captchaErrorCodes.requestFailed, 'mcaptcha-failed: mcaptcha didn\'t return 200 OK'); } const resp = (await result.json()) as { valid: boolean }; if (!resp.valid) { - throw new Error('mcaptcha-request-failed'); + throw new CaptchaError(captchaErrorCodes.verificationFailed, 'mcaptcha-request-failed'); } } @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> { if (response == null) { - throw new Error('turnstile-failed: no response provided'); + throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'turnstile-failed: no response provided'); } const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { - throw new Error(`turnstile-request-failed: ${err}`); + throw new CaptchaError(captchaErrorCodes.requestFailed, `turnstile-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new Error(`turnstile-failed: ${errorCodes}`); + throw new CaptchaError(captchaErrorCodes.verificationFailed, `turnstile-failed: ${errorCodes}`); } } @bindThis public async verifyTestcaptcha(response: string | null | undefined): Promise<void> { if (response == null) { - throw new Error('testcaptcha-failed: no response provided'); + throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'testcaptcha-failed: no response provided'); } const success = response === 'testcaptcha-passed'; if (!success) { - throw new Error('testcaptcha-failed'); + throw new CaptchaError(captchaErrorCodes.verificationFailed, 'testcaptcha-failed'); + } + } + + @bindThis + public async get(): Promise<CaptchaSetting> { + const meta = await this.metaService.fetch(true); + + let provider: CaptchaProvider; + switch (true) { + case meta.enableHcaptcha: { + provider = 'hcaptcha'; + break; + } + case meta.enableMcaptcha: { + provider = 'mcaptcha'; + break; + } + case meta.enableRecaptcha: { + provider = 'recaptcha'; + break; + } + case meta.enableTurnstile: { + provider = 'turnstile'; + break; + } + case meta.enableTestcaptcha: { + provider = 'testcaptcha'; + break; + } + default: { + provider = 'none'; + break; + } + } + + return { + provider: provider, + hcaptcha: { + siteKey: meta.hcaptchaSiteKey, + secretKey: meta.hcaptchaSecretKey, + }, + mcaptcha: { + siteKey: meta.mcaptchaSitekey, + secretKey: meta.mcaptchaSecretKey, + instanceUrl: meta.mcaptchaInstanceUrl, + }, + recaptcha: { + siteKey: meta.recaptchaSiteKey, + secretKey: meta.recaptchaSecretKey, + }, + turnstile: { + siteKey: meta.turnstileSiteKey, + secretKey: meta.turnstileSecretKey, + }, + }; + } + + /** + * captchaã®è¨å®šã‚’æ›´æ–°ã—ã¾ã™. ãã®éš›ã€ãƒ•ãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰å´ã§å—ã‘å–ã£ãŸcaptchaã‹ã‚‰ã®æˆ»ã‚Šå€¤ã‚’検証ã—ã€passã—ãŸå ´åˆã®ã¿è¨å®šã‚’æ›´æ–°ã—ã¾ã™. + * å®Ÿéš›ã®æ¤œè¨¼å‡¦ç†ã¯ã‚µãƒ¼ãƒ“ス内ã§å®šç¾©ã•れã¦ã„ã‚‹å„captchaプãƒãƒã‚¤ãƒ€ã®æ¤œè¨¼é–¢æ•°ã«å§”è²ã—ã¾ã™. + * + * @param provider 検証ã™ã‚‹captchaã®ãƒ—ãƒãƒã‚¤ãƒ€ + * @param params + * @param params.sitekey hcaptcha, recaptcha, turnstile, mcaptchaã®å ´åˆã«æŒ‡å®šã™ã‚‹sitekey. ãれ以外ã®ãƒ—ãƒãƒã‚¤ãƒ€ã§ã¯ç„¡è¦–ã•れã¾ã™ + * @param params.secret hcaptcha, recaptcha, turnstile, mcaptchaã®å ´åˆã«æŒ‡å®šã™ã‚‹secret. ãれ以外ã®ãƒ—ãƒãƒã‚¤ãƒ€ã§ã¯ç„¡è¦–ã•れã¾ã™ + * @param params.instanceUrl mcaptchaã®å ´åˆã«æŒ‡å®šã™ã‚‹ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®URL. ãれ以外ã®ãƒ—ãƒãƒã‚¤ãƒ€ã§ã¯ç„¡è¦–ã•れã¾ã™ + * @param params.captchaResult フãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰å´ã§å—ã‘å–ã£ãŸcaptchaプãƒãƒã‚¤ãƒ€ã‹ã‚‰ã®æˆ»ã‚Šå€¤. ã“ã®å€¤ã‚’使ã£ã¦ã‚µãƒ¼ãƒã‚µã‚¤ãƒ‰ã§ã®æ¤œè¨¼ã‚’行ã„ã¾ã™ + * @see verifyHcaptcha + * @see verifyMcaptcha + * @see verifyRecaptcha + * @see verifyTurnstile + * @see verifyTestcaptcha + */ + @bindThis + public async save( + provider: CaptchaProvider, + params?: { + sitekey?: string | null; + secret?: string | null; + instanceUrl?: string | null; + captchaResult?: string | null; + }, + ): Promise<CaptchaSaveResult> { + if (!supportedCaptchaProviders.includes(provider)) { + return { + success: false, + error: new CaptchaError(captchaErrorCodes.invalidProvider, `Invalid captcha provider: ${provider}`), + }; + } + + const operation = { + none: async () => { + await this.updateMeta(provider, params); + }, + hcaptcha: async () => { + if (!params?.secret || !params.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'hcaptcha-failed: secret and captureResult are required'); + } + + await this.verifyHcaptcha(params.secret, params.captchaResult); + await this.updateMeta(provider, params); + }, + mcaptcha: async () => { + if (!params?.secret || !params.sitekey || !params.instanceUrl || !params.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'mcaptcha-failed: secret, sitekey, instanceUrl and captureResult are required'); + } + + await this.verifyMcaptcha(params.secret, params.sitekey, params.instanceUrl, params.captchaResult); + await this.updateMeta(provider, params); + }, + recaptcha: async () => { + if (!params?.secret || !params.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'recaptcha-failed: secret and captureResult are required'); + } + + await this.verifyRecaptcha(params.secret, params.captchaResult); + await this.updateMeta(provider, params); + }, + turnstile: async () => { + if (!params?.secret || !params.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: secret and captureResult are required'); + } + + await this.verifyTurnstile(params.secret, params.captchaResult); + await this.updateMeta(provider, params); + }, + testcaptcha: async () => { + if (!params?.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: captureResult are required'); + } + + await this.verifyTestcaptcha(params.captchaResult); + await this.updateMeta(provider, params); + }, + }[provider]; + + return operation() + .then(() => ({ success: true }) as CaptchaSaveSuccess) + .catch(err => { + this.logger.info(err); + const error = err instanceof CaptchaError + ? err + : new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`); + return { + success: false, + error, + }; + }); + } + + @bindThis + private async updateMeta( + provider: CaptchaProvider, + params?: { + sitekey?: string | null; + secret?: string | null; + instanceUrl?: string | null; + }, + ) { + const metaPartial: Partial< + Pick< + MiMeta, + ('enableHcaptcha' | 'hcaptchaSiteKey' | 'hcaptchaSecretKey') | + ('enableMcaptcha' | 'mcaptchaSitekey' | 'mcaptchaSecretKey' | 'mcaptchaInstanceUrl') | + ('enableRecaptcha' | 'recaptchaSiteKey' | 'recaptchaSecretKey') | + ('enableTurnstile' | 'turnstileSiteKey' | 'turnstileSecretKey') | + ('enableTestcaptcha') + > + > = { + enableHcaptcha: provider === 'hcaptcha', + enableMcaptcha: provider === 'mcaptcha', + enableRecaptcha: provider === 'recaptcha', + enableTurnstile: provider === 'turnstile', + enableTestcaptcha: provider === 'testcaptcha', + }; + + const updateIfNotUndefined = <K extends keyof typeof metaPartial>(key: K, value: typeof metaPartial[K]) => { + if (value !== undefined) { + metaPartial[key] = value; + } + }; + switch (provider) { + case 'hcaptcha': { + updateIfNotUndefined('hcaptchaSiteKey', params?.sitekey); + updateIfNotUndefined('hcaptchaSecretKey', params?.secret); + break; + } + case 'mcaptcha': { + updateIfNotUndefined('mcaptchaSitekey', params?.sitekey); + updateIfNotUndefined('mcaptchaSecretKey', params?.secret); + updateIfNotUndefined('mcaptchaInstanceUrl', params?.instanceUrl); + break; + } + case 'recaptcha': { + updateIfNotUndefined('recaptchaSiteKey', params?.sitekey); + updateIfNotUndefined('recaptchaSecretKey', params?.secret); + break; + } + case 'turnstile': { + updateIfNotUndefined('turnstileSiteKey', params?.sitekey); + updateIfNotUndefined('turnstileSecretKey', params?.secret); + break; + } } + + await this.metaService.update(metaPartial); } } diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index cc33fb5c0b..3f7ad5b947 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -4,19 +4,18 @@ */ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import { In, IsNull } from 'typeorm'; import * as Redis from 'ioredis'; -import { DI } from '@/di-symbols.js'; -import { IdService } from '@/core/IdService.js'; +import { In, IsNull } from 'typeorm'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiEmoji } from '@/models/Emoji.js'; -import type { DriveFilesRepository, EmojisRepository, MiRole, MiUser } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { DI } from '@/di-symbols.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { query } from '@/misc/prelude/url.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import type { DriveFilesRepository, EmojisRepository, MiRole, MiUser } from '@/models/_.js'; +import type { MiEmoji } from '@/models/Emoji.js'; import type { Serialized } from '@/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Config } from '@/config.js'; @@ -24,6 +23,42 @@ import { DriveService } from './DriveService.js'; const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/; +export const fetchEmojisHostTypes = [ + 'local', + 'remote', + 'all', +] as const; +export type FetchEmojisHostTypes = typeof fetchEmojisHostTypes[number]; +export const fetchEmojisSortKeys = [ + '+id', + '-id', + '+updatedAt', + '-updatedAt', + '+name', + '-name', + '+host', + '-host', + '+uri', + '-uri', + '+publicUrl', + '-publicUrl', + '+type', + '-type', + '+aliases', + '-aliases', + '+category', + '-category', + '+license', + '-license', + '+isSensitive', + '-isSensitive', + '+localOnly', + '-localOnly', + '+roleIdsThatCanBeUsedThisEmojiAsReaction', + '-roleIdsThatCanBeUsedThisEmojiAsReaction', +] as const; +export type FetchEmojisSortKeys = typeof fetchEmojisSortKeys[number]; + @Injectable() export class CustomEmojiService implements OnApplicationShutdown { private emojisCache: MemoryKVCache<MiEmoji | null>; @@ -32,16 +67,12 @@ export class CustomEmojiService implements OnApplicationShutdown { constructor( @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.config) private config: Config, - @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private utilityService: UtilityService, private idService: IdService, private emojiEntityService: EmojiEntityService, @@ -67,7 +98,9 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis public async add(data: { - driveFile: MiDriveFile; + originalUrl: string; + publicUrl: string; + fileType: string; name: string; category: string | null; aliases: string[]; @@ -84,9 +117,9 @@ export class CustomEmojiService implements OnApplicationShutdown { category: data.category, host: data.host, aliases: data.aliases, - originalUrl: data.driveFile.url, - publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, - type: data.driveFile.webpublicType ?? data.driveFile.type, + originalUrl: data.originalUrl, + publicUrl: data.publicUrl, + type: data.fileType, license: data.license, isSensitive: data.isSensitive, localOnly: data.localOnly, @@ -114,8 +147,10 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis public async update(data: ( { id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], } - ) & { - driveFile?: MiDriveFile; + ) & { + originalUrl?: string; + publicUrl?: string; + fileType?: string; category?: string | null; aliases?: string[]; license?: string | null; @@ -140,6 +175,17 @@ export class CustomEmojiService implements OnApplicationShutdown { if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS'; } + // If we're changing the file, then we need to delete the old one + if (data.originalUrl != null && data.originalUrl !== emoji.originalUrl) { + const oldFile = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() }); + const newFile = await this.driveFilesRepository.findOneBy({ url: data.originalUrl, userHost: emoji.host ? emoji.host : IsNull() }); + + // But DON'T delete if this is the same file reference, otherwise we'll break the emoji! + if (oldFile && newFile && oldFile.id !== newFile.id) { + await this.driveService.deleteFile(oldFile, false, moderator ? moderator : undefined); + } + } + await this.emojisRepository.update(emoji.id, { updatedAt: new Date(), name: data.name, @@ -148,21 +194,14 @@ export class CustomEmojiService implements OnApplicationShutdown { license: data.license, isSensitive: data.isSensitive, localOnly: data.localOnly, - originalUrl: data.driveFile != null ? data.driveFile.url : undefined, - publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, - type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, + originalUrl: data.originalUrl, + publicUrl: data.publicUrl, + type: data.fileType, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, }); this.localEmojisCache.refresh(); - if (data.driveFile != null) { - const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() }); - if (file && file.id !== data.driveFile.id) { - await this.driveService.deleteFile(file, false, moderator ? moderator : undefined); - } - } - const packed = await this.emojiEntityService.packDetailed(emoji.id); if (!doNameUpdate) { @@ -336,7 +375,7 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { - // クエリã«ä½¿ã†ãƒ›ã‚¹ãƒˆ + // クエリã«ä½¿ã†ãƒ›ã‚¹ãƒˆ let host = src === '.' ? null // .ã¯ãƒãƒ¼ã‚«ãƒ«ãƒ›ã‚¹ãƒˆ (ã“ã“ãŒãƒžãƒƒãƒã™ã‚‹ã®ã¯ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã®ã¿) : src === undefined ? noteUserHost // ノートãªã©ã§ãƒ›ã‚¹ãƒˆçœç•¥è¡¨è¨˜ã®å ´åˆã¯ãƒãƒ¼ã‚«ãƒ«ãƒ›ã‚¹ãƒˆ (ã“ã“ãŒãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«ãƒžãƒƒãƒã™ã‚‹ã“ã¨ã¯ãªã„) : this.utilityService.isSelfHost(src) ? null // 自ホスト指定 @@ -445,6 +484,151 @@ export class CustomEmojiService implements OnApplicationShutdown { } @bindThis + public async fetchEmojis( + params?: { + query?: { + updatedAtFrom?: string; + updatedAtTo?: string; + name?: string; + host?: string; + uri?: string; + publicUrl?: string; + type?: string; + aliases?: string; + category?: string; + license?: string; + isSensitive?: boolean; + localOnly?: boolean; + hostType?: FetchEmojisHostTypes; + roleIds?: string[]; + }, + sinceId?: string; + untilId?: string; + }, + opts?: { + limit?: number; + page?: number; + sortKeys?: FetchEmojisSortKeys[] + }, + ) { + function multipleWordsToQuery(words: string) { + return words.split(/\s/).filter(x => x.length > 0).map(x => `%${sqlLikeEscape(x)}%`); + } + + const builder = this.emojisRepository.createQueryBuilder('emoji'); + if (params?.query) { + const q = params.query; + if (q.updatedAtFrom) { + // noIndexScan + builder.andWhere('CAST(emoji.updatedAt AS DATE) >= :updateAtFrom', { updateAtFrom: q.updatedAtFrom }); + } + if (q.updatedAtTo) { + // noIndexScan + builder.andWhere('CAST(emoji.updatedAt AS DATE) <= :updateAtTo', { updateAtTo: q.updatedAtTo }); + } + if (q.name) { + builder.andWhere('emoji.name ~~ ANY(ARRAY[:...name])', { name: multipleWordsToQuery(q.name) }); + } + + switch (true) { + case q.hostType === 'local': { + builder.andWhere('emoji.host IS NULL'); + break; + } + case q.hostType === 'remote': { + if (q.host) { + // noIndexScan + builder.andWhere('emoji.host ~~ ANY(ARRAY[:...host])', { host: multipleWordsToQuery(q.host) }); + } else { + builder.andWhere('emoji.host IS NOT NULL'); + } + break; + } + } + + if (q.uri) { + // noIndexScan + builder.andWhere('emoji.uri ~~ ANY(ARRAY[:...uri])', { uri: multipleWordsToQuery(q.uri) }); + } + if (q.publicUrl) { + // noIndexScan + builder.andWhere('emoji.publicUrl ~~ ANY(ARRAY[:...publicUrl])', { publicUrl: multipleWordsToQuery(q.publicUrl) }); + } + if (q.type) { + // noIndexScan + builder.andWhere('emoji.type ~~ ANY(ARRAY[:...type])', { type: multipleWordsToQuery(q.type) }); + } + if (q.aliases) { + // noIndexScan + const subQueryBuilder = builder.subQuery() + .select('COUNT(0)', 'count') + .from( + sq2 => sq2 + .select('unnest(subEmoji.aliases)', 'alias') + .addSelect('subEmoji.id', 'id') + .from('emoji', 'subEmoji'), + 'aliasTable', + ) + .where('"emoji"."id" = "aliasTable"."id"') + .andWhere('"aliasTable"."alias" ~~ ANY(ARRAY[:...aliases])', { aliases: multipleWordsToQuery(q.aliases) }); + + builder.andWhere(`(${subQueryBuilder.getQuery()}) > 0`); + } + if (q.category) { + builder.andWhere('emoji.category ~~ ANY(ARRAY[:...category])', { category: multipleWordsToQuery(q.category) }); + } + if (q.license) { + // noIndexScan + builder.andWhere('emoji.license ~~ ANY(ARRAY[:...license])', { license: multipleWordsToQuery(q.license) }); + } + if (q.isSensitive != null) { + // noIndexScan + builder.andWhere('emoji.isSensitive = :isSensitive', { isSensitive: q.isSensitive }); + } + if (q.localOnly != null) { + // noIndexScan + builder.andWhere('emoji.localOnly = :localOnly', { localOnly: q.localOnly }); + } + if (q.roleIds && q.roleIds.length > 0) { + builder.andWhere('emoji.roleIdsThatCanBeUsedThisEmojiAsReaction && ARRAY[:...roleIds]::VARCHAR[]', { roleIds: q.roleIds }); + } + } + + if (params?.sinceId) { + builder.andWhere('emoji.id > :sinceId', { sinceId: params.sinceId }); + } + if (params?.untilId) { + builder.andWhere('emoji.id < :untilId', { untilId: params.untilId }); + } + + if (opts?.sortKeys && opts.sortKeys.length > 0) { + for (const sortKey of opts.sortKeys) { + const direction = sortKey.startsWith('-') ? 'DESC' : 'ASC'; + const key = sortKey.replace(/^[+-]/, ''); + builder.addOrderBy(`emoji.${key}`, direction); + } + } else { + builder.addOrderBy('emoji.id', 'DESC'); + } + + const limit = opts?.limit ?? 10; + if (opts?.page) { + builder.skip((opts.page - 1) * limit); + } + + builder.take(limit); + + const [emojis, count] = await builder.getManyAndCount(); + + return { + emojis, + count: (count > limit ? emojis.length : count), + allCount: count, + allPages: Math.ceil(count / limit), + }; + } + + @bindThis public dispose(): void { this.emojisCache.dispose(); } diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 987999bce7..ce3af7c774 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -181,7 +181,7 @@ export class FetchInstanceMetadataService { } @bindThis - private async fetchDom(instance: MiInstance): Promise<DOMWindow['document']> { + private async fetchDom(instance: MiInstance): Promise<Document> { this.logger.info(`Fetching HTML of ${instance.host} ...`); const url = 'https://' + instance.host; @@ -206,7 +206,7 @@ export class FetchInstanceMetadataService { } @bindThis - private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise<string | null> { + private async fetchFaviconUrl(instance: MiInstance, doc: Document | null): Promise<string | null> { const url = 'https://' + instance.host; if (doc) { @@ -232,7 +232,7 @@ export class FetchInstanceMetadataService { } @bindThis - private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { + private async fetchIconUrl(instance: MiInstance, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> { if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { const url = 'https://' + instance.host; return (new URL(manifest.icons[0].src, url)).href; @@ -261,7 +261,7 @@ export class FetchInstanceMetadataService { } @bindThis - private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { + private async getThemeColor(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> { const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color; if (themeColor) { @@ -273,7 +273,7 @@ export class FetchInstanceMetadataService { } @bindThis - private async getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { + private async getSiteName(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> { if (info && info.metadata) { if (typeof info.metadata.nodeName === 'string') { return info.metadata.nodeName; @@ -298,7 +298,7 @@ export class FetchInstanceMetadataService { } @bindThis - private async getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { + private async getDescription(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> { if (info && info.metadata) { if (typeof info.metadata.nodeDescription === 'string') { return info.metadata.nodeDescription; diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index cc66e9fe3a..dc4483ad3f 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -16,6 +16,7 @@ import * as blurhash from 'blurhash'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import type { PredictionType } from 'nsfwjs'; export type FileInfo = { size: number; diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 42676d6f98..1aca3737b3 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -179,6 +179,39 @@ export class MfmService { break; } + case 'ruby': { + let ruby: [string, string][] = []; + for (const child of node.childNodes) { + if (child.nodeName === 'rp') { + continue; + } + if (treeAdapter.isTextNode(child) && !/\s|\[|\]/.test(child.value)) { + ruby.push([child.value, '']); + continue; + } + if (child.nodeName === 'rt' && ruby.length > 0) { + const rt = getText(child); + if (/\s|\[|\]/.test(rt)) { + // If any space is included in rt, it is treated as a normal text + ruby = []; + appendChildren(node.childNodes); + break; + } else { + ruby.at(-1)![1] = rt; + continue; + } + } + // If any other element is included in ruby, it is treated as a normal text + ruby = []; + appendChildren(node.childNodes); + break; + } + for (const [base, rt] of ruby) { + text += `$[ruby ${base} ${rt}]`; + } + break; + } + // block code (<pre><code>) case 'pre': { if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') { diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 96bb30a0d6..3bfced1d80 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -678,14 +678,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.roleService.addNoteToRoleTimeline(noteObj); - this.webhookService.getActiveWebhooks().then(webhooks => { - webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'note', { - note: noteObj, - }); - } - }); + this.webhookService.enqueueUserWebhook(user.id, 'note', { note: noteObj }); const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note); @@ -717,13 +710,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (!isThreadMuted && !muted) { nm.push(data.reply.userId, 'reply'); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'reply', { - note: noteObj, - }); - } + this.webhookService.enqueueUserWebhook(data.reply.userId, 'reply', { note: noteObj }); } } } @@ -757,20 +744,14 @@ export class NoteCreateService implements OnApplicationShutdown { // Publish event if ((user.id !== data.renote.userId) && data.renote.userHost === null) { this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'renote', { - note: noteObj, - }); - } + this.webhookService.enqueueUserWebhook(data.renote.userId, 'renote', { note: noteObj }); } } nm.notify(); //#region AP deliver - if (this.userEntityService.isLocalUser(user)) { + if (!data.localOnly && this.userEntityService.isLocalUser(user)) { (async () => { const noteActivity = await this.renderNoteOrRenoteActivity(data, note); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); @@ -905,13 +886,7 @@ export class NoteCreateService implements OnApplicationShutdown { }); this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'mention', { - note: detailPackedNote, - }); - } + this.webhookService.enqueueUserWebhook(u.id, 'mention', { note: detailPackedNote }); // Create notification nm.push(u.id, 'mention'); diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index bb2a463354..37721d2bf1 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -28,7 +28,7 @@ export class S3Service { ? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}` : `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent - const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy); + const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy, true); const handlerOption: NodeHttpHandlerOptions = {}; if (meta.objectStorageUseSSL) { handlerOption.httpsAgent = agent as https.Agent; diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 6dc3e85fc8..431cc0234e 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -6,16 +6,17 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +import { type Config, FulltextSearchProvider } from '@/config.js'; import { bindThis } from '@/decorators.js'; import { MiNote } from '@/models/Note.js'; -import { MiUser } from '@/models/_.js'; import type { NotesRepository } from '@/models/_.js'; +import { MiUser } from '@/models/_.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; import { IdService } from '@/core/IdService.js'; +import { LoggerService } from '@/core/LoggerService.js'; import type { Index, MeiliSearch } from 'meilisearch'; type K = string; @@ -27,12 +28,27 @@ type Q = { op: '<', k: K, v: number } | { op: '>=', k: K, v: number } | { op: '<=', k: K, v: number } | - { op: 'is null', k: K} | - { op: 'is not null', k: K} | + { op: 'is null', k: K } | + { op: 'is not null', k: K } | { op: 'and', qs: Q[] } | { op: 'or', qs: Q[] } | { op: 'not', q: Q }; +export type SearchOpts = { + userId?: MiNote['userId'] | null; + channelId?: MiNote['channelId'] | null; + host?: string | null; + filetype?: string | null; + order?: string | null; + disableMeili?: boolean | null; +}; + +export type SearchPagination = { + untilId?: MiNote['id']; + sinceId?: MiNote['id']; + limit: number; +}; + function compileValue(value: V): string { if (typeof value === 'string') { return `'${value}'`; // TODO: escape @@ -64,7 +80,8 @@ function compileQuery(q: Q): string { @Injectable() export class SearchService { private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local'; - private meilisearchNoteIndex: Index | null = null; + private readonly meilisearchNoteIndex: Index | null = null; + private readonly provider: FulltextSearchProvider; constructor( @Inject(DI.config) @@ -79,6 +96,7 @@ export class SearchService { private cacheService: CacheService, private queryService: QueryService, private idService: IdService, + private loggerService: LoggerService, ) { if (meilisearch) { this.meilisearchNoteIndex = meilisearch.index(`${this.config.meilisearch?.index}---notes`); @@ -110,189 +128,202 @@ export class SearchService { if (this.config.meilisearch?.scope) { this.meilisearchIndexScope = this.config.meilisearch.scope; } + + this.provider = config.fulltextSearch?.provider ?? 'sqlLike'; + this.loggerService.getLogger('SearchService').info(`-- Provider: ${this.provider}`); } @bindThis public async indexNote(note: MiNote): Promise<void> { + if (!this.meilisearch) return; if (note.text == null && note.cw == null) return; if (!['home', 'public'].includes(note.visibility)) return; - if (this.meilisearch) { - switch (this.meilisearchIndexScope) { - case 'global': - break; + switch (this.meilisearchIndexScope) { + case 'global': + break; - case 'local': - if (note.userHost == null) break; - return; + case 'local': + if (note.userHost == null) break; + return; - default: { - if (note.userHost == null) break; - if (this.meilisearchIndexScope.includes(note.userHost)) break; - return; - } + default: { + if (note.userHost == null) break; + if (this.meilisearchIndexScope.includes(note.userHost)) break; + return; } - - await this.meilisearchNoteIndex?.addDocuments([{ - id: note.id, - createdAt: this.idService.parse(note.id).date.getTime(), - userId: note.userId, - userHost: note.userHost, - channelId: note.channelId, - cw: note.cw, - text: note.text, - tags: note.tags, - attachedFileTypes: note.attachedFileTypes, - }], { - primaryKey: 'id', - }); } + + await this.meilisearchNoteIndex?.addDocuments([{ + id: note.id, + createdAt: this.idService.parse(note.id).date.getTime(), + userId: note.userId, + userHost: note.userHost, + channelId: note.channelId, + cw: note.cw, + text: note.text, + tags: note.tags, + attachedFileTypes: note.attachedFileTypes, + }], { + primaryKey: 'id', + }); } @bindThis public async unindexNote(note: MiNote): Promise<void> { + if (!this.meilisearch) return; if (!['home', 'public'].includes(note.visibility)) return; - if (this.meilisearch) { - this.meilisearchNoteIndex!.deleteDocument(note.id); - } + await this.meilisearchNoteIndex?.deleteDocument(note.id); + await this.meilisearchNoteIndex?.deleteDocument(note.id); } @bindThis - public async searchNote(q: string, me: MiUser | null, opts: { - userId?: MiNote['userId'] | null; - channelId?: MiNote['channelId'] | null; - host?: string | null; - filetype?: string | null; - order?: string | null; - disableMeili?: boolean | null; - }, pagination: { - untilId?: MiNote['id']; - sinceId?: MiNote['id']; - limit?: number; - }): Promise<MiNote[]> { - if (this.meilisearch && !opts.disableMeili) { - const filter: Q = { - op: 'and', - qs: [], - }; - if (pagination.untilId) filter.qs.push({ op: '<', k: 'createdAt', v: this.idService.parse(pagination.untilId).date.getTime() }); - if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() }); - if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId }); - if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId }); - if (opts.host) { - if (opts.host === '.') { - filter.qs.push({ op: 'is null', k: 'userHost' }); - } else { - filter.qs.push({ op: '=', k: 'userHost', v: opts.host }); - } + public async searchNote( + q: string, + me: MiUser | null, + opts: SearchOpts, + pagination: SearchPagination, + ): Promise<MiNote[]> { + switch (this.provider) { + case 'sqlLike': + case 'sqlPgroonga': { + // ã»ã¨ã‚“ã©å†…容ã«å·®ãŒãªã„ã®ã§sqlLikeã¨sqlPgroongaã‚’åŒã˜å‡¦ç†ã«ã—ã¦ã„ã‚‹. + // ä»Šå¾Œã®æ‹¡å¼µã§å·®ãŒå‡ºã‚‹ç”¨ã§ã‚れã°é–¢æ•°ã‚’分ã‘ã‚‹. + return this.searchNoteByLike(q, me, opts, pagination); } - if (opts.filetype) { - if (opts.filetype === 'image') { - filter.qs.push({ op: 'or', qs: [ - { op: '=', k: 'attachedFileTypes', v: 'image/webp' }, - { op: '=', k: 'attachedFileTypes', v: 'image/png' }, - { op: '=', k: 'attachedFileTypes', v: 'image/jpeg' }, - { op: '=', k: 'attachedFileTypes', v: 'image/avif' }, - { op: '=', k: 'attachedFileTypes', v: 'image/apng' }, - { op: '=', k: 'attachedFileTypes', v: 'image/gif' }, - ] }); - } else if (opts.filetype === 'video') { - filter.qs.push({ op: 'or', qs: [ - { op: '=', k: 'attachedFileTypes', v: 'video/mp4' }, - { op: '=', k: 'attachedFileTypes', v: 'video/webm' }, - { op: '=', k: 'attachedFileTypes', v: 'video/mpeg' }, - { op: '=', k: 'attachedFileTypes', v: 'video/x-m4v' }, - ] }); - } else if (opts.filetype === 'audio') { - filter.qs.push({ op: 'or', qs: [ - { op: '=', k: 'attachedFileTypes', v: 'audio/mpeg' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/flac' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/wav' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/aac' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/webm' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/opus' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/ogg' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/x-m4a' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/mod' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/s3m' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/xm' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/it' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/x-mod' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/x-s3m' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/x-xm' }, - { op: '=', k: 'attachedFileTypes', v: 'audio/x-it' }, - ] }); - } + case 'meilisearch': { + return this.searchNoteByMeiliSearch(q, me, opts, pagination); } - const res = await this.meilisearchNoteIndex!.search(q, { - sort: [`createdAt:${opts.order ? opts.order : 'desc'}`], - matchingStrategy: 'all', - attributesToRetrieve: ['id', 'createdAt'], - filter: compileQuery(filter), - limit: pagination.limit, - }); - if (res.hits.length === 0) return []; - const [ - userIdsWhoMeMuting, - userIdsWhoBlockingMe, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]) : [new Set<string>(), new Set<string>()]; - const notes = (await this.notesRepository.findBy({ - id: In(res.hits.map(x => x.id)), - })).filter(note => { - if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - return true; - }); - return notes.sort((a, b) => a.id > b.id ? -1 : 1); + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const typeCheck: never = this.provider; + return []; + } + } + } + + @bindThis + private async searchNoteByLike( + q: string, + me: MiUser | null, + opts: SearchOpts, + pagination: SearchPagination, + ): Promise<MiNote[]> { + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId); + + if (opts.userId) { + query.andWhere('note.userId = :userId', { userId: opts.userId }); + } else if (opts.channelId) { + query.andWhere('note.channelId = :channelId', { channelId: opts.channelId }); + } + + query + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + if (this.config.fulltextSearch?.provider === 'sqlPgroonga') { + query.andWhere('note.text &@ :q', { q }); } else { - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId); + query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` }); + } - if (opts.userId) { - query.andWhere('note.userId = :userId', { userId: opts.userId }); - } else if (opts.channelId) { - query.andWhere('note.channelId = :channelId', { channelId: opts.channelId }); + if (opts.host) { + if (opts.host === '.') { + query.andWhere('user.host IS NULL'); + } else { + query.andWhere('user.host = :host', { host: opts.host }); } + } - query - .andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser'); + if (opts.filetype) { + /* this is very ugly, but the "correct" solution would + be `and exists (select 1 from + unnest(note."attachedFileTypes") x(t) where t like + :type)` and I can't find a way to get TypeORM to + generate that; this hack works because `~*` is + "regexp match, ignoring case" and the stringified + version of an array of varchars (which is what + `attachedFileTypes` is) looks like `{foo,bar}`, so + we're looking for opts.filetype as the first half of + a MIME type, either at start of the array (after the + `{`) or later (after a `,`) */ + query.andWhere('note."attachedFileTypes"::varchar ~* :type', { type: `[{,]${opts.filetype}/` }); + } - if (opts.host) { - if (opts.host === '.') { - query.andWhere('user.host IS NULL'); - } else { - query.andWhere('user.host = :host', { host: opts.host }); - } - } + this.queryService.generateVisibilityQuery(query, me); + if (me) this.queryService.generateMutedUserQuery(query, me); + if (me) this.queryService.generateBlockedUserQuery(query, me); - if (opts.filetype) { - /* this is very ugly, but the "correct" solution would - be `and exists (select 1 from - unnest(note."attachedFileTypes") x(t) where t like - :type)` and I can't find a way to get TypeORM to - generate that; this hack works because `~*` is - "regexp match, ignoring case" and the stringified - version of an array of varchars (which is what - `attachedFileTypes` is) looks like `{foo,bar}`, so - we're looking for opts.filetype as the first half of - a MIME type, either at start of the array (after the - `{`) or later (after a `,`) */ - query.andWhere(`note."attachedFileTypes"::varchar ~* :type`, { type: `[{,]${opts.filetype}/` }); - } + return await query.limit(pagination.limit).getMany(); + } - this.queryService.generateVisibilityQuery(query, me); - if (me) this.queryService.generateMutedUserQuery(query, me); - if (me) this.queryService.generateBlockedUserQuery(query, me); + @bindThis + private async searchNoteByMeiliSearch( + q: string, + me: MiUser | null, + opts: SearchOpts, + pagination: SearchPagination, + ): Promise<MiNote[]> { + if (!this.meilisearch || !this.meilisearchNoteIndex) { + throw new Error('MeiliSearch is not available'); + } + + const filter: Q = { + op: 'and', + qs: [], + }; + if (pagination.untilId) filter.qs.push({ + op: '<', + k: 'createdAt', + v: this.idService.parse(pagination.untilId).date.getTime(), + }); + if (pagination.sinceId) filter.qs.push({ + op: '>', + k: 'createdAt', + v: this.idService.parse(pagination.sinceId).date.getTime(), + }); + if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId }); + if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId }); + if (opts.host) { + if (opts.host === '.') { + filter.qs.push({ op: 'is null', k: 'userHost' }); + } else { + filter.qs.push({ op: '=', k: 'userHost', v: opts.host }); + } + } - return await query.limit(pagination.limit).getMany(); + const res = await this.meilisearchNoteIndex.search(q, { + sort: ['createdAt:desc'], + matchingStrategy: 'all', + attributesToRetrieve: ['id', 'createdAt'], + filter: compileQuery(filter), + limit: pagination.limit, + }); + if (res.hits.length === 0) { + return []; } + + const [ + userIdsWhoMeMuting, + userIdsWhoBlockingMe, + ] = me + ? await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + ]) + : [new Set<string>(), new Set<string>()]; + const notes = (await this.notesRepository.findBy({ + id: In(res.hits.map(x => x.id)), + })).filter(note => { + if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; + return true; + }); + + return notes.sort((a, b) => a.id > b.id ? -1 : 1); } } diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts index de00169612..8239490adc 100644 --- a/packages/backend/src/core/SystemWebhookService.ts +++ b/packages/backend/src/core/SystemWebhookService.ts @@ -50,7 +50,6 @@ export type SystemWebhookPayload<T extends SystemWebhookEventType> = @Injectable() export class SystemWebhookService implements OnApplicationShutdown { - private logger: Logger; private activeSystemWebhooksFetched = false; private activeSystemWebhooks: MiSystemWebhook[] = []; @@ -62,11 +61,9 @@ export class SystemWebhookService implements OnApplicationShutdown { private idService: IdService, private queueService: QueueService, private moderationLogService: ModerationLogService, - private loggerService: LoggerService, private globalEventService: GlobalEventService, ) { this.redisForSub.on('message', this.onMessage); - this.logger = this.loggerService.getLogger('webhook'); } @bindThis @@ -193,28 +190,24 @@ export class SystemWebhookService implements OnApplicationShutdown { /** * SystemWebhook ã‚’Webhooké…é€ã‚ューã«è¿½åŠ ã™ã‚‹ * @see QueueService.systemWebhookDeliver - * // TODO: contentã®åž‹ã‚’åŽ³æ ¼åŒ–ã™ã‚‹ */ @bindThis public async enqueueSystemWebhook<T extends SystemWebhookEventType>( - webhook: MiSystemWebhook | MiSystemWebhook['id'], type: T, content: SystemWebhookPayload<T>, + opts?: { + excludes?: MiSystemWebhook['id'][]; + }, ) { - const webhookEntity = typeof webhook === 'string' - ? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook) - : webhook; - if (!webhookEntity || !webhookEntity.isActive) { - this.logger.info(`SystemWebhook is not active or not found : ${webhook}`); - return; - } - - if (!webhookEntity.on.includes(type)) { - this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`); - return; - } - - return this.queueService.systemWebhookDeliver(webhookEntity, type, content); + const webhooks = await this.fetchActiveSystemWebhooks() + .then(webhooks => { + return webhooks.filter(webhook => !opts?.excludes?.includes(webhook.id) && webhook.on.includes(type)); + }); + return Promise.all( + webhooks.map(webhook => { + return this.queueService.systemWebhookDeliver(webhook, type, content); + }), + ); } @bindThis diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 2f1310b8ef..8da1bb2092 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -118,13 +118,7 @@ export class UserBlockingService implements OnModuleInit { schema: 'UserDetailedNotMe', }).then(async packed => { this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'unfollow', { - user: packed, - }); - } + this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packed }); }); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 8963003057..b98ca97ec9 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -333,13 +333,7 @@ export class UserFollowingService implements OnModuleInit { schema: 'UserDetailedNotMe', }).then(async packed => { this.globalEventService.publishMainStream(follower.id, 'follow', packed); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'follow', { - user: packed, - }); - } + this.webhookService.enqueueUserWebhook(follower.id, 'follow', { user: packed }); }); } @@ -347,13 +341,7 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(async packed => { this.globalEventService.publishMainStream(followee.id, 'followed', packed); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'followed', { - user: packed, - }); - } + this.webhookService.enqueueUserWebhook(followee.id, 'followed', { user: packed }); }); // é€šçŸ¥ã‚’ä½œæˆ @@ -400,13 +388,7 @@ export class UserFollowingService implements OnModuleInit { schema: 'UserDetailedNotMe', }).then(async packed => { this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'unfollow', { - user: packed, - }); - } + this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packed }); }); } @@ -744,13 +726,7 @@ export class UserFollowingService implements OnModuleInit { }); this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee); - - const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); - for (const webhook of webhooks) { - this.queueService.userWebhookDeliver(webhook, 'unfollow', { - user: packedFollowee, - }); - } + this.webhookService.enqueueUserWebhook(follower.id, 'unfollow', { user: packedFollowee }); } @bindThis diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts index 9b1961c631..1f471513f3 100644 --- a/packages/backend/src/core/UserService.ts +++ b/packages/backend/src/core/UserService.ts @@ -63,13 +63,6 @@ export class UserService { @bindThis public async notifySystemWebhook(user: MiUser, type: 'userCreated') { const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' }); - const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] }); - for (const webhookId of recipientWebhookIds) { - await this.systemWebhookService.enqueueSystemWebhook( - webhookId, - type, - packedUser, - ); - } + return this.systemWebhookService.enqueueSystemWebhook(type, packedUser); } } diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts index 911efdf768..08db4c9afc 100644 --- a/packages/backend/src/core/UserWebhookService.ts +++ b/packages/backend/src/core/UserWebhookService.ts @@ -5,13 +5,14 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import { type WebhooksRepository } from '@/models/_.js'; +import { MiUser, type WebhooksRepository } from '@/models/_.js'; import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; +import { QueueService } from '@/core/QueueService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; export type UserWebhookPayload<T extends WebhookEventTypes> = T extends 'note' | 'reply' | 'renote' |'mention' | 'edited' ? { @@ -34,6 +35,7 @@ export class UserWebhookService implements OnApplicationShutdown { private redisForSub: Redis.Redis, @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, + private queueService: QueueService, ) { this.redisForSub.on('message', this.onMessage); } @@ -75,6 +77,25 @@ export class UserWebhookService implements OnApplicationShutdown { return query.getMany(); } + /** + * UserWebhook ã‚’Webhooké…é€ã‚ューã«è¿½åŠ ã™ã‚‹ + * @see QueueService.userWebhookDeliver + */ + @bindThis + public async enqueueUserWebhook<T extends WebhookEventTypes>( + userId: MiUser['id'], + type: T, + content: UserWebhookPayload<T>, + ) { + const webhooks = await this.getActiveWebhooks() + .then(webhooks => webhooks.filter(webhook => webhook.userId === userId && webhook.on.includes(type))); + return Promise.all( + webhooks.map(webhook => { + return this.queueService.userWebhookDeliver(webhook, type, content); + }), + ); + } + @bindThis private async onMessage(_: string, data: string): Promise<void> { const obj = JSON.parse(data); diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index f905914022..81eaa5f95d 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { URL } from 'node:url'; -import punycode from 'punycode/punycode.js'; +import { URL, domainToASCII } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import RE2 from 're2'; import psl from 'psl'; @@ -107,13 +106,13 @@ export class UtilityService { @bindThis public toPuny(host: string): string { - return punycode.toASCII(host.toLowerCase()); + return domainToASCII(host.toLowerCase()); } @bindThis public toPunyNullable(host: string | null | undefined): string | null { if (host == null) return null; - return punycode.toASCII(host.toLowerCase()); + return domainToASCII(host.toLowerCase()); } @bindThis diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index ad53192f18..ed75e4f467 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -189,14 +189,12 @@ export class WebAuthnService { */ @bindThis public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> { - const challenge = await this.redisClient.get(`webauthn:challenge:${context}`); + const challenge = await this.redisClient.getdel(`webauthn:challenge:${context}`); if (!challenge) { throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`); } - await this.redisClient.del(`webauthn:challenge:${context}`); - const key = await this.userSecurityKeysRepository.findOneBy({ id: response.id, }); diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index fb706a775f..a71c0edcf5 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -192,6 +192,9 @@ export class ApRendererService { // || emoji.originalUrl ã—ã¦ã‚‹ã®ã¯å¾Œæ–¹äº’æ›æ€§ã®ãŸã‚(publicUrlã¯stringãªã®ã§??ã¯ã ã‚) url: emoji.publicUrl || emoji.originalUrl, }, + _misskey_license: { + freeText: emoji.license + }, }; } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index c82a9be3b1..a0c3a4846c 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -5,7 +5,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; -import { UnrecoverableError } from 'bullmq'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; @@ -17,6 +16,7 @@ import { bindThis } from '@/decorators.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; import { fromTuple } from '@/misc/from-tuple.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; @@ -68,7 +68,7 @@ export class Resolver { if (isCollectionOrOrderedCollection(collection)) { return collection; } else { - throw new UnrecoverableError(`unrecognized collection type: ${collection.type}`); + throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `unrecognized collection type: ${collection.type}`); } } @@ -85,15 +85,15 @@ export class Resolver { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). // Avoid strange behaviour by not trying to resolve these at all. - throw new UnrecoverableError(`cannot resolve URL with fragment: ${value}`); + throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `cannot resolve URL with fragment: ${value}`); } if (this.history.has(value)) { - throw new Error(`cannot resolve already resolved URL: ${value}`); + throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', `cannot resolve already resolved URL: ${value}`); } if (this.history.size > this.recursionLimit) { - throw new Error(`hit recursion limit: ${value}`); + throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `hit recursion limit: ${value}`); } this.history.add(value); @@ -104,7 +104,7 @@ export class Resolver { } if (!this.utilityService.isFederationAllowedHost(host)) { - throw new UnrecoverableError(`cannot fetch AP object ${value}: blocked instance ${host}`); + throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', `cannot fetch AP object ${value}: blocked instance ${host}`); } if (this.config.signToActivityPubGet && !this.user) { @@ -120,13 +120,13 @@ export class Resolver { !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' ) { - throw new UnrecoverableError(`invalid AP object ${value}: does not have ActivityStreams context`); + throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', `invalid AP object ${value}: does not have ActivityStreams context`); } // Since redirects are allowed, we cannot safely validate an anonymous object. // Reject any responses without an ID, as all other checks depend on that value. if (object.id == null) { - throw new UnrecoverableError(`invalid AP object ${value}: missing id`); + throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', `invalid AP object ${value}: missing id`); } // We allow some limited cross-domain redirects, which means the host may have changed during fetch. @@ -135,12 +135,12 @@ export class Resolver { if (finalHost !== host) { // Make sure the redirect stayed within the same authority. if (this.utilityService.punyHostPSLDomain(object.id) !== this.utilityService.punyHostPSLDomain(value)) { - throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`); + throw new IdentifiableError('fd93c2fa-69a8-440f-880b-bf178e0ec877', `invalid AP object ${value}: id ${object.id} has different host`); } // Check if the redirect bounce from [allowed domain] to [blocked domain]. if (!this.utilityService.isFederationAllowedHost(finalHost)) { - throw new UnrecoverableError(`cannot fetch AP object ${value}: redirected to blocked instance ${finalHost}`); + throw new IdentifiableError('0a72bf24-2d9b-4f1d-886b-15aaa31adeda', `cannot fetch AP object ${value}: redirected to blocked instance ${finalHost}`); } } @@ -150,7 +150,7 @@ export class Resolver { @bindThis private resolveLocal(url: string): Promise<IObject> { const parsed = this.apDbResolverService.parseUri(url); - if (!parsed.local) throw new UnrecoverableError(`resolveLocal - not a local URL: ${url}`); + if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${url}`); switch (parsed.type) { case 'notes': @@ -179,7 +179,7 @@ export class Resolver { case 'follows': return this.followRequestsRepository.findOneBy({ id: parsed.id }) .then(async followRequest => { - if (followRequest == null) throw new UnrecoverableError(`resolveLocal - invalid follow request ID ${parsed.id}: ${url}`); + if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', `resolveLocal - invalid follow request ID ${parsed.id}: ${url}`); const [follower, followee] = await Promise.all([ this.usersRepository.findOneBy({ id: followRequest.followerId, @@ -191,12 +191,12 @@ export class Resolver { }), ]); if (follower == null || followee == null) { - throw new Error(`resolveLocal - follower or followee does not exist: ${url}`); + throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', `resolveLocal - follower or followee does not exist: ${url}`); } return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)); }); default: - throw new UnrecoverableError(`resolveLocal: type ${parsed.type} unhandled: ${url}`); + throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `resolveLocal: type ${parsed.type} unhandled: ${url}`); } } } diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index d7b6fc6589..5c0b8ffcbb 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -561,6 +561,11 @@ const extension_context_definition = { '_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents', '_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore', '_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore', + '_misskey_license': 'misskey:_misskey_license', + 'freeText': { + '@id': 'misskey:freeText', + '@type': 'schema:text', + }, 'isCat': 'misskey:isCat', // Firefish firefish: 'https://joinfirefish.org/ns#', diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index e4c4fe54b5..9fc6945edb 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -690,6 +690,8 @@ export class ApNoteService { originalUrl: tag.icon.url, publicUrl: tag.icon.url, updatedAt: new Date(), + // _misskey_license ãŒå˜åœ¨ã—ãªã‘れ㰠`null` + license: (tag._misskey_license?.freeText ?? null), }); const emoji = await this.emojisRepository.findOneBy({ host, name }); @@ -711,6 +713,8 @@ export class ApNoteService { publicUrl: tag.icon.url, updatedAt: new Date(), aliases: [], + // _misskey_license ãŒå˜åœ¨ã—ãªã‘れ㰠`null` + license: (tag._misskey_license?.freeText ?? null) }); })); } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 119a9d8ccb..21eff4ddb7 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -270,6 +270,11 @@ export interface IApEmoji extends IObject { type: 'Emoji'; name: string; updated: string; + // Misskeyæ‹¡å¼µã€‚å¾Œæ–¹äº’æ›æ€§ã®ãŸã‚ã«optional。 + // å°†æ¥ã®æ‹¡å¼µæ€§ã‚’考慮ã—ã¦objectã«ã—ã¦ã„ã‚‹ + _misskey_license?: { + freeText: string | null; + }; } export const isEmoji = (object: IObject): object is IApEmoji => diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 841bd731c0..490d3f2511 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -4,10 +4,10 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository } from '@/models/_.js'; +import type { EmojisRepository, MiRole, RolesRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; import type { MiEmoji } from '@/models/Emoji.js'; import { bindThis } from '@/decorators.js'; @@ -16,6 +16,8 @@ export class EmojiEntityService { constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, ) { } @@ -68,8 +70,90 @@ export class EmojiEntityService { @bindThis public packDetailedMany( emojis: any[], - ) { + ): Promise<Packed<'EmojiDetailed'>[]> { return Promise.all(emojis.map(x => this.packDetailed(x))); } + + @bindThis + public async packDetailedAdmin( + src: MiEmoji['id'] | MiEmoji, + hint?: { + roles?: Map<MiRole['id'], MiRole> + }, + ): Promise<Packed<'EmojiDetailedAdmin'>> { + const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); + + const roles = Array.of<MiRole>(); + if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0) { + if (hint?.roles) { + const hintRoles = hint.roles; + roles.push( + ...emoji.roleIdsThatCanBeUsedThisEmojiAsReaction + .filter(x => hintRoles.has(x)) + .map(x => hintRoles.get(x)!), + ); + } else { + roles.push( + ...await this.rolesRepository.findBy({ id: In(emoji.roleIdsThatCanBeUsedThisEmojiAsReaction) }), + ); + } + + roles.sort((a, b) => { + if (a.displayOrder !== b.displayOrder) { + return b.displayOrder - a.displayOrder; + } + + return a.id.localeCompare(b.id); + }); + } + + return { + id: emoji.id, + updatedAt: emoji.updatedAt?.toISOString() ?? null, + name: emoji.name, + host: emoji.host, + uri: emoji.uri, + type: emoji.type, + aliases: emoji.aliases, + category: emoji.category, + publicUrl: emoji.publicUrl, + originalUrl: emoji.originalUrl, + license: emoji.license, + localOnly: emoji.localOnly, + isSensitive: emoji.isSensitive, + roleIdsThatCanBeUsedThisEmojiAsReaction: roles.map(it => ({ id: it.id, name: it.name })), + }; + } + + @bindThis + public async packDetailedAdminMany( + emojis: MiEmoji['id'][] | MiEmoji[], + hint?: { + roles?: Map<MiRole['id'], MiRole> + }, + ): Promise<Packed<'EmojiDetailedAdmin'>[]> { + // IDã®ã¿ã®è¦ç´ をピックアップã—ã€DBã‹ã‚‰ãƒ¬ã‚³ãƒ¼ãƒ‰ã‚’å–り出ã—ã¦ä»–ã®å€¤ã‚’補完ã™ã‚‹ + const emojiEntities = emojis.filter(x => typeof x === 'object') as MiEmoji[]; + const emojiIdOnlyList = emojis.filter(x => typeof x === 'string') as string[]; + if (emojiIdOnlyList.length > 0) { + emojiEntities.push(...await this.emojisRepository.findBy({ id: In(emojiIdOnlyList) })); + } + + // 特定ãƒãƒ¼ãƒ«å°‚用ã®çµµæ–‡å—ã§ã‚ã‚‹å ´åˆã€ãã®ãƒãƒ¼ãƒ«æƒ…å ±ã‚’ã‚らã‹ã˜ã‚ã¾ã¨ã‚ã¦å–å¾—ã—ã¦ãŠã(packå´ã§éƒ½åº¦å–得も出æ¥ã‚‹ãŒè² è·ãŒé«˜ã„ã®ã§ï¼‰ + let hintRoles: Map<MiRole['id'], MiRole>; + if (hint?.roles) { + hintRoles = hint.roles; + } else { + const roles = Array.of<MiRole>(); + const roleIds = [...new Set(emojiEntities.flatMap(x => x.roleIdsThatCanBeUsedThisEmojiAsReaction))]; + if (roleIds.length > 0) { + roles.push(...await this.rolesRepository.findBy({ id: In(roleIds) })); + } + + hintRoles = new Map(roles.map(x => [x.id, x])); + } + + return Promise.all(emojis.map(x => this.packDetailedAdmin(x, { roles: hintRoles }))); + } } diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 857e8f5a7b..4e127f17ca 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -145,6 +145,7 @@ export class MetaEntityService { enableUrlPreview: instance.urlPreviewEnabled, noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local', maxFileSize: this.config.maxFileSize, + federation: this.meta.federation, }; return packed; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index be45e75d74..e5b575c219 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -110,8 +110,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> { - // FIXME: ã“ã®visibility変更処ç†ãŒå½“関数ã«ã‚ã‚‹ã®ã¯è‹¥å¹²ä¸è‡ªç„¶ã‹ã‚‚ã—れãªã„(関数åã‚’ treatVisibility ã¨ã‹ã«å¤‰ãˆã‚‹æ‰‹ã‚‚ã‚ã‚‹) + private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] { if (packedNote.visibility === 'public' || packedNote.visibility === 'home') { const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore; if ((followersOnlyBefore != null) @@ -123,7 +122,11 @@ export class NoteEntityService implements OnModuleInit { packedNote.visibility = 'followers'; } } + return packedNote.visibility; + } + @bindThis + private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> { if (meId === packedNote.userId) return; // TODO: isVisibleForMe を使ã†ã‚ˆã†ã«ã—ã¦ã‚‚良ã•ãã†(åž‹é•ã†ã‘ã©) @@ -500,6 +503,8 @@ export class NoteEntityService implements OnModuleInit { } : {}), }); + this.treatVisibility(packed); + if (!opts.skipHide) { await this.hideNote(packed, meId); } diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 040e36228c..f612591eda 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -33,7 +33,11 @@ import { packedClipSchema } from '@/models/json-schema/clip.js'; import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; -import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; +import { + packedEmojiDetailedAdminSchema, + packedEmojiDetailedSchema, + packedEmojiSimpleSchema, +} from '@/models/json-schema/emoji.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; @@ -95,6 +99,7 @@ export const refs = { GalleryPost: packedGalleryPostSchema, EmojiSimple: packedEmojiSimpleSchema, EmojiDetailed: packedEmojiDetailedSchema, + EmojiDetailedAdmin: packedEmojiDetailedAdminSchema, Flash: packedFlashSchema, Signin: packedSigninSchema, RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema, diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 62686ad5ae..3cd263fa37 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -104,3 +104,86 @@ export const packedEmojiDetailedSchema = { }, }, } as const; + +export const packedEmojiDetailedAdminSchema = { + type: 'object', + properties: { + id: { + type: 'string', + format: 'id', + optional: false, nullable: false, + }, + updatedAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: true, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + host: { + type: 'string', + optional: false, nullable: true, + description: 'The local host is represented with `null`.', + }, + publicUrl: { + type: 'string', + optional: false, nullable: false, + }, + originalUrl: { + type: 'string', + optional: false, nullable: false, + }, + uri: { + type: 'string', + optional: false, nullable: true, + }, + type: { + type: 'string', + optional: false, nullable: true, + }, + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + format: 'id', + optional: false, nullable: false, + }, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + license: { + type: 'string', + optional: false, nullable: true, + }, + localOnly: { + type: 'boolean', + optional: false, nullable: false, + }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 29fdb4f6be..bf68208c37 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -321,6 +321,11 @@ export const packedMetaLiteSchema = { type: 'number', optional: false, nullable: false, }, + federation: { + type: 'string', + enum: ['all', 'specified', 'none'], + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index c964c3ffee..98405052c6 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -92,27 +92,65 @@ export const dbLogger = new MisskeyLogger('db'); const sqlLogger = dbLogger.createSubLogger('sql', 'gray'); +export type LoggerProps = { + disableQueryTruncation?: boolean; + enableQueryParamLogging?: boolean; +} + +function highlightSql(sql: string) { + return highlight.highlight(sql, { + language: 'sql', ignoreIllegals: true, + }); +} + +function truncateSql(sql: string) { + return sql.length > 100 ? `${sql.substring(0, 100)}...` : sql; +} + +function stringifyParameter(param: any) { + if (param instanceof Date) { + return param.toISOString(); + } else { + return param; + } +} + class MyCustomLogger implements Logger { + constructor(private props: LoggerProps = {}) { + } + + @bindThis + private transformQueryLog(sql: string) { + let modded = sql; + if (!this.props.disableQueryTruncation) { + modded = truncateSql(modded); + } + + return highlightSql(modded); + } + @bindThis - private highlight(sql: string) { - return highlight.highlight(sql, { - language: 'sql', ignoreIllegals: true, - }); + private transformParameters(parameters?: any[]) { + if (this.props.enableQueryParamLogging && parameters && parameters.length > 0) { + return parameters.map(stringifyParameter); + } + + return undefined; } @bindThis public logQuery(query: string, parameters?: any[]) { - sqlLogger.info(this.highlight(query).substring(0, 100)); + sqlLogger.info(this.transformQueryLog(query), this.transformParameters(parameters)); } @bindThis public logQueryError(error: string, query: string, parameters?: any[]) { - sqlLogger.error(this.highlight(query)); + sqlLogger.error(this.transformQueryLog(query), this.transformParameters(parameters)); } @bindThis public logQuerySlow(time: number, query: string, parameters?: any[]) { - sqlLogger.warn(this.highlight(query)); + sqlLogger.warn(this.transformQueryLog(query), this.transformParameters(parameters)); } @bindThis @@ -249,7 +287,12 @@ export function createPostgresDataSource(config: Config) { }, } : false, logging: log, - logger: log ? new MyCustomLogger() : undefined, + logger: log + ? new MyCustomLogger({ + disableQueryTruncation: config.logging?.sql?.disableQueryTruncation, + enableQueryParamLogging: config.logging?.sql?.enableQueryParamLogging, + }) + : undefined, maxQueryExecutionTime: 300, entities: entities, migrations: ['../../migration/*.js'], diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts index b81987cc15..ef21b6142e 100644 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -215,15 +215,10 @@ export class CheckModeratorsActivityProcessorService { // -- SystemWebhook - const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks() - .then(it => it.filter(it => it.on.includes('inactiveModeratorsWarning'))); - for (const systemWebhook of systemWebhooks) { - this.systemWebhookService.enqueueSystemWebhook( - systemWebhook, - 'inactiveModeratorsWarning', - { remainingTime: remainingTime }, - ); - } + return this.systemWebhookService.enqueueSystemWebhook( + 'inactiveModeratorsWarning', + { remainingTime: remainingTime }, + ); } @bindThis @@ -253,15 +248,10 @@ export class CheckModeratorsActivityProcessorService { // -- SystemWebhook - const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks() - .then(it => it.filter(it => it.on.includes('inactiveModeratorsInvitationOnlyChanged'))); - for (const systemWebhook of systemWebhooks) { - this.systemWebhookService.enqueueSystemWebhook( - systemWebhook, - 'inactiveModeratorsInvitationOnlyChanged', - {}, - ); - } + return this.systemWebhookService.enqueueSystemWebhook( + 'inactiveModeratorsInvitationOnlyChanged', + {}, + ); } @bindThis diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 19f98c0d51..8c5faa8d07 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -48,6 +48,7 @@ export class CleanChartsProcessorService { public async process(): Promise<void> { this.logger.info('Clean charts...'); + // DBã¸ã®åŒæ™‚接続をé¿ã‘ã‚‹ãŸã‚ã«Promise.allを使ã‚ãšã²ã¨ã¤ãšã¤å®Ÿè¡Œã™ã‚‹ await this.federationChart.clean(); await this.notesChart.clean(); await this.usersChart.clean(); diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 666a709ab9..383fa0c26a 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -14,10 +14,10 @@ import { createTempDir } from '@/misc/create-temp.js'; import { DriveService } from '@/core/DriveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { bindThis } from '@/decorators.js'; +import type { Config } from '@/config.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; -import type { Config } from '@/config.js'; // TODO: åå‰è¡çªæ™‚ã®å‹•作をé¸ã¹ã‚‹ã‚ˆã†ã«ã™ã‚‹ @Injectable() @@ -92,6 +92,7 @@ export class ImportCustomEmojisProcessorService { await this.emojisRepository.delete({ name: nameNfc, }); + try { const driveFile = await this.driveService.addFile({ user: null, @@ -100,11 +101,13 @@ export class ImportCustomEmojisProcessorService { force: true, }); await this.customEmojiService.add({ + originalUrl: driveFile.url, + publicUrl: driveFile.webpublicUrl ?? driveFile.url, + fileType: driveFile.webpublicType ?? driveFile.type, name: nameNfc, category: emojiInfo.category?.normalize('NFC'), host: null, aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')), - driveFile, license: emojiInfo.license, isSensitive: emojiInfo.isSensitive, localOnly: emojiInfo.localOnly, diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index 46e1adf173..0c47fdedb3 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -29,6 +29,7 @@ export class ResyncChartsProcessorService { public async process(): Promise<void> { this.logger.info('Resync charts...'); + // DBã¸ã®åŒæ™‚接続をé¿ã‘ã‚‹ãŸã‚ã«Promise.allを使ã‚ãšã²ã¨ã¤ãšã¤å®Ÿè¡Œã™ã‚‹ // TODO: ユーザーã”ã¨ã®ãƒãƒ£ãƒ¼ãƒˆã‚‚æ›´æ–°ã™ã‚‹ // TODO: インスタンスã”ã¨ã®ãƒãƒ£ãƒ¼ãƒˆã‚‚æ›´æ–°ã™ã‚‹ await this.driveChart.resync(); diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index c09cbccc57..fc8856a271 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -48,6 +48,7 @@ export class TickChartsProcessorService { public async process(): Promise<void> { this.logger.info('Tick charts...'); + // DBã¸ã®åŒæ™‚接続をé¿ã‘ã‚‹ãŸã‚ã«Promise.allを使ã‚ãšã²ã¨ã¤ãšã¤å®Ÿè¡Œã™ã‚‹ await this.federationChart.tick(false); await this.notesChart.tick(false); await this.usersChart.tick(false); diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 9433392df5..a900675a86 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -6,9 +6,12 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; +import type { SystemWebhookEventType } from '@/models/SystemWebhook.js'; import type { MiUser } from '@/models/User.js'; -import type { MiWebhook } from '@/models/Webhook.js'; +import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; import type { IActivity } from '@/core/activitypub/type.js'; +import type { SystemWebhookPayload } from '@/core/SystemWebhookService.js'; +import type { UserWebhookPayload } from '@/core/UserWebhookService.js'; import type httpSignature from '@peertube/http-signature'; export type DeliverJobData = { @@ -131,9 +134,9 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; -export type SystemWebhookDeliverJobData = { - type: string; - content: unknown; +export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = { + type: T; + content: SystemWebhookPayload<T>; webhookId: MiWebhook['id']; to: string; secret: string; @@ -141,9 +144,9 @@ export type SystemWebhookDeliverJobData = { eventId: string; }; -export type UserWebhookDeliverJobData = { - type: string; - content: unknown; +export type UserWebhookDeliverJobData<T extends WebhookEventTypes = WebhookEventTypes> = { + type: T; + content: UserWebhookPayload<T>; webhookId: MiWebhook['id']; userId: MiUser['id']; to: string; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 815bf278c7..a4dddbd6c3 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -653,8 +653,8 @@ export class ActivityPubServerService { }, deriveConstraint(request: IncomingMessage) { const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]); - const isAp = typeof accepted === 'string' && !accepted.match(/html/); - return isAp ? 'ap' : 'html'; + if (accepted === false) return null; + return accepted !== 'html' ? 'ap' : 'html'; }, }); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index e319d6e0a4..9cfb2f0ac0 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -6,816 +6,13 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; -import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js'; -import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js'; -import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js'; -import * as ep___admin_abuseReport_notificationRecipient_update from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js'; -import * as ep___admin_abuseReport_notificationRecipient_delete from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js'; -import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; -import * as ep___admin_meta from './endpoints/admin/meta.js'; -import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; -import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; -import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js'; -import * as ep___admin_ad_create from './endpoints/admin/ad/create.js'; -import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js'; -import * as ep___admin_ad_list from './endpoints/admin/ad/list.js'; -import * as ep___admin_ad_update from './endpoints/admin/ad/update.js'; -import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js'; -import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; -import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; -import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; -import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js'; -import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js'; -import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; -import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; -import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; -import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; -import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; -import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; -import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; -import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; -import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; -import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; -import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; -import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; -import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; -import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; -import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; -import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; -import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; -import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; -import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; -import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; -import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; -import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; -import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; -import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; -import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; -import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; -import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; -import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; -import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; -import * as ep___admin_invite_create from './endpoints/admin/invite/create.js'; -import * as ep___admin_invite_list from './endpoints/admin/invite/list.js'; -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'; -import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; -import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; -import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; -import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js'; -import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js'; -import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; -import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; -import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; -import * as ep___admin_showUser from './endpoints/admin/show-user.js'; -import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; -import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js'; -import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js'; -import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js'; -import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; -import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; -import * as ep___admin_approveUser from './endpoints/admin/approve-user.js'; -import * as ep___admin_declineUser from './endpoints/admin/decline-user.js'; -import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; -import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; -import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; -import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; -import * as ep___admin_roles_create from './endpoints/admin/roles/create.js'; -import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js'; -import * as ep___admin_roles_list from './endpoints/admin/roles/list.js'; -import * as ep___admin_roles_show from './endpoints/admin/roles/show.js'; -import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; -import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; -import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; -import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; -import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; -import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js'; -import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js'; -import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js'; -import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js'; -import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js'; -import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js'; -import * as ep___announcements from './endpoints/announcements.js'; -import * as ep___announcements_show from './endpoints/announcements/show.js'; -import * as ep___antennas_create from './endpoints/antennas/create.js'; -import * as ep___antennas_delete from './endpoints/antennas/delete.js'; -import * as ep___antennas_list from './endpoints/antennas/list.js'; -import * as ep___antennas_notes from './endpoints/antennas/notes.js'; -import * as ep___antennas_show from './endpoints/antennas/show.js'; -import * as ep___antennas_update from './endpoints/antennas/update.js'; -import * as ep___ap_get from './endpoints/ap/get.js'; -import * as ep___ap_show from './endpoints/ap/show.js'; -import * as ep___app_create from './endpoints/app/create.js'; -import * as ep___app_show from './endpoints/app/show.js'; -import * as ep___auth_accept from './endpoints/auth/accept.js'; -import * as ep___auth_session_generate from './endpoints/auth/session/generate.js'; -import * as ep___auth_session_show from './endpoints/auth/session/show.js'; -import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js'; -import * as ep___blocking_create from './endpoints/blocking/create.js'; -import * as ep___blocking_delete from './endpoints/blocking/delete.js'; -import * as ep___blocking_list from './endpoints/blocking/list.js'; -import * as ep___channels_create from './endpoints/channels/create.js'; -import * as ep___channels_featured from './endpoints/channels/featured.js'; -import * as ep___channels_follow from './endpoints/channels/follow.js'; -import * as ep___channels_followed from './endpoints/channels/followed.js'; -import * as ep___channels_owned from './endpoints/channels/owned.js'; -import * as ep___channels_show from './endpoints/channels/show.js'; -import * as ep___channels_timeline from './endpoints/channels/timeline.js'; -import * as ep___channels_unfollow from './endpoints/channels/unfollow.js'; -import * as ep___channels_update from './endpoints/channels/update.js'; -import * as ep___channels_favorite from './endpoints/channels/favorite.js'; -import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js'; -import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js'; -import * as ep___channels_search from './endpoints/channels/search.js'; -import * as ep___charts_activeUsers from './endpoints/charts/active-users.js'; -import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; -import * as ep___charts_drive from './endpoints/charts/drive.js'; -import * as ep___charts_federation from './endpoints/charts/federation.js'; -import * as ep___charts_instance from './endpoints/charts/instance.js'; -import * as ep___charts_notes from './endpoints/charts/notes.js'; -import * as ep___charts_user_drive from './endpoints/charts/user/drive.js'; -import * as ep___charts_user_following from './endpoints/charts/user/following.js'; -import * as ep___charts_user_notes from './endpoints/charts/user/notes.js'; -import * as ep___charts_user_pv from './endpoints/charts/user/pv.js'; -import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js'; -import * as ep___charts_users from './endpoints/charts/users.js'; -import * as ep___clips_addNote from './endpoints/clips/add-note.js'; -import * as ep___clips_removeNote from './endpoints/clips/remove-note.js'; -import * as ep___clips_create from './endpoints/clips/create.js'; -import * as ep___clips_delete from './endpoints/clips/delete.js'; -import * as ep___clips_list from './endpoints/clips/list.js'; -import * as ep___clips_notes from './endpoints/clips/notes.js'; -import * as ep___clips_show from './endpoints/clips/show.js'; -import * as ep___clips_update from './endpoints/clips/update.js'; -import * as ep___clips_favorite from './endpoints/clips/favorite.js'; -import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js'; -import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js'; -import * as ep___drive from './endpoints/drive.js'; -import * as ep___drive_files from './endpoints/drive/files.js'; -import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js'; -import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js'; -import * as ep___drive_files_create from './endpoints/drive/files/create.js'; -import * as ep___drive_files_delete from './endpoints/drive/files/delete.js'; -import * as ep___drive_files_findByHash from './endpoints/drive/files/find-by-hash.js'; -import * as ep___drive_files_find from './endpoints/drive/files/find.js'; -import * as ep___drive_files_show from './endpoints/drive/files/show.js'; -import * as ep___drive_files_update from './endpoints/drive/files/update.js'; -import * as ep___drive_files_uploadFromUrl from './endpoints/drive/files/upload-from-url.js'; -import * as ep___drive_folders from './endpoints/drive/folders.js'; -import * as ep___drive_folders_create from './endpoints/drive/folders/create.js'; -import * as ep___drive_folders_delete from './endpoints/drive/folders/delete.js'; -import * as ep___drive_folders_find from './endpoints/drive/folders/find.js'; -import * as ep___drive_folders_show from './endpoints/drive/folders/show.js'; -import * as ep___drive_folders_update from './endpoints/drive/folders/update.js'; -import * as ep___drive_stream from './endpoints/drive/stream.js'; -import * as ep___emailAddress_available from './endpoints/email-address/available.js'; -import * as ep___endpoint from './endpoints/endpoint.js'; -import * as ep___endpoints from './endpoints/endpoints.js'; -import * as ep___exportCustomEmojis from './endpoints/export-custom-emojis.js'; -import * as ep___federation_followers from './endpoints/federation/followers.js'; -import * as ep___federation_following from './endpoints/federation/following.js'; -import * as ep___federation_instances from './endpoints/federation/instances.js'; -import * as ep___federation_showInstance from './endpoints/federation/show-instance.js'; -import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js'; -import * as ep___federation_users from './endpoints/federation/users.js'; -import * as ep___federation_stats from './endpoints/federation/stats.js'; -import * as ep___following_create from './endpoints/following/create.js'; -import * as ep___following_delete from './endpoints/following/delete.js'; -import * as ep___following_update from './endpoints/following/update.js'; -import * as ep___following_update_all from './endpoints/following/update-all.js'; -import * as ep___following_invalidate from './endpoints/following/invalidate.js'; -import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; -import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; -import * as ep___following_requests_list from './endpoints/following/requests/list.js'; -import * as ep___following_requests_sent from './endpoints/following/requests/sent.js'; -import * as ep___following_requests_reject from './endpoints/following/requests/reject.js'; -import * as ep___gallery_featured from './endpoints/gallery/featured.js'; -import * as ep___gallery_popular from './endpoints/gallery/popular.js'; -import * as ep___gallery_posts from './endpoints/gallery/posts.js'; -import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js'; -import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js'; -import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js'; -import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js'; -import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; -import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; -import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; -import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js'; -import * as ep___hashtags_list from './endpoints/hashtags/list.js'; -import * as ep___hashtags_search from './endpoints/hashtags/search.js'; -import * as ep___hashtags_show from './endpoints/hashtags/show.js'; -import * as ep___hashtags_trend from './endpoints/hashtags/trend.js'; -import * as ep___hashtags_users from './endpoints/hashtags/users.js'; -import * as ep___i from './endpoints/i.js'; -import * as ep___i_2fa_done from './endpoints/i/2fa/done.js'; -import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js'; -import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js'; -import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js'; -import * as ep___i_2fa_register from './endpoints/i/2fa/register.js'; -import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js'; -import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; -import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; -import * as ep___i_apps from './endpoints/i/apps.js'; -import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; -import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; -import * as ep___i_changePassword from './endpoints/i/change-password.js'; -import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; -import * as ep___i_exportData from './endpoints/i/export-data.js'; -import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; -import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; -import * as ep___i_exportMute from './endpoints/i/export-mute.js'; -import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; -import * as ep___i_exportClips from './endpoints/i/export-clips.js'; -import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; -import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; -import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; -import * as ep___i_favorites from './endpoints/i/favorites.js'; -import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; -import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; -import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; -import * as ep___i_importFollowing from './endpoints/i/import-following.js'; -import * as ep___i_importNotes from './endpoints/i/import-notes.js'; -import * as ep___i_importMuting from './endpoints/i/import-muting.js'; -import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; -import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; -import * as ep___i_notifications from './endpoints/i/notifications.js'; -import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js'; -import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; -import * as ep___i_pages from './endpoints/i/pages.js'; -import * as ep___i_pin from './endpoints/i/pin.js'; -import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js'; -import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js'; -import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js'; -import * as ep___i_registry_getAll from './endpoints/i/registry/get-all.js'; -import * as ep___i_registry_getUnsecure from './endpoints/i/registry/get-unsecure.js'; -import * as ep___i_registry_getDetail from './endpoints/i/registry/get-detail.js'; -import * as ep___i_registry_get from './endpoints/i/registry/get.js'; -import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; -import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; -import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; -import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js'; -import * as ep___i_registry_set from './endpoints/i/registry/set.js'; -import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; -import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; -import * as ep___i_unpin from './endpoints/i/unpin.js'; -import * as ep___i_updateEmail from './endpoints/i/update-email.js'; -import * as ep___i_update from './endpoints/i/update.js'; -import * as ep___i_move from './endpoints/i/move.js'; -import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; -import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; -import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; -import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; -import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; -import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js'; -import * as ep___invite_create from './endpoints/invite/create.js'; -import * as ep___invite_delete from './endpoints/invite/delete.js'; -import * as ep___invite_list from './endpoints/invite/list.js'; -import * as ep___invite_limit from './endpoints/invite/limit.js'; -import * as ep___meta from './endpoints/meta.js'; -import * as ep___emojis from './endpoints/emojis.js'; -import * as ep___emoji from './endpoints/emoji.js'; -import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; -import * as ep___mute_create from './endpoints/mute/create.js'; -import * as ep___mute_delete from './endpoints/mute/delete.js'; -import * as ep___mute_list from './endpoints/mute/list.js'; -import * as ep___renoteMute_create from './endpoints/renote-mute/create.js'; -import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js'; -import * as ep___renoteMute_list from './endpoints/renote-mute/list.js'; -import * as ep___my_apps from './endpoints/my/apps.js'; -import * as ep___notes from './endpoints/notes.js'; -import * as ep___notes_children from './endpoints/notes/children.js'; -import * as ep___notes_clips from './endpoints/notes/clips.js'; -import * as ep___notes_conversation from './endpoints/notes/conversation.js'; -import * as ep___notes_create from './endpoints/notes/create.js'; -import * as ep___notes_delete from './endpoints/notes/delete.js'; -import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; -import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; -import * as ep___notes_featured from './endpoints/notes/featured.js'; -import * as ep___notes_following from './endpoints/notes/following.js'; -import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; -import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; -import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; -import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; -import * as ep___notes_mentions from './endpoints/notes/mentions.js'; -import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; -import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; -import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js'; -import * as ep___notes_reactions from './endpoints/notes/reactions.js'; -import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js'; -import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; -import * as ep___notes_like from './endpoints/notes/like.js'; -import * as ep___notes_renotes from './endpoints/notes/renotes.js'; -import * as ep___notes_replies from './endpoints/notes/replies.js'; -import * as ep___notes_edit from './endpoints/notes/edit.js'; -import * as ep___notes_versions from './endpoints/notes/versions.js'; -import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js'; -import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; -import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; -import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; -import * as ep___notes_search from './endpoints/notes/search.js'; -import * as ep___notes_show from './endpoints/notes/show.js'; -import * as ep___notes_state from './endpoints/notes/state.js'; -import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js'; -import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js'; -import * as ep___notes_timeline from './endpoints/notes/timeline.js'; -import * as ep___notes_translate from './endpoints/notes/translate.js'; -import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; -import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; -import * as ep___notifications_create from './endpoints/notifications/create.js'; -import * as ep___notifications_flush from './endpoints/notifications/flush.js'; -import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; -import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; -import * as ep___pagePush from './endpoints/page-push.js'; -import * as ep___pages_create from './endpoints/pages/create.js'; -import * as ep___pages_delete from './endpoints/pages/delete.js'; -import * as ep___pages_featured from './endpoints/pages/featured.js'; -import * as ep___pages_like from './endpoints/pages/like.js'; -import * as ep___pages_show from './endpoints/pages/show.js'; -import * as ep___pages_unlike from './endpoints/pages/unlike.js'; -import * as ep___pages_update from './endpoints/pages/update.js'; -import * as ep___flash_create from './endpoints/flash/create.js'; -import * as ep___flash_delete from './endpoints/flash/delete.js'; -import * as ep___flash_featured from './endpoints/flash/featured.js'; -import * as ep___flash_like from './endpoints/flash/like.js'; -import * as ep___flash_show from './endpoints/flash/show.js'; -import * as ep___flash_unlike from './endpoints/flash/unlike.js'; -import * as ep___flash_update from './endpoints/flash/update.js'; -import * as ep___flash_my from './endpoints/flash/my.js'; -import * as ep___flash_myLikes from './endpoints/flash/my-likes.js'; -import * as ep___ping from './endpoints/ping.js'; -import * as ep___pinnedUsers from './endpoints/pinned-users.js'; -import * as ep___promo_read from './endpoints/promo/read.js'; -import * as ep___roles_list from './endpoints/roles/list.js'; -import * as ep___roles_show from './endpoints/roles/show.js'; -import * as ep___roles_users from './endpoints/roles/users.js'; -import * as ep___roles_notes from './endpoints/roles/notes.js'; -import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; -import * as ep___resetDb from './endpoints/reset-db.js'; -import * as ep___resetPassword from './endpoints/reset-password.js'; -import * as ep___serverInfo from './endpoints/server-info.js'; -import * as ep___stats from './endpoints/stats.js'; -import * as ep___sw_show_registration from './endpoints/sw/show-registration.js'; -import * as ep___sw_update_registration from './endpoints/sw/update-registration.js'; -import * as ep___sw_register from './endpoints/sw/register.js'; -import * as ep___sw_unregister from './endpoints/sw/unregister.js'; -import * as ep___test from './endpoints/test.js'; -import * as ep___username_available from './endpoints/username/available.js'; -import * as ep___users from './endpoints/users.js'; -import * as ep___users_clips from './endpoints/users/clips.js'; -import * as ep___users_followers from './endpoints/users/followers.js'; -import * as ep___users_following from './endpoints/users/following.js'; -import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; -import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; -import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; -import * as ep___users_lists_create from './endpoints/users/lists/create.js'; -import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; -import * as ep___users_lists_list from './endpoints/users/lists/list.js'; -import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; -import * as ep___users_lists_push from './endpoints/users/lists/push.js'; -import * as ep___users_lists_show from './endpoints/users/lists/show.js'; -import * as ep___users_lists_update from './endpoints/users/lists/update.js'; -import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; -import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; -import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js'; -import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js'; -import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js'; -import * as ep___users_notes from './endpoints/users/notes.js'; -import * as ep___users_pages from './endpoints/users/pages.js'; -import * as ep___users_flashs from './endpoints/users/flashs.js'; -import * as ep___users_reactions from './endpoints/users/reactions.js'; -import * as ep___users_recommendation from './endpoints/users/recommendation.js'; -import * as ep___users_relation from './endpoints/users/relation.js'; -import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; -import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; -import * as ep___users_search from './endpoints/users/search.js'; -import * as ep___users_show from './endpoints/users/show.js'; -import * as ep___users_achievements from './endpoints/users/achievements.js'; -import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; -import * as ep___fetchRss from './endpoints/fetch-rss.js'; -import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js'; -import * as ep___retention from './endpoints/retention.js'; -import * as ep___sponsors from './endpoints/sponsors.js'; -import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js'; -import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; -import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js'; -import * as ep___reversi_games from './endpoints/reversi/games.js'; -import * as ep___reversi_match from './endpoints/reversi/match.js'; -import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; -import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; -import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; -import * as ep___reversi_verify from './endpoints/reversi/verify.js'; +import * as endpointsObject from './endpoint-list.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; -const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default }; -const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default }; -const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default }; -const $admin_abuseReport_notificationRecipient_show: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/show', useClass: ep___admin_abuseReport_notificationRecipient_show.default }; -const $admin_abuseReport_notificationRecipient_create: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/create', useClass: ep___admin_abuseReport_notificationRecipient_create.default }; -const $admin_abuseReport_notificationRecipient_update: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/update', useClass: ep___admin_abuseReport_notificationRecipient_update.default }; -const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/delete', useClass: ep___admin_abuseReport_notificationRecipient_delete.default }; -const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default }; -const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default }; -const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default }; -const $admin_ad_create: Provider = { provide: 'ep:admin/ad/create', useClass: ep___admin_ad_create.default }; -const $admin_ad_delete: Provider = { provide: 'ep:admin/ad/delete', useClass: ep___admin_ad_delete.default }; -const $admin_ad_list: Provider = { provide: 'ep:admin/ad/list', useClass: ep___admin_ad_list.default }; -const $admin_ad_update: Provider = { provide: 'ep:admin/ad/update', useClass: ep___admin_ad_update.default }; -const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements/create', useClass: ep___admin_announcements_create.default }; -const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default }; -const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default }; -const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default }; -const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-decorations/create', useClass: ep___admin_avatarDecorations_create.default }; -const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default }; -const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default }; -const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default }; -const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; -const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default }; -const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default }; -const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; -const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; -const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; -const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default }; -const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default }; -const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.default }; -const $admin_emoji_copy: Provider = { provide: 'ep:admin/emoji/copy', useClass: ep___admin_emoji_copy.default }; -const $admin_emoji_deleteBulk: Provider = { provide: 'ep:admin/emoji/delete-bulk', useClass: ep___admin_emoji_deleteBulk.default }; -const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default }; -const $admin_emoji_importZip: Provider = { provide: 'ep:admin/emoji/import-zip', useClass: ep___admin_emoji_importZip.default }; -const $admin_emoji_listRemote: Provider = { provide: 'ep:admin/emoji/list-remote', useClass: ep___admin_emoji_listRemote.default }; -const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass: ep___admin_emoji_list.default }; -const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default }; -const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default }; -const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default }; -const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default }; -const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default }; -const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default }; -const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default }; -const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default }; -const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federation/update-instance', useClass: ep___admin_federation_updateInstance.default }; -const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default }; -const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default }; -const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default }; -const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default }; -const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default }; -const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default }; -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 }; -const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default }; -const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default }; -const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default }; -const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default }; -const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default }; -const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default }; -const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default }; -const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default }; -const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default }; -const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default }; -const $admin_nsfwUser: Provider = { provide: 'ep:admin/nsfw-user', useClass: ep___admin_nsfwUser.default }; -const $admin_unnsfwUser: Provider = { provide: 'ep:admin/unnsfw-user', useClass: ep___admin_unnsfwUser.default }; -const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClass: ep___admin_silenceUser.default }; -const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default }; -const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; -const $admin_approveUser: Provider = { provide: 'ep:admin/approve-user', useClass: ep___admin_approveUser.default }; -const $admin_declineUser: Provider = { provide: 'ep:admin/decline-user', useClass: ep___admin_declineUser.default }; -const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default }; -const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default }; -const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default }; -const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default }; -const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default }; -const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default }; -const $admin_roles_list: Provider = { provide: 'ep:admin/roles/list', useClass: ep___admin_roles_list.default }; -const $admin_roles_show: Provider = { provide: 'ep:admin/roles/show', useClass: ep___admin_roles_show.default }; -const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default }; -const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default }; -const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default }; -const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default }; -const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default }; -const $admin_systemWebhook_create: Provider = { provide: 'ep:admin/system-webhook/create', useClass: ep___admin_systemWebhook_create.default }; -const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhook/delete', useClass: ep___admin_systemWebhook_delete.default }; -const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default }; -const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default }; -const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default }; -const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default }; -const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; -const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default }; -const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; -const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default }; -const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default }; -const $antennas_notes: Provider = { provide: 'ep:antennas/notes', useClass: ep___antennas_notes.default }; -const $antennas_show: Provider = { provide: 'ep:antennas/show', useClass: ep___antennas_show.default }; -const $antennas_update: Provider = { provide: 'ep:antennas/update', useClass: ep___antennas_update.default }; -const $ap_get: Provider = { provide: 'ep:ap/get', useClass: ep___ap_get.default }; -const $ap_show: Provider = { provide: 'ep:ap/show', useClass: ep___ap_show.default }; -const $app_create: Provider = { provide: 'ep:app/create', useClass: ep___app_create.default }; -const $app_show: Provider = { provide: 'ep:app/show', useClass: ep___app_show.default }; -const $auth_accept: Provider = { provide: 'ep:auth/accept', useClass: ep___auth_accept.default }; -const $auth_session_generate: Provider = { provide: 'ep:auth/session/generate', useClass: ep___auth_session_generate.default }; -const $auth_session_show: Provider = { provide: 'ep:auth/session/show', useClass: ep___auth_session_show.default }; -const $auth_session_userkey: Provider = { provide: 'ep:auth/session/userkey', useClass: ep___auth_session_userkey.default }; -const $blocking_create: Provider = { provide: 'ep:blocking/create', useClass: ep___blocking_create.default }; -const $blocking_delete: Provider = { provide: 'ep:blocking/delete', useClass: ep___blocking_delete.default }; -const $blocking_list: Provider = { provide: 'ep:blocking/list', useClass: ep___blocking_list.default }; -const $channels_create: Provider = { provide: 'ep:channels/create', useClass: ep___channels_create.default }; -const $channels_featured: Provider = { provide: 'ep:channels/featured', useClass: ep___channels_featured.default }; -const $channels_follow: Provider = { provide: 'ep:channels/follow', useClass: ep___channels_follow.default }; -const $channels_followed: Provider = { provide: 'ep:channels/followed', useClass: ep___channels_followed.default }; -const $channels_owned: Provider = { provide: 'ep:channels/owned', useClass: ep___channels_owned.default }; -const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___channels_show.default }; -const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default }; -const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default }; -const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default }; -const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default }; -const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default }; -const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default }; -const $channels_search: Provider = { provide: 'ep:channels/search', useClass: ep___channels_search.default }; -const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default }; -const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default }; -const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default }; -const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default }; -const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default }; -const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default }; -const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default }; -const $charts_user_following: Provider = { provide: 'ep:charts/user/following', useClass: ep___charts_user_following.default }; -const $charts_user_notes: Provider = { provide: 'ep:charts/user/notes', useClass: ep___charts_user_notes.default }; -const $charts_user_pv: Provider = { provide: 'ep:charts/user/pv', useClass: ep___charts_user_pv.default }; -const $charts_user_reactions: Provider = { provide: 'ep:charts/user/reactions', useClass: ep___charts_user_reactions.default }; -const $charts_users: Provider = { provide: 'ep:charts/users', useClass: ep___charts_users.default }; -const $clips_addNote: Provider = { provide: 'ep:clips/add-note', useClass: ep___clips_addNote.default }; -const $clips_removeNote: Provider = { provide: 'ep:clips/remove-note', useClass: ep___clips_removeNote.default }; -const $clips_create: Provider = { provide: 'ep:clips/create', useClass: ep___clips_create.default }; -const $clips_delete: Provider = { provide: 'ep:clips/delete', useClass: ep___clips_delete.default }; -const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_list.default }; -const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default }; -const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default }; -const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default }; -const $clips_favorite: Provider = { provide: 'ep:clips/favorite', useClass: ep___clips_favorite.default }; -const $clips_unfavorite: Provider = { provide: 'ep:clips/unfavorite', useClass: ep___clips_unfavorite.default }; -const $clips_myFavorites: Provider = { provide: 'ep:clips/my-favorites', useClass: ep___clips_myFavorites.default }; -const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default }; -const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default }; -const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default }; -const $drive_files_checkExistence: Provider = { provide: 'ep:drive/files/check-existence', useClass: ep___drive_files_checkExistence.default }; -const $drive_files_create: Provider = { provide: 'ep:drive/files/create', useClass: ep___drive_files_create.default }; -const $drive_files_delete: Provider = { provide: 'ep:drive/files/delete', useClass: ep___drive_files_delete.default }; -const $drive_files_findByHash: Provider = { provide: 'ep:drive/files/find-by-hash', useClass: ep___drive_files_findByHash.default }; -const $drive_files_find: Provider = { provide: 'ep:drive/files/find', useClass: ep___drive_files_find.default }; -const $drive_files_show: Provider = { provide: 'ep:drive/files/show', useClass: ep___drive_files_show.default }; -const $drive_files_update: Provider = { provide: 'ep:drive/files/update', useClass: ep___drive_files_update.default }; -const $drive_files_uploadFromUrl: Provider = { provide: 'ep:drive/files/upload-from-url', useClass: ep___drive_files_uploadFromUrl.default }; -const $drive_folders: Provider = { provide: 'ep:drive/folders', useClass: ep___drive_folders.default }; -const $drive_folders_create: Provider = { provide: 'ep:drive/folders/create', useClass: ep___drive_folders_create.default }; -const $drive_folders_delete: Provider = { provide: 'ep:drive/folders/delete', useClass: ep___drive_folders_delete.default }; -const $drive_folders_find: Provider = { provide: 'ep:drive/folders/find', useClass: ep___drive_folders_find.default }; -const $drive_folders_show: Provider = { provide: 'ep:drive/folders/show', useClass: ep___drive_folders_show.default }; -const $drive_folders_update: Provider = { provide: 'ep:drive/folders/update', useClass: ep___drive_folders_update.default }; -const $drive_stream: Provider = { provide: 'ep:drive/stream', useClass: ep___drive_stream.default }; -const $emailAddress_available: Provider = { provide: 'ep:email-address/available', useClass: ep___emailAddress_available.default }; -const $endpoint: Provider = { provide: 'ep:endpoint', useClass: ep___endpoint.default }; -const $endpoints: Provider = { provide: 'ep:endpoints', useClass: ep___endpoints.default }; -const $exportCustomEmojis: Provider = { provide: 'ep:export-custom-emojis', useClass: ep___exportCustomEmojis.default }; -const $federation_followers: Provider = { provide: 'ep:federation/followers', useClass: ep___federation_followers.default }; -const $federation_following: Provider = { provide: 'ep:federation/following', useClass: ep___federation_following.default }; -const $federation_instances: Provider = { provide: 'ep:federation/instances', useClass: ep___federation_instances.default }; -const $federation_showInstance: Provider = { provide: 'ep:federation/show-instance', useClass: ep___federation_showInstance.default }; -const $federation_updateRemoteUser: Provider = { provide: 'ep:federation/update-remote-user', useClass: ep___federation_updateRemoteUser.default }; -const $federation_users: Provider = { provide: 'ep:federation/users', useClass: ep___federation_users.default }; -const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default }; -const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default }; -const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default }; -const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default }; -const $following_update_all: Provider = { provide: 'ep:following/update-all', useClass: ep___following_update_all.default }; -const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default }; -const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default }; -const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default }; -const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default }; -const $following_requests_sent: Provider = { provide: 'ep:following/requests/sent', useClass: ep___following_requests_sent.default }; -const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default }; -const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default }; -const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default }; -const $gallery_posts: Provider = { provide: 'ep:gallery/posts', useClass: ep___gallery_posts.default }; -const $gallery_posts_create: Provider = { provide: 'ep:gallery/posts/create', useClass: ep___gallery_posts_create.default }; -const $gallery_posts_delete: Provider = { provide: 'ep:gallery/posts/delete', useClass: ep___gallery_posts_delete.default }; -const $gallery_posts_like: Provider = { provide: 'ep:gallery/posts/like', useClass: ep___gallery_posts_like.default }; -const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useClass: ep___gallery_posts_show.default }; -const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default }; -const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default }; -const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default }; -const $getAvatarDecorations: Provider = { provide: 'ep:get-avatar-decorations', useClass: ep___getAvatarDecorations.default }; -const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default }; -const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default }; -const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default }; -const $hashtags_trend: Provider = { provide: 'ep:hashtags/trend', useClass: ep___hashtags_trend.default }; -const $hashtags_users: Provider = { provide: 'ep:hashtags/users', useClass: ep___hashtags_users.default }; -const $i: Provider = { provide: 'ep:i', useClass: ep___i.default }; -const $i_2fa_done: Provider = { provide: 'ep:i/2fa/done', useClass: ep___i_2fa_done.default }; -const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___i_2fa_keyDone.default }; -const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default }; -const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default }; -const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default }; -const $i_2fa_updateKey: Provider = { provide: 'ep:i/2fa/update-key', useClass: ep___i_2fa_updateKey.default }; -const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default }; -const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default }; -const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default }; -const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default }; -const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default }; -const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default }; -const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default }; -const $i_exportData: Provider = { provide: 'ep:i/export-data', useClass: ep___i_exportData.default }; -const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default }; -const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default }; -const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default }; -const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default }; -const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default }; -const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default }; -const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default }; -const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default }; -const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default }; -const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default }; -const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default }; -const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default }; -const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default }; -const $i_importNotes: Provider = { provide: 'ep:i/import-notes', useClass: ep___i_importNotes.default }; -const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default }; -const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default }; -const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default }; -const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default }; -const $i_notificationsGrouped: Provider = { provide: 'ep:i/notifications-grouped', useClass: ep___i_notificationsGrouped.default }; -const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; -const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; -const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default }; -const $i_readAllUnreadNotes: Provider = { provide: 'ep:i/read-all-unread-notes', useClass: ep___i_readAllUnreadNotes.default }; -const $i_readAnnouncement: Provider = { provide: 'ep:i/read-announcement', useClass: ep___i_readAnnouncement.default }; -const $i_regenerateToken: Provider = { provide: 'ep:i/regenerate-token', useClass: ep___i_regenerateToken.default }; -const $i_registry_getAll: Provider = { provide: 'ep:i/registry/get-all', useClass: ep___i_registry_getAll.default }; -const $i_registry_getUnsecure: Provider = { provide: 'ep:i/registry/get-unsecure', useClass: ep___i_registry_getUnsecure.default }; -const $i_registry_getDetail: Provider = { provide: 'ep:i/registry/get-detail', useClass: ep___i_registry_getDetail.default }; -const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep___i_registry_get.default }; -const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default }; -const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default }; -const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default }; -const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default }; -const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default }; -const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default }; -const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default }; -const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default }; -const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default }; -const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default }; -const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default }; -const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default }; -const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default }; -const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default }; -const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default }; -const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default }; -const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default }; -const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default }; -const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default }; -const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default }; -const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default }; -const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; -const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; -const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default }; -const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; -const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; -const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; -const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default }; -const $renoteMute_create: Provider = { provide: 'ep:renote-mute/create', useClass: ep___renoteMute_create.default }; -const $renoteMute_delete: Provider = { provide: 'ep:renote-mute/delete', useClass: ep___renoteMute_delete.default }; -const $renoteMute_list: Provider = { provide: 'ep:renote-mute/list', useClass: ep___renoteMute_list.default }; -const $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default }; -const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default }; -const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.default }; -const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default }; -const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; -const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; -const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; -const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; -const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; -const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; -const $notes_following: Provider = { provide: 'ep:notes/following', useClass: ep___notes_following.default }; -const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; -const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default }; -const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; -const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default }; -const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; -const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default }; -const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default }; -const $notes_polls_refresh: Provider = { provide: 'ep:notes/polls/refresh', useClass: ep___notes_polls_refresh.default }; -const $notes_reactions: Provider = { provide: 'ep:notes/reactions', useClass: ep___notes_reactions.default }; -const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create', useClass: ep___notes_reactions_create.default }; -const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default }; -const $notes_like: Provider = { provide: 'ep:notes/like', useClass: ep___notes_like.default }; -const $notes_renotes: Provider = { provide: 'ep:notes/renotes', useClass: ep___notes_renotes.default }; -const $notes_replies: Provider = { provide: 'ep:notes/replies', useClass: ep___notes_replies.default }; -const $notes_schedule_create: Provider = { provide: 'ep:notes/schedule/create', useClass: ep___notes_schedule_create.default }; -const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default }; -const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default }; -const $notes_searchByTag: Provider = { provide: 'ep:notes/search-by-tag', useClass: ep___notes_searchByTag.default }; -const $notes_search: Provider = { provide: 'ep:notes/search', useClass: ep___notes_search.default }; -const $notes_show: Provider = { provide: 'ep:notes/show', useClass: ep___notes_show.default }; -const $notes_state: Provider = { provide: 'ep:notes/state', useClass: ep___notes_state.default }; -const $notes_threadMuting_create: Provider = { provide: 'ep:notes/thread-muting/create', useClass: ep___notes_threadMuting_create.default }; -const $notes_threadMuting_delete: Provider = { provide: 'ep:notes/thread-muting/delete', useClass: ep___notes_threadMuting_delete.default }; -const $notes_timeline: Provider = { provide: 'ep:notes/timeline', useClass: ep___notes_timeline.default }; -const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep___notes_translate.default }; -const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default }; -const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; -const $notes_edit: Provider = { provide: 'ep:notes/edit', useClass: ep___notes_edit.default }; -const $notes_versions: Provider = { provide: 'ep:notes/versions', useClass: ep___notes_versions.default }; -const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; -const $notifications_flush: Provider = { provide: 'ep:notifications/flush', useClass: ep___notifications_flush.default }; -const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; -const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default }; -const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default }; -const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default }; -const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default }; -const $pages_featured: Provider = { provide: 'ep:pages/featured', useClass: ep___pages_featured.default }; -const $pages_like: Provider = { provide: 'ep:pages/like', useClass: ep___pages_like.default }; -const $pages_show: Provider = { provide: 'ep:pages/show', useClass: ep___pages_show.default }; -const $pages_unlike: Provider = { provide: 'ep:pages/unlike', useClass: ep___pages_unlike.default }; -const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pages_update.default }; -const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default }; -const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default }; -const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default }; -const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default }; -const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default }; -const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default }; -const $flash_update: Provider = { provide: 'ep:flash/update', useClass: ep___flash_update.default }; -const $flash_my: Provider = { provide: 'ep:flash/my', useClass: ep___flash_my.default }; -const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___flash_myLikes.default }; -const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default }; -const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default }; -const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default }; -const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default }; -const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default }; -const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default }; -const $roles_notes: Provider = { provide: 'ep:roles/notes', useClass: ep___roles_notes.default }; -const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default }; -const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default }; -const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default }; -const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default }; -const $stats: Provider = { provide: 'ep:stats', useClass: ep___stats.default }; -const $sw_show_registration: Provider = { provide: 'ep:sw/show-registration', useClass: ep___sw_show_registration.default }; -const $sw_update_registration: Provider = { provide: 'ep:sw/update-registration', useClass: ep___sw_update_registration.default }; -const $sw_register: Provider = { provide: 'ep:sw/register', useClass: ep___sw_register.default }; -const $sw_unregister: Provider = { provide: 'ep:sw/unregister', useClass: ep___sw_unregister.default }; -const $test: Provider = { provide: 'ep:test', useClass: ep___test.default }; -const $username_available: Provider = { provide: 'ep:username/available', useClass: ep___username_available.default }; -const $users: Provider = { provide: 'ep:users', useClass: ep___users.default }; -const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users_clips.default }; -const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default }; -const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; -const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; -const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; -const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default }; -const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; -const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default }; -const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default }; -const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: ep___users_lists_pull.default }; -const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default }; -const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default }; -const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default }; -const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default }; -const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default }; -const $users_lists_createFromPublic: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_createFromPublic.default }; -const $users_lists_updateMembership: Provider = { provide: 'ep:users/lists/update-membership', useClass: ep___users_lists_updateMembership.default }; -const $users_lists_getMemberships: Provider = { provide: 'ep:users/lists/get-memberships', useClass: ep___users_lists_getMemberships.default }; -const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; -const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; -const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default }; -const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default }; -const $users_recommendation: Provider = { provide: 'ep:users/recommendation', useClass: ep___users_recommendation.default }; -const $users_relation: Provider = { provide: 'ep:users/relation', useClass: ep___users_relation.default }; -const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClass: ep___users_reportAbuse.default }; -const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default }; -const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; -const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; -const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; -const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default }; -const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; -const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default }; -const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; -const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.default }; -const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default }; -const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default }; -const $reversi_cancelMatch: Provider = { provide: 'ep:reversi/cancel-match', useClass: ep___reversi_cancelMatch.default }; -const $reversi_games: Provider = { provide: 'ep:reversi/games', useClass: ep___reversi_games.default }; -const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___reversi_match.default }; -const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default }; -const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default }; -const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default }; -const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default }; +const endpoints = Object.entries(endpointsObject); +const endpointProviders = endpoints.map(([path, endpoint]): Provider => ({ provide: `ep:${path}`, useClass: endpoint.default })); @Module({ imports: [ @@ -824,811 +21,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ providers: [ GetterService, ApiLoggerService, - $admin_meta, - $admin_abuseUserReports, - $admin_abuseReport_notificationRecipient_list, - $admin_abuseReport_notificationRecipient_show, - $admin_abuseReport_notificationRecipient_create, - $admin_abuseReport_notificationRecipient_update, - $admin_abuseReport_notificationRecipient_delete, - $admin_accounts_create, - $admin_accounts_delete, - $admin_accounts_findByEmail, - $admin_ad_create, - $admin_ad_delete, - $admin_ad_list, - $admin_ad_update, - $admin_announcements_create, - $admin_announcements_delete, - $admin_announcements_list, - $admin_announcements_update, - $admin_avatarDecorations_create, - $admin_avatarDecorations_delete, - $admin_avatarDecorations_list, - $admin_avatarDecorations_update, - $admin_deleteAllFilesOfAUser, - $admin_unsetUserAvatar, - $admin_unsetUserBanner, - $admin_drive_cleanRemoteFiles, - $admin_drive_cleanup, - $admin_drive_files, - $admin_drive_showFile, - $admin_emoji_addAliasesBulk, - $admin_emoji_add, - $admin_emoji_copy, - $admin_emoji_deleteBulk, - $admin_emoji_delete, - $admin_emoji_importZip, - $admin_emoji_listRemote, - $admin_emoji_list, - $admin_emoji_removeAliasesBulk, - $admin_emoji_setAliasesBulk, - $admin_emoji_setCategoryBulk, - $admin_emoji_setLicenseBulk, - $admin_emoji_update, - $admin_federation_deleteAllFiles, - $admin_federation_refreshRemoteInstanceMetadata, - $admin_federation_removeAllFollowing, - $admin_federation_updateInstance, - $admin_getIndexStats, - $admin_getTableStats, - $admin_getUserIps, - $admin_invite_create, - $admin_invite_list, - $admin_promo_create, - $admin_queue_clear, - $admin_queue_deliverDelayed, - $admin_queue_inboxDelayed, - $admin_queue_promote, - $admin_queue_stats, - $admin_relays_add, - $admin_relays_list, - $admin_relays_remove, - $admin_resetPassword, - $admin_resolveAbuseUserReport, - $admin_forwardAbuseUserReport, - $admin_updateAbuseUserReport, - $admin_sendEmail, - $admin_serverInfo, - $admin_showModerationLogs, - $admin_showUser, - $admin_showUsers, - $admin_nsfwUser, - $admin_unnsfwUser, - $admin_silenceUser, - $admin_unsilenceUser, - $admin_suspendUser, - $admin_approveUser, - $admin_declineUser, - $admin_unsuspendUser, - $admin_updateMeta, - $admin_deleteAccount, - $admin_updateUserNote, - $admin_roles_create, - $admin_roles_delete, - $admin_roles_list, - $admin_roles_show, - $admin_roles_update, - $admin_roles_assign, - $admin_roles_unassign, - $admin_roles_updateDefaultPolicies, - $admin_roles_users, - $admin_systemWebhook_create, - $admin_systemWebhook_delete, - $admin_systemWebhook_list, - $admin_systemWebhook_show, - $admin_systemWebhook_update, - $admin_systemWebhook_test, - $announcements, - $announcements_show, - $antennas_create, - $antennas_delete, - $antennas_list, - $antennas_notes, - $antennas_show, - $antennas_update, - $ap_get, - $ap_show, - $app_create, - $app_show, - $auth_accept, - $auth_session_generate, - $auth_session_show, - $auth_session_userkey, - $blocking_create, - $blocking_delete, - $blocking_list, - $channels_create, - $channels_featured, - $channels_follow, - $channels_followed, - $channels_owned, - $channels_show, - $channels_timeline, - $channels_unfollow, - $channels_update, - $channels_favorite, - $channels_unfavorite, - $channels_myFavorites, - $channels_search, - $charts_activeUsers, - $charts_apRequest, - $charts_drive, - $charts_federation, - $charts_instance, - $charts_notes, - $charts_user_drive, - $charts_user_following, - $charts_user_notes, - $charts_user_pv, - $charts_user_reactions, - $charts_users, - $clips_addNote, - $clips_removeNote, - $clips_create, - $clips_delete, - $clips_list, - $clips_notes, - $clips_show, - $clips_update, - $clips_favorite, - $clips_unfavorite, - $clips_myFavorites, - $drive, - $drive_files, - $drive_files_attachedNotes, - $drive_files_checkExistence, - $drive_files_create, - $drive_files_delete, - $drive_files_findByHash, - $drive_files_find, - $drive_files_show, - $drive_files_update, - $drive_files_uploadFromUrl, - $drive_folders, - $drive_folders_create, - $drive_folders_delete, - $drive_folders_find, - $drive_folders_show, - $drive_folders_update, - $drive_stream, - $emailAddress_available, - $endpoint, - $endpoints, - $exportCustomEmojis, - $federation_followers, - $federation_following, - $federation_instances, - $federation_showInstance, - $federation_updateRemoteUser, - $federation_users, - $federation_stats, - $following_create, - $following_delete, - $following_update, - $following_update_all, - $following_invalidate, - $following_requests_accept, - $following_requests_cancel, - $following_requests_list, - $following_requests_sent, - $following_requests_reject, - $gallery_featured, - $gallery_popular, - $gallery_posts, - $gallery_posts_create, - $gallery_posts_delete, - $gallery_posts_like, - $gallery_posts_show, - $gallery_posts_unlike, - $gallery_posts_update, - $getOnlineUsersCount, - $getAvatarDecorations, - $hashtags_list, - $hashtags_search, - $hashtags_show, - $hashtags_trend, - $hashtags_users, - $i, - $i_2fa_done, - $i_2fa_keyDone, - $i_2fa_passwordLess, - $i_2fa_registerKey, - $i_2fa_register, - $i_2fa_updateKey, - $i_2fa_removeKey, - $i_2fa_unregister, - $i_apps, - $i_authorizedApps, - $i_claimAchievement, - $i_changePassword, - $i_deleteAccount, - $i_exportData, - $i_exportBlocking, - $i_exportFollowing, - $i_exportMute, - $i_exportNotes, - $i_exportClips, - $i_exportFavorites, - $i_exportUserLists, - $i_exportAntennas, - $i_favorites, - $i_gallery_likes, - $i_gallery_posts, - $i_importBlocking, - $i_importFollowing, - $i_importNotes, - $i_importMuting, - $i_importUserLists, - $i_importAntennas, - $i_notifications, - $i_notificationsGrouped, - $i_pageLikes, - $i_pages, - $i_pin, - $i_readAllUnreadNotes, - $i_readAnnouncement, - $i_regenerateToken, - $i_registry_getAll, - $i_registry_getUnsecure, - $i_registry_getDetail, - $i_registry_get, - $i_registry_keysWithType, - $i_registry_keys, - $i_registry_remove, - $i_registry_scopesWithDomain, - $i_registry_set, - $i_revokeToken, - $i_signinHistory, - $i_unpin, - $i_updateEmail, - $i_update, - $i_move, - $i_webhooks_create, - $i_webhooks_list, - $i_webhooks_show, - $i_webhooks_update, - $i_webhooks_delete, - $i_webhooks_test, - $invite_create, - $invite_delete, - $invite_list, - $invite_limit, - $meta, - $emojis, - $emoji, - $miauth_genToken, - $mute_create, - $mute_delete, - $mute_list, - $renoteMute_create, - $renoteMute_delete, - $renoteMute_list, - $my_apps, - $notes, - $notes_children, - $notes_clips, - $notes_conversation, - $notes_create, - $notes_delete, - $notes_favorites_create, - $notes_favorites_delete, - $notes_featured, - $notes_following, - $notes_globalTimeline, - $notes_bubbleTimeline, - $notes_hybridTimeline, - $notes_localTimeline, - $notes_mentions, - $notes_polls_recommendation, - $notes_polls_vote, - $notes_polls_refresh, - $notes_reactions, - $notes_reactions_create, - $notes_reactions_delete, - $notes_like, - $notes_renotes, - $notes_replies, - $notes_schedule_create, - $notes_schedule_delete, - $notes_schedule_list, - $notes_searchByTag, - $notes_search, - $notes_show, - $notes_state, - $notes_threadMuting_create, - $notes_threadMuting_delete, - $notes_timeline, - $notes_translate, - $notes_unrenote, - $notes_userListTimeline, - $notes_edit, - $notes_versions, - $notifications_create, - $notifications_flush, - $notifications_markAllAsRead, - $notifications_testNotification, - $pagePush, - $pages_create, - $pages_delete, - $pages_featured, - $pages_like, - $pages_show, - $pages_unlike, - $pages_update, - $flash_create, - $flash_delete, - $flash_featured, - $flash_like, - $flash_show, - $flash_unlike, - $flash_update, - $flash_my, - $flash_myLikes, - $ping, - $pinnedUsers, - $promo_read, - $roles_list, - $roles_show, - $roles_users, - $roles_notes, - $requestResetPassword, - $resetDb, - $resetPassword, - $serverInfo, - $stats, - $sw_show_registration, - $sw_update_registration, - $sw_register, - $sw_unregister, - $test, - $username_available, - $users, - $users_clips, - $users_followers, - $users_following, - $users_gallery_posts, - $users_getFrequentlyRepliedUsers, - $users_featuredNotes, - $users_lists_create, - $users_lists_delete, - $users_lists_list, - $users_lists_pull, - $users_lists_push, - $users_lists_show, - $users_lists_update, - $users_lists_favorite, - $users_lists_unfavorite, - $users_lists_createFromPublic, - $users_lists_updateMembership, - $users_lists_getMemberships, - $users_notes, - $users_pages, - $users_flashs, - $users_reactions, - $users_recommendation, - $users_relation, - $users_reportAbuse, - $users_searchByUsernameAndHost, - $users_search, - $users_show, - $users_achievements, - $users_updateMemo, - $fetchRss, - $fetchExternalResources, - $retention, - $sponsors, - $bubbleGame_register, - $bubbleGame_ranking, - $reversi_cancelMatch, - $reversi_games, - $reversi_match, - $reversi_invitations, - $reversi_showGame, - $reversi_surrender, - $reversi_verify, + ...endpointProviders, ], exports: [ - $admin_meta, - $admin_abuseUserReports, - $admin_abuseReport_notificationRecipient_list, - $admin_abuseReport_notificationRecipient_show, - $admin_abuseReport_notificationRecipient_create, - $admin_abuseReport_notificationRecipient_update, - $admin_abuseReport_notificationRecipient_delete, - $admin_accounts_create, - $admin_accounts_delete, - $admin_accounts_findByEmail, - $admin_ad_create, - $admin_ad_delete, - $admin_ad_list, - $admin_ad_update, - $admin_announcements_create, - $admin_announcements_delete, - $admin_announcements_list, - $admin_announcements_update, - $admin_avatarDecorations_create, - $admin_avatarDecorations_delete, - $admin_avatarDecorations_list, - $admin_avatarDecorations_update, - $admin_deleteAllFilesOfAUser, - $admin_unsetUserAvatar, - $admin_unsetUserBanner, - $admin_drive_cleanRemoteFiles, - $admin_drive_cleanup, - $admin_drive_files, - $admin_drive_showFile, - $admin_emoji_addAliasesBulk, - $admin_emoji_add, - $admin_emoji_copy, - $admin_emoji_deleteBulk, - $admin_emoji_delete, - $admin_emoji_importZip, - $admin_emoji_listRemote, - $admin_emoji_list, - $admin_emoji_removeAliasesBulk, - $admin_emoji_setAliasesBulk, - $admin_emoji_setCategoryBulk, - $admin_emoji_setLicenseBulk, - $admin_emoji_update, - $admin_federation_deleteAllFiles, - $admin_federation_refreshRemoteInstanceMetadata, - $admin_federation_removeAllFollowing, - $admin_federation_updateInstance, - $admin_getIndexStats, - $admin_getTableStats, - $admin_getUserIps, - $admin_invite_create, - $admin_invite_list, - $admin_promo_create, - $admin_queue_clear, - $admin_queue_deliverDelayed, - $admin_queue_inboxDelayed, - $admin_queue_promote, - $admin_queue_stats, - $admin_relays_add, - $admin_relays_list, - $admin_relays_remove, - $admin_resetPassword, - $admin_resolveAbuseUserReport, - $admin_forwardAbuseUserReport, - $admin_updateAbuseUserReport, - $admin_sendEmail, - $admin_serverInfo, - $admin_showModerationLogs, - $admin_showUser, - $admin_showUsers, - $admin_nsfwUser, - $admin_unnsfwUser, - $admin_silenceUser, - $admin_unsilenceUser, - $admin_suspendUser, - $admin_approveUser, - $admin_declineUser, - $admin_unsuspendUser, - $admin_updateMeta, - $admin_deleteAccount, - $admin_updateUserNote, - $admin_roles_create, - $admin_roles_delete, - $admin_roles_list, - $admin_roles_show, - $admin_roles_update, - $admin_roles_assign, - $admin_roles_unassign, - $admin_roles_updateDefaultPolicies, - $admin_roles_users, - $admin_systemWebhook_create, - $admin_systemWebhook_delete, - $admin_systemWebhook_list, - $admin_systemWebhook_show, - $admin_systemWebhook_update, - $admin_systemWebhook_test, - $announcements, - $announcements_show, - $antennas_create, - $antennas_delete, - $antennas_list, - $antennas_notes, - $antennas_show, - $antennas_update, - $ap_get, - $ap_show, - $app_create, - $app_show, - $auth_accept, - $auth_session_generate, - $auth_session_show, - $auth_session_userkey, - $blocking_create, - $blocking_delete, - $blocking_list, - $channels_create, - $channels_featured, - $channels_follow, - $channels_followed, - $channels_owned, - $channels_show, - $channels_timeline, - $channels_unfollow, - $channels_update, - $channels_favorite, - $channels_unfavorite, - $channels_myFavorites, - $channels_search, - $charts_activeUsers, - $charts_apRequest, - $charts_drive, - $charts_federation, - $charts_instance, - $charts_notes, - $charts_user_drive, - $charts_user_following, - $charts_user_notes, - $charts_user_pv, - $charts_user_reactions, - $charts_users, - $clips_addNote, - $clips_removeNote, - $clips_create, - $clips_delete, - $clips_list, - $clips_notes, - $clips_show, - $clips_update, - $clips_favorite, - $clips_unfavorite, - $clips_myFavorites, - $drive, - $drive_files, - $drive_files_attachedNotes, - $drive_files_checkExistence, - $drive_files_create, - $drive_files_delete, - $drive_files_findByHash, - $drive_files_find, - $drive_files_show, - $drive_files_update, - $drive_files_uploadFromUrl, - $drive_folders, - $drive_folders_create, - $drive_folders_delete, - $drive_folders_find, - $drive_folders_show, - $drive_folders_update, - $drive_stream, - $emailAddress_available, - $endpoint, - $endpoints, - $exportCustomEmojis, - $federation_followers, - $federation_following, - $federation_instances, - $federation_showInstance, - $federation_updateRemoteUser, - $federation_users, - $federation_stats, - $following_create, - $following_delete, - $following_update, - $following_update_all, - $following_invalidate, - $following_requests_accept, - $following_requests_cancel, - $following_requests_list, - $following_requests_reject, - $gallery_featured, - $gallery_popular, - $gallery_posts, - $gallery_posts_create, - $gallery_posts_delete, - $gallery_posts_like, - $gallery_posts_show, - $gallery_posts_unlike, - $gallery_posts_update, - $getOnlineUsersCount, - $getAvatarDecorations, - $hashtags_list, - $hashtags_search, - $hashtags_show, - $hashtags_trend, - $hashtags_users, - $i, - $i_2fa_done, - $i_2fa_keyDone, - $i_2fa_passwordLess, - $i_2fa_registerKey, - $i_2fa_register, - $i_2fa_updateKey, - $i_2fa_removeKey, - $i_2fa_unregister, - $i_apps, - $i_authorizedApps, - $i_claimAchievement, - $i_changePassword, - $i_deleteAccount, - $i_exportData, - $i_exportBlocking, - $i_exportFollowing, - $i_exportMute, - $i_exportNotes, - $i_exportClips, - $i_exportFavorites, - $i_exportUserLists, - $i_exportAntennas, - $i_favorites, - $i_gallery_likes, - $i_gallery_posts, - $i_importBlocking, - $i_importFollowing, - $i_importNotes, - $i_importMuting, - $i_importUserLists, - $i_importAntennas, - $i_notifications, - $i_notificationsGrouped, - $i_pageLikes, - $i_pages, - $i_pin, - $i_readAllUnreadNotes, - $i_readAnnouncement, - $i_regenerateToken, - $i_registry_getAll, - $i_registry_getUnsecure, - $i_registry_getDetail, - $i_registry_get, - $i_registry_keysWithType, - $i_registry_keys, - $i_registry_remove, - $i_registry_scopesWithDomain, - $i_registry_set, - $i_revokeToken, - $i_signinHistory, - $i_unpin, - $i_updateEmail, - $i_update, - $i_move, - $i_webhooks_create, - $i_webhooks_list, - $i_webhooks_show, - $i_webhooks_update, - $i_webhooks_delete, - $i_webhooks_test, - $invite_create, - $invite_delete, - $invite_list, - $invite_limit, - $meta, - $emojis, - $emoji, - $miauth_genToken, - $mute_create, - $mute_delete, - $mute_list, - $renoteMute_create, - $renoteMute_delete, - $renoteMute_list, - $my_apps, - $notes, - $notes_children, - $notes_clips, - $notes_conversation, - $notes_create, - $notes_delete, - $notes_favorites_create, - $notes_favorites_delete, - $notes_featured, - $notes_following, - $notes_globalTimeline, - $notes_bubbleTimeline, - $notes_hybridTimeline, - $notes_localTimeline, - $notes_mentions, - $notes_polls_recommendation, - $notes_polls_vote, - $notes_polls_refresh, - $notes_reactions, - $notes_reactions_create, - $notes_reactions_delete, - $notes_like, - $notes_renotes, - $notes_replies, - $notes_schedule_create, - $notes_schedule_delete, - $notes_schedule_list, - $notes_searchByTag, - $notes_search, - $notes_show, - $notes_state, - $notes_threadMuting_create, - $notes_threadMuting_delete, - $notes_timeline, - $notes_translate, - $notes_unrenote, - $notes_userListTimeline, - $notes_edit, - $notes_versions, - $notifications_create, - $notifications_flush, - $notifications_markAllAsRead, - $notifications_testNotification, - $pagePush, - $pages_create, - $pages_delete, - $pages_featured, - $pages_like, - $pages_show, - $pages_unlike, - $pages_update, - $flash_create, - $flash_delete, - $flash_featured, - $flash_like, - $flash_show, - $flash_unlike, - $flash_update, - $flash_my, - $flash_myLikes, - $ping, - $pinnedUsers, - $promo_read, - $roles_list, - $roles_show, - $roles_users, - $roles_notes, - $requestResetPassword, - $resetDb, - $resetPassword, - $serverInfo, - $stats, - $sw_register, - $sw_unregister, - $test, - $username_available, - $users, - $users_clips, - $users_followers, - $users_following, - $users_gallery_posts, - $users_getFrequentlyRepliedUsers, - $users_featuredNotes, - $users_lists_create, - $users_lists_delete, - $users_lists_list, - $users_lists_pull, - $users_lists_push, - $users_lists_show, - $users_lists_update, - $users_lists_favorite, - $users_lists_unfavorite, - $users_lists_createFromPublic, - $users_lists_updateMembership, - $users_lists_getMemberships, - $users_notes, - $users_pages, - $users_flashs, - $users_reactions, - $users_recommendation, - $users_relation, - $users_reportAbuse, - $users_searchByUsernameAndHost, - $users_search, - $users_show, - $users_achievements, - $users_updateMemo, - $fetchRss, - $fetchExternalResources, - $retention, - $sponsors, - $bubbleGame_register, - $bubbleGame_ranking, - $reversi_cancelMatch, - $reversi_games, - $reversi_match, - $reversi_invitations, - $reversi_showGame, - $reversi_surrender, - $reversi_verify, + ...endpointProviders, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts new file mode 100644 index 0000000000..28f7cfea04 --- /dev/null +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -0,0 +1,399 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* + * This file contains list of all endpoints exported as pathname of API endpoint + * + * When you add new endpoint, you should add it to this file. + * This file is used to generate API documentation and EndpointsModule. + */ + +export * as 'admin/abuse-report/notification-recipient/create' from './endpoints/admin/abuse-report/notification-recipient/create.js'; +export * as 'admin/abuse-report/notification-recipient/delete' from './endpoints/admin/abuse-report/notification-recipient/delete.js'; +export * as 'admin/abuse-report/notification-recipient/list' from './endpoints/admin/abuse-report/notification-recipient/list.js'; +export * as 'admin/abuse-report/notification-recipient/show' from './endpoints/admin/abuse-report/notification-recipient/show.js'; +export * as 'admin/abuse-report/notification-recipient/update' from './endpoints/admin/abuse-report/notification-recipient/update.js'; +export * as 'admin/abuse-user-reports' from './endpoints/admin/abuse-user-reports.js'; +export * as 'admin/accounts/create' from './endpoints/admin/accounts/create.js'; +export * as 'admin/accounts/delete' from './endpoints/admin/accounts/delete.js'; +export * as 'admin/accounts/find-by-email' from './endpoints/admin/accounts/find-by-email.js'; +export * as 'admin/ad/create' from './endpoints/admin/ad/create.js'; +export * as 'admin/ad/delete' from './endpoints/admin/ad/delete.js'; +export * as 'admin/ad/list' from './endpoints/admin/ad/list.js'; +export * as 'admin/ad/update' from './endpoints/admin/ad/update.js'; +export * as 'admin/announcements/create' from './endpoints/admin/announcements/create.js'; +export * as 'admin/announcements/delete' from './endpoints/admin/announcements/delete.js'; +export * as 'admin/announcements/list' from './endpoints/admin/announcements/list.js'; +export * as 'admin/announcements/update' from './endpoints/admin/announcements/update.js'; +export * as 'admin/avatar-decorations/create' from './endpoints/admin/avatar-decorations/create.js'; +export * as 'admin/avatar-decorations/delete' from './endpoints/admin/avatar-decorations/delete.js'; +export * as 'admin/avatar-decorations/list' from './endpoints/admin/avatar-decorations/list.js'; +export * as 'admin/avatar-decorations/update' from './endpoints/admin/avatar-decorations/update.js'; +export * as 'admin/captcha/current' from './endpoints/admin/captcha/current.js'; +export * as 'admin/captcha/save' from './endpoints/admin/captcha/save.js'; +export * as 'admin/delete-account' from './endpoints/admin/delete-account.js'; +export * as 'admin/delete-all-files-of-a-user' from './endpoints/admin/delete-all-files-of-a-user.js'; +export * as 'admin/drive/clean-remote-files' from './endpoints/admin/drive/clean-remote-files.js'; +export * as 'admin/drive/cleanup' from './endpoints/admin/drive/cleanup.js'; +export * as 'admin/drive/files' from './endpoints/admin/drive/files.js'; +export * as 'admin/drive/show-file' from './endpoints/admin/drive/show-file.js'; +export * as 'admin/emoji/add' from './endpoints/admin/emoji/add.js'; +export * as 'admin/emoji/add-aliases-bulk' from './endpoints/admin/emoji/add-aliases-bulk.js'; +export * as 'admin/emoji/copy' from './endpoints/admin/emoji/copy.js'; +export * as 'admin/emoji/delete' from './endpoints/admin/emoji/delete.js'; +export * as 'admin/emoji/delete-bulk' from './endpoints/admin/emoji/delete-bulk.js'; +export * as 'admin/emoji/import-zip' from './endpoints/admin/emoji/import-zip.js'; +export * as 'admin/emoji/list' from './endpoints/admin/emoji/list.js'; +export * as 'admin/emoji/list-remote' from './endpoints/admin/emoji/list-remote.js'; +export * as 'admin/emoji/remove-aliases-bulk' from './endpoints/admin/emoji/remove-aliases-bulk.js'; +export * as 'admin/emoji/set-aliases-bulk' from './endpoints/admin/emoji/set-aliases-bulk.js'; +export * as 'admin/emoji/set-category-bulk' from './endpoints/admin/emoji/set-category-bulk.js'; +export * as 'admin/emoji/set-license-bulk' from './endpoints/admin/emoji/set-license-bulk.js'; +export * as 'admin/emoji/update' from './endpoints/admin/emoji/update.js'; +export * as 'admin/federation/delete-all-files' from './endpoints/admin/federation/delete-all-files.js'; +export * as 'admin/federation/refresh-remote-instance-metadata' from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; +export * as 'admin/federation/remove-all-following' from './endpoints/admin/federation/remove-all-following.js'; +export * as 'admin/federation/update-instance' from './endpoints/admin/federation/update-instance.js'; +export * as 'admin/forward-abuse-user-report' from './endpoints/admin/forward-abuse-user-report.js'; +export * as 'admin/get-index-stats' from './endpoints/admin/get-index-stats.js'; +export * as 'admin/get-table-stats' from './endpoints/admin/get-table-stats.js'; +export * as 'admin/get-user-ips' from './endpoints/admin/get-user-ips.js'; +export * as 'admin/invite/create' from './endpoints/admin/invite/create.js'; +export * as 'admin/invite/list' from './endpoints/admin/invite/list.js'; +export * as 'admin/meta' from './endpoints/admin/meta.js'; +export * as 'admin/promo/create' from './endpoints/admin/promo/create.js'; +export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js'; +export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js'; +export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js'; +export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js'; +export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js'; +export * as 'admin/relays/add' from './endpoints/admin/relays/add.js'; +export * as 'admin/relays/list' from './endpoints/admin/relays/list.js'; +export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js'; +export * as 'admin/reset-password' from './endpoints/admin/reset-password.js'; +export * as 'admin/resolve-abuse-user-report' from './endpoints/admin/resolve-abuse-user-report.js'; +export * as 'admin/roles/assign' from './endpoints/admin/roles/assign.js'; +export * as 'admin/roles/create' from './endpoints/admin/roles/create.js'; +export * as 'admin/roles/delete' from './endpoints/admin/roles/delete.js'; +export * as 'admin/roles/list' from './endpoints/admin/roles/list.js'; +export * as 'admin/roles/show' from './endpoints/admin/roles/show.js'; +export * as 'admin/roles/unassign' from './endpoints/admin/roles/unassign.js'; +export * as 'admin/roles/update' from './endpoints/admin/roles/update.js'; +export * as 'admin/roles/update-default-policies' from './endpoints/admin/roles/update-default-policies.js'; +export * as 'admin/roles/users' from './endpoints/admin/roles/users.js'; +export * as 'admin/send-email' from './endpoints/admin/send-email.js'; +export * as 'admin/server-info' from './endpoints/admin/server-info.js'; +export * as 'admin/show-moderation-logs' from './endpoints/admin/show-moderation-logs.js'; +export * as 'admin/show-user' from './endpoints/admin/show-user.js'; +export * as 'admin/show-users' from './endpoints/admin/show-users.js'; +export * as 'admin/suspend-user' from './endpoints/admin/suspend-user.js'; +export * as 'admin/system-webhook/create' from './endpoints/admin/system-webhook/create.js'; +export * as 'admin/system-webhook/delete' from './endpoints/admin/system-webhook/delete.js'; +export * as 'admin/system-webhook/list' from './endpoints/admin/system-webhook/list.js'; +export * as 'admin/system-webhook/show' from './endpoints/admin/system-webhook/show.js'; +export * as 'admin/system-webhook/test' from './endpoints/admin/system-webhook/test.js'; +export * as 'admin/system-webhook/update' from './endpoints/admin/system-webhook/update.js'; +export * as 'admin/unset-user-avatar' from './endpoints/admin/unset-user-avatar.js'; +export * as 'admin/unset-user-banner' from './endpoints/admin/unset-user-banner.js'; +export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js'; +export * as 'admin/update-abuse-user-report' from './endpoints/admin/update-abuse-user-report.js'; +export * as 'admin/update-meta' from './endpoints/admin/update-meta.js'; +export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js'; +export * as 'announcements' from './endpoints/announcements.js'; +export * as 'announcements/show' from './endpoints/announcements/show.js'; +export * as 'antennas/create' from './endpoints/antennas/create.js'; +export * as 'antennas/delete' from './endpoints/antennas/delete.js'; +export * as 'antennas/list' from './endpoints/antennas/list.js'; +export * as 'antennas/notes' from './endpoints/antennas/notes.js'; +export * as 'antennas/show' from './endpoints/antennas/show.js'; +export * as 'antennas/update' from './endpoints/antennas/update.js'; +export * as 'ap/get' from './endpoints/ap/get.js'; +export * as 'ap/show' from './endpoints/ap/show.js'; +export * as 'app/create' from './endpoints/app/create.js'; +export * as 'app/show' from './endpoints/app/show.js'; +export * as 'auth/accept' from './endpoints/auth/accept.js'; +export * as 'auth/session/generate' from './endpoints/auth/session/generate.js'; +export * as 'auth/session/show' from './endpoints/auth/session/show.js'; +export * as 'auth/session/userkey' from './endpoints/auth/session/userkey.js'; +export * as 'blocking/create' from './endpoints/blocking/create.js'; +export * as 'blocking/delete' from './endpoints/blocking/delete.js'; +export * as 'blocking/list' from './endpoints/blocking/list.js'; +export * as 'bubble-game/ranking' from './endpoints/bubble-game/ranking.js'; +export * as 'bubble-game/register' from './endpoints/bubble-game/register.js'; +export * as 'channels/create' from './endpoints/channels/create.js'; +export * as 'channels/favorite' from './endpoints/channels/favorite.js'; +export * as 'channels/featured' from './endpoints/channels/featured.js'; +export * as 'channels/follow' from './endpoints/channels/follow.js'; +export * as 'channels/followed' from './endpoints/channels/followed.js'; +export * as 'channels/my-favorites' from './endpoints/channels/my-favorites.js'; +export * as 'channels/owned' from './endpoints/channels/owned.js'; +export * as 'channels/search' from './endpoints/channels/search.js'; +export * as 'channels/show' from './endpoints/channels/show.js'; +export * as 'channels/timeline' from './endpoints/channels/timeline.js'; +export * as 'channels/unfavorite' from './endpoints/channels/unfavorite.js'; +export * as 'channels/unfollow' from './endpoints/channels/unfollow.js'; +export * as 'channels/update' from './endpoints/channels/update.js'; +export * as 'charts/active-users' from './endpoints/charts/active-users.js'; +export * as 'charts/ap-request' from './endpoints/charts/ap-request.js'; +export * as 'charts/drive' from './endpoints/charts/drive.js'; +export * as 'charts/federation' from './endpoints/charts/federation.js'; +export * as 'charts/instance' from './endpoints/charts/instance.js'; +export * as 'charts/notes' from './endpoints/charts/notes.js'; +export * as 'charts/user/drive' from './endpoints/charts/user/drive.js'; +export * as 'charts/user/following' from './endpoints/charts/user/following.js'; +export * as 'charts/user/notes' from './endpoints/charts/user/notes.js'; +export * as 'charts/user/pv' from './endpoints/charts/user/pv.js'; +export * as 'charts/user/reactions' from './endpoints/charts/user/reactions.js'; +export * as 'charts/users' from './endpoints/charts/users.js'; +export * as 'clips/add-note' from './endpoints/clips/add-note.js'; +export * as 'clips/create' from './endpoints/clips/create.js'; +export * as 'clips/delete' from './endpoints/clips/delete.js'; +export * as 'clips/favorite' from './endpoints/clips/favorite.js'; +export * as 'clips/list' from './endpoints/clips/list.js'; +export * as 'clips/my-favorites' from './endpoints/clips/my-favorites.js'; +export * as 'clips/notes' from './endpoints/clips/notes.js'; +export * as 'clips/remove-note' from './endpoints/clips/remove-note.js'; +export * as 'clips/show' from './endpoints/clips/show.js'; +export * as 'clips/unfavorite' from './endpoints/clips/unfavorite.js'; +export * as 'clips/update' from './endpoints/clips/update.js'; +export * as 'drive' from './endpoints/drive.js'; +export * as 'drive/files' from './endpoints/drive/files.js'; +export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js'; +export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js'; +export * as 'drive/files/create' from './endpoints/drive/files/create.js'; +export * as 'drive/files/delete' from './endpoints/drive/files/delete.js'; +export * as 'drive/files/find' from './endpoints/drive/files/find.js'; +export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js'; +export * as 'drive/files/show' from './endpoints/drive/files/show.js'; +export * as 'drive/files/update' from './endpoints/drive/files/update.js'; +export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js'; +export * as 'drive/folders' from './endpoints/drive/folders.js'; +export * as 'drive/folders/create' from './endpoints/drive/folders/create.js'; +export * as 'drive/folders/delete' from './endpoints/drive/folders/delete.js'; +export * as 'drive/folders/find' from './endpoints/drive/folders/find.js'; +export * as 'drive/folders/show' from './endpoints/drive/folders/show.js'; +export * as 'drive/folders/update' from './endpoints/drive/folders/update.js'; +export * as 'drive/stream' from './endpoints/drive/stream.js'; +export * as 'email-address/available' from './endpoints/email-address/available.js'; +export * as 'emoji' from './endpoints/emoji.js'; +export * as 'emojis' from './endpoints/emojis.js'; +export * as 'endpoint' from './endpoints/endpoint.js'; +export * as 'endpoints' from './endpoints/endpoints.js'; +export * as 'export-custom-emojis' from './endpoints/export-custom-emojis.js'; +export * as 'federation/followers' from './endpoints/federation/followers.js'; +export * as 'federation/following' from './endpoints/federation/following.js'; +export * as 'federation/instances' from './endpoints/federation/instances.js'; +export * as 'federation/show-instance' from './endpoints/federation/show-instance.js'; +export * as 'federation/stats' from './endpoints/federation/stats.js'; +export * as 'federation/update-remote-user' from './endpoints/federation/update-remote-user.js'; +export * as 'federation/users' from './endpoints/federation/users.js'; +export * as 'fetch-external-resources' from './endpoints/fetch-external-resources.js'; +export * as 'fetch-rss' from './endpoints/fetch-rss.js'; +export * as 'flash/create' from './endpoints/flash/create.js'; +export * as 'flash/delete' from './endpoints/flash/delete.js'; +export * as 'flash/featured' from './endpoints/flash/featured.js'; +export * as 'flash/like' from './endpoints/flash/like.js'; +export * as 'flash/my' from './endpoints/flash/my.js'; +export * as 'flash/my-likes' from './endpoints/flash/my-likes.js'; +export * as 'flash/show' from './endpoints/flash/show.js'; +export * as 'flash/unlike' from './endpoints/flash/unlike.js'; +export * as 'flash/update' from './endpoints/flash/update.js'; +export * as 'following/create' from './endpoints/following/create.js'; +export * as 'following/delete' from './endpoints/following/delete.js'; +export * as 'following/invalidate' from './endpoints/following/invalidate.js'; +export * as 'following/requests/accept' from './endpoints/following/requests/accept.js'; +export * as 'following/requests/cancel' from './endpoints/following/requests/cancel.js'; +export * as 'following/requests/list' from './endpoints/following/requests/list.js'; +export * as 'following/requests/reject' from './endpoints/following/requests/reject.js'; +export * as 'following/requests/sent' from './endpoints/following/requests/sent.js'; +export * as 'following/update' from './endpoints/following/update.js'; +export * as 'following/update-all' from './endpoints/following/update-all.js'; +export * as 'gallery/featured' from './endpoints/gallery/featured.js'; +export * as 'gallery/popular' from './endpoints/gallery/popular.js'; +export * as 'gallery/posts' from './endpoints/gallery/posts.js'; +export * as 'gallery/posts/create' from './endpoints/gallery/posts/create.js'; +export * as 'gallery/posts/delete' from './endpoints/gallery/posts/delete.js'; +export * as 'gallery/posts/like' from './endpoints/gallery/posts/like.js'; +export * as 'gallery/posts/show' from './endpoints/gallery/posts/show.js'; +export * as 'gallery/posts/unlike' from './endpoints/gallery/posts/unlike.js'; +export * as 'gallery/posts/update' from './endpoints/gallery/posts/update.js'; +export * as 'get-avatar-decorations' from './endpoints/get-avatar-decorations.js'; +export * as 'get-online-users-count' from './endpoints/get-online-users-count.js'; +export * as 'hashtags/list' from './endpoints/hashtags/list.js'; +export * as 'hashtags/search' from './endpoints/hashtags/search.js'; +export * as 'hashtags/show' from './endpoints/hashtags/show.js'; +export * as 'hashtags/trend' from './endpoints/hashtags/trend.js'; +export * as 'hashtags/users' from './endpoints/hashtags/users.js'; +export * as 'i' from './endpoints/i.js'; +export * as 'i/2fa/done' from './endpoints/i/2fa/done.js'; +export * as 'i/2fa/key-done' from './endpoints/i/2fa/key-done.js'; +export * as 'i/2fa/password-less' from './endpoints/i/2fa/password-less.js'; +export * as 'i/2fa/register' from './endpoints/i/2fa/register.js'; +export * as 'i/2fa/register-key' from './endpoints/i/2fa/register-key.js'; +export * as 'i/2fa/remove-key' from './endpoints/i/2fa/remove-key.js'; +export * as 'i/2fa/unregister' from './endpoints/i/2fa/unregister.js'; +export * as 'i/2fa/update-key' from './endpoints/i/2fa/update-key.js'; +export * as 'i/apps' from './endpoints/i/apps.js'; +export * as 'i/authorized-apps' from './endpoints/i/authorized-apps.js'; +export * as 'i/change-password' from './endpoints/i/change-password.js'; +export * as 'i/claim-achievement' from './endpoints/i/claim-achievement.js'; +export * as 'i/delete-account' from './endpoints/i/delete-account.js'; +export * as 'i/export-antennas' from './endpoints/i/export-antennas.js'; +export * as 'i/export-blocking' from './endpoints/i/export-blocking.js'; +export * as 'i/export-clips' from './endpoints/i/export-clips.js'; +export * as 'i/export-favorites' from './endpoints/i/export-favorites.js'; +export * as 'i/export-following' from './endpoints/i/export-following.js'; +export * as 'i/export-mute' from './endpoints/i/export-mute.js'; +export * as 'i/export-notes' from './endpoints/i/export-notes.js'; +export * as 'i/export-user-lists' from './endpoints/i/export-user-lists.js'; +export * as 'i/favorites' from './endpoints/i/favorites.js'; +export * as 'i/gallery/likes' from './endpoints/i/gallery/likes.js'; +export * as 'i/gallery/posts' from './endpoints/i/gallery/posts.js'; +export * as 'i/import-antennas' from './endpoints/i/import-antennas.js'; +export * as 'i/import-blocking' from './endpoints/i/import-blocking.js'; +export * as 'i/import-following' from './endpoints/i/import-following.js'; +export * as 'i/import-muting' from './endpoints/i/import-muting.js'; +export * as 'i/import-user-lists' from './endpoints/i/import-user-lists.js'; +export * as 'i/move' from './endpoints/i/move.js'; +export * as 'i/notifications' from './endpoints/i/notifications.js'; +export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.js'; +export * as 'i/page-likes' from './endpoints/i/page-likes.js'; +export * as 'i/pages' from './endpoints/i/pages.js'; +export * as 'i/pin' from './endpoints/i/pin.js'; +export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js'; +export * as 'i/read-announcement' from './endpoints/i/read-announcement.js'; +export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js'; +export * as 'i/registry/get' from './endpoints/i/registry/get.js'; +export * as 'i/registry/get-all' from './endpoints/i/registry/get-all.js'; +export * as 'i/registry/get-detail' from './endpoints/i/registry/get-detail.js'; +export * as 'i/registry/keys' from './endpoints/i/registry/keys.js'; +export * as 'i/registry/keys-with-type' from './endpoints/i/registry/keys-with-type.js'; +export * as 'i/registry/remove' from './endpoints/i/registry/remove.js'; +export * as 'i/registry/scopes-with-domain' from './endpoints/i/registry/scopes-with-domain.js'; +export * as 'i/registry/set' from './endpoints/i/registry/set.js'; +export * as 'i/revoke-token' from './endpoints/i/revoke-token.js'; +export * as 'i/signin-history' from './endpoints/i/signin-history.js'; +export * as 'i/unpin' from './endpoints/i/unpin.js'; +export * as 'i/update' from './endpoints/i/update.js'; +export * as 'i/update-email' from './endpoints/i/update-email.js'; +export * as 'i/webhooks/create' from './endpoints/i/webhooks/create.js'; +export * as 'i/webhooks/delete' from './endpoints/i/webhooks/delete.js'; +export * as 'i/webhooks/list' from './endpoints/i/webhooks/list.js'; +export * as 'i/webhooks/show' from './endpoints/i/webhooks/show.js'; +export * as 'i/webhooks/test' from './endpoints/i/webhooks/test.js'; +export * as 'i/webhooks/update' from './endpoints/i/webhooks/update.js'; +export * as 'invite/create' from './endpoints/invite/create.js'; +export * as 'invite/delete' from './endpoints/invite/delete.js'; +export * as 'invite/limit' from './endpoints/invite/limit.js'; +export * as 'invite/list' from './endpoints/invite/list.js'; +export * as 'meta' from './endpoints/meta.js'; +export * as 'miauth/gen-token' from './endpoints/miauth/gen-token.js'; +export * as 'mute/create' from './endpoints/mute/create.js'; +export * as 'mute/delete' from './endpoints/mute/delete.js'; +export * as 'mute/list' from './endpoints/mute/list.js'; +export * as 'my/apps' from './endpoints/my/apps.js'; +export * as 'notes' from './endpoints/notes.js'; +export * as 'notes/children' from './endpoints/notes/children.js'; +export * as 'notes/clips' from './endpoints/notes/clips.js'; +export * as 'notes/conversation' from './endpoints/notes/conversation.js'; +export * as 'notes/create' from './endpoints/notes/create.js'; +export * as 'notes/delete' from './endpoints/notes/delete.js'; +export * as 'notes/favorites/create' from './endpoints/notes/favorites/create.js'; +export * as 'notes/favorites/delete' from './endpoints/notes/favorites/delete.js'; +export * as 'notes/featured' from './endpoints/notes/featured.js'; +export * as 'notes/global-timeline' from './endpoints/notes/global-timeline.js'; +export * as 'notes/hybrid-timeline' from './endpoints/notes/hybrid-timeline.js'; +export * as 'notes/local-timeline' from './endpoints/notes/local-timeline.js'; +export * as 'notes/mentions' from './endpoints/notes/mentions.js'; +export * as 'notes/polls/recommendation' from './endpoints/notes/polls/recommendation.js'; +export * as 'notes/polls/vote' from './endpoints/notes/polls/vote.js'; +export * as 'notes/reactions' from './endpoints/notes/reactions.js'; +export * as 'notes/reactions/create' from './endpoints/notes/reactions/create.js'; +export * as 'notes/reactions/delete' from './endpoints/notes/reactions/delete.js'; +export * as 'notes/renotes' from './endpoints/notes/renotes.js'; +export * as 'notes/replies' from './endpoints/notes/replies.js'; +export * as 'notes/search' from './endpoints/notes/search.js'; +export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js'; +export * as 'notes/show' from './endpoints/notes/show.js'; +export * as 'notes/state' from './endpoints/notes/state.js'; +export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js'; +export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js'; +export * as 'notes/timeline' from './endpoints/notes/timeline.js'; +export * as 'notes/translate' from './endpoints/notes/translate.js'; +export * as 'notes/unrenote' from './endpoints/notes/unrenote.js'; +export * as 'notes/user-list-timeline' from './endpoints/notes/user-list-timeline.js'; +export * as 'notifications/create' from './endpoints/notifications/create.js'; +export * as 'notifications/flush' from './endpoints/notifications/flush.js'; +export * as 'notifications/mark-all-as-read' from './endpoints/notifications/mark-all-as-read.js'; +export * as 'notifications/test-notification' from './endpoints/notifications/test-notification.js'; +export * as 'page-push' from './endpoints/page-push.js'; +export * as 'pages/create' from './endpoints/pages/create.js'; +export * as 'pages/delete' from './endpoints/pages/delete.js'; +export * as 'pages/featured' from './endpoints/pages/featured.js'; +export * as 'pages/like' from './endpoints/pages/like.js'; +export * as 'pages/show' from './endpoints/pages/show.js'; +export * as 'pages/unlike' from './endpoints/pages/unlike.js'; +export * as 'pages/update' from './endpoints/pages/update.js'; +export * as 'ping' from './endpoints/ping.js'; +export * as 'pinned-users' from './endpoints/pinned-users.js'; +export * as 'promo/read' from './endpoints/promo/read.js'; +export * as 'renote-mute/create' from './endpoints/renote-mute/create.js'; +export * as 'renote-mute/delete' from './endpoints/renote-mute/delete.js'; +export * as 'renote-mute/list' from './endpoints/renote-mute/list.js'; +export * as 'request-reset-password' from './endpoints/request-reset-password.js'; +export * as 'reset-db' from './endpoints/reset-db.js'; +export * as 'reset-password' from './endpoints/reset-password.js'; +export * as 'retention' from './endpoints/retention.js'; +export * as 'reversi/cancel-match' from './endpoints/reversi/cancel-match.js'; +export * as 'reversi/games' from './endpoints/reversi/games.js'; +export * as 'reversi/invitations' from './endpoints/reversi/invitations.js'; +export * as 'reversi/match' from './endpoints/reversi/match.js'; +export * as 'reversi/show-game' from './endpoints/reversi/show-game.js'; +export * as 'reversi/surrender' from './endpoints/reversi/surrender.js'; +export * as 'reversi/verify' from './endpoints/reversi/verify.js'; +export * as 'roles/list' from './endpoints/roles/list.js'; +export * as 'roles/notes' from './endpoints/roles/notes.js'; +export * as 'roles/show' from './endpoints/roles/show.js'; +export * as 'roles/users' from './endpoints/roles/users.js'; +export * as 'server-info' from './endpoints/server-info.js'; +export * as 'stats' from './endpoints/stats.js'; +export * as 'sw/register' from './endpoints/sw/register.js'; +export * as 'sw/show-registration' from './endpoints/sw/show-registration.js'; +export * as 'sw/unregister' from './endpoints/sw/unregister.js'; +export * as 'sw/update-registration' from './endpoints/sw/update-registration.js'; +export * as 'test' from './endpoints/test.js'; +export * as 'username/available' from './endpoints/username/available.js'; +export * as 'users' from './endpoints/users.js'; +export * as 'users/achievements' from './endpoints/users/achievements.js'; +export * as 'users/clips' from './endpoints/users/clips.js'; +export * as 'users/featured-notes' from './endpoints/users/featured-notes.js'; +export * as 'users/flashs' from './endpoints/users/flashs.js'; +export * as 'users/followers' from './endpoints/users/followers.js'; +export * as 'users/following' from './endpoints/users/following.js'; +export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js'; +export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js'; +export * as 'users/lists/create' from './endpoints/users/lists/create.js'; +export * as 'users/lists/create-from-public' from './endpoints/users/lists/create-from-public.js'; +export * as 'users/lists/delete' from './endpoints/users/lists/delete.js'; +export * as 'users/lists/favorite' from './endpoints/users/lists/favorite.js'; +export * as 'users/lists/get-memberships' from './endpoints/users/lists/get-memberships.js'; +export * as 'users/lists/list' from './endpoints/users/lists/list.js'; +export * as 'users/lists/pull' from './endpoints/users/lists/pull.js'; +export * as 'users/lists/push' from './endpoints/users/lists/push.js'; +export * as 'users/lists/show' from './endpoints/users/lists/show.js'; +export * as 'users/lists/unfavorite' from './endpoints/users/lists/unfavorite.js'; +export * as 'users/lists/update' from './endpoints/users/lists/update.js'; +export * as 'users/lists/update-membership' from './endpoints/users/lists/update-membership.js'; +export * as 'users/notes' from './endpoints/users/notes.js'; +export * as 'users/pages' from './endpoints/users/pages.js'; +export * as 'users/reactions' from './endpoints/users/reactions.js'; +export * as 'users/recommendation' from './endpoints/users/recommendation.js'; +export * as 'users/relation' from './endpoints/users/relation.js'; +export * as 'users/report-abuse' from './endpoints/users/report-abuse.js'; +export * as 'users/search' from './endpoints/users/search.js'; +export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js'; +export * as 'users/show' from './endpoints/users/show.js'; +export * as 'users/update-memo' from './endpoints/users/update-memo.js'; +export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js'; diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index b4f36234f0..fd6b9bb14b 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -7,821 +7,7 @@ import { permissions } from 'misskey-js'; import type { KeyOf, Schema } from '@/misc/json-schema.js'; import type { RateLimit } from '@/misc/rate-limit-utils.js'; -import * as ep___admin_abuseReport_notificationRecipient_list - from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js'; -import * as ep___admin_abuseReport_notificationRecipient_show - from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js'; -import * as ep___admin_abuseReport_notificationRecipient_create - from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js'; -import * as ep___admin_abuseReport_notificationRecipient_update - from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js'; -import * as ep___admin_abuseReport_notificationRecipient_delete - from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js'; -import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; -import * as ep___admin_meta from './endpoints/admin/meta.js'; -import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; -import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; -import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js'; -import * as ep___admin_ad_create from './endpoints/admin/ad/create.js'; -import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js'; -import * as ep___admin_ad_list from './endpoints/admin/ad/list.js'; -import * as ep___admin_ad_update from './endpoints/admin/ad/update.js'; -import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js'; -import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; -import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; -import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; -import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js'; -import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js'; -import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; -import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; -import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; -import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; -import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; -import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; -import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; -import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; -import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; -import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; -import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; -import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; -import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; -import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; -import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js'; -import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js'; -import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js'; -import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js'; -import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js'; -import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; -import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; -import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; -import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; -import * as ep___admin_federation_refreshRemoteInstanceMetadata - from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; -import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; -import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; -import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; -import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; -import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; -import * as ep___admin_invite_create from './endpoints/admin/invite/create.js'; -import * as ep___admin_invite_list from './endpoints/admin/invite/list.js'; -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'; -import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; -import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; -import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; -import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js'; -import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js'; -import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; -import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; -import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; -import * as ep___admin_showUser from './endpoints/admin/show-user.js'; -import * as ep___admin_showUsers from './endpoints/admin/show-users.js'; -import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js'; -import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js'; -import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js'; -import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; -import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; -import * as ep___admin_approveUser from './endpoints/admin/approve-user.js'; -import * as ep___admin_declineUser from './endpoints/admin/decline-user.js'; -import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; -import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; -import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; -import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; -import * as ep___admin_roles_create from './endpoints/admin/roles/create.js'; -import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js'; -import * as ep___admin_roles_list from './endpoints/admin/roles/list.js'; -import * as ep___admin_roles_show from './endpoints/admin/roles/show.js'; -import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; -import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; -import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; -import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; -import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; -import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js'; -import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js'; -import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js'; -import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js'; -import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js'; -import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js'; -import * as ep___announcements from './endpoints/announcements.js'; -import * as ep___announcements_show from './endpoints/announcements/show.js'; -import * as ep___antennas_create from './endpoints/antennas/create.js'; -import * as ep___antennas_delete from './endpoints/antennas/delete.js'; -import * as ep___antennas_list from './endpoints/antennas/list.js'; -import * as ep___antennas_notes from './endpoints/antennas/notes.js'; -import * as ep___antennas_show from './endpoints/antennas/show.js'; -import * as ep___antennas_update from './endpoints/antennas/update.js'; -import * as ep___ap_get from './endpoints/ap/get.js'; -import * as ep___ap_show from './endpoints/ap/show.js'; -import * as ep___app_create from './endpoints/app/create.js'; -import * as ep___app_show from './endpoints/app/show.js'; -import * as ep___auth_accept from './endpoints/auth/accept.js'; -import * as ep___auth_session_generate from './endpoints/auth/session/generate.js'; -import * as ep___auth_session_show from './endpoints/auth/session/show.js'; -import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js'; -import * as ep___blocking_create from './endpoints/blocking/create.js'; -import * as ep___blocking_delete from './endpoints/blocking/delete.js'; -import * as ep___blocking_list from './endpoints/blocking/list.js'; -import * as ep___channels_create from './endpoints/channels/create.js'; -import * as ep___channels_featured from './endpoints/channels/featured.js'; -import * as ep___channels_follow from './endpoints/channels/follow.js'; -import * as ep___channels_followed from './endpoints/channels/followed.js'; -import * as ep___channels_owned from './endpoints/channels/owned.js'; -import * as ep___channels_show from './endpoints/channels/show.js'; -import * as ep___channels_timeline from './endpoints/channels/timeline.js'; -import * as ep___channels_unfollow from './endpoints/channels/unfollow.js'; -import * as ep___channels_update from './endpoints/channels/update.js'; -import * as ep___channels_favorite from './endpoints/channels/favorite.js'; -import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js'; -import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js'; -import * as ep___channels_search from './endpoints/channels/search.js'; -import * as ep___charts_activeUsers from './endpoints/charts/active-users.js'; -import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; -import * as ep___charts_drive from './endpoints/charts/drive.js'; -import * as ep___charts_federation from './endpoints/charts/federation.js'; -import * as ep___charts_instance from './endpoints/charts/instance.js'; -import * as ep___charts_notes from './endpoints/charts/notes.js'; -import * as ep___charts_user_drive from './endpoints/charts/user/drive.js'; -import * as ep___charts_user_following from './endpoints/charts/user/following.js'; -import * as ep___charts_user_notes from './endpoints/charts/user/notes.js'; -import * as ep___charts_user_pv from './endpoints/charts/user/pv.js'; -import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js'; -import * as ep___charts_users from './endpoints/charts/users.js'; -import * as ep___clips_addNote from './endpoints/clips/add-note.js'; -import * as ep___clips_removeNote from './endpoints/clips/remove-note.js'; -import * as ep___clips_create from './endpoints/clips/create.js'; -import * as ep___clips_delete from './endpoints/clips/delete.js'; -import * as ep___clips_list from './endpoints/clips/list.js'; -import * as ep___clips_notes from './endpoints/clips/notes.js'; -import * as ep___clips_show from './endpoints/clips/show.js'; -import * as ep___clips_update from './endpoints/clips/update.js'; -import * as ep___clips_favorite from './endpoints/clips/favorite.js'; -import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js'; -import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js'; -import * as ep___drive from './endpoints/drive.js'; -import * as ep___drive_files from './endpoints/drive/files.js'; -import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js'; -import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js'; -import * as ep___drive_files_create from './endpoints/drive/files/create.js'; -import * as ep___drive_files_delete from './endpoints/drive/files/delete.js'; -import * as ep___drive_files_findByHash from './endpoints/drive/files/find-by-hash.js'; -import * as ep___drive_files_find from './endpoints/drive/files/find.js'; -import * as ep___drive_files_show from './endpoints/drive/files/show.js'; -import * as ep___drive_files_update from './endpoints/drive/files/update.js'; -import * as ep___drive_files_uploadFromUrl from './endpoints/drive/files/upload-from-url.js'; -import * as ep___drive_folders from './endpoints/drive/folders.js'; -import * as ep___drive_folders_create from './endpoints/drive/folders/create.js'; -import * as ep___drive_folders_delete from './endpoints/drive/folders/delete.js'; -import * as ep___drive_folders_find from './endpoints/drive/folders/find.js'; -import * as ep___drive_folders_show from './endpoints/drive/folders/show.js'; -import * as ep___drive_folders_update from './endpoints/drive/folders/update.js'; -import * as ep___drive_stream from './endpoints/drive/stream.js'; -import * as ep___emailAddress_available from './endpoints/email-address/available.js'; -import * as ep___endpoint from './endpoints/endpoint.js'; -import * as ep___endpoints from './endpoints/endpoints.js'; -import * as ep___exportCustomEmojis from './endpoints/export-custom-emojis.js'; -import * as ep___federation_followers from './endpoints/federation/followers.js'; -import * as ep___federation_following from './endpoints/federation/following.js'; -import * as ep___federation_instances from './endpoints/federation/instances.js'; -import * as ep___federation_showInstance from './endpoints/federation/show-instance.js'; -import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js'; -import * as ep___federation_users from './endpoints/federation/users.js'; -import * as ep___federation_stats from './endpoints/federation/stats.js'; -import * as ep___following_create from './endpoints/following/create.js'; -import * as ep___following_delete from './endpoints/following/delete.js'; -import * as ep___following_update from './endpoints/following/update.js'; -import * as ep___following_update_all from './endpoints/following/update-all.js'; -import * as ep___following_invalidate from './endpoints/following/invalidate.js'; -import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; -import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; -import * as ep___following_requests_list from './endpoints/following/requests/list.js'; -import * as ep___following_requests_sent from './endpoints/following/requests/sent.js'; -import * as ep___following_requests_reject from './endpoints/following/requests/reject.js'; -import * as ep___gallery_featured from './endpoints/gallery/featured.js'; -import * as ep___gallery_popular from './endpoints/gallery/popular.js'; -import * as ep___gallery_posts from './endpoints/gallery/posts.js'; -import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js'; -import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js'; -import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js'; -import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js'; -import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; -import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; -import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; -import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js'; -import * as ep___hashtags_list from './endpoints/hashtags/list.js'; -import * as ep___hashtags_search from './endpoints/hashtags/search.js'; -import * as ep___hashtags_show from './endpoints/hashtags/show.js'; -import * as ep___hashtags_trend from './endpoints/hashtags/trend.js'; -import * as ep___hashtags_users from './endpoints/hashtags/users.js'; -import * as ep___i from './endpoints/i.js'; -import * as ep___i_2fa_done from './endpoints/i/2fa/done.js'; -import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js'; -import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js'; -import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js'; -import * as ep___i_2fa_register from './endpoints/i/2fa/register.js'; -import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js'; -import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; -import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; -import * as ep___i_apps from './endpoints/i/apps.js'; -import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; -import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; -import * as ep___i_changePassword from './endpoints/i/change-password.js'; -import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; -import * as ep___i_exportData from './endpoints/i/export-data.js'; -import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; -import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; -import * as ep___i_exportMute from './endpoints/i/export-mute.js'; -import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; -import * as ep___i_exportClips from './endpoints/i/export-clips.js'; -import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; -import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; -import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; -import * as ep___i_favorites from './endpoints/i/favorites.js'; -import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; -import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; -import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; -import * as ep___i_importFollowing from './endpoints/i/import-following.js'; -import * as ep___i_importNotes from './endpoints/i/import-notes.js'; -import * as ep___i_importMuting from './endpoints/i/import-muting.js'; -import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; -import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; -import * as ep___i_notifications from './endpoints/i/notifications.js'; -import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js'; -import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; -import * as ep___i_pages from './endpoints/i/pages.js'; -import * as ep___i_pin from './endpoints/i/pin.js'; -import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js'; -import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js'; -import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js'; -import * as ep___i_registry_getAll from './endpoints/i/registry/get-all.js'; -import * as ep___i_registry_getUnsecure from './endpoints/i/registry/get-unsecure.js'; -import * as ep___i_registry_getDetail from './endpoints/i/registry/get-detail.js'; -import * as ep___i_registry_get from './endpoints/i/registry/get.js'; -import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; -import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; -import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; -import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js'; -import * as ep___i_registry_set from './endpoints/i/registry/set.js'; -import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; -import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; -import * as ep___i_unpin from './endpoints/i/unpin.js'; -import * as ep___i_updateEmail from './endpoints/i/update-email.js'; -import * as ep___i_update from './endpoints/i/update.js'; -import * as ep___i_move from './endpoints/i/move.js'; -import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; -import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; -import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; -import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; -import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; -import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js'; -import * as ep___invite_create from './endpoints/invite/create.js'; -import * as ep___invite_delete from './endpoints/invite/delete.js'; -import * as ep___invite_list from './endpoints/invite/list.js'; -import * as ep___invite_limit from './endpoints/invite/limit.js'; -import * as ep___meta from './endpoints/meta.js'; -import * as ep___emojis from './endpoints/emojis.js'; -import * as ep___emoji from './endpoints/emoji.js'; -import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; -import * as ep___mute_create from './endpoints/mute/create.js'; -import * as ep___mute_delete from './endpoints/mute/delete.js'; -import * as ep___mute_list from './endpoints/mute/list.js'; -import * as ep___renoteMute_create from './endpoints/renote-mute/create.js'; -import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js'; -import * as ep___renoteMute_list from './endpoints/renote-mute/list.js'; -import * as ep___my_apps from './endpoints/my/apps.js'; -import * as ep___notes from './endpoints/notes.js'; -import * as ep___notes_children from './endpoints/notes/children.js'; -import * as ep___notes_clips from './endpoints/notes/clips.js'; -import * as ep___notes_conversation from './endpoints/notes/conversation.js'; -import * as ep___notes_create from './endpoints/notes/create.js'; -import * as ep___notes_delete from './endpoints/notes/delete.js'; -import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; -import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; -import * as ep___notes_featured from './endpoints/notes/featured.js'; -import * as ep___notes_following from './endpoints/notes/following.js'; -import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; -import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; -import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; -import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; -import * as ep___notes_mentions from './endpoints/notes/mentions.js'; -import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; -import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; -import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js'; -import * as ep___notes_reactions from './endpoints/notes/reactions.js'; -import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js'; -import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; -import * as ep___notes_like from './endpoints/notes/like.js'; -import * as ep___notes_renotes from './endpoints/notes/renotes.js'; -import * as ep___notes_replies from './endpoints/notes/replies.js'; -import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js'; -import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; -import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; -import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; -import * as ep___notes_search from './endpoints/notes/search.js'; -import * as ep___notes_show from './endpoints/notes/show.js'; -import * as ep___notes_state from './endpoints/notes/state.js'; -import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js'; -import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js'; -import * as ep___notes_timeline from './endpoints/notes/timeline.js'; -import * as ep___notes_translate from './endpoints/notes/translate.js'; -import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; -import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; -import * as ep___notes_edit from './endpoints/notes/edit.js'; -import * as ep___notes_versions from './endpoints/notes/versions.js'; -import * as ep___notifications_create from './endpoints/notifications/create.js'; -import * as ep___notifications_flush from './endpoints/notifications/flush.js'; -import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; -import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; -import * as ep___pagePush from './endpoints/page-push.js'; -import * as ep___pages_create from './endpoints/pages/create.js'; -import * as ep___pages_delete from './endpoints/pages/delete.js'; -import * as ep___pages_featured from './endpoints/pages/featured.js'; -import * as ep___pages_like from './endpoints/pages/like.js'; -import * as ep___pages_show from './endpoints/pages/show.js'; -import * as ep___pages_unlike from './endpoints/pages/unlike.js'; -import * as ep___pages_update from './endpoints/pages/update.js'; -import * as ep___flash_create from './endpoints/flash/create.js'; -import * as ep___flash_delete from './endpoints/flash/delete.js'; -import * as ep___flash_featured from './endpoints/flash/featured.js'; -import * as ep___flash_like from './endpoints/flash/like.js'; -import * as ep___flash_show from './endpoints/flash/show.js'; -import * as ep___flash_unlike from './endpoints/flash/unlike.js'; -import * as ep___flash_update from './endpoints/flash/update.js'; -import * as ep___flash_my from './endpoints/flash/my.js'; -import * as ep___flash_myLikes from './endpoints/flash/my-likes.js'; -import * as ep___ping from './endpoints/ping.js'; -import * as ep___pinnedUsers from './endpoints/pinned-users.js'; -import * as ep___promo_read from './endpoints/promo/read.js'; -import * as ep___roles_list from './endpoints/roles/list.js'; -import * as ep___roles_show from './endpoints/roles/show.js'; -import * as ep___roles_users from './endpoints/roles/users.js'; -import * as ep___roles_notes from './endpoints/roles/notes.js'; -import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; -import * as ep___resetDb from './endpoints/reset-db.js'; -import * as ep___resetPassword from './endpoints/reset-password.js'; -import * as ep___serverInfo from './endpoints/server-info.js'; -import * as ep___stats from './endpoints/stats.js'; -import * as ep___sw_show_registration from './endpoints/sw/show-registration.js'; -import * as ep___sw_update_registration from './endpoints/sw/update-registration.js'; -import * as ep___sw_register from './endpoints/sw/register.js'; -import * as ep___sw_unregister from './endpoints/sw/unregister.js'; -import * as ep___test from './endpoints/test.js'; -import * as ep___username_available from './endpoints/username/available.js'; -import * as ep___users from './endpoints/users.js'; -import * as ep___users_clips from './endpoints/users/clips.js'; -import * as ep___users_followers from './endpoints/users/followers.js'; -import * as ep___users_following from './endpoints/users/following.js'; -import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; -import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; -import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js'; -import * as ep___users_lists_create from './endpoints/users/lists/create.js'; -import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; -import * as ep___users_lists_list from './endpoints/users/lists/list.js'; -import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; -import * as ep___users_lists_push from './endpoints/users/lists/push.js'; -import * as ep___users_lists_show from './endpoints/users/lists/show.js'; -import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; -import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; -import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js'; -import * as ep___users_lists_update from './endpoints/users/lists/update.js'; -import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js'; -import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js'; -import * as ep___users_notes from './endpoints/users/notes.js'; -import * as ep___users_pages from './endpoints/users/pages.js'; -import * as ep___users_flashs from './endpoints/users/flashs.js'; -import * as ep___users_reactions from './endpoints/users/reactions.js'; -import * as ep___users_recommendation from './endpoints/users/recommendation.js'; -import * as ep___users_relation from './endpoints/users/relation.js'; -import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; -import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; -import * as ep___users_search from './endpoints/users/search.js'; -import * as ep___users_show from './endpoints/users/show.js'; -import * as ep___users_achievements from './endpoints/users/achievements.js'; -import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; -import * as ep___fetchRss from './endpoints/fetch-rss.js'; -import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js'; -import * as ep___retention from './endpoints/retention.js'; -import * as ep___sponsors from './endpoints/sponsors.js'; -import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js'; -import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; -import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js'; -import * as ep___reversi_games from './endpoints/reversi/games.js'; -import * as ep___reversi_match from './endpoints/reversi/match.js'; -import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; -import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; -import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; -import * as ep___reversi_verify from './endpoints/reversi/verify.js'; - -const eps = [ - ['admin/meta', ep___admin_meta], - ['admin/abuse-user-reports', ep___admin_abuseUserReports], - ['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list], - ['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show], - ['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create], - ['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update], - ['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete], - ['admin/accounts/create', ep___admin_accounts_create], - ['admin/accounts/delete', ep___admin_accounts_delete], - ['admin/accounts/find-by-email', ep___admin_accounts_findByEmail], - ['admin/ad/create', ep___admin_ad_create], - ['admin/ad/delete', ep___admin_ad_delete], - ['admin/ad/list', ep___admin_ad_list], - ['admin/ad/update', ep___admin_ad_update], - ['admin/announcements/create', ep___admin_announcements_create], - ['admin/announcements/delete', ep___admin_announcements_delete], - ['admin/announcements/list', ep___admin_announcements_list], - ['admin/announcements/update', ep___admin_announcements_update], - ['admin/avatar-decorations/create', ep___admin_avatarDecorations_create], - ['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete], - ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list], - ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update], - ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], - ['admin/unset-user-avatar', ep___admin_unsetUserAvatar], - ['admin/unset-user-banner', ep___admin_unsetUserBanner], - ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], - ['admin/drive/cleanup', ep___admin_drive_cleanup], - ['admin/drive/files', ep___admin_drive_files], - ['admin/drive/show-file', ep___admin_drive_showFile], - ['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk], - ['admin/emoji/add', ep___admin_emoji_add], - ['admin/emoji/copy', ep___admin_emoji_copy], - ['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk], - ['admin/emoji/delete', ep___admin_emoji_delete], - ['admin/emoji/import-zip', ep___admin_emoji_importZip], - ['admin/emoji/list-remote', ep___admin_emoji_listRemote], - ['admin/emoji/list', ep___admin_emoji_list], - ['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk], - ['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk], - ['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk], - ['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk], - ['admin/emoji/update', ep___admin_emoji_update], - ['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles], - ['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata], - ['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing], - ['admin/federation/update-instance', ep___admin_federation_updateInstance], - ['admin/get-index-stats', ep___admin_getIndexStats], - ['admin/get-table-stats', ep___admin_getTableStats], - ['admin/get-user-ips', ep___admin_getUserIps], - ['admin/invite/create', ep___admin_invite_create], - ['admin/invite/list', ep___admin_invite_list], - ['admin/promo/create', ep___admin_promo_create], - ['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], - ['admin/relays/remove', ep___admin_relays_remove], - ['admin/reset-password', ep___admin_resetPassword], - ['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport], - ['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport], - ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport], - ['admin/send-email', ep___admin_sendEmail], - ['admin/server-info', ep___admin_serverInfo], - ['admin/show-moderation-logs', ep___admin_showModerationLogs], - ['admin/show-user', ep___admin_showUser], - ['admin/show-users', ep___admin_showUsers], - ['admin/nsfw-user', ep___admin_nsfwUser], - ['admin/unnsfw-user', ep___admin_unnsfwUser], - ['admin/silence-user', ep___admin_silenceUser], - ['admin/unsilence-user', ep___admin_unsilenceUser], - ['admin/suspend-user', ep___admin_suspendUser], - ['admin/approve-user', ep___admin_approveUser], - ['admin/decline-user', ep___admin_declineUser], - ['admin/unsuspend-user', ep___admin_unsuspendUser], - ['admin/update-meta', ep___admin_updateMeta], - ['admin/delete-account', ep___admin_deleteAccount], - ['admin/update-user-note', ep___admin_updateUserNote], - ['admin/roles/create', ep___admin_roles_create], - ['admin/roles/delete', ep___admin_roles_delete], - ['admin/roles/list', ep___admin_roles_list], - ['admin/roles/show', ep___admin_roles_show], - ['admin/roles/update', ep___admin_roles_update], - ['admin/roles/assign', ep___admin_roles_assign], - ['admin/roles/unassign', ep___admin_roles_unassign], - ['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies], - ['admin/roles/users', ep___admin_roles_users], - ['admin/system-webhook/create', ep___admin_systemWebhook_create], - ['admin/system-webhook/delete', ep___admin_systemWebhook_delete], - ['admin/system-webhook/list', ep___admin_systemWebhook_list], - ['admin/system-webhook/show', ep___admin_systemWebhook_show], - ['admin/system-webhook/update', ep___admin_systemWebhook_update], - ['admin/system-webhook/test', ep___admin_systemWebhook_test], - ['announcements', ep___announcements], - ['announcements/show', ep___announcements_show], - ['antennas/create', ep___antennas_create], - ['antennas/delete', ep___antennas_delete], - ['antennas/list', ep___antennas_list], - ['antennas/notes', ep___antennas_notes], - ['antennas/show', ep___antennas_show], - ['antennas/update', ep___antennas_update], - ['ap/get', ep___ap_get], - ['ap/show', ep___ap_show], - ['app/create', ep___app_create], - ['app/show', ep___app_show], - ['auth/accept', ep___auth_accept], - ['auth/session/generate', ep___auth_session_generate], - ['auth/session/show', ep___auth_session_show], - ['auth/session/userkey', ep___auth_session_userkey], - ['blocking/create', ep___blocking_create], - ['blocking/delete', ep___blocking_delete], - ['blocking/list', ep___blocking_list], - ['channels/create', ep___channels_create], - ['channels/featured', ep___channels_featured], - ['channels/follow', ep___channels_follow], - ['channels/followed', ep___channels_followed], - ['channels/owned', ep___channels_owned], - ['channels/show', ep___channels_show], - ['channels/timeline', ep___channels_timeline], - ['channels/unfollow', ep___channels_unfollow], - ['channels/update', ep___channels_update], - ['channels/favorite', ep___channels_favorite], - ['channels/unfavorite', ep___channels_unfavorite], - ['channels/my-favorites', ep___channels_myFavorites], - ['channels/search', ep___channels_search], - ['charts/active-users', ep___charts_activeUsers], - ['charts/ap-request', ep___charts_apRequest], - ['charts/drive', ep___charts_drive], - ['charts/federation', ep___charts_federation], - ['charts/instance', ep___charts_instance], - ['charts/notes', ep___charts_notes], - ['charts/user/drive', ep___charts_user_drive], - ['charts/user/following', ep___charts_user_following], - ['charts/user/notes', ep___charts_user_notes], - ['charts/user/pv', ep___charts_user_pv], - ['charts/user/reactions', ep___charts_user_reactions], - ['charts/users', ep___charts_users], - ['clips/add-note', ep___clips_addNote], - ['clips/remove-note', ep___clips_removeNote], - ['clips/create', ep___clips_create], - ['clips/delete', ep___clips_delete], - ['clips/list', ep___clips_list], - ['clips/notes', ep___clips_notes], - ['clips/show', ep___clips_show], - ['clips/update', ep___clips_update], - ['clips/favorite', ep___clips_favorite], - ['clips/unfavorite', ep___clips_unfavorite], - ['clips/my-favorites', ep___clips_myFavorites], - ['drive', ep___drive], - ['drive/files', ep___drive_files], - ['drive/files/attached-notes', ep___drive_files_attachedNotes], - ['drive/files/check-existence', ep___drive_files_checkExistence], - ['drive/files/create', ep___drive_files_create], - ['drive/files/delete', ep___drive_files_delete], - ['drive/files/find-by-hash', ep___drive_files_findByHash], - ['drive/files/find', ep___drive_files_find], - ['drive/files/show', ep___drive_files_show], - ['drive/files/update', ep___drive_files_update], - ['drive/files/upload-from-url', ep___drive_files_uploadFromUrl], - ['drive/folders', ep___drive_folders], - ['drive/folders/create', ep___drive_folders_create], - ['drive/folders/delete', ep___drive_folders_delete], - ['drive/folders/find', ep___drive_folders_find], - ['drive/folders/show', ep___drive_folders_show], - ['drive/folders/update', ep___drive_folders_update], - ['drive/stream', ep___drive_stream], - ['email-address/available', ep___emailAddress_available], - ['endpoint', ep___endpoint], - ['endpoints', ep___endpoints], - ['export-custom-emojis', ep___exportCustomEmojis], - ['federation/followers', ep___federation_followers], - ['federation/following', ep___federation_following], - ['federation/instances', ep___federation_instances], - ['federation/show-instance', ep___federation_showInstance], - ['federation/update-remote-user', ep___federation_updateRemoteUser], - ['federation/users', ep___federation_users], - ['federation/stats', ep___federation_stats], - ['following/create', ep___following_create], - ['following/delete', ep___following_delete], - ['following/update', ep___following_update], - ['following/update-all', ep___following_update_all], - ['following/invalidate', ep___following_invalidate], - ['following/requests/accept', ep___following_requests_accept], - ['following/requests/cancel', ep___following_requests_cancel], - ['following/requests/list', ep___following_requests_list], - ['following/requests/sent', ep___following_requests_sent], - ['following/requests/reject', ep___following_requests_reject], - ['gallery/featured', ep___gallery_featured], - ['gallery/popular', ep___gallery_popular], - ['gallery/posts', ep___gallery_posts], - ['gallery/posts/create', ep___gallery_posts_create], - ['gallery/posts/delete', ep___gallery_posts_delete], - ['gallery/posts/like', ep___gallery_posts_like], - ['gallery/posts/show', ep___gallery_posts_show], - ['gallery/posts/unlike', ep___gallery_posts_unlike], - ['gallery/posts/update', ep___gallery_posts_update], - ['get-online-users-count', ep___getOnlineUsersCount], - ['get-avatar-decorations', ep___getAvatarDecorations], - ['hashtags/list', ep___hashtags_list], - ['hashtags/search', ep___hashtags_search], - ['hashtags/show', ep___hashtags_show], - ['hashtags/trend', ep___hashtags_trend], - ['hashtags/users', ep___hashtags_users], - ['i', ep___i], - ['i/2fa/done', ep___i_2fa_done], - ['i/2fa/key-done', ep___i_2fa_keyDone], - ['i/2fa/password-less', ep___i_2fa_passwordLess], - ['i/2fa/register-key', ep___i_2fa_registerKey], - ['i/2fa/register', ep___i_2fa_register], - ['i/2fa/update-key', ep___i_2fa_updateKey], - ['i/2fa/remove-key', ep___i_2fa_removeKey], - ['i/2fa/unregister', ep___i_2fa_unregister], - ['i/apps', ep___i_apps], - ['i/authorized-apps', ep___i_authorizedApps], - ['i/claim-achievement', ep___i_claimAchievement], - ['i/change-password', ep___i_changePassword], - ['i/delete-account', ep___i_deleteAccount], - ['i/export-data', ep___i_exportData], - ['i/export-blocking', ep___i_exportBlocking], - ['i/export-following', ep___i_exportFollowing], - ['i/export-mute', ep___i_exportMute], - ['i/export-notes', ep___i_exportNotes], - ['i/export-clips', ep___i_exportClips], - ['i/export-favorites', ep___i_exportFavorites], - ['i/export-user-lists', ep___i_exportUserLists], - ['i/export-antennas', ep___i_exportAntennas], - ['i/favorites', ep___i_favorites], - ['i/gallery/likes', ep___i_gallery_likes], - ['i/gallery/posts', ep___i_gallery_posts], - ['i/import-blocking', ep___i_importBlocking], - ['i/import-following', ep___i_importFollowing], - ['i/import-notes', ep___i_importNotes], - ['i/import-muting', ep___i_importMuting], - ['i/import-user-lists', ep___i_importUserLists], - ['i/import-antennas', ep___i_importAntennas], - ['i/notifications', ep___i_notifications], - ['i/notifications-grouped', ep___i_notificationsGrouped], - ['i/page-likes', ep___i_pageLikes], - ['i/pages', ep___i_pages], - ['i/pin', ep___i_pin], - ['i/read-all-unread-notes', ep___i_readAllUnreadNotes], - ['i/read-announcement', ep___i_readAnnouncement], - ['i/regenerate-token', ep___i_regenerateToken], - ['i/registry/get-all', ep___i_registry_getAll], - ['i/registry/get-unsecure', ep___i_registry_getUnsecure], - ['i/registry/get-detail', ep___i_registry_getDetail], - ['i/registry/get', ep___i_registry_get], - ['i/registry/keys-with-type', ep___i_registry_keysWithType], - ['i/registry/keys', ep___i_registry_keys], - ['i/registry/remove', ep___i_registry_remove], - ['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain], - ['i/registry/set', ep___i_registry_set], - ['i/revoke-token', ep___i_revokeToken], - ['i/signin-history', ep___i_signinHistory], - ['i/unpin', ep___i_unpin], - ['i/update-email', ep___i_updateEmail], - ['i/update', ep___i_update], - ['i/move', ep___i_move], - ['i/webhooks/create', ep___i_webhooks_create], - ['i/webhooks/list', ep___i_webhooks_list], - ['i/webhooks/show', ep___i_webhooks_show], - ['i/webhooks/update', ep___i_webhooks_update], - ['i/webhooks/delete', ep___i_webhooks_delete], - ['i/webhooks/test', ep___i_webhooks_test], - ['invite/create', ep___invite_create], - ['invite/delete', ep___invite_delete], - ['invite/list', ep___invite_list], - ['invite/limit', ep___invite_limit], - ['meta', ep___meta], - ['emojis', ep___emojis], - ['emoji', ep___emoji], - ['miauth/gen-token', ep___miauth_genToken], - ['mute/create', ep___mute_create], - ['mute/delete', ep___mute_delete], - ['mute/list', ep___mute_list], - ['renote-mute/create', ep___renoteMute_create], - ['renote-mute/delete', ep___renoteMute_delete], - ['renote-mute/list', ep___renoteMute_list], - ['my/apps', ep___my_apps], - ['notes', ep___notes], - ['notes/children', ep___notes_children], - ['notes/clips', ep___notes_clips], - ['notes/conversation', ep___notes_conversation], - ['notes/create', ep___notes_create], - ['notes/delete', ep___notes_delete], - ['notes/favorites/create', ep___notes_favorites_create], - ['notes/favorites/delete', ep___notes_favorites_delete], - ['notes/featured', ep___notes_featured], - ['notes/following', ep___notes_following], - ['notes/global-timeline', ep___notes_globalTimeline], - ['notes/bubble-timeline', ep___notes_bubbleTimeline], - ['notes/hybrid-timeline', ep___notes_hybridTimeline], - ['notes/local-timeline', ep___notes_localTimeline], - ['notes/mentions', ep___notes_mentions], - ['notes/polls/recommendation', ep___notes_polls_recommendation], - ['notes/polls/vote', ep___notes_polls_vote], - ['notes/polls/refresh', ep___notes_polls_refresh], - ['notes/reactions', ep___notes_reactions], - ['notes/reactions/create', ep___notes_reactions_create], - ['notes/reactions/delete', ep___notes_reactions_delete], - ['notes/like', ep___notes_like], - ['notes/renotes', ep___notes_renotes], - ['notes/replies', ep___notes_replies], - ['notes/schedule/create', ep___notes_schedule_create], - ['notes/schedule/delete', ep___notes_schedule_delete], - ['notes/schedule/list', ep___notes_schedule_list], - ['notes/search-by-tag', ep___notes_searchByTag], - ['notes/search', ep___notes_search], - ['notes/show', ep___notes_show], - ['notes/state', ep___notes_state], - ['notes/thread-muting/create', ep___notes_threadMuting_create], - ['notes/thread-muting/delete', ep___notes_threadMuting_delete], - ['notes/timeline', ep___notes_timeline], - ['notes/translate', ep___notes_translate], - ['notes/unrenote', ep___notes_unrenote], - ['notes/user-list-timeline', ep___notes_userListTimeline], - ['notes/edit', ep___notes_edit], - ['notes/versions', ep___notes_versions], - ['notifications/create', ep___notifications_create], - ['notifications/flush', ep___notifications_flush], - ['notifications/mark-all-as-read', ep___notifications_markAllAsRead], - ['notifications/test-notification', ep___notifications_testNotification], - ['page-push', ep___pagePush], - ['pages/create', ep___pages_create], - ['pages/delete', ep___pages_delete], - ['pages/featured', ep___pages_featured], - ['pages/like', ep___pages_like], - ['pages/show', ep___pages_show], - ['pages/unlike', ep___pages_unlike], - ['pages/update', ep___pages_update], - ['flash/create', ep___flash_create], - ['flash/delete', ep___flash_delete], - ['flash/featured', ep___flash_featured], - ['flash/like', ep___flash_like], - ['flash/show', ep___flash_show], - ['flash/unlike', ep___flash_unlike], - ['flash/update', ep___flash_update], - ['flash/my', ep___flash_my], - ['flash/my-likes', ep___flash_myLikes], - ['ping', ep___ping], - ['pinned-users', ep___pinnedUsers], - ['promo/read', ep___promo_read], - ['roles/list', ep___roles_list], - ['roles/show', ep___roles_show], - ['roles/users', ep___roles_users], - ['roles/notes', ep___roles_notes], - ['request-reset-password', ep___requestResetPassword], - ['reset-db', ep___resetDb], - ['reset-password', ep___resetPassword], - ['server-info', ep___serverInfo], - ['stats', ep___stats], - ['sw/show-registration', ep___sw_show_registration], - ['sw/update-registration', ep___sw_update_registration], - ['sw/register', ep___sw_register], - ['sw/unregister', ep___sw_unregister], - ['test', ep___test], - ['username/available', ep___username_available], - ['users', ep___users], - ['users/clips', ep___users_clips], - ['users/followers', ep___users_followers], - ['users/following', ep___users_following], - ['users/gallery/posts', ep___users_gallery_posts], - ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers], - ['users/featured-notes', ep___users_featuredNotes], - ['users/lists/create', ep___users_lists_create], - ['users/lists/delete', ep___users_lists_delete], - ['users/lists/list', ep___users_lists_list], - ['users/lists/pull', ep___users_lists_pull], - ['users/lists/push', ep___users_lists_push], - ['users/lists/show', ep___users_lists_show], - ['users/lists/favorite', ep___users_lists_favorite], - ['users/lists/unfavorite', ep___users_lists_unfavorite], - ['users/lists/update', ep___users_lists_update], - ['users/lists/create-from-public', ep___users_lists_createFromPublic], - ['users/lists/update-membership', ep___users_lists_updateMembership], - ['users/lists/get-memberships', ep___users_lists_getMemberships], - ['users/notes', ep___users_notes], - ['users/pages', ep___users_pages], - ['users/flashs', ep___users_flashs], - ['users/reactions', ep___users_reactions], - ['users/recommendation', ep___users_recommendation], - ['users/relation', ep___users_relation], - ['users/report-abuse', ep___users_reportAbuse], - ['users/search-by-username-and-host', ep___users_searchByUsernameAndHost], - ['users/search', ep___users_search], - ['users/show', ep___users_show], - ['users/achievements', ep___users_achievements], - ['users/update-memo', ep___users_updateMemo], - ['fetch-rss', ep___fetchRss], - ['fetch-external-resources', ep___fetchExternalResources], - ['retention', ep___retention], - ['sponsors', ep___sponsors], - ['bubble-game/register', ep___bubbleGame_register], - ['bubble-game/ranking', ep___bubbleGame_ranking], - ['reversi/cancel-match', ep___reversi_cancelMatch], - ['reversi/games', ep___reversi_games], - ['reversi/match', ep___reversi_match], - ['reversi/invitations', ep___reversi_invitations], - ['reversi/show-game', ep___reversi_showGame], - ['reversi/surrender', ep___reversi_surrender], - ['reversi/verify', ep___reversi_verify], -]; +import * as endpointsObject from './endpoint-list.js'; interface IEndpointMetaBase { readonly stability?: 'deprecated' | 'experimental' | 'stable'; @@ -922,7 +108,7 @@ export interface IEndpoint { params: Schema; } -const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { +const endpoints: IEndpoint[] = Object.entries(endpointsObject).map(([name, ep]) => { return { name: name, get meta() { diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/current.ts b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts new file mode 100644 index 0000000000..63ec740348 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js'; + +export const meta = { + tags: ['admin', 'captcha'], + + requireCredential: true, + requireAdmin: true, + + // 実態ã¯metaã®å–å¾—ã§ã‚ã‚‹ãŸã‚ + kind: 'read:admin:meta', + + res: { + type: 'object', + properties: { + provider: { + type: 'string', + enum: supportedCaptchaProviders, + }, + hcaptcha: { + type: 'object', + properties: { + siteKey: { type: 'string', nullable: true }, + secretKey: { type: 'string', nullable: true }, + }, + }, + mcaptcha: { + type: 'object', + properties: { + siteKey: { type: 'string', nullable: true }, + secretKey: { type: 'string', nullable: true }, + instanceUrl: { type: 'string', nullable: true }, + }, + }, + recaptcha: { + type: 'object', + properties: { + siteKey: { type: 'string', nullable: true }, + secretKey: { type: 'string', nullable: true }, + }, + }, + turnstile: { + type: 'object', + properties: { + siteKey: { type: 'string', nullable: true }, + secretKey: { type: 'string', nullable: true }, + }, + }, + }, + }, +} as const; + +export const paramDef = {} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private captchaService: CaptchaService, + ) { + super(meta, paramDef, async () => { + return this.captchaService.get(); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/save.ts b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts new file mode 100644 index 0000000000..98ec278ebe --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { captchaErrorCodes, CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['admin', 'captcha'], + + requireCredential: true, + requireAdmin: true, + + // 実態ã¯metaã®æ›´æ–°ã§ã‚ã‚‹ãŸã‚ + kind: 'write:admin:meta', + + errors: { + invalidProvider: { + message: 'Invalid provider.', + code: 'INVALID_PROVIDER', + id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0', + httpStatusCode: 400, + }, + invalidParameters: { + message: 'Invalid parameters.', + code: 'INVALID_PARAMETERS', + id: '26654194-410e-44e2-b42e-460ff6f92476', + httpStatusCode: 400, + }, + noResponseProvided: { + message: 'No response provided.', + code: 'NO_RESPONSE_PROVIDED', + id: '40acbba8-0937-41fb-bb3f-474514d40afe', + httpStatusCode: 400, + }, + requestFailed: { + message: 'Request failed.', + code: 'REQUEST_FAILED', + id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd', + httpStatusCode: 500, + }, + verificationFailed: { + message: 'Verification failed.', + code: 'VERIFICATION_FAILED', + id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214', + httpStatusCode: 400, + }, + unknown: { + message: 'unknown', + code: 'UNKNOWN', + id: 'f868d509-e257-42a9-99c1-42614b031a97', + httpStatusCode: 500, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + provider: { + type: 'string', + enum: supportedCaptchaProviders, + }, + captchaResult: { + type: 'string', nullable: true, + }, + sitekey: { + type: 'string', nullable: true, + }, + secret: { + type: 'string', nullable: true, + }, + instanceUrl: { + type: 'string', nullable: true, + }, + }, + required: ['provider'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private captchaService: CaptchaService, + ) { + super(meta, paramDef, async (ps) => { + const result = await this.captchaService.save(ps.provider, { + sitekey: ps.sitekey, + secret: ps.secret, + instanceUrl: ps.instanceUrl, + captchaResult: ps.captchaResult, + }); + + if (!result.success) { + switch (result.error.code) { + case captchaErrorCodes.invalidProvider: + throw new ApiError({ + ...meta.errors.invalidProvider, + message: result.error.message, + }); + case captchaErrorCodes.invalidParameters: + throw new ApiError({ + ...meta.errors.invalidParameters, + message: result.error.message, + }); + case captchaErrorCodes.noResponseProvided: + throw new ApiError({ + ...meta.errors.noResponseProvided, + message: result.error.message, + }); + case captchaErrorCodes.requestFailed: + throw new ApiError({ + ...meta.errors.requestFailed, + message: result.error.message, + }); + case captchaErrorCodes.verificationFailed: + throw new ApiError({ + ...meta.errors.verificationFailed, + message: result.error.message, + }); + default: + throw new ApiError(meta.errors.unknown); + } + } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index b45a3c7156..1c5316a002 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -9,6 +9,7 @@ import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import { FILE_TYPE_IMAGE } from '@/const.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -24,6 +25,11 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', }, + unsupportedFileType: { + message: 'Unsupported file type.', + code: 'UNSUPPORTED_FILE_TYPE', + id: 'f7599d96-8750-af68-1633-9575d625c1a7', + }, duplicateName: { message: 'Duplicate name.', code: 'DUPLICATE_NAME', @@ -47,15 +53,21 @@ export const paramDef = { nullable: true, description: 'Use `null` to reset the category.', }, - aliases: { type: 'array', items: { - type: 'string', - } }, + aliases: { + type: 'array', + items: { + type: 'string', + }, + }, license: { type: 'string', nullable: true }, isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, - roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { - type: 'string', - } }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { + type: 'array', + items: { + type: 'string', + }, + }, }, required: ['name', 'fileId'], } as const; @@ -67,9 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private customEmojiService: CustomEmojiService, - private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { @@ -78,11 +88,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc); if (isDuplicate) throw new ApiError(meta.errors.duplicateName); + if (!FILE_TYPE_IMAGE.includes(driveFile.type)) throw new ApiError(meta.errors.unsupportedFileType); if (driveFile.user !== null) await this.driveFilesRepository.update(driveFile.id, { user: null }); const emoji = await this.customEmojiService.add({ - driveFile, + originalUrl: driveFile.url, + publicUrl: driveFile.webpublicUrl ?? driveFile.url, + fileType: driveFile.webpublicType ?? driveFile.type, name: nameNfc, category: ps.category?.normalize('NFC') ?? null, aliases: ps.aliases?.map(a => a.normalize('NFC')) ?? [], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index acd2494131..07ffa0b1c7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -4,7 +4,6 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { EmojisRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; @@ -88,10 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (isDuplicate) throw new ApiError(meta.errors.duplicateName); const addedEmoji = await this.customEmojiService.add({ - driveFile, + originalUrl: driveFile.url, + publicUrl: driveFile.webpublicUrl ?? driveFile.url, + fileType: driveFile.webpublicType ?? driveFile.type, name: nameNfc, category: emoji.category?.normalize('NFC') ?? null, - aliases: emoji.aliases?.map(a => a.normalize('NFC')), + aliases: emoji.aliases.map(a => a.normalize('NFC')), host: null, license: emoji.license, isSensitive: emoji.isSensitive, 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 071ddbef18..fd6db9c4ab 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -86,7 +86,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const error = await this.customEmojiService.update({ ...required, - driveFile, + originalUrl: driveFile != null ? driveFile.url : undefined, + publicUrl: driveFile != null ? (driveFile.webpublicUrl ?? driveFile.url) : undefined, + fileType: driveFile != null ? (driveFile.webpublicType ?? driveFile.type) : undefined, category: ps.category?.normalize('NFC'), aliases: ps.aliases?.map(a => a.normalize('NFC')), license: ps.license, diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 616a77e337..19ca3ceb8e 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -19,6 +19,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '../../error.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; export const meta = { tags: ['federation'], @@ -32,6 +33,31 @@ export const meta = { }, errors: { + federationNotAllowed: { + message: 'Federation for this host is not allowed.', + code: 'FEDERATION_NOT_ALLOWED', + id: '974b799e-1a29-4889-b706-18d4dd93e266', + }, + uriInvalid: { + message: 'URI is invalid.', + code: 'URI_INVALID', + id: '1a5eab56-e47b-48c2-8d5e-217b897d70db', + }, + requestFailed: { + message: 'Request failed.', + code: 'REQUEST_FAILED', + id: '81b539cf-4f57-4b29-bc98-032c33c0792e', + }, + responseInvalid: { + message: 'Response from remote server is invalid.', + code: 'RESPONSE_INVALID', + id: '70193c39-54f3-4813-82f0-70a680f7495b', + }, + responseInvalidIdHostNotMatch: { + message: 'Requested URI and response URI host does not match.', + code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH', + id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a', + }, noSuchObject: { message: 'No such object.', code: 'NO_SUCH_OBJECT', @@ -110,7 +136,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- */ @bindThis private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> { - if (!this.utilityService.isFederationAllowedUri(uri)) return null; + if (!this.utilityService.isFederationAllowedUri(uri)) { + throw new ApiError(meta.errors.federationNotAllowed); + } let local = await this.mergePack(me, ...await Promise.all([ this.apDbResolverService.getUserFromApId(uri), @@ -125,7 +153,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // リモートã‹ã‚‰ä¸€æ—¦ã‚ªãƒ–ジェクトフェッムconst resolver = this.apResolverService.createResolver(); - const object = await resolver.resolve(uri) as any; + const object = await resolver.resolve(uri).catch((err) => { + if (err instanceof IdentifiableError) { + switch (err.id) { + // resolve + case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2': + throw new ApiError(meta.errors.uriInvalid); + case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5': + case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2': + throw new ApiError(meta.errors.requestFailed); + case '09d79f9e-64f1-4316-9cfa-e75c4d091574': + throw new ApiError(meta.errors.federationNotAllowed); + case '72180409-793c-4973-868e-5a118eb5519b': + case 'ad2dc287-75c1-44c4-839d-3d2e64576675': + throw new ApiError(meta.errors.responseInvalid); + case 'fd93c2fa-69a8-440f-880b-bf178e0ec877': + throw new ApiError(meta.errors.responseInvalidIdHostNotMatch); + + // resolveLocal + case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8': + throw new ApiError(meta.errors.uriInvalid); + case 'a9d946e5-d276-47f8-95fb-f04230289bb0': + case '06ae3170-1796-4d93-a697-2611ea6d83b6': + throw new ApiError(meta.errors.noSuchObject); + case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0': + throw new ApiError(meta.errors.responseInvalid); + } + } + + throw new ApiError(meta.errors.requestFailed); + }); + + if (object.id == null) { + throw new ApiError(meta.errors.responseInvalid); + } // /@user ã®ã‚ˆã†ãªæ£è¦id以外ã§å–å¾—ã§ãã‚‹URIãŒæŒ‡å®šã•れã¦ã„ãŸå ´åˆã€ã“ã“ã§åˆã‚ã¦æ£è¦URIãŒç¢ºå®šã™ã‚‹ // ã“れã¯DBã«å˜åœ¨ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ãŸã‚å†åº¦DB検索 diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 661fa257a6..f290ff6844 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- name: token.name ?? token.app?.name, createdAt: this.idService.parse(token.id).date.toISOString(), lastUsedAt: token.lastUsedAt?.toISOString(), - permission: token.permission, + permission: token.app ? token.app.permission : token.permission, }))); }); } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 09c06a108d..a80e5ed033 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -592,7 +592,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const html = await this.httpRequestService.getHtml(url); const { window } = new JSDOM(html); - const doc = window.document; + const doc: Document = window.document; const myLink = `${this.config.url}/@${user.username}`; diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index f11bbbcb1a..e52d9c32df 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -102,15 +102,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - await this.pagesRepository.findBy({ - id: Not(ps.pageId), - userId: me.id, - name: ps.name, - }).then(result => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); - } - }); + if (ps.name != null) { + await this.pagesRepository.findBy({ + id: Not(ps.pageId), + userId: me.id, + name: ps.name, + }).then(result => { + if (result.length > 0) { + throw new ApiError(meta.errors.nameAlreadyExists); + } + }); + } await this.pagesRepository.update(page.id, { updatedAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts new file mode 100644 index 0000000000..9426318e34 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import { CustomEmojiService, fetchEmojisHostTypes, fetchEmojisSortKeys } from '@/core/CustomEmojiService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', + kind: 'read:admin:emoji', + + res: { + type: 'object', + properties: { + emojis: { + type: 'array', + items: { + type: 'object', + ref: 'EmojiDetailedAdmin', + }, + }, + count: { type: 'integer' }, + allCount: { type: 'integer' }, + allPages: { type: 'integer' }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + query: { + type: 'object', + nullable: true, + properties: { + updatedAtFrom: { type: 'string' }, + updatedAtTo: { type: 'string' }, + name: { type: 'string' }, + host: { type: 'string' }, + uri: { type: 'string' }, + publicUrl: { type: 'string' }, + originalUrl: { type: 'string' }, + type: { type: 'string' }, + aliases: { type: 'string' }, + category: { type: 'string' }, + license: { type: 'string' }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + hostType: { + type: 'string', + enum: fetchEmojisHostTypes, + default: 'all', + }, + roleIds: { + type: 'array', + items: { type: 'string', format: 'misskey:id' }, + }, + }, + }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + page: { type: 'integer' }, + sortKeys: { + type: 'array', + default: ['-id'], + items: { + type: 'string', + enum: fetchEmojisSortKeys, + }, + }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private customEmojiService: CustomEmojiService, + private emojiEntityService: EmojiEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const q = ps.query; + const result = await this.customEmojiService.fetchEmojis( + { + query: { + updatedAtFrom: q?.updatedAtFrom, + updatedAtTo: q?.updatedAtTo, + name: q?.name, + host: q?.host, + uri: q?.uri, + publicUrl: q?.publicUrl, + type: q?.type, + aliases: q?.aliases, + category: q?.category, + license: q?.license, + isSensitive: q?.isSensitive, + localOnly: q?.localOnly, + hostType: q?.hostType, + roleIds: q?.roleIds, + }, + sinceId: ps.sinceId, + untilId: ps.untilId, + }, + { + limit: ps.limit, + page: ps.page, + sortKeys: ps.sortKeys, + }, + ); + + return { + emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis), + count: result.count, + allCount: result.allCount, + allPages: result.allPages, + }; + }); + } +} diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index eb854a7141..c80dda8d96 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -3,13 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { deepClone } from '@/misc/clone.js'; import type { Schema } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js'; export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res', includeSelfRef: boolean): any { // optional, nullable, refã¯ã‚¹ã‚ーマ定義ã«å«ã¾ã‚Œãªã„ã®ã§åˆ†é›¢ã—ã¦ãŠã // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { optional, nullable, ref, selfRef, ...res }: any = schema; + const { optional, nullable, ref, selfRef, ..._res }: any = schema; + const res = deepClone(_res); if (schema.type === 'object' && schema.properties) { if (type === 'res') { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index ae923ada1f..cd1df1605c 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -323,16 +323,19 @@ export class ClientServerService { done(); }); } else { + const configUrl = new URL(this.config.url); + const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, ''); + const port = (process.env.VITE_PORT ?? '5173'); fastify.register(fastifyProxy, { - upstream: 'http://localhost:' + port, + upstream: urlOriginWithoutPort + ':' + port, prefix: '/vite', rewritePrefix: '/vite', }); const embedPort = (process.env.EMBED_VITE_PORT ?? '5174'); fastify.register(fastifyProxy, { - upstream: 'http://localhost:' + embedPort, + upstream: urlOriginWithoutPort + ':' + embedPort, prefix: '/embed_vite', rewritePrefix: '/embed_vite', }); @@ -536,6 +539,7 @@ export class ClientServerService { host: host ?? IsNull(), isSuspended: false, enableRss: true, + requireSigninToViewContents: false, }); return user && await this.feedService.packFeed(user); @@ -840,6 +844,7 @@ export class ClientServerService { fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => { const announcement = await this.announcementsRepository.findOneBy({ id: request.params.announcementId, + userId: IsNull(), }); if (announcement) { diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml index 8c38f16919..8b270e58f7 100644 --- a/packages/backend/test-federation/compose.tpl.yml +++ b/packages/backend/test-federation/compose.tpl.yml @@ -17,6 +17,7 @@ services: - ./.config/docker.env environment: - NODE_ENV=production + - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../../../built diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml index 62d7e977c0..a5a7223982 100644 --- a/packages/backend/test-federation/compose.yml +++ b/packages/backend/test-federation/compose.yml @@ -25,6 +25,7 @@ services: environment: - NODE_ENV=development - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt + - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../package.json @@ -85,6 +86,8 @@ services: depends_on: redis.test: condition: service_healthy + environment: + - COREPACK_DEFAULT_TO_LATEST=0 volumes: - type: bind source: ../package.json diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts index bacc4cc54f..220c22e198 100644 --- a/packages/backend/test-federation/test/note.test.ts +++ b/packages/backend/test-federation/test/note.test.ts @@ -131,11 +131,7 @@ describe('Note', () => { rejects( async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }), (err: any) => { - /** - * FIXME: this error is not handled - * @see https://github.com/misskey-dev/misskey/issues/12736 - */ - strictEqual(err.code, 'INTERNAL_ERROR'); + strictEqual(err.code, 'REQUEST_FAILED'); return true; }, ); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index d12be2a9ac..319c8581f4 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -397,7 +397,7 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); - }, 1000 * 10); + }, 1000 * 15); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒãƒ£ãƒ³ãƒãƒ«æŠ•稿ãŒå«ã¾ã‚Œãªã„', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts index 235af29f0d..1326003c5e 100644 --- a/packages/backend/test/unit/AbuseReportNotificationService.ts +++ b/packages/backend/test/unit/AbuseReportNotificationService.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { jest } from '@jest/globals'; +import { describe, jest } from '@jest/globals'; import { Test, TestingModule } from '@nestjs/testing'; import { randomString } from '../utils.js'; import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient, + MiAbuseUserReport, MiSystemWebhook, MiUser, SystemWebhooksRepository, @@ -112,7 +113,10 @@ describe('AbuseReportNotificationService', () => { provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }), }, { - provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }), + provide: UserEntityService, useFactory: () => ({ + pack: (v: any) => Promise.resolve(v), + packMany: (v: any) => Promise.resolve(v), + }), }, { provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), @@ -344,4 +348,46 @@ describe('AbuseReportNotificationService', () => { expect(recipients).toEqual([recipient3]); }); }); + + describe('notifySystemWebhook', () => { + test('éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªé€šå ±é€šçŸ¥ã¯Webhooké€ä¿¡ã‹ã‚‰é™¤å¤–ã•れる', async () => { + const recipient1 = await createRecipient({ + method: 'webhook', + systemWebhookId: systemWebhook1.id, + isActive: true, + }); + const recipient2 = await createRecipient({ + method: 'webhook', + systemWebhookId: systemWebhook2.id, + isActive: false, + }); + + const reports: MiAbuseUserReport[] = [ + { + id: idService.gen(), + targetUserId: alice.id, + targetUser: alice, + reporterId: bob.id, + reporter: bob, + assigneeId: null, + assignee: null, + resolved: false, + forwarded: false, + comment: 'test', + moderationNote: '', + resolvedAs: null, + targetUserHost: null, + reporterHost: null, + }, + ]; + + await service.notifySystemWebhook(reports, 'abuseReport'); + + // 実際ã«é™¤å¤–ã•れるã‹ã¯SystemWebhookServiceå´ã§ç¢ºèªã™ã‚‹. + // ã“ã“ã§ã¯éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªé€šå ±é€šçŸ¥ã‚’除外è¨å®šã§ãã¦ã„ã‚‹ã‹ã‚’確èªã™ã‚‹ + expect(webhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1); + expect(webhookService.enqueueSystemWebhook.mock.calls[0][0]).toBe('abuseReport'); + expect(webhookService.enqueueSystemWebhook.mock.calls[0][2]).toEqual({ excludes: [systemWebhook2.id] }); + }); + }); }); diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts new file mode 100644 index 0000000000..51b70b05a1 --- /dev/null +++ b/packages/backend/test/unit/CaptchaService.ts @@ -0,0 +1,622 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { afterAll, beforeAll, beforeEach, describe, expect, jest } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'node-fetch'; +import { + CaptchaError, + CaptchaErrorCode, + captchaErrorCodes, + CaptchaSaveResult, + CaptchaService, +} from '@/core/CaptchaService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { MiMeta } from '@/models/Meta.js'; +import { LoggerService } from '@/core/LoggerService.js'; + +describe('CaptchaService', () => { + let app: TestingModule; + let service: CaptchaService; + let httpRequestService: jest.Mocked<HttpRequestService>; + let metaService: jest.Mocked<MetaService>; + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + CaptchaService, + LoggerService, + { + provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }), + }, + { + provide: MetaService, useFactory: () => ({ + fetch: jest.fn(), + update: jest.fn(), + }), + }, + ], + }).compile(); + + app.enableShutdownHooks(); + + service = app.get(CaptchaService); + httpRequestService = app.get(HttpRequestService) as jest.Mocked<HttpRequestService>; + metaService = app.get(MetaService) as jest.Mocked<MetaService>; + }); + + beforeEach(() => { + httpRequestService.send.mockClear(); + metaService.update.mockClear(); + metaService.fetch.mockClear(); + }); + + afterAll(async () => { + await app.close(); + }); + + function successMock(result: object) { + httpRequestService.send.mockResolvedValue({ + ok: true, + status: 200, + json: async () => (result), + } as Response); + } + + function failureHttpMock() { + httpRequestService.send.mockResolvedValue({ + ok: false, + status: 400, + } as Response); + } + + function failureVerificationMock(result: object) { + httpRequestService.send.mockResolvedValue({ + ok: true, + status: 200, + json: async () => (result), + } as Response); + } + + async function testCaptchaError(code: CaptchaErrorCode, test: () => Promise<void>) { + try { + await test(); + expect(false).toBe(true); + } catch (e) { + expect(e instanceof CaptchaError).toBe(true); + + const _e = e as CaptchaError; + expect(_e.code).toBe(code); + } + } + + describe('verifyRecaptcha', () => { + test('success', async () => { + successMock({ success: true }); + await service.verifyRecaptcha('secret', 'response'); + }); + + test('noResponseProvided', async () => { + await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyRecaptcha('secret', null)); + }); + + test('requestFailed', async () => { + failureHttpMock(); + await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyRecaptcha('secret', 'response')); + }); + + test('verificationFailed', async () => { + failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] }); + await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyRecaptcha('secret', 'response')); + }); + }); + + describe('verifyHcaptcha', () => { + test('success', async () => { + successMock({ success: true }); + await service.verifyHcaptcha('secret', 'response'); + }); + + test('noResponseProvided', async () => { + await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyHcaptcha('secret', null)); + }); + + test('requestFailed', async () => { + failureHttpMock(); + await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyHcaptcha('secret', 'response')); + }); + + test('verificationFailed', async () => { + failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] }); + await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyHcaptcha('secret', 'response')); + }); + }); + + describe('verifyMcaptcha', () => { + const host = 'https://localhost'; + + test('success', async () => { + successMock({ valid: true }); + await service.verifyMcaptcha('secret', 'sitekey', host, 'response'); + }); + + test('noResponseProvided', async () => { + await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyMcaptcha('secret', 'sitekey', host, null)); + }); + + test('requestFailed', async () => { + failureHttpMock(); + await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response')); + }); + + test('verificationFailed', async () => { + failureVerificationMock({ valid: false }); + await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response')); + }); + }); + + describe('verifyTurnstile', () => { + test('success', async () => { + successMock({ success: true }); + await service.verifyTurnstile('secret', 'response'); + }); + + test('noResponseProvided', async () => { + await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTurnstile('secret', null)); + }); + + test('requestFailed', async () => { + failureHttpMock(); + await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyTurnstile('secret', 'response')); + }); + + test('verificationFailed', async () => { + failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] }); + await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTurnstile('secret', 'response')); + }); + }); + + describe('verifyTestcaptcha', () => { + test('success', async () => { + await service.verifyTestcaptcha('testcaptcha-passed'); + }); + + test('noResponseProvided', async () => { + await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTestcaptcha(null)); + }); + + test('verificationFailed', async () => { + await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTestcaptcha('testcaptcha-failed')); + }); + }); + + describe('get', () => { + function setupMeta(meta: Partial<MiMeta>) { + metaService.fetch.mockResolvedValue(meta as MiMeta); + } + + test('values', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + hcaptchaSiteKey: 'hcaptcha-sitekey', + hcaptchaSecretKey: 'hcaptcha-secret', + mcaptchaSitekey: 'mcaptcha-sitekey', + mcaptchaSecretKey: 'mcaptcha-secret', + mcaptchaInstanceUrl: 'https://localhost', + recaptchaSiteKey: 'recaptcha-sitekey', + recaptchaSecretKey: 'recaptcha-secret', + turnstileSiteKey: 'turnstile-sitekey', + turnstileSecretKey: 'turnstile-secret', + }); + + const result = await service.get(); + expect(result.provider).toBe('none'); + expect(result.hcaptcha.siteKey).toBe('hcaptcha-sitekey'); + expect(result.hcaptcha.secretKey).toBe('hcaptcha-secret'); + expect(result.mcaptcha.siteKey).toBe('mcaptcha-sitekey'); + expect(result.mcaptcha.secretKey).toBe('mcaptcha-secret'); + expect(result.mcaptcha.instanceUrl).toBe('https://localhost'); + expect(result.recaptcha.siteKey).toBe('recaptcha-sitekey'); + expect(result.recaptcha.secretKey).toBe('recaptcha-secret'); + expect(result.turnstile.siteKey).toBe('turnstile-sitekey'); + expect(result.turnstile.secretKey).toBe('turnstile-secret'); + }); + + describe('provider', () => { + test('none', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('none'); + }); + + test('hcaptcha', async () => { + setupMeta({ + enableHcaptcha: true, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('hcaptcha'); + }); + + test('mcaptcha', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: true, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('mcaptcha'); + }); + + test('recaptcha', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: true, + enableTurnstile: false, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('recaptcha'); + }); + + test('turnstile', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: true, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('turnstile'); + }); + + test('testcaptcha', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: true, + }); + + const result = await service.get(); + expect(result.provider).toBe('testcaptcha'); + }); + }); + }); + + describe('save', () => { + const host = 'https://localhost'; + + describe('[success] æ¤œè¨¼ã«æˆåŠŸã—ãŸæ™‚ã ã‘ä¿å˜ã§ãる+他ã®ãƒ—ãƒãƒã‚¤ãƒ€ã®è¨å®šå€¤ã‚’誤ã£ã¦æ›´æ–°ã—ãªã„', () => { + beforeEach(() => { + successMock({ success: true, valid: true }); + }); + + async function assertSuccess(promise: Promise<CaptchaSaveResult>, expectMeta: Partial<MiMeta>) { + await expect(promise) + .resolves + .toStrictEqual({ success: true }); + const partialParams = metaService.update.mock.calls[0][0]; + expect(partialParams).toStrictEqual(expectMeta); + } + + test('none', async () => { + await assertSuccess( + service.save('none'), + { + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + }, + ); + }); + + test('hcaptcha', async () => { + await assertSuccess( + service.save('hcaptcha', { + sitekey: 'hcaptcha-sitekey', + secret: 'hcaptcha-secret', + captchaResult: 'hcaptcha-passed', + }), + { + enableHcaptcha: true, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + hcaptchaSiteKey: 'hcaptcha-sitekey', + hcaptchaSecretKey: 'hcaptcha-secret', + }, + ); + }); + + test('mcaptcha', async () => { + await assertSuccess( + service.save('mcaptcha', { + sitekey: 'mcaptcha-sitekey', + secret: 'mcaptcha-secret', + instanceUrl: host, + captchaResult: 'mcaptcha-passed', + }), + { + enableHcaptcha: false, + enableMcaptcha: true, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + mcaptchaSitekey: 'mcaptcha-sitekey', + mcaptchaSecretKey: 'mcaptcha-secret', + mcaptchaInstanceUrl: host, + }, + ); + }); + + test('recaptcha', async () => { + await assertSuccess( + service.save('recaptcha', { + sitekey: 'recaptcha-sitekey', + secret: 'recaptcha-secret', + captchaResult: 'recaptcha-passed', + }), + { + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: true, + enableTurnstile: false, + enableTestcaptcha: false, + recaptchaSiteKey: 'recaptcha-sitekey', + recaptchaSecretKey: 'recaptcha-secret', + }, + ); + }); + + test('turnstile', async () => { + await assertSuccess( + service.save('turnstile', { + sitekey: 'turnstile-sitekey', + secret: 'turnstile-secret', + captchaResult: 'turnstile-passed', + }), + { + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: true, + enableTestcaptcha: false, + turnstileSiteKey: 'turnstile-sitekey', + turnstileSecretKey: 'turnstile-secret', + }, + ); + }); + + test('testcaptcha', async () => { + await assertSuccess( + service.save('testcaptcha', { + sitekey: 'testcaptcha-sitekey', + secret: 'testcaptcha-secret', + captchaResult: 'testcaptcha-passed', + }), + { + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: true, + }, + ); + }); + }); + + describe('[failure] 検証ã«å¤±æ•—ã—ãŸå ´åˆã¯ä¿å˜ã§ããªã„+è¨å®šå€¤ã®æ›´æ–°ãã®ã‚‚ã®ãŒç™ºç”Ÿã—ãªã„', () => { + async function assertFailure(code: CaptchaErrorCode, promise: Promise<CaptchaSaveResult>) { + const res = await promise; + expect(res.success).toBe(false); + if (!res.success) { + expect(res.error.code).toBe(code); + } + expect(metaService.update).not.toBeCalled(); + } + + describe('invalidParameters', () => { + test('hcaptcha', async () => { + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('hcaptcha', { + sitekey: 'hcaptcha-sitekey', + secret: 'hcaptcha-secret', + captchaResult: null, + }), + ); + }); + + test('mcaptcha', async () => { + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('mcaptcha', { + sitekey: 'mcaptcha-sitekey', + secret: 'mcaptcha-secret', + instanceUrl: host, + captchaResult: null, + }), + ); + }); + + test('recaptcha', async () => { + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('recaptcha', { + sitekey: 'recaptcha-sitekey', + secret: 'recaptcha-secret', + captchaResult: null, + }), + ); + }); + + test('turnstile', async () => { + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('turnstile', { + sitekey: 'turnstile-sitekey', + secret: 'turnstile-secret', + captchaResult: null, + }), + ); + }); + + test('testcaptcha', async () => { + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('testcaptcha', { + captchaResult: null, + }), + ); + }); + }); + + describe('requestFailed', () => { + beforeEach(() => { + failureHttpMock(); + }); + + test('hcaptcha', async () => { + await assertFailure( + captchaErrorCodes.requestFailed, + service.save('hcaptcha', { + sitekey: 'hcaptcha-sitekey', + secret: 'hcaptcha-secret', + captchaResult: 'hcaptcha-passed', + }), + ); + }); + + test('mcaptcha', async () => { + await assertFailure( + captchaErrorCodes.requestFailed, + service.save('mcaptcha', { + sitekey: 'mcaptcha-sitekey', + secret: 'mcaptcha-secret', + instanceUrl: host, + captchaResult: 'mcaptcha-passed', + }), + ); + }); + + test('recaptcha', async () => { + await assertFailure( + captchaErrorCodes.requestFailed, + service.save('recaptcha', { + sitekey: 'recaptcha-sitekey', + secret: 'recaptcha-secret', + captchaResult: 'recaptcha-passed', + }), + ); + }); + + test('turnstile', async () => { + await assertFailure( + captchaErrorCodes.requestFailed, + service.save('turnstile', { + sitekey: 'turnstile-sitekey', + secret: 'turnstile-secret', + captchaResult: 'turnstile-passed', + }), + ); + }); + + // testchapchaã¯requestFailedãŒãªã„ + }); + + describe('verificationFailed', () => { + beforeEach(() => { + failureVerificationMock({ success: false, valid: false, 'error-codes': ['code01', 'code02'] }); + }); + + test('hcaptcha', async () => { + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('hcaptcha', { + sitekey: 'hcaptcha-sitekey', + secret: 'hcaptcha-secret', + captchaResult: 'hccaptcha-passed', + }), + ); + }); + + test('mcaptcha', async () => { + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('mcaptcha', { + sitekey: 'mcaptcha-sitekey', + secret: 'mcaptcha-secret', + instanceUrl: host, + captchaResult: 'mcaptcha-passed', + }), + ); + }); + + test('recaptcha', async () => { + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('recaptcha', { + sitekey: 'recaptcha-sitekey', + secret: 'recaptcha-secret', + captchaResult: 'recaptcha-passed', + }), + ); + }); + + test('turnstile', async () => { + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('turnstile', { + sitekey: 'turnstile-sitekey', + secret: 'turnstile-secret', + captchaResult: 'turnstile-passed', + }), + ); + }); + + test('testcaptcha', async () => { + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('testcaptcha', { + captchaResult: 'testcaptcha-failed', + }), + ); + }); + }); + }); + }); +}); diff --git a/packages/backend/test/unit/CustomEmojiService.ts b/packages/backend/test/unit/CustomEmojiService.ts new file mode 100644 index 0000000000..10b687c6a0 --- /dev/null +++ b/packages/backend/test/unit/CustomEmojiService.ts @@ -0,0 +1,817 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { afterEach, beforeAll, describe, test } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { IdService } from '@/core/IdService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { EmojisRepository } from '@/models/_.js'; +import { MiEmoji } from '@/models/Emoji.js'; + +describe('CustomEmojiService', () => { + let app: TestingModule; + let service: CustomEmojiService; + + let emojisRepository: EmojisRepository; + let idService: IdService; + + beforeAll(async () => { + app = await Test + .createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + CustomEmojiService, + UtilityService, + IdService, + EmojiEntityService, + ModerationLogService, + GlobalEventService, + ], + }) + .compile(); + app.enableShutdownHooks(); + + service = app.get<CustomEmojiService>(CustomEmojiService); + emojisRepository = app.get<EmojisRepository>(DI.emojisRepository); + idService = app.get<IdService>(IdService); + }); + + describe('fetchEmojis', () => { + async function insert(data: Partial<MiEmoji>[]) { + for (const d of data) { + const id = idService.gen(); + await emojisRepository.insert({ + id: id, + updatedAt: new Date(), + ...d, + }); + } + } + + function call(params: Parameters<CustomEmojiService['fetchEmojis']>['0']) { + return service.fetchEmojis( + params, + { + // テストå‘ã‘ã« + sortKeys: ['+id'], + }, + ); + } + + function defaultData(suffix: string, override?: Partial<MiEmoji>): Partial<MiEmoji> { + return { + name: `emoji${suffix}`, + host: null, + category: 'default', + originalUrl: `https://example.com/emoji${suffix}.png`, + publicUrl: `https://example.com/emoji${suffix}.png`, + type: 'image/png', + aliases: [`emoji${suffix}`], + license: 'CC0', + isSensitive: false, + localOnly: false, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], + ...override, + }; + } + + afterEach(async () => { + await emojisRepository.delete({}); + }); + + describe('å˜ç‹¬', () => { + test('updatedAtFrom', async () => { + await insert([ + defaultData('001', { updatedAt: new Date('2021-01-01T00:00:00.000Z') }), + defaultData('002', { updatedAt: new Date('2021-01-02T00:00:00.000Z') }), + defaultData('003', { updatedAt: new Date('2021-01-03T00:00:00.000Z') }), + ]); + + const actual = await call({ + query: { + updatedAtFrom: '2021-01-02T00:00:00.000Z', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji002'); + expect(actual.emojis[1].name).toBe('emoji003'); + }); + + test('updatedAtTo', async () => { + await insert([ + defaultData('001', { updatedAt: new Date('2021-01-01T00:00:00.000Z') }), + defaultData('002', { updatedAt: new Date('2021-01-02T00:00:00.000Z') }), + defaultData('003', { updatedAt: new Date('2021-01-03T00:00:00.000Z') }), + ]); + + const actual = await call({ + query: { + updatedAtTo: '2021-01-02T00:00:00.000Z', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji002'); + }); + + describe('name', () => { + test('single', async () => { + await insert([ + defaultData('001'), + defaultData('002'), + ]); + + const actual = await call({ + query: { + name: 'emoji001', + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji001'); + }); + + test('multi', async () => { + await insert([ + defaultData('001'), + defaultData('002'), + ]); + + const actual = await call({ + query: { + name: 'emoji001 emoji002', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji002'); + }); + + test('keyword', async () => { + await insert([ + defaultData('001'), + defaultData('002'), + defaultData('003', { name: 'em003' }), + ]); + + const actual = await call({ + query: { + name: 'oji', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji002'); + }); + + test('escape', async () => { + await insert([ + defaultData('001'), + ]); + + const actual = await call({ + query: { + name: '%', + }, + }); + + expect(actual.allCount).toBe(0); + }); + }); + + describe('host', () => { + test('single', async () => { + await insert([ + defaultData('001', { host: 'example.com' }), + defaultData('002', { host: 'example.com' }), + defaultData('003', { host: '1.example.com' }), + defaultData('004', { host: '2.example.com' }), + ]); + + const actual = await call({ + query: { + host: 'example.com', + hostType: 'remote', + }, + }); + + expect(actual.allCount).toBe(4); + }); + + test('multi', async () => { + await insert([ + defaultData('001', { host: 'example.com' }), + defaultData('002', { host: 'example.com' }), + defaultData('003', { host: '1.example.com' }), + defaultData('004', { host: '2.example.com' }), + ]); + + const actual = await call({ + query: { + host: '1.example.com 2.example.com', + hostType: 'remote', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji003'); + expect(actual.emojis[1].name).toBe('emoji004'); + }); + + test('keyword', async () => { + await insert([ + defaultData('001', { host: 'example.com' }), + defaultData('002', { host: 'example.com' }), + defaultData('003', { host: '1.example.com' }), + defaultData('004', { host: '2.example.com' }), + ]); + + const actual = await call({ + query: { + host: 'example', + hostType: 'remote', + }, + }); + + expect(actual.allCount).toBe(4); + }); + + test('escape', async () => { + await insert([ + defaultData('001', { host: 'example.com' }), + ]); + + const actual = await call({ + query: { + host: '%', + hostType: 'remote', + }, + }); + + expect(actual.allCount).toBe(0); + }); + }); + + describe('uri', () => { + test('single', async () => { + await insert([ + defaultData('001', { uri: 'uri001' }), + defaultData('002', { uri: 'uri002' }), + defaultData('003', { uri: 'uri003' }), + ]); + + const actual = await call({ + query: { + uri: 'uri002', + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji002'); + }); + + test('multi', async () => { + await insert([ + defaultData('001', { uri: 'uri001' }), + defaultData('002', { uri: 'uri002' }), + defaultData('003', { uri: 'uri003' }), + ]); + + const actual = await call({ + query: { + uri: 'uri001 uri003', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji003'); + }); + + test('keyword', async () => { + await insert([ + defaultData('001', { uri: 'uri001' }), + defaultData('002', { uri: 'uri002' }), + defaultData('003', { uri: 'uri003' }), + ]); + + const actual = await call({ + query: { + uri: 'ri', + }, + }); + + expect(actual.allCount).toBe(3); + }); + + test('escape', async () => { + await insert([ + defaultData('001', { uri: 'uri001' }), + ]); + + const actual = await call({ + query: { + uri: '%', + }, + }); + + expect(actual.allCount).toBe(0); + }); + }); + + describe('publicUrl', () => { + test('single', async () => { + await insert([ + defaultData('001', { publicUrl: 'publicUrl001' }), + defaultData('002', { publicUrl: 'publicUrl002' }), + defaultData('003', { publicUrl: 'publicUrl003' }), + ]); + + const actual = await call({ + query: { + publicUrl: 'publicUrl002', + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji002'); + }); + + test('multi', async () => { + await insert([ + defaultData('001', { publicUrl: 'publicUrl001' }), + defaultData('002', { publicUrl: 'publicUrl002' }), + defaultData('003', { publicUrl: 'publicUrl003' }), + ]); + + const actual = await call({ + query: { + publicUrl: 'publicUrl001 publicUrl003', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji003'); + }); + + test('keyword', async () => { + await insert([ + defaultData('001', { publicUrl: 'publicUrl001' }), + defaultData('002', { publicUrl: 'publicUrl002' }), + defaultData('003', { publicUrl: 'publicUrl003' }), + ]); + + const actual = await call({ + query: { + publicUrl: 'Url', + }, + }); + + expect(actual.allCount).toBe(3); + }); + + test('escape', async () => { + await insert([ + defaultData('001', { publicUrl: 'publicUrl001' }), + ]); + + const actual = await call({ + query: { + publicUrl: '%', + }, + }); + + expect(actual.allCount).toBe(0); + }); + }); + + describe('type', () => { + test('single', async () => { + await insert([ + defaultData('001', { type: 'type001' }), + defaultData('002', { type: 'type002' }), + defaultData('003', { type: 'type003' }), + ]); + + const actual = await call({ + query: { + type: 'type002', + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji002'); + }); + + test('multi', async () => { + await insert([ + defaultData('001', { type: 'type001' }), + defaultData('002', { type: 'type002' }), + defaultData('003', { type: 'type003' }), + ]); + + const actual = await call({ + query: { + type: 'type001 type003', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji003'); + }); + + test('keyword', async () => { + await insert([ + defaultData('001', { type: 'type001' }), + defaultData('002', { type: 'type002' }), + defaultData('003', { type: 'type003' }), + ]); + + const actual = await call({ + query: { + type: 'pe', + }, + }); + + expect(actual.allCount).toBe(3); + }); + + test('escape', async () => { + await insert([ + defaultData('001', { type: 'type001' }), + ]); + + const actual = await call({ + query: { + type: '%', + }, + }); + + expect(actual.allCount).toBe(0); + }); + }); + + describe('aliases', () => { + test('single', async () => { + await insert([ + defaultData('001', { aliases: ['alias001', 'alias002'] }), + defaultData('002', { aliases: ['alias002'] }), + defaultData('003', { aliases: ['alias003'] }), + ]); + + const actual = await call({ + query: { + aliases: 'alias002', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji002'); + }); + + test('multi', async () => { + await insert([ + defaultData('001', { aliases: ['alias001', 'alias002'] }), + defaultData('002', { aliases: ['alias002', 'alias004'] }), + defaultData('003', { aliases: ['alias003'] }), + defaultData('004', { aliases: ['alias004'] }), + ]); + + const actual = await call({ + query: { + aliases: 'alias001 alias004', + }, + }); + + expect(actual.allCount).toBe(3); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji002'); + expect(actual.emojis[2].name).toBe('emoji004'); + }); + + test('keyword', async () => { + await insert([ + defaultData('001', { aliases: ['alias001', 'alias002'] }), + defaultData('002', { aliases: ['alias002', 'alias004'] }), + defaultData('003', { aliases: ['alias003'] }), + defaultData('004', { aliases: ['alias004'] }), + ]); + + const actual = await call({ + query: { + aliases: 'ias', + }, + }); + + expect(actual.allCount).toBe(4); + }); + + test('escape', async () => { + await insert([ + defaultData('001', { aliases: ['alias001', 'alias002'] }), + ]); + + const actual = await call({ + query: { + aliases: '%', + }, + }); + + expect(actual.allCount).toBe(0); + }); + }); + + describe('category', () => { + test('single', async () => { + await insert([ + defaultData('001', { category: 'category001' }), + defaultData('002', { category: 'category002' }), + defaultData('003', { category: 'category003' }), + ]); + + const actual = await call({ + query: { + category: 'category002', + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji002'); + }); + + test('multi', async () => { + await insert([ + defaultData('001', { category: 'category001' }), + defaultData('002', { category: 'category002' }), + defaultData('003', { category: 'category003' }), + ]); + + const actual = await call({ + query: { + category: 'category001 category003', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji003'); + }); + + test('keyword', async () => { + await insert([ + defaultData('001', { category: 'category001' }), + defaultData('002', { category: 'category002' }), + defaultData('003', { category: 'category003' }), + ]); + + const actual = await call({ + query: { + category: 'egory', + }, + }); + + expect(actual.allCount).toBe(3); + }); + + test('escape', async () => { + await insert([ + defaultData('001', { category: 'category001' }), + ]); + + const actual = await call({ + query: { + category: '%', + }, + }); + + expect(actual.allCount).toBe(0); + }); + }); + + describe('license', () => { + test('single', async () => { + await insert([ + defaultData('001', { license: 'license001' }), + defaultData('002', { license: 'license002' }), + defaultData('003', { license: 'license003' }), + ]); + + const actual = await call({ + query: { + license: 'license002', + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji002'); + }); + + test('multi', async () => { + await insert([ + defaultData('001', { license: 'license001' }), + defaultData('002', { license: 'license002' }), + defaultData('003', { license: 'license003' }), + ]); + + const actual = await call({ + query: { + license: 'license001 license003', + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji003'); + }); + + test('keyword', async () => { + await insert([ + defaultData('001', { license: 'license001' }), + defaultData('002', { license: 'license002' }), + defaultData('003', { license: 'license003' }), + ]); + + const actual = await call({ + query: { + license: 'cense', + }, + }); + + expect(actual.allCount).toBe(3); + }); + + test('escape', async () => { + await insert([ + defaultData('001', { license: 'license001' }), + ]); + + const actual = await call({ + query: { + license: '%', + }, + }); + + expect(actual.allCount).toBe(0); + }); + }); + + describe('isSensitive', () => { + test('true', async () => { + await insert([ + defaultData('001', { isSensitive: true }), + defaultData('002', { isSensitive: false }), + defaultData('003', { isSensitive: true }), + ]); + + const actual = await call({ + query: { + isSensitive: true, + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji003'); + }); + + test('false', async () => { + await insert([ + defaultData('001', { isSensitive: true }), + defaultData('002', { isSensitive: false }), + defaultData('003', { isSensitive: true }), + ]); + + const actual = await call({ + query: { + isSensitive: false, + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji002'); + }); + + test('null', async () => { + await insert([ + defaultData('001', { isSensitive: true }), + defaultData('002', { isSensitive: false }), + defaultData('003', { isSensitive: true }), + ]); + + const actual = await call({ + query: {}, + }); + + expect(actual.allCount).toBe(3); + }); + }); + + describe('localOnly', () => { + test('true', async () => { + await insert([ + defaultData('001', { localOnly: true }), + defaultData('002', { localOnly: false }), + defaultData('003', { localOnly: true }), + ]); + + const actual = await call({ + query: { + localOnly: true, + }, + }); + + expect(actual.allCount).toBe(2); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji003'); + }); + + test('false', async () => { + await insert([ + defaultData('001', { localOnly: true }), + defaultData('002', { localOnly: false }), + defaultData('003', { localOnly: true }), + ]); + + const actual = await call({ + query: { + localOnly: false, + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji002'); + }); + + test('null', async () => { + await insert([ + defaultData('001', { localOnly: true }), + defaultData('002', { localOnly: false }), + defaultData('003', { localOnly: true }), + ]); + + const actual = await call({ + query: {}, + }); + + expect(actual.allCount).toBe(3); + }); + }); + + describe('roleId', () => { + test('single', async () => { + await insert([ + defaultData('001', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role001'] }), + defaultData('002', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role002'] }), + defaultData('003', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role003'] }), + ]); + + const actual = await call({ + query: { + roleIds: ['role002'], + }, + }); + + expect(actual.allCount).toBe(1); + expect(actual.emojis[0].name).toBe('emoji002'); + }); + + test('multi', async () => { + await insert([ + defaultData('001', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role001'] }), + defaultData('002', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role002', 'role003'] }), + defaultData('003', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role003'] }), + defaultData('004', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role004'] }), + ]); + + const actual = await call({ + query: { + roleIds: ['role001', 'role003'], + }, + }); + + expect(actual.allCount).toBe(3); + expect(actual.emojis[0].name).toBe('emoji001'); + expect(actual.emojis[1].name).toBe('emoji002'); + expect(actual.emojis[2].name).toBe('emoji003'); + }); + }); + }); + }); +}); diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 8d5683329f..73d9c7b60c 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -108,6 +108,24 @@ describe('MfmService', () => { assert.deepStrictEqual(mfmService.fromHtml('<p>a <a></a> d</p>'), 'a d'); }); + test('ruby', () => { + assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスã‚ー</rt><rp>)</rp></ruby> b</p>'), 'a $[ruby Misskey ミスã‚ー] b'); + assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスã‚ー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスã‚ー</rt><rp>)</rp></ruby> b</p>'), 'a $[ruby Misskey ミスã‚ー]$[ruby Misskey ミスã‚ー] b'); + }); + + test('ruby with spaces', () => { + assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Miss key<rp>(</rp><rt>ミスã‚ー</rt><rp>)</rp> b</ruby> c</p>'), 'a Miss key(ミスã‚ー) b c'); + assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミス ã‚ー</rt><rp>)</rp> b</ruby> c</p>'), 'a Misskey(ミス ã‚ー) b c'); + assert.deepStrictEqual( + mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスã‚ー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミス ã‚ー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスã‚ー</rt><rp>)</rp></ruby> b</p>'), + 'a Misskey(ミスã‚ー)Misskey(ミス ã‚ー)Misskey(ミスã‚ー) b' + ); + }); + + test('ruby with other inline tags', () => { + assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby><strong>Misskey</strong><rp>(</rp><rt>ミスã‚ー</rt><rp>)</rp> b</ruby> c</p>'), 'a **Misskey**(ミスã‚ー) b c'); + }); + test('mention', () => { assert.deepStrictEqual(mfmService.fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d'); }); diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts index 5401dd74d8..fee4acb305 100644 --- a/packages/backend/test/unit/SystemWebhookService.ts +++ b/packages/backend/test/unit/SystemWebhookService.ts @@ -314,9 +314,10 @@ describe('SystemWebhookService', () => { isActive: true, on: ['abuseReport'], }); - await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any); + await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any); - expect(queueService.systemWebhookDeliver).toHaveBeenCalled(); + expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1); + expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook); }); test('éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªWebhookã¯ã‚ューã«è¿½åŠ ã•れãªã„', async () => { @@ -324,7 +325,7 @@ describe('SystemWebhookService', () => { isActive: false, on: ['abuseReport'], }); - await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any); + await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any); expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); }); @@ -338,11 +339,49 @@ describe('SystemWebhookService', () => { isActive: true, on: ['abuseReportResolved'], }); - await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any); - await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any); + await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any); expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); }); + + test('混在ã—ãŸæ™‚ã€æœ‰åйã‹ã¤è¨±å¯ã•れãŸã‚¤ãƒ™ãƒ³ãƒˆç¨®åˆ¥ã®ã¿', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + const webhook2 = await createWebhook({ + isActive: true, + on: ['abuseReportResolved'], + }); + const webhook3 = await createWebhook({ + isActive: false, + on: ['abuseReport'], + }); + const webhook4 = await createWebhook({ + isActive: false, + on: ['abuseReportResolved'], + }); + await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any); + + expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1); + expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook1); + }); + + test('除外指定ã—ãŸå ´åˆã¯é€ä¿¡ã•れãªã„', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + const webhook2 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + + await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any, { excludes: [webhook2.id] }); + + expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1); + expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook1); + }); }); describe('fetchActiveSystemWebhooks', () => { diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts index 0e88835a02..db8f96df28 100644 --- a/packages/backend/test/unit/UserWebhookService.ts +++ b/packages/backend/test/unit/UserWebhookService.ts @@ -1,4 +1,3 @@ - /* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only @@ -71,7 +70,7 @@ describe('UserWebhookService', () => { LoggerService, GlobalEventService, { - provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }), + provide: QueueService, useFactory: () => ({ userWebhookDeliver: jest.fn() }), }, ], }) @@ -242,4 +241,92 @@ describe('UserWebhookService', () => { }); }); }); + + describe('アプリを毎回作り直ã™å¿…è¦ãŒã‚るグループ', () => { + beforeEach(async () => { + await beforeAllImpl(); + await beforeEachImpl(); + }); + + afterEach(async () => { + await afterEachImpl(); + await afterAllImpl(); + }); + + describe('enqueueUserWebhook', () => { + test('ã‚ューã«è¿½åŠ æˆåŠŸ', async () => { + const webhook = await createWebhook({ + active: true, + on: ['note'], + }); + await service.enqueueUserWebhook(webhook.userId, 'note', { foo: 'bar' } as any); + + expect(queueService.userWebhookDeliver).toHaveBeenCalledTimes(1); + expect(queueService.userWebhookDeliver.mock.calls[0][0] as MiWebhook).toEqual(webhook); + }); + + test('éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªWebhookã¯ã‚ューã«è¿½åŠ ã•れãªã„', async () => { + const webhook = await createWebhook({ + active: false, + on: ['note'], + }); + await service.enqueueUserWebhook(webhook.userId, 'note', { foo: 'bar' } as any); + + expect(queueService.userWebhookDeliver).not.toHaveBeenCalled(); + }); + + test('未許å¯ã®ã‚¤ãƒ™ãƒ³ãƒˆç¨®åˆ¥ãŒæ¸¡ã•れãŸå ´åˆã¯Webhookã¯ã‚ューã«è¿½åŠ ã•れãªã„', async () => { + const webhook1 = await createWebhook({ + active: true, + on: [], + }); + const webhook2 = await createWebhook({ + active: true, + on: ['note'], + }); + await service.enqueueUserWebhook(webhook1.userId, 'renote', { foo: 'bar' } as any); + await service.enqueueUserWebhook(webhook2.userId, 'renote', { foo: 'bar' } as any); + + expect(queueService.userWebhookDeliver).not.toHaveBeenCalled(); + }); + + test('ユーザIDãŒç•°ãªã‚‹Webhookã¯ã‚ューã«è¿½åŠ ã•れãªã„', async () => { + const webhook = await createWebhook({ + active: true, + on: ['note'], + }); + await service.enqueueUserWebhook(idService.gen(), 'note', { foo: 'bar' } as any); + + expect(queueService.userWebhookDeliver).not.toHaveBeenCalled(); + }); + + test('混在ã—ãŸæ™‚ã€æœ‰åйã‹ã¤è¨±å¯ã•れãŸã‚¤ãƒ™ãƒ³ãƒˆç¨®åˆ¥ã®ã¿', async () => { + const userId = root.id; + const webhook1 = await createWebhook({ + userId, + active: true, + on: ['note'], + }); + const webhook2 = await createWebhook({ + userId, + active: true, + on: ['renote'], + }); + const webhook3 = await createWebhook({ + userId, + active: false, + on: ['note'], + }); + const webhook4 = await createWebhook({ + userId, + active: false, + on: ['renote'], + }); + await service.enqueueUserWebhook(userId, 'note', { foo: 'bar' } as any); + + expect(queueService.userWebhookDeliver).toHaveBeenCalledTimes(1); + expect(queueService.userWebhookDeliver.mock.calls[0][0] as MiWebhook).toEqual(webhook1); + }); + }); + }); }); diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts index 1506283a3c..d96e6b916a 100644 --- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -18,6 +18,7 @@ import { QueueLoggerService } from '@/queue/QueueLoggerService.js'; import { EmailService } from '@/core/EmailService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { AnnouncementService } from '@/core/AnnouncementService.js'; +import { SystemWebhookEventType } from '@/models/SystemWebhook.js'; const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0)); @@ -334,9 +335,10 @@ describe('CheckModeratorsActivityProcessorService', () => { mockModeratorRole([user1]); await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 }); - expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2); - expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1); - expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2); + // typeã¨activeã«ã‚ˆã‚‹çµžã‚Šè¾¼ã¿ãŒæ©Ÿèƒ½ã—ã¦ã„ã‚‹ã‹ã¯SystemWebhookServiceã®ãƒ†ã‚¹ãƒˆã§ç¢ºèªã™ã‚‹. + // ã“ã“ã§ã¯å‘¼ã³å‡ºã•れã¦ã„ã‚‹ã‹ã€typeãŒæ£ã—ã„ã‹ã®ã¿ã‚’確èªã™ã‚‹ + expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1); + expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0] as SystemWebhookEventType).toEqual('inactiveModeratorsWarning'); }); }); @@ -372,8 +374,10 @@ describe('CheckModeratorsActivityProcessorService', () => { mockModeratorRole([user1]); await service.notifyChangeToInvitationOnly(); + // typeã¨activeã«ã‚ˆã‚‹çµžã‚Šè¾¼ã¿ãŒæ©Ÿèƒ½ã—ã¦ã„ã‚‹ã‹ã¯SystemWebhookServiceã®ãƒ†ã‚¹ãƒˆã§ç¢ºèªã™ã‚‹. + // ã“ã“ã§ã¯å‘¼ã³å‡ºã•れã¦ã„ã‚‹ã‹ã€typeãŒæ£ã—ã„ã‹ã®ã¿ã‚’確èªã™ã‚‹ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1); - expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2); + expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0] as SystemWebhookEventType).toEqual('inactiveModeratorsInvitationOnlyChanged'); }); }); }); diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 528faf0a85..163e6096f8 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -4,7 +4,6 @@ "type": "module", "scripts": { "watch": "vite", - "dev": "vite --config vite.config.local-dev.ts --debug hmr", "build": "vite build", "typecheck": "vue-tsc --noEmit", "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", @@ -25,7 +24,7 @@ "estree-walker": "3.0.3", "misskey-js": "workspace:*", "frontend-shared": "workspace:*", - "punycode": "2.3.1", + "punycode.js": "2.3.1", "rollup": "4.26.0", "sass": "1.79.4", "shiki": "1.22.2", @@ -44,7 +43,7 @@ "@types/estree": "1.0.6", "@types/micromatch": "4.0.9", "@types/node": "22.9.0", - "@types/punycode": "2.1.4", + "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", "@types/ws": "8.5.13", diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index 71a3156311..c5072caba4 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -15,11 +15,11 @@ import { applyTheme, assertIsTheme } from '@/theme.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { DI } from '@/di.js'; import { serverMetadata } from '@/server-metadata.js'; -import { url } from '@@/js/config.js'; +import { url, version, locale, lang, updateLocale } from '@@/js/config.js'; import { parseEmbedParams } from '@@/js/embed-page.js'; import { postMessageToParentWindow, setIframeId } from '@/post-message.js'; import { serverContext } from '@/server-context.js'; -import { i18n } from '@/i18n.js'; +import { i18n, updateI18n } from '@/i18n.js'; import type { Theme } from '@/theme.js'; @@ -69,6 +69,22 @@ if (embedParams.colorMode === 'dark') { } //#endregion +//#region Detect language & fetch translations +const localeVersion = localStorage.getItem('localeVersion'); +const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null); +if (localeOutdated) { + const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); + if (res.status === 200) { + const newLocale = await res.text(); + const parsedNewLocale = JSON.parse(newLocale); + localStorage.setItem('locale', newLocale); + localStorage.setItem('localeVersion', version); + updateLocale(parsedNewLocale); + updateI18n(parsedNewLocale); + } +} +//#endregion + // サイズã®åˆ¶é™ document.documentElement.style.maxWidth = '500px'; diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue index 6856b8272e..ff794d9b6e 100644 --- a/packages/frontend-embed/src/components/EmAcct.vue +++ b/packages/frontend-embed/src/components/EmAcct.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { toUnicode } from 'punycode/'; +import { toUnicode } from 'punycode.js'; import { host as hostRaw } from '@@/js/config.js'; defineProps<{ diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue index a71364237d..b5aaa95894 100644 --- a/packages/frontend-embed/src/components/EmMention.vue +++ b/packages/frontend-embed/src/components/EmMention.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode.js'; import { } from 'vue'; import tinycolor from 'tinycolor2'; import { host as localHost } from '@@/js/config.js'; diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue index 4211261e19..4e0ae005df 100644 --- a/packages/frontend-embed/src/components/EmNotes.vue +++ b/packages/frontend-embed/src/components/EmNotes.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #default="{ items: notes }"> <div :class="[$style.root]"> - <EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/> + <EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note as Misskey.entities.Note"/> </div> </template> </EmPagination> @@ -24,6 +24,7 @@ import { useTemplateRef } from 'vue'; import EmNote from '@/components/EmNote.vue'; import EmPagination, { Paging } from '@/components/EmPagination.vue'; import { i18n } from '@/i18n.js'; +import * as Misskey from 'misskey-js'; withDefaults(defineProps<{ pagination: Paging; diff --git a/packages/frontend-embed/src/components/EmUrl.vue b/packages/frontend-embed/src/components/EmUrl.vue index 94424cab28..2dbbe90858 100644 --- a/packages/frontend-embed/src/components/EmUrl.vue +++ b/packages/frontend-embed/src/components/EmUrl.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; -import { toUnicode as decodePunycode } from 'punycode/'; +import { toUnicode as decodePunycode } from 'punycode.js'; import EmA from './EmA.vue'; import { url as local } from '@@/js/config.js'; diff --git a/packages/frontend-embed/src/index.html b/packages/frontend-embed/src/index.html index d94ada5ea8..e69de29bb2 100644 --- a/packages/frontend-embed/src/index.html +++ b/packages/frontend-embed/src/index.html @@ -1,38 +0,0 @@ -<!-- - SPDX-FileCopyrightText: syuilo and misskey-project - SPDX-License-Identifier: AGPL-3.0-only ---> - -<!-- - 開発モードã®viteã¯ã“ã®ãƒ•ァイルを起点ã«ã‚µãƒ¼ãƒãƒ¼ã‚’èµ·å‹•ã—ã¾ã™ã€‚ - ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã«æ›¸ã‹ã‚ŒãŸ [t]js ã®ãƒªãƒ³ã‚¯ã¨ (s)cssã®ãƒªãƒ³ã‚¯ã¨ã€ãã®ä¾å˜é–¢ä¿‚ã«ã‚るファイルã¯ãƒ“ルドã•れã¾ã™ ---> - -<!DOCTYPE html> -<html> -<head> - <meta charset="UTF-8" /> - <title>[DEV] Loading...</title> - <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> - <meta - http-equiv="Content-Security-Policy" - content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/; - worker-src 'self'; - script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh https://cdn.jsdelivr.net; - style-src 'self' 'unsafe-inline'; - img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; - media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; - connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com; - frame-src *;" - /> - <meta property="og:site_name" content="[DEV BUILD] Sharkey" /> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel='stylesheet' href='/assets/phosphor-icons/bold/style.css'> - <link rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css'> -</head> - -<body> -<div id="sharkey_app"></div> -<script type="module" src="./boot.ts"></script> -</body> -</html> diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts index 4664ad4880..680ab80167 100644 --- a/packages/frontend-embed/src/theme.ts +++ b/packages/frontend-embed/src/theme.ts @@ -75,16 +75,21 @@ function compile(theme: Theme): Record<string, string> { return getColor(theme.props[val]); } else if (val[0] === ':') { // func const parts = val.split('<'); - const func = parts.shift().substring(1); - const arg = parseFloat(parts.shift()); - const color = getColor(parts.join('<')); + const funcTxt = parts.shift(); + const argTxt = parts.shift(); - switch (func) { - case 'darken': return color.darken(arg); - case 'lighten': return color.lighten(arg); - case 'alpha': return color.setAlpha(arg); - case 'hue': return color.spin(arg); - case 'saturate': return color.saturate(arg); + if (funcTxt && argTxt) { + const func = funcTxt.substring(1); + const arg = parseFloat(argTxt); + const color = getColor(parts.join('<')); + + switch (func) { + case 'darken': return color.darken(arg); + case 'lighten': return color.lighten(arg); + case 'alpha': return color.setAlpha(arg); + case 'hue': return color.spin(arg); + case 'saturate': return color.saturate(arg); + } } } diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json index 1af34f378c..ff04a689bf 100644 --- a/packages/frontend-embed/tsconfig.json +++ b/packages/frontend-embed/tsconfig.json @@ -10,8 +10,8 @@ "declaration": false, "sourceMap": false, "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "ES2022", + "moduleResolution": "Bundler", "removeComments": false, "noLib": false, "strict": true, diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts deleted file mode 100644 index bf2f478887..0000000000 --- a/packages/frontend-embed/vite.config.local-dev.ts +++ /dev/null @@ -1,96 +0,0 @@ -import dns from 'dns'; -import { readFile } from 'node:fs/promises'; -import type { IncomingMessage } from 'node:http'; -import { defineConfig } from 'vite'; -import type { UserConfig } from 'vite'; -import * as yaml from 'js-yaml'; -import locales from '../../locales/index.js'; -import { getConfig } from './vite.config.js'; - -dns.setDefaultResultOrder('ipv4first'); - -const defaultConfig = getConfig(); - -const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8')); - -const httpUrl = `http://localhost:${port}/`; -const websocketUrl = `ws://localhost:${port}/`; - -// activitypubリクエストã¯Proxyを通ã—ã€ãれ以外ã¯Viteã®é–‹ç™ºã‚µãƒ¼ãƒãƒ¼ã‚’返㙠-function varyHandler(req: IncomingMessage) { - if (req.headers.accept?.includes('application/activity+json')) { - return null; - } - return '/index.html'; -} - -const devConfig: UserConfig = { - // 基本ã®è¨å®šã¯ vite.config.js ã‹ã‚‰å¼•ãç¶™ã - ...defaultConfig, - root: 'src', - publicDir: '../assets', - base: '/embed', - server: { - host: 'localhost', - port: 5174, - proxy: { - '/api': { - changeOrigin: true, - target: httpUrl, - }, - '/assets': httpUrl, - '/static-assets': httpUrl, - '/client-assets': httpUrl, - '/files': httpUrl, - '/twemoji': httpUrl, - '/fluent-emoji': httpUrl, - '/sw.js': httpUrl, - '/streaming': { - target: websocketUrl, - ws: true, - }, - '/favicon.ico': httpUrl, - '/robots.txt': httpUrl, - '/embed.js': httpUrl, - '/identicon': { - target: httpUrl, - rewrite(path) { - return path.replace('@localhost:5173', ''); - }, - }, - '/url': httpUrl, - '/proxy': httpUrl, - '/_info_card_': httpUrl, - '/bios': httpUrl, - '/cli': httpUrl, - '/inbox': httpUrl, - '/emoji/': httpUrl, - '/notes': { - target: httpUrl, - bypass: varyHandler, - }, - '/users': { - target: httpUrl, - bypass: varyHandler, - }, - '/.well-known': { - target: httpUrl, - }, - }, - }, - build: { - ...defaultConfig.build, - rollupOptions: { - ...defaultConfig.build?.rollupOptions, - input: 'index.html', - }, - }, - - define: { - ...defaultConfig.define, - _LANGS_FULL_: JSON.stringify(Object.entries(locales)), - }, -}; - -export default defineConfig(({ command, mode }) => devConfig); - diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index bf6f558893..13f272612c 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -2,12 +2,18 @@ import path from 'path'; import pluginVue from '@vitejs/plugin-vue'; import { type UserConfig, defineConfig } from 'vite'; import { localesVersion } from '../../locales/version.js'; +import * as yaml from 'js-yaml'; +import { promises as fsp } from 'fs'; + import locales from '../../locales/index.js'; import meta from '../../package.json'; import packageInfo from './package.json' with { type: 'json' }; import pluginJson5 from './vite.json5.js'; import { pluginReplaceIcons } from '../frontend/vite.replaceIcons.js'; +const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null; +const host = url ? (new URL(url)).hostname : undefined; + const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; /** @@ -63,7 +69,14 @@ export function getConfig(): UserConfig { base: '/embed_vite/', server: { + host, port: 5174, + hmr: { + // ãƒãƒƒã‚¯ã‚¨ãƒ³ãƒ‰çµŒç”±ã§ã®èµ·å‹•時ã€Viteã¯5174経由ã§ã‚¢ã‚»ãƒƒãƒˆã‚’å‚ç…§ã—ã¦ã„ã‚‹ã¨æ€ã„込んã§ã„ã‚‹ãŒå®Ÿéš›ã¯3000ã‹ã‚‰é…ä¿¡ã•れる + // ãã®ãŸã‚ã€ãƒãƒƒã‚¯ã‚¨ãƒ³ãƒ‰ã®WSサーãƒãƒ¼ã«HMRã®WSリクエストãŒå¸åŽã•れã¦ã—ã¾ã„ã€æ£ã—ãHMRãŒæ©Ÿèƒ½ã—ãªã„ + // クライアントå´ã®WSãƒãƒ¼ãƒˆã‚’Viteサーãƒãƒ¼ã®ãƒãƒ¼ãƒˆã«å¼·åˆ¶ã•ã›ã‚‹ã“ã¨ã§ã€æ£ã—ãHMRãŒæ©Ÿèƒ½ã™ã‚‹ã‚ˆã†ã«ãªã‚‹ + clientPort: 5174, + }, }, plugins: [ diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js index 17b6da8d30..9941114757 100644 --- a/packages/frontend-shared/build.js +++ b/packages/frontend-shared/build.js @@ -23,10 +23,14 @@ const options = { sourcemap: 'linked', }; +const args = process.argv.slice(2).map(arg => arg.toLowerCase()); + // js-builté…下をã™ã¹ã¦å‰Šé™¤ã™ã‚‹ -fs.rmSync('./js-built', { recursive: true, force: true }); +if (!args.includes('--no-clean')) { + fs.rmSync('./js-built', { recursive: true, force: true }); +} -if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { +if (args.includes('--watch')) { await watchSrc(); } else { await buildSrc(); diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index 8bf25da161..7537974f1d 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -26,6 +26,7 @@ "@typescript-eslint/parser": "7.17.0", "esbuild": "0.24.0", "eslint-plugin-vue": "9.31.0", + "nodemon": "3.1.7", "typescript": "5.6.3", "vue-eslint-parser": "9.4.3" }, diff --git a/packages/frontend/.storybook/fake-utils.ts b/packages/frontend/.storybook/fake-utils.ts new file mode 100644 index 0000000000..c777cbbe72 --- /dev/null +++ b/packages/frontend/.storybook/fake-utils.ts @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import seedrandom from 'seedrandom'; + +/** + * AIã§ç”Ÿæˆã—ãŸç„¡ä½œç‚ºãªãƒ•ァーストãƒãƒ¼ãƒ + */ +export const firstNameDict = [ + 'Ethan', 'Olivia', 'Jackson', 'Emma', 'Liam', 'Ava', 'Aiden', 'Sophia', 'Mason', 'Isabella', + 'Noah', 'Mia', 'Lucas', 'Harper', 'Caleb', 'Abigail', 'Samuel', 'Emily', 'Logan', + 'Madison', 'Benjamin', 'Chloe', 'Elijah', 'Grace', 'Alexander', 'Scarlett', 'William', 'Zoey', 'James', 'Lily', +] + +/** + * AIã§ç”Ÿæˆã—ãŸç„¡ä½œç‚ºãªãƒ©ã‚¹ãƒˆãƒãƒ¼ãƒ + */ +export const lastNameDict = [ + 'Anderson', 'Johnson', 'Thompson', 'Davis', 'Rodriguez', 'Smith', 'Patel', 'Williams', 'Lee', 'Brown', + 'Garcia', 'Jackson', 'Martinez', 'Taylor', 'Harris', 'Nguyen', 'Miller', 'Jones', 'Wilson', + 'White', 'Thomas', 'Garcia', 'Martinez', 'Robinson', 'Turner', 'Lewis', 'Hall', 'King', 'Baker', 'Cooper', +] + +/** + * AIã§ç”Ÿæˆã—ãŸç„¡ä½œç‚ºãªå›½å + */ +export const countryDict = [ + 'Japan', 'Canada', 'Brazil', 'Australia', 'Italy', 'SouthAfrica', 'Mexico', 'Sweden', 'Russia', 'India', + 'Germany', 'Argentina', 'South Korea', 'France', 'Nigeria', 'Turkey', 'Spain', 'Egypt', 'Thailand', + 'Vietnam', 'Kenya', 'Saudi Arabia', 'Netherlands', 'Colombia', 'Poland', 'Chile', 'Malaysia', 'Ukraine', 'New Zealand', 'Peru', +] + +export function text(length: number = 10, seed?: string): string { + let result = ""; + + // シード値を使ã†å ´åˆã€åŒã˜æ•°å€¤ãŒç¾…列ã•れるãŒã€ãƒ©ãƒ³ãƒ€ãƒ æ–‡å—列ã¨ã„ã†æ„味ã§ã¯æº€ãŸã›ã¦ã„ã‚‹ã¨æ€ã†ã®ã§ã“ã®ã¾ã¾ä½¿ã£ã¦ãŠã + const rand = seed ? seedrandom(seed)() : Math.random(); + while (result.length < length) { + result += rand.toString(36).substring(2); + } + + return result.substring(0, length); +} + +export function integer(min: number = 0, max: number = 9999, seed?: string): number { + const rand = seed ? seedrandom(seed)() : Math.random(); + return Math.floor(rand * (max - min)) + min; +} + +export function date(params?: { + yearMin?: number, + yearMax?: number, + monthMin?: number, + monthMax?: number, + dayMin?: number, + dayMax?: number, + hourMin?: number, + hourMax?: number, + minuteMin?: number, + minuteMax?: number, + secondMin?: number, + secondMax?: number, + millisecondMin?: number, + millisecondMax?: number, +}, seed?: string): Date { + const year = integer(params?.yearMin ?? 1970, params?.yearMax ?? (new Date()).getFullYear(), seed); + const month = integer(params?.monthMin ?? 1, params?.monthMax ?? 12, seed); + let day = integer(params?.dayMin ?? 1, params?.dayMax ?? 31, seed); + if (month === 2) { + day = Math.min(day, 28); + } else if ([4, 6, 9, 11].includes(month)) { + day = Math.min(day, 30); + } else { + day = Math.min(day, 31); + } + + const hour = integer(params?.hourMin ?? 0, params?.hourMax ?? 23, seed); + const minute = integer(params?.minuteMin ?? 0, params?.minuteMax ?? 59, seed); + const second = integer(params?.secondMin ?? 0, params?.secondMax ?? 59, seed); + const millisecond = integer(params?.millisecondMin ?? 0, params?.millisecondMax ?? 999, seed); + + return new Date(year, month - 1, day, hour, minute, second, millisecond); +} + +export function boolean(seed?: string): boolean { + const rand = seed ? seedrandom(seed)() : Math.random(); + return rand < 0.5; +} + +export function choose<T>(array: T[], seed?: string): T { + const rand = seed ? seedrandom(seed)() : Math.random(); + return array[Math.floor(rand * array.length)]; +} + +export function firstName(seed?: string): string { + return choose(firstNameDict, seed); +} + +export function lastName(seed?: string): string { + return choose(lastNameDict, seed); +} + +export function country(seed?: string): string { + return choose(countryDict, seed); +} + +const TIME2000 = 946684800000; +export function fakeId(seed?: string): string { + let time = new Date().getTime(); + + time = time - TIME2000; + if (time < 0) time = 0; + + const timeStr = time.toString(36).padStart(8, '0'); + const noiseStr = text(2, seed); + + return timeStr + noiseStr; +} + +export function imageDataUrl(options?: { + size?: { + width?: number, + height?: number, + }, + color?: { + red?: number, + green?: number, + blue?: number, + alpha?: number, + } +}, seed?: string): string { + const canvas = document.createElement('canvas'); + canvas.width = options?.size?.width ?? 100; + canvas.height = options?.size?.height ?? 100; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get 2d context'); + } + + ctx.beginPath() + + const red = options?.color?.red ?? integer(0, 255, seed); + const green = options?.color?.green ?? integer(0, 255, seed); + const blue = options?.color?.blue ?? integer(0, 255, seed); + const alpha = options?.color?.alpha ?? 1; + ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2, true); + ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${alpha})`; + ctx.fill(); + + return canvas.toDataURL('image/png', 1.0); +} diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 758827c196..377d26d6a3 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -5,6 +5,7 @@ import { AISCRIPT_VERSION } from '@syuilo/aiscript'; import type { entities } from 'misskey-js' +import { date, imageDataUrl, text } from "./fake-utils.js"; export function abuseUserReport() { return { @@ -302,3 +303,93 @@ export function inviteCode(isUsed = false, hasExpiration = false, isExpired = fa used: isUsed, } } + +export function role(params: { + id?: string, + name?: string, + color?: string | null, + iconUrl?: string | null, + description?: string, + isModerator?: boolean, + isAdministrator?: boolean, + displayOrder?: number, + createdAt?: string, + updatedAt?: string, + target?: 'manual' | 'conditional', + isPublic?: boolean, + isExplorable?: boolean, + asBadge?: boolean, + canEditMembersByModerator?: boolean, + usersCount?: number, +}, seed?: string): entities.Role { + const prefix = params.displayOrder ? params.displayOrder.toString().padStart(3, '0') + '-' : ''; + const genId = text(36, seed); + const createdAt = params.createdAt ?? date({}, seed).toISOString(); + const updatedAt = params.updatedAt ?? date({}, seed).toISOString(); + + return { + id: params.id ?? genId, + name: params.name ?? `${prefix}TestRole-${genId}`, + color: params.color ?? '#445566', + iconUrl: params.iconUrl ?? null, + description: params.description ?? '', + isModerator: params.isModerator ?? false, + isAdministrator: params.isAdministrator ?? false, + displayOrder: params.displayOrder ?? 0, + createdAt: createdAt, + updatedAt: updatedAt, + target: params.target ?? 'manual', + isPublic: params.isPublic ?? true, + isExplorable: params.isExplorable ?? true, + asBadge: params.asBadge ?? true, + canEditMembersByModerator: params.canEditMembersByModerator ?? false, + usersCount: params.usersCount ?? 10, + condFormula: { + id: '', + type: 'or', + values: [] + }, + policies: {}, + } +} + +export function emoji(params?: { + id?: string, + name?: string, + host?: string, + uri?: string, + publicUrl?: string, + originalUrl?: string, + type?: string, + aliases?: string[], + category?: string, + license?: string, + isSensitive?: boolean, + localOnly?: boolean, + roleIdsThatCanBeUsedThisEmojiAsReaction?: {id:string, name:string}[], + updatedAt?: string, +}, seed?: string): entities.EmojiDetailedAdmin { + const _seed = seed ?? (params?.id ?? "DEFAULT_SEED"); + const id = params?.id ?? text(32, _seed); + const name = params?.name ?? text(8, _seed); + const updatedAt = params?.updatedAt ?? date({}, _seed).toISOString(); + + const image = imageDataUrl({}, _seed) + + return { + id: id, + name: name, + host: params?.host ?? null, + uri: params?.uri ?? null, + publicUrl: params?.publicUrl ?? image, + originalUrl: params?.originalUrl ?? image, + type: params?.type ?? 'image/png', + aliases: params?.aliases ?? [`alias1-${name}`, `alias2-${name}`], + category: params?.category ?? null, + license: params?.license ?? null, + isSensitive: params?.isSensitive ?? false, + localOnly: params?.localOnly ?? false, + roleIdsThatCanBeUsedThisEmojiAsReaction: params?.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], + updatedAt: updatedAt, + } +} diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index f2bdc631d2..8830523810 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -416,6 +416,10 @@ function toStories(component: string): Promise<string> { glob('src/components/MkUserSetupDialog.*.vue'), glob('src/components/MkInstanceCardMini.vue'), glob('src/components/MkInviteCode.vue'), + glob('src/components/MkTagItem.vue'), + glob('src/components/MkRoleSelectDialog.vue'), + glob('src/components/grid/MkGrid.vue'), + glob('src/pages/admin/custom-emojis-manager2.vue'), glob('src/pages/admin/overview.ap-requests.vue'), glob('src/pages/user/home.vue'), glob('src/pages/search.vue'), diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 63eaded968..528a1aef34 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,7 +4,6 @@ "type": "module", "scripts": { "watch": "vite", - "dev": "vite --config vite.config.local-dev.ts --debug hmr", "build": "vite build", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", @@ -31,7 +30,7 @@ "@twemoji/parser": "15.1.1", "@vitejs/plugin-vue": "5.2.0", "@vue/compiler-sfc": "3.5.12", - "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11", + "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", "astring": "1.9.0", "broadcast-channel": "7.0.0", "buraha": "0.0.1", @@ -59,7 +58,7 @@ "misskey-reversi": "workspace:*", "moment": "^2.30.1", "photoswipe": "5.4.4", - "punycode": "2.3.1", + "punycode.js": "2.3.1", "rollup": "4.26.0", "sanitize-html": "2.13.1", "sass": "1.79.3", @@ -108,7 +107,7 @@ "@types/matter-js": "0.19.7", "@types/micromatch": "4.0.9", "@types/node": "22.9.0", - "@types/punycode": "2.1.4", + "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/sanitize-html": "2.13.0", "@types/seedrandom": "3.0.8", "@types/throttle-debounce": "5.0.2", diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts deleted file mode 100644 index f312765dcf..0000000000 --- a/packages/frontend/src/_dev_boot_.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -await main(); - -import('@/_boot_.js'); - -/** - * backend/src/server/web/boot.jsã§å·®ã—è¾¼ã¾ã‚Œã¦ã„る起動処ç†ã®ã†ã¡ã€æœ€ä½Žé™å¿…è¦ãªã‚‚ã®ã‚’模倣ã™ã‚‹ãŸã‚ã®å‡¦ç† - */ -async function main() { - const forceError = localStorage.getItem('forceError'); - if (forceError != null) { - renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); - } - - //#region Detect language & fetch translations - - // dev-modeã®å ´åˆã¯å¸¸ã«å–り直㙠- const supportedLangs = _LANGS_.map(it => it[0]); - let lang: string | null | undefined = localStorage.getItem('lang'); - if (lang == null || !supportedLangs.includes(lang)) { - if (supportedLangs.includes(navigator.language)) { - lang = navigator.language; - } else { - lang = supportedLangs.find(x => x.split('-')[0] === navigator.language); - - // Fallback - if (lang == null) lang = 'en-US'; - } - } - - // TODO:今ã®ã¾ã¾ã ã¨è¨€èªžãƒ•ァイル変更後ã¯pnpm devをリスタートã™ã‚‹å¿…è¦ãŒã‚ã‚‹ã®ã§ã€chokidarを使ã£ãŸã‚Šç‰ã§å¯¾å¿œã§ãるよã†ã«ã™ã‚‹ - const locale = _LANGS_FULL_.find(it => it[0] === lang); - localStorage.setItem('lang', lang); - localStorage.setItem('locale', JSON.stringify(locale[1])); - localStorage.setItem('localeVersion', _VERSION_); - //#endregion - - //#region Theme - const theme = localStorage.getItem('theme'); - if (theme) { - for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); - - // HTMLã® theme-color é©ç”¨ - if (k === 'htmlThemeColor') { - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', v); - break; - } - } - } - } - } - const colorScheme = localStorage.getItem('colorScheme'); - if (colorScheme) { - document.documentElement.style.setProperty('color-scheme', colorScheme); - } - //#endregion - - const fontSize = localStorage.getItem('fontSize'); - if (fontSize) { - document.documentElement.classList.add('f-' + fontSize); - } - - const useSystemFont = localStorage.getItem('useSystemFont'); - if (useSystemFont) { - document.documentElement.classList.add('useSystemFont'); - } - - const wallpaper = localStorage.getItem('wallpaper'); - if (wallpaper) { - document.documentElement.style.backgroundImage = `url(${wallpaper})`; - } - - const customCss = localStorage.getItem('customCss'); - if (customCss && customCss.length > 0) { - const style = document.createElement('style'); - style.innerHTML = customCss; - document.head.appendChild(style); - } -} - -function renderError(code: string, details?: string) { - console.log(code, details); -} diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 366345b5b3..5005b17b3c 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -102,6 +102,9 @@ export async function removeAccount(idOrToken: Account['id']) { } function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> { + document.cookie = `token=; path=/; max-age=0${ location.protocol === 'https:' ? '; Secure' : ''}`; + document.cookie = `token=; path=/queue; max-age=86400${ location.protocol === 'https:' ? '; SameSite=Strict; Secure' : ''}`; // bull dashboardã®èªè¨¼ã¨ã‹ã§ä½¿ã† + return new Promise((done, fail) => { window.fetch(`${apiUrl}/i`, { method: 'POST', @@ -150,9 +153,9 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr } else if (res.error.id === 'd5826d14-3982-4d2e-8011-b9e9f02499ef') { // rate limited const timeToWait = res.error.info?.resetMs ?? 1000; - window.setTimeout(timeToWait, () => { + window.setTimeout(() => { fetchAccount(token, id, forceShowDialog).then(done, fail); - }); + }, timeToWait); return; } else { await alert({ @@ -221,7 +224,6 @@ export async function login(token: Account['token'], redirect?: string) { throw reason; }); miLocalStorage.setItem('account', JSON.stringify(me)); - document.cookie = `token=${token}; path=/; max-age=31536000${ location.protocol === 'https:' ? '; Secure' : ''}`; // bull dashboardã®èªè¨¼ã¨ã‹ã§ä½¿ã† await addAccount(me.id, token); if (redirect) { diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index d43a2b0799..46ec4533ec 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -99,6 +99,11 @@ export async function common(createVue: () => App<Element>) { // タッãƒãƒ‡ãƒã‚¤ã‚¹ã§CSSã®:hoverを機能ã•ã›ã‚‹ document.addEventListener('touchend', () => {}, { passive: true }); + // URLã«#pswpã‚’å«ã‚€å ´åˆã¯å–り除ã + if (location.hash === '#pswp') { + history.replaceState(null, '', location.href.replace('#pswp', '')); + } + // 一斉リãƒãƒ¼ãƒ‰ reloadChannel.addEventListener('message', path => { if (path !== null) location.href = path; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index eb8a4d30d2..6c544feb2a 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -7,6 +7,7 @@ import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { ui } from '@@/js/config.js'; import { common } from './common.js'; import type * as Misskey from 'misskey-js'; +import type { Component } from 'vue'; import { i18n } from '@/i18n.js'; import { alert, confirm, popup, post, toast } from '@/os.js'; import { useStream } from '@/stream.js'; @@ -26,13 +27,38 @@ import { type Keymap, makeHotkey } from '@/scripts/hotkey.js'; import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; export async function mainBoot() { - const { isClientUpdated } = await common(() => createApp( - new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) : - !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : - ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : - ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : - defineAsyncComponent(() => import('@/ui/universal.vue')), - )); + const { isClientUpdated } = await common(() => { + let uiStyle = ui; + const searchParams = new URLSearchParams(window.location.search); + + if (!$i) uiStyle = 'visitor'; + + if (searchParams.has('zen')) uiStyle = 'zen'; + if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen'; + + if (searchParams.has('ui')) uiStyle = searchParams.get('ui'); + + let rootComponent: Component; + switch (uiStyle) { + case 'zen': + rootComponent = defineAsyncComponent(() => import('@/ui/zen.vue')); + break; + case 'deck': + rootComponent = defineAsyncComponent(() => import('@/ui/deck.vue')); + break; + case 'visitor': + rootComponent = defineAsyncComponent(() => import('@/ui/visitor.vue')); + break; + case 'classic': + rootComponent = defineAsyncComponent(() => import('@/ui/classic.vue')); + break; + default: + rootComponent = defineAsyncComponent(() => import('@/ui/universal.vue')); + break; + } + + return createApp(rootComponent); + }); reactionPicker.init(); emojiPicker.init(); diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index c28dbc7ffa..564d1fe7e3 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template> </MkInput> - <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onChange"> + <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate"> <template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template> <option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option> @@ -77,8 +77,8 @@ import MkPostForm from '@/components/MkPostForm.vue'; const props = withDefaults(defineProps<{ component: AsUiComponent; components: Ref<AsUiComponent>[]; - size: 'small' | 'medium' | 'large'; - align: 'left' | 'center' | 'right'; + size?: 'small' | 'medium' | 'large'; + align?: 'left' | 'center' | 'right'; }>(), { size: 'medium', align: 'left', @@ -86,7 +86,7 @@ const props = withDefaults(defineProps<{ const c = props.component; -function g(id) { +function g(id: string) { const v = props.components.find(x => x.value.id === id)?.value; if (v) return v; @@ -122,13 +122,22 @@ const containerStyle = computed(() => { const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false); -function onSwitchUpdate(v) { +function onSwitchUpdate(v: boolean) { valueForSwitch.value = v; if ('onChange' in c && c.onChange) { c.onChange(v as never); } } +const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null); + +function onSelectUpdate(v) { + valueForSelect.value = v; + if ('onChange' in c && c.onChange) { + c.onChange(v as never); + } +} + function openPostForm() { const form = (c as AsUiPostFormButton).form; if (!form) return; diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index e9493edbd1..aeed90722f 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -30,6 +30,9 @@ import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmount import { defaultStore } from '@/store.js'; // APIs provided by Captcha services +// see: https://docs.hcaptcha.com/configuration/#javascript-api +// see: https://developers.google.com/recaptcha/docs/display?hl=ja +// see: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget export type Captcha = { render(container: string | Node, options: { readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown; @@ -56,6 +59,7 @@ declare global { const props = defineProps<{ provider: CaptchaProvider; sitekey: string | null; // null will show error on request + secretKey?: string | null; instanceUrl?: string | null; modelValue?: string | null; }>(); @@ -67,7 +71,7 @@ const emit = defineEmits<{ const available = ref(false); const captchaEl = shallowRef<HTMLDivElement | undefined>(); - +const captchaWidgetId = ref<string | undefined>(undefined); const testcaptchaInput = ref(''); const testcaptchaPassed = ref(false); @@ -99,6 +103,15 @@ const scriptId = computed(() => `script-${props.provider}`); const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha); +watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => { + // 変更ãŒã‚ã£ãŸã¨ãã¯ãƒªãƒ•レッシュã¨å†ãƒ¬ãƒ³ãƒ€ãƒªãƒ³ã‚°ã‚’ã—ã¦ãŠã‹ãªã„ã¨ã€å¤‰æ›´å¾Œã®å€¤ã§å†æ¤œè¨¼ãŒå‡ºæ¥ãªã„ + if (available.value) { + callback(undefined); + clearWidget(); + await requestRender(); + } +}); + if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') { available.value = true; } else if (src.value !== null) { @@ -111,14 +124,38 @@ if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') } function reset() { - if (captcha.value.reset) captcha.value.reset(); + if (captcha.value.reset && captchaWidgetId.value !== undefined) { + try { + captcha.value.reset(captchaWidgetId.value); + } catch (error: unknown) { + // ignore + if (_DEV_) console.warn(error); + } + } testcaptchaPassed.value = false; testcaptchaInput.value = ''; } +function remove() { + if (captcha.value.remove && captchaWidgetId.value) { + try { + if (_DEV_) console.log('remove', props.provider, captchaWidgetId.value); + captcha.value.remove(captchaWidgetId.value); + } catch (error: unknown) { + // ignore + if (_DEV_) console.warn(error); + } + } +} + async function requestRender() { - if (captcha.value.render && captchaEl.value instanceof Element) { - captcha.value.render(captchaEl.value, { + if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) { + // reCAPTCHAã®ãƒ¬ãƒ³ãƒ€ãƒªãƒ³ã‚°é‡è¤‡åˆ¤å®šã‚’回é¿ã™ã‚‹ãŸã‚ã€captchaElé…下ã«ä»®ã®divを用æ„ã™ã‚‹. + // (åŒã˜divã«å¯¾ã—ã¦è¤‡æ•°å›žrenderを呼ã³å‡ºã™ã¨reCAPTCHAã¯ã‚¨ãƒ©ãƒ¼ã‚’è¿”ã™ã®ã§ï¼‰ + const elem = document.createElement('div'); + captchaEl.value.appendChild(elem); + + captchaWidgetId.value = captcha.value.render(elem, { sitekey: props.sitekey, theme: defaultStore.state.darkMode ? 'dark' : 'light', callback: callback, @@ -146,6 +183,23 @@ async function requestRender() { } } +function clearWidget() { + if (props.provider === 'mcaptcha') { + const container = document.getElementById('mcaptcha__widget-container'); + if (container) { + container.innerHTML = ''; + } + } else { + reset(); + remove(); + + if (captchaEl.value) { + // レンダリング先ã®ã‚³ãƒ³ãƒ†ãƒŠã®ä¸èº«ã‚’掃除ã—ã€ãƒ•ォームãŒå¢—æ®–ã™ã‚‹ã®ã‚’æŠ‘æ¢ + captchaEl.value.innerHTML = ''; + } + } +} + function callback(response?: string) { emit('update:modelValue', typeof response === 'string' ? response : null); } @@ -178,7 +232,7 @@ onUnmounted(() => { }); onBeforeUnmount(() => { - reset(); + clearWidget(); }); defineExpose({ diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index e036fec528..7ff9da1ced 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -125,7 +125,9 @@ const bannerStyle = computed(() => { position: absolute; top: 16px; left: 16px; + max-width: calc(100% - 32px); padding: 12px 16px; + box-sizing: border-box; background: rgba(0, 0, 0, 0.7); color: #fff; font-size: 1.2em; diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index f4d20c7d8c..30a9b26bef 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <div v-show="showBody" ref="contentEl" :class="[$style.content, { [$style.omitted]: omitted }]"> <slot></slot> - <button v-if="omitted" :class="$style.fade" class="_button" @click="() => { ignoreOmit = true; omitted = false; }"> + <button v-if="omitted" :class="$style.fade" class="_button" @click="showMore"> <span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span> </button> </div> @@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{ thin?: boolean; naked?: boolean; foldable?: boolean; + onUnfold?: () => boolean; // return false to prevent unfolding scrollable?: boolean; expanded?: boolean; maxHeight?: number | null; @@ -101,6 +102,13 @@ const omitObserver = new ResizeObserver((entries, observer) => { calcOmit(); }); +function showMore() { + if (props.onUnfold && !props.onUnfold()) return; + + ignoreOmit.value = true; + omitted.value = false; +} + onMounted(() => { watch(showBody, v => { if (!rootEl.value) return; diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue index ecbee864dc..e6ab17417d 100644 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue @@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { defineProps, shallowRef } from 'vue'; +import { shallowRef } from 'vue'; import MkLink from '@/components/MkLink.vue'; import { i18n } from '@/i18n.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 1079e52030..5ba5de0c4a 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -5,13 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div - ref="thumbnail" - :class="[ - $style.root, - { [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive }, - ]" + v-panel + :class="[$style.root, { + [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive, + [$style.large]: large, + }]" > - <ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/> + <ImgWithBlurhash + v-if="isThumbnailAvailable" + :hash="file.blurhash" + :src="file.thumbnailUrl" + :alt="file.name" + :title="file.name" + :cover="fit !== 'contain'" + :forceBlurhash="forceBlurhash" + /> <i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i> <i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i> <i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i> @@ -34,6 +42,8 @@ const props = defineProps<{ file: Misskey.entities.DriveFile; fit: 'cover' | 'contain'; highlightWhenSensitive?: boolean; + forceBlurhash?: boolean; + large?: boolean; }>(); const is = computed(() => { @@ -60,7 +70,7 @@ const is = computed(() => { const isThumbnailAvailable = computed(() => { return props.file.thumbnailUrl - ? (is.value === 'image' as const || is.value === 'video') + ? (is.value === 'image' || is.value === 'video') : false; }); </script> @@ -101,4 +111,8 @@ const isThumbnailAvailable = computed(() => { font-size: 32px; color: #777; } + +.large .icon { + font-size: 40px; +} </style> diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 0b4114d252..084c81bb52 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <KeepAlive> <div v-show="opened"> - <MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22"> + <MkSpacer v-if="withSpacer" :marginMin="spacerMin" :marginMax="spacerMax"> <slot></slot> </MkSpacer> <div v-else> @@ -64,10 +64,14 @@ const props = withDefaults(defineProps<{ defaultOpen?: boolean; maxHeight?: number | null; withSpacer?: boolean; + spacerMin?: number; + spacerMax?: number; }>(), { defaultOpen: false, maxHeight: null, withSpacer: true, + spacerMin: 14, + spacerMax: 22, }); const rootEl = shallowRef<HTMLElement>(); diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue index f409f6ce50..96214a9542 100644 --- a/packages/frontend/src/components/MkFormFooter.vue +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div> <div style="margin-left: auto;" class="_buttons"> <MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton> - <MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded :disabled="!canSaving" @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </div> </template> @@ -18,7 +18,7 @@ import { } from 'vue'; import MkButton from './MkButton.vue'; import { i18n } from '@/i18n.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ form: { modifiedCount: { value: number; @@ -26,7 +26,10 @@ const props = defineProps<{ discard: () => void; save: () => void; }; -}>(); + canSaving?: boolean; +}>(), { + canSaving: true, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index 8ccbf61e48..d8066857fe 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.chart"> <div class="selects"> <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> - <optgroup :label="i18n.ts.federation"> + <optgroup v-if="shouldShowFederation" :label="i18n.ts.federation"> <option value="federation">{{ i18n.ts._charts.federation }}</option> <option value="ap-request">{{ i18n.ts._charts.apRequest }}</option> </optgroup> @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <optgroup :label="i18n.ts.notes"> <option value="notes">{{ i18n.ts._charts.notesIncDec }}</option> <option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option> - <option value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option> + <option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option> <option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option> </optgroup> <optgroup :label="i18n.ts.drive"> @@ -46,9 +46,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;"> <option value="active-users">Active users</option> <option value="notes">Notes</option> - <option value="ap-requests-inbox-received">AP Requests: inboxReceived</option> - <option value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option> - <option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option> + <option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option> + <option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option> + <option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option> </MkSelect> <div class="_panel" :class="$style.heatmap"> <MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/> @@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFoldableSection> - <MkFoldableSection class="item"> + <MkFoldableSection v-if="shouldShowFederation" class="item"> <template #header>Federation</template> <div :class="$style.federation"> <div class="pies"> @@ -84,13 +84,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef } from 'vue'; +import { onMounted, ref, computed, shallowRef } from 'vue'; import { Chart } from 'chart.js'; import MkSelect from '@/components/MkSelect.vue'; import MkChart from '@/components/MkChart.vue'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; +import { $i } from '@/account.js'; import * as os from '@/os.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import MkHeatmap, { type HeatmapSource } from '@/components/MkHeatmap.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; @@ -100,6 +102,8 @@ import { initChart } from '@/scripts/init-chart.js'; initChart(); +const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator); + const chartLimit = 500; const chartSpan = ref<'hour' | 'day'>('hour'); const chartSrc = ref('active-users'); diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 2a8d5c9f71..9d9cc76822 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -4,19 +4,20 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="$style.root" :style="bg"> +<div :class="$style.root" :style="themeColorStyle"> <img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/> - <div :class="$style.name">{{ instance.name }}</div> + <div :class="$style.name">{{ instanceName }}</div> </div> </template> <script lang="ts" setup> -import { computed } from 'vue'; -import { instanceName } from '@@/js/config.js'; -import { instance as Instance } from '@/instance.js'; +import { computed, type CSSProperties } from 'vue'; +import { instanceName as localInstanceName } from '@@/js/config.js'; +import { instance as localInstance } from '@/instance.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ + host: string | null; instance?: { faviconUrl?: string | null name?: string | null @@ -25,18 +26,28 @@ const props = defineProps<{ }>(); // if no instance data is given, this is for the local instance -const instance = props.instance ?? { - name: instanceName, - themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content, -}; +const instanceName = computed(() => props.host == null ? localInstanceName : props.instance?.name ?? props.host); -const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico'); - -const themeColor = instance.themeColor ?? '#777777'; +const faviconUrl = computed(() => { + let imageSrc: string | null = null; + if (props.host == null) { + if (localInstance.iconUrl == null) { + return '/favicon.ico'; + } else { + imageSrc = localInstance.iconUrl; + } + } else { + imageSrc = props.instance?.faviconUrl ?? null; + } + return getProxiedImageUrlNullable(imageSrc); +}); -const bg = { - background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`, -}; +const themeColorStyle = computed<CSSProperties>(() => { + const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777'; + return { + background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`, + }; +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index f64ca4bc77..ac50d82a63 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode.js'; import { computed } from 'vue'; import { host as localHost } from '@@/js/config.js'; import { $i } from '@/account.js'; diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index c766a33823..a446dad0ab 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -288,20 +288,23 @@ const align = () => { const onOpened = () => { emit('opened'); - // NOTE: Chromatic テストã®éš›ã« undefined ã«ãªã‚‹å ´åˆãŒã‚ã‚‹ - if (content.value == null) return; + // contentã®åè¦ç´ ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ãŸã‚レンダリングã®å®Œäº†ã‚’å¾…ã¤å¿…è¦ãŒã‚る(nextTickãŒå¿…è¦ï¼‰ + nextTick(() => { + // NOTE: Chromatic テストã®éš›ã« undefined ã«ãªã‚‹å ´åˆãŒã‚ã‚‹ + if (content.value == null) return; - // モーダルコンテンツã«ãƒžã‚¦ã‚¹ãƒœã‚¿ãƒ³ãŒæŠ¼ã•れã€ã‚³ãƒ³ãƒ†ãƒ³ãƒ„外ã§ãƒžã‚¦ã‚¹ãƒœã‚¿ãƒ³ãŒé›¢ã•れãŸã¨ãã«ãƒ¢ãƒ¼ãƒ€ãƒ«ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã‚¯ãƒªãƒƒã‚¯ã¨åˆ¤å®šã•ã›ãªã„ãŸã‚ã«ãƒžã‚¦ã‚¹ã‚¤ãƒ™ãƒ³ãƒˆã‚’監視ã—フラグ管ç†ã™ã‚‹ - const el = content.value.children[0]; - el.addEventListener('mousedown', ev => { - contentClicking = true; - window.addEventListener('mouseup', ev => { - // click イベントより先㫠mouseup イベントãŒç™ºç”Ÿã™ã‚‹ã‹ã‚‚ã—れãªã„ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠- window.setTimeout(() => { - contentClicking = false; - }, 100); - }, { passive: true, once: true }); - }, { passive: true }); + // モーダルコンテンツã«ãƒžã‚¦ã‚¹ãƒœã‚¿ãƒ³ãŒæŠ¼ã•れã€ã‚³ãƒ³ãƒ†ãƒ³ãƒ„外ã§ãƒžã‚¦ã‚¹ãƒœã‚¿ãƒ³ãŒé›¢ã•れãŸã¨ãã«ãƒ¢ãƒ¼ãƒ€ãƒ«ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã‚¯ãƒªãƒƒã‚¯ã¨åˆ¤å®šã•ã›ãªã„ãŸã‚ã«ãƒžã‚¦ã‚¹ã‚¤ãƒ™ãƒ³ãƒˆã‚’監視ã—フラグ管ç†ã™ã‚‹ + const el = content.value.children[0]; + el.addEventListener('mousedown', ev => { + contentClicking = true; + window.addEventListener('mouseup', ev => { + // click イベントより先㫠mouseup イベントãŒç™ºç”Ÿã™ã‚‹ã‹ã‚‚ã—れãªã„ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠+ window.setTimeout(() => { + contentClicking = false; + }, 100); + }, { passive: true, once: true }); + }, { passive: true }); + }); }; const onClosed = () => { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 5c6c6f45bb..c5e552b8f0 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/> <div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined"> <MkNoteHeader :note="appearNote" :mini="true" @click.stop/> - <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> + <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> <bdi> <p v-if="appearNote.cw != null" :class="$style.cw"> @@ -100,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.files && appearNote.files.length > 0"> <MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/> </div> - <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" @click.stop/> + <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/> <div v-if="isEnabledUrlPreview"> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :class="$style.urlPreview" @click.stop/> </div> @@ -179,13 +179,23 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> </template> </I18n> - <I18n v-else :src="i18n.ts.userSaysSomething" tag="small"> + <I18n v-else-if="showSoftWordMutedWord !== true" :src="i18n.ts.userSaysSomething" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> <MkUserName :user="appearNote.user"/> </MkA> </template> </I18n> + <I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small"> + <template #name> + <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> + <MkUserName :user="appearNote.user"/> + </MkA> + </template> + <template #word> + {{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }} + </template> + </I18n> </div> <div v-else> <!-- @@ -319,6 +329,7 @@ const isDeleted = ref(false); const renoted = ref(false); const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true)); +const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord); const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translating = ref(false); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); @@ -343,13 +354,18 @@ const renoteTooltip = computeRenoteTooltip(renoted); /* Overload Functionã«LintãŒå¯¾å¿œã—ã¦ã„ãªã„ã®ã§ã‚³ãƒ¡ãƒ³ãƒˆã‚¢ã‚¦ãƒˆ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean; -function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; +function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): Array<string | string[]> | false | 'sensitiveMute'; */ -function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' { +function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' { if (mutedWords != null) { - if (checkWordMute(noteToCheck, $i, mutedWords)) return true; - if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; - if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; + const result = checkWordMute(noteToCheck, $i, mutedWords); + if (Array.isArray(result)) return result; + + const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords); + if (Array.isArray(replyResult)) return replyResult; + + const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords); + if (Array.isArray(renoteResult)) return renoteResult; } if (checkOnly) return false; diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 93a543dff1..e33c574900 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only <img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/> </div> </div> - <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> + <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/> </div> </header> <div :class="$style.noteContent"> @@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.files && appearNote.files.length > 0"> <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/> </div> - <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll"/> + <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/> <div v-if="isEnabledUrlPreview"> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" style="margin-top: 6px;"/> </div> diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue new file mode 100644 index 0000000000..bf105c3c27 --- /dev/null +++ b/packages/frontend/src/components/MkNoteMediaGrid.vue @@ -0,0 +1,109 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> + <template v-for="file in note.files"> + <div + v-if="((( + (defaultStore.state.nsfw === 'force' || file.isSensitive) && + defaultStore.state.nsfw !== 'ignore' + ) || (defaultStore.state.dataSaver.media && file.type.startsWith('image/'))) && + !showingFiles.has(file.id) + )" + :class="[$style.filePreview, { [$style.square]: square }]" + @click="showingFiles.add(file.id)" + > + <MkDriveFileThumbnail + :file="file" + fit="cover" + :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia" + :forceBlurhash="true" + :large="true" + :class="$style.file" + /> + <div :class="$style.sensitive"> + <div> + <div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div> + <div v-else><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div> + <div>{{ i18n.ts.clickToShow }}</div> + </div> + </div> + </div> + <MkA v-else :class="[$style.filePreview, { [$style.square]: square }]" :to="notePage(note)"> + <MkDriveFileThumbnail + :file="file" + fit="cover" + :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia" + :large="true" + :class="$style.file" + /> + </MkA> + </template> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import { notePage } from '@/filters/note.js'; +import { i18n } from '@/i18n.js'; +import * as Misskey from 'misskey-js'; +import { defaultStore } from '@/store.js'; +import bytes from '@/filters/bytes.js'; + +import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; + +defineProps<{ + note: Misskey.entities.Note; + square?: boolean; +}>(); + +const showingFiles = ref<Set<string>>(new Set()); +</script> + +<style lang="scss" module> +.square { + width: 100%; + height: auto; + aspect-ratio: 1; +} + +.filePreview { + position: relative; + height: 128px; + border-radius: calc(var(--MI-radius) / 2); + overflow: clip; + + &:hover { + text-decoration: none; + } + + &.square { + height: 100%; + } +} + +.file { + width: 100%; + height: 100%; + border-radius: calc(var(--MI-radius) / 2); +} + +.sensitive { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: grid; + place-items: center; + font-size: 0.8em; + text-align: center; + padding: 8px; + box-sizing: border-box; + color: #fff; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); + cursor: pointer; +} +</style> diff --git a/packages/frontend/src/components/MkPagingButtons.vue b/packages/frontend/src/components/MkPagingButtons.vue new file mode 100644 index 0000000000..fe59efd83a --- /dev/null +++ b/packages/frontend/src/components/MkPagingButtons.vue @@ -0,0 +1,124 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <MkButton primary :disabled="min === current" @click="onToPrevButtonClicked"><</MkButton> + + <div :class="$style.buttons"> + <div v-if="prevDotVisible" :class="$style.headTailButtons"> + <MkButton @click="onToHeadButtonClicked">{{ min }}</MkButton> + <span class="ti ti-dots"/> + </div> + + <MkButton + v-for="i in buttonRanges" :key="i" + :disabled="current === i" + @click="onNumberButtonClicked(i)" + > + {{ i }} + </MkButton> + + <div v-if="nextDotVisible" :class="$style.headTailButtons"> + <span class="ti ti-dots"/> + <MkButton @click="onToTailButtonClicked">{{ max }}</MkButton> + </div> + </div> + + <MkButton primary :disabled="max === current" @click="onToNextButtonClicked">></MkButton> +</div> +</template> + +<script setup lang="ts"> + +import { computed, toRefs } from 'vue'; +import MkButton from '@/components/MkButton.vue'; + +const min = 1; + +const emit = defineEmits<{ + (ev: 'pageChanged', pageNumber: number): void; +}>(); + +const props = defineProps<{ + current: number; + max: number; + buttonCount: number; +}>(); + +const { current, max } = toRefs(props); + +const buttonCount = computed(() => Math.min(max.value, props.buttonCount)); +const buttonCountHalf = computed(() => Math.floor(buttonCount.value / 2)); +const buttonCountStart = computed(() => Math.min(Math.max(min, current.value - buttonCountHalf.value), max.value - buttonCount.value + 1)); +const buttonRanges = computed(() => Array.from({ length: buttonCount.value }, (_, i) => buttonCountStart.value + i)); + +const prevDotVisible = computed(() => (current.value - 1 > buttonCountHalf.value) && (max.value > buttonCount.value)); +const nextDotVisible = computed(() => (current.value < max.value - buttonCountHalf.value) && (max.value > buttonCount.value)); + +if (_DEV_) { + console.log('[MkPagingButtons]', current.value, max.value, buttonCount.value, buttonCountHalf.value); + console.log('[MkPagingButtons]', current.value < max.value - buttonCountHalf.value); + console.log('[MkPagingButtons]', max.value > buttonCount.value); +} + +function onNumberButtonClicked(pageNumber: number) { + emit('pageChanged', pageNumber); +} + +function onToHeadButtonClicked() { + emit('pageChanged', min); +} + +function onToPrevButtonClicked() { + const newPageNumber = current.value <= min ? min : current.value - 1; + emit('pageChanged', newPageNumber); +} + +function onToNextButtonClicked() { + const newPageNumber = current.value >= max.value ? max.value : current.value + 1; + emit('pageChanged', newPageNumber); +} + +function onToTailButtonClicked() { + emit('pageChanged', max.value); +} +</script> + +<style module lang="scss"> +.root { + display: flex; + justify-content: center; + align-items: center; + gap: 24px; + + button { + border-radius: 9999px; + min-width: 2.5em; + min-height: 2.5em; + max-width: 2.5em; + max-height: 2.5em; + padding: 4px; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.headTailButtons { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + + span { + font-size: 0.75em; + } +} +</style> diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index a414676bda..f6218de4c8 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <span :class="$style.fg"> <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template> - <Mfm :text="choice.text" :plain="true"/> + <Mfm :text="choice.text" :plain="true" :author="author" :emojiUrls="emojiUrls"/> <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span> </span> </li> @@ -48,6 +48,8 @@ const props = defineProps<{ poll: NonNullable<Misskey.entities.Note['poll']>; readOnly?: boolean; local?: boolean; + emojiUrls?: Record<string, string>; + author?: Misskey.entities.UserLite; }>(); const remaining = ref(-1); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 11ae6dbd6a..41d443a388 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -46,14 +46,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="posted"></template> <template v-else-if="posting"><MkEllipsis/></template> <template v-else>{{ submitText }}</template> - <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i> + <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renoteTargetNote ? 'ti ti-quote' : 'ti ti-send'"></i> </div> </button> </div> </header> <MkNoteSimple v-if="reply" :class="$style.targetNote" :hideFiles="true" :note="reply"/> - <MkNoteSimple v-if="renote" :class="$style.targetNote" :hideFiles="true" :note="renote"/> - <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div> + <MkNoteSimple v-if="renoteTargetNote" :class="$style.targetNote" :hideFiles="true" :note="renoteTargetNote"/> + <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null; renoteTargetNote = null;"><i class="ti ti-x"></i></button></div> <div v-if="visibility === 'specified'" :class="$style.toSpecified"> <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> <div :class="$style.visibleUsers"> @@ -106,13 +106,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, toRaw } from 'vue'; +import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, toRaw, type ShallowRef } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; -import { toASCII } from 'punycode/'; +import { toASCII } from 'punycode.js'; import { host, url } from '@@/js/config.js'; import type { MenuItem } from '@/types/menu.js'; +import type { PostFormProps } from '@/types/post-form.js'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; @@ -136,7 +137,6 @@ import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; -import type { PostFormProps } from '@/types/post-form.js'; import MkScheduleEditor from '@/components/MkScheduleEditor.vue'; const $i = signinRequired(); @@ -202,12 +202,13 @@ const justEndedComposition = ref(false); const scheduleNote = ref<{ scheduledAt: number | null; } | null>(null); +const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote); const draftKey = computed((): string => { let key = props.channel ? `channel:${props.channel.id}` : ''; - if (props.renote) { - key += `renote:${props.renote.id}`; + if (renoteTargetNote.value) { + key += `renote:${renoteTargetNote.value.id}`; } else if (props.reply) { key += `reply:${props.reply.id}`; } else { @@ -218,7 +219,7 @@ const draftKey = computed((): string => { }); const placeholder = computed((): string => { - if (props.renote) { + if (renoteTargetNote.value) { return i18n.ts._postForm.quotePlaceholder; } else if (props.reply) { return i18n.ts._postForm.replyPlaceholder; @@ -238,7 +239,7 @@ const placeholder = computed((): string => { }); const submitText = computed((): string => { - return props.renote + return renoteTargetNote.value ? i18n.ts.quote : props.reply ? i18n.ts.reply @@ -262,11 +263,12 @@ const canPost = computed((): boolean => { 1 <= textLength.value || 1 <= files.value.length || poll.value != null || - props.renote != null || + renoteTargetNote.value != null || quoteId.value != null ) && (textLength.value <= maxTextLength.value) && (cwLength.value <= maxCwLength.value) && + (files.value.length <= 16) && (!poll.value || poll.value.choices.length >= 2); }); @@ -624,7 +626,7 @@ async function onPaste(ev: ClipboardEvent) { const paste = ev.clipboardData.getData('text'); - if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) { + if (!renoteTargetNote.value && !quoteId.value && paste.startsWith(url + '/notes/')) { ev.preventDefault(); os.confirm({ @@ -840,7 +842,7 @@ async function post(ev?: MouseEvent) { text: text.value === '' ? null : text.value, fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined, replyId: props.reply ? props.reply.id : undefined, - renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined, + renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : undefined, channelId: props.channel ? props.channel.id : undefined, poll: poll.value, cw: useCw.value ? cw.value ?? '' : null, @@ -930,7 +932,7 @@ async function post(ev?: MouseEvent) { claimAchievement('brainDiver'); } - if (props.renote && (props.renote.userId === $i.id) && text.length > 0) { + if (renoteTargetNote.value && (renoteTargetNote.value.userId === $i.id) && text.length > 0) { claimAchievement('selfQuote'); } @@ -1140,7 +1142,7 @@ onMounted(() => { users.forEach(u => pushVisibleUser(u)); }); } - quoteId.value = init.renote ? init.renote.id : null; + quoteId.value = renoteTargetNote.value ? renoteTargetNote.value.id : null; reactionAcceptance.value = init.reactionAcceptance; if (init.isSchedule) { scheduleNote.value = { diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 11444d8d78..bab7d22112 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -22,7 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> </Sortable> - <p :class="$style.remain">{{ 16 - props.modelValue.length }}/16</p> + <p :class="[$style.remain, { + [$style.exceeded]: props.modelValue.length > 16, + }]">{{ 16 - props.modelValue.length }}/16</p> </div> </template> @@ -239,5 +241,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar margin: 0; padding: 0; font-size: 90%; + + &.exceeded { + color: var(--MI_THEME-error); + } } </style> diff --git a/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue new file mode 100644 index 0000000000..873b276b3d --- /dev/null +++ b/packages/frontend/src/components/MkRemoteEmojiEditDialog.vue @@ -0,0 +1,132 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkWindow + ref="windowEl" + :initialWidth="400" + :initialHeight="500" + :canResize="true" + @close="windowEl?.close()" + @closed="emit('closed')" +> + <template #header>:{{ name }}:</template> + + <div style="display: flex; flex-direction: column; min-height: 100%;"> + <MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;"> + <div class="_gaps_m"> + <div v-if="imgUrl != null" :class="$style.imgs"> + <div style="background: #000;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img" :alt="name"/> + </div> + <div style="background: #222;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img" :alt="name"/> + </div> + <div style="background: #ddd;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img" :alt="name"/> + </div> + <div style="background: #fff;" :class="$style.imgContainer"> + <img :src="imgUrl" :class="$style.img" :alt="name"/> + </div> + </div> + + <MkKeyValue> + <template #key>{{ i18n.ts.id }}</template> + <template #value>{{ name }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.host }}</template> + <template #value>{{ host }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.license }}</template> + <template #value>{{ license }}</template> + </MkKeyValue> + </div> + </MkSpacer> + <div :class="$style.footer"> + <MkButton primary rounded style="margin: 0 auto;" @click="done"> + <i class="ti ti-plus"></i> {{ i18n.ts.import }} + </MkButton> + </div> + </div> +</MkWindow> +</template> + +<script lang="ts" setup> +import { computed, ref } from 'vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; +import MkWindow from '@/components/MkWindow.vue'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; + +const props = defineProps<{ + emoji: { + id: string, + name: string, + host: string, + license: string | null, + url: string + }, +}>(); + +const emit = defineEmits<{ + // å¿…è¦ãªã‚‰æˆ»ã‚Šå€¤ã‚’増や㙠+ (ev: 'done'): void, + (ev: 'closed'): void +}>(); + +const windowEl = ref<InstanceType<typeof MkWindow> | null>(null); + +const name = computed(() => props.emoji.name); +const host = computed(() => props.emoji.host); +const license = computed(() => props.emoji.license); +const imgUrl = computed(() => props.emoji.url); + +async function done() { + await os.apiWithDialog('admin/emoji/copy', { + emojiId: props.emoji.id, + }); + + emit('done'); + windowEl.value?.close(); +} +</script> + +<style lang="scss" module> +.imgs { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} + +.imgContainer { + padding: 8px; + border-radius: 6px; +} + +.img { + display: block; + height: 64px; + width: 64px; + object-fit: contain; +} + +.footer { + position: sticky; + z-index: 10000; + bottom: 0; + left: 0; + padding: 12px; + border-top: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-acrylicBg); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); +} +</style> diff --git a/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts b/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts new file mode 100644 index 0000000000..411d62edf9 --- /dev/null +++ b/packages/frontend/src/components/MkRoleSelectDialog.stories.impl.ts @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import { http, HttpResponse } from 'msw'; +import { role } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkRoleSelectDialog from '@/components/MkRoleSelectDialog.vue'; + +const roles = [ + role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'), role({ displayOrder: 1 }, '1'), + role({ displayOrder: 2 }, '2'), role({ displayOrder: 2 }, '2'), role({ displayOrder: 3 }, '3'), role({ displayOrder: 3 }, '3'), + role({ displayOrder: 4 }, '4'), role({ displayOrder: 5 }, '5'), role({ displayOrder: 6 }, '6'), role({ displayOrder: 7 }, '7'), + role({ displayOrder: 999, name: 'privateRole', isPublic: false }, '999'), +]; + +export const Default = { + render(args) { + return { + components: { + MkRoleSelectDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkRoleSelectDialog v-bind="props" />', + }; + }, + args: { + initialRoleIds: undefined, + infoMessage: undefined, + title: undefined, + publicOnly: true, + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/admin/roles/list', ({ params }) => { + return HttpResponse.json(roles); + }), + ], + }, + }, + decorators: [() => ({ + template: '<div style="width:100cqmin"><story/></div>', + })], +} satisfies StoryObj<typeof MkRoleSelectDialog>; + +export const InitialIds = { + ...Default, + args: { + ...Default.args, + initialRoleIds: [roles[0].id, roles[1].id, roles[4].id, roles[6].id, roles[8].id, roles[10].id], + }, +} satisfies StoryObj<typeof MkRoleSelectDialog>; + +export const InfoMessage = { + ...Default, + args: { + ...Default.args, + infoMessage: 'This is a message.', + }, +} satisfies StoryObj<typeof MkRoleSelectDialog>; + +export const Title = { + ...Default, + args: { + ...Default.args, + title: 'Select roles', + }, +} satisfies StoryObj<typeof MkRoleSelectDialog>; + +export const Full = { + ...Default, + args: { + ...Default.args, + initialRoleIds: roles.map(it => it.id), + infoMessage: InfoMessage.args.infoMessage, + title: Title.args.title, + }, +} satisfies StoryObj<typeof MkRoleSelectDialog>; + +export const FullWithPrivate = { + ...Default, + args: { + ...Default.args, + initialRoleIds: roles.map(it => it.id), + infoMessage: InfoMessage.args.infoMessage, + title: Title.args.title, + publicOnly: false, + }, +} satisfies StoryObj<typeof MkRoleSelectDialog>; diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue new file mode 100644 index 0000000000..32f35ed5ad --- /dev/null +++ b/packages/frontend/src/components/MkRoleSelectDialog.vue @@ -0,0 +1,200 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="windowEl" + :withOkButton="false" + :okButtonDisabled="false" + :width="400" + :height="500" + @close="onCloseModalWindow" + @closed="console.log('MkRoleSelectDialog: closed') ; $emit('dispose')" +> + <template #header>{{ title }}</template> + <MkSpacer :marginMin="20" :marginMax="28"> + <MkLoading v-if="fetching"/> + <div v-else class="_gaps" :class="$style.root"> + <div :class="$style.header"> + <MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + </div> + + <div v-if="selectedRoles.length > 0" class="_gaps" :class="$style.roleItemArea"> + <div v-for="role in selectedRoles" :key="role.id" :class="$style.roleItem"> + <MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/> + <button class="_button" :class="$style.roleUnAssign" @click="removeRole(role.id)"><i class="ti ti-x"></i></button> + </div> + </div> + <div v-else :class="$style.roleItemArea" style="text-align: center"> + {{ i18n.ts._roleSelectDialog.notSelected }} + </div> + + <MkInfo v-if="infoMessage">{{ infoMessage }}</MkInfo> + + <div :class="$style.buttons"> + <MkButton primary @click="onOkClicked">{{ i18n.ts.ok }}</MkButton> + <MkButton @click="onCancelClicked">{{ i18n.ts.cancel }}</MkButton> + </div> + </div> + </MkSpacer> +</MkModalWindow> +</template> + +<script setup lang="ts"> +import { computed, ref, toRefs } from 'vue'; +import * as Misskey from 'misskey-js'; +import { i18n } from '@/i18n.js'; +import MkButton from '@/components/MkButton.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkRolePreview from '@/components/MkRolePreview.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import * as os from '@/os.js'; +import MkSpacer from '@/components/global/MkSpacer.vue'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import MkLoading from '@/components/global/MkLoading.vue'; + +const emit = defineEmits<{ + (ev: 'done', value: Misskey.entities.Role[]), + (ev: 'close'), + (ev: 'dispose'), +}>(); + +const props = withDefaults(defineProps<{ + initialRoleIds?: string[], + infoMessage?: string, + title?: string, + publicOnly: boolean, +}>(), { + initialRoleIds: undefined, + infoMessage: undefined, + title: undefined, + publicOnly: true, +}); + +const { initialRoleIds, infoMessage, title, publicOnly } = toRefs(props); + +const windowEl = ref<InstanceType<typeof MkModalWindow>>(); +const roles = ref<Misskey.entities.Role[]>([]); +const selectedRoleIds = ref<string[]>(initialRoleIds.value ?? []); +const fetching = ref(false); + +const selectedRoles = computed(() => { + const r = roles.value.filter(role => selectedRoleIds.value.includes(role.id)); + r.sort((a, b) => { + if (a.displayOrder !== b.displayOrder) { + return b.displayOrder - a.displayOrder; + } + + return a.id.localeCompare(b.id); + }); + return r; +}); + +async function fetchRoles() { + fetching.value = true; + const result = await misskeyApi('admin/roles/list', {}); + roles.value = result.filter(it => publicOnly.value ? it.isPublic : true); + fetching.value = false; +} + +async function addRole() { + const items = roles.value + .filter(r => r.isPublic) + .filter(r => !selectedRoleIds.value.includes(r.id)) + .map(r => ({ text: r.name, value: r })); + + const { canceled, result: role } = await os.select({ items }); + if (canceled) { + return; + } + + selectedRoleIds.value.push(role.id); +} + +async function removeRole(roleId: string) { + selectedRoleIds.value = selectedRoleIds.value.filter(x => x !== roleId); +} + +function onOkClicked() { + emit('done', selectedRoles.value); + windowEl.value?.close(); +} + +function onCancelClicked() { + emit('close'); + windowEl.value?.close(); +} + +function onCloseModalWindow() { + emit('close'); + windowEl.value?.close(); +} + +fetchRoles(); +</script> + +<style module lang="scss"> +.root { + max-height: 410px; + height: 410px; + display: flex; + flex-direction: column; +} + +.roleItemArea { + background-color: var(--MI_THEME-acrylicBg); + border-radius: var(--MI-radius); + padding: 12px; + overflow-y: auto; +} + +.roleItem { + display: flex; +} + +.role { + flex: 1; +} + +.roleUnAssign { + width: 32px; + height: 32px; + margin-left: 8px; + align-self: center; +} + +.header { + display: flex; + align-items: center; + justify-content: flex-start; +} + +.title { + flex: 1; +} + +.addRoleButton { + min-width: 32px; + min-height: 32px; + max-width: 32px; + max-height: 32px; + margin-left: 8px; + align-self: center; + padding: 0; +} + +.buttons { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + margin-top: auto; +} + +.divider { + border-top: solid 0.5px var(--MI_THEME-divider); +} + +</style> diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue index 34c22abc31..e98ac9cfd2 100644 --- a/packages/frontend/src/components/MkSignin.input.vue +++ b/packages/frontend/src/components/MkSignin.input.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { ref } from 'vue'; -import { toUnicode } from 'punycode/'; +import { toUnicode } from 'punycode.js'; import { query, extractDomain } from '@@/js/url.js'; import { host as configHost } from '@@/js/config.js'; diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index e636712389..3560bebace 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -85,7 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; -import { toUnicode } from 'punycode/'; +import { toUnicode } from 'punycode.js'; import * as Misskey from 'misskey-js'; import * as config from '@@/js/config.js'; import MkButton from './MkButton.vue'; diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 06481b808c..d1685c6990 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -10,8 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps_m"> - <div v-if="instance.disableRegistration"> - <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> + <div v-if="instance.disableRegistration || instance.federation !== 'all'" class="_gaps_s"> + <MkInfo v-if="instance.disableRegistration" warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> + <MkInfo v-if="instance.federation === 'specified'" warn>{{ i18n.ts.federationSpecified }}</MkInfo> + <MkInfo v-else-if="instance.federation === 'none'" warn>{{ i18n.ts.federationDisabled }}</MkInfo> </div> <div style="text-align: center;"> diff --git a/packages/frontend/src/components/MkSortOrderEditor.define.ts b/packages/frontend/src/components/MkSortOrderEditor.define.ts new file mode 100644 index 0000000000..f023b5d72b --- /dev/null +++ b/packages/frontend/src/components/MkSortOrderEditor.define.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type SortOrderDirection = '+' | '-' + +export type SortOrder<T extends string> = { + key: T; + direction: SortOrderDirection; +} diff --git a/packages/frontend/src/components/MkSortOrderEditor.vue b/packages/frontend/src/components/MkSortOrderEditor.vue new file mode 100644 index 0000000000..9decacc5f5 --- /dev/null +++ b/packages/frontend/src/components/MkSortOrderEditor.vue @@ -0,0 +1,118 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.sortOrderArea"> + <div :class="$style.sortOrderAreaTags"> + <MkTagItem + v-for="order in currentOrders" + :key="order.key" + :iconClass="order.direction === '+' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'" + :exButtonIconClass="'ti ti-x'" + :content="order.key" + :class="$style.sortOrderTag" + @click="onToggleSortOrderButtonClicked(order)" + @exButtonClick="onRemoveSortOrderButtonClicked(order)" + /> + </div> + <MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked"> + <span class="ti ti-plus"></span> + </MkButton> +</div> +</template> + +<script setup lang="ts" generic="T extends string"> +import { toRefs } from 'vue'; +import MkTagItem from '@/components/MkTagItem.vue'; +import MkButton from '@/components/MkButton.vue'; +import { MenuItem } from '@/types/menu.js'; +import * as os from '@/os.js'; +import { SortOrder } from '@/components/MkSortOrderEditor.define.js'; + +const emit = defineEmits<{ + (ev: 'update', sortOrders: SortOrder<T>[]): void; +}>(); + +const props = defineProps<{ + baseOrderKeyNames: T[]; + currentOrders: SortOrder<T>[]; +}>(); + +const { currentOrders } = toRefs(props); + +function onToggleSortOrderButtonClicked(order: SortOrder<T>) { + switch (order.direction) { + case '+': + order.direction = '-'; + break; + case '-': + order.direction = '+'; + break; + } + + emitOrder(currentOrders.value); +} + +function onAddSortOrderButtonClicked(ev: MouseEvent) { + const menuItems: MenuItem[] = props.baseOrderKeyNames + .filter(baseKey => !currentOrders.value.map(it => it.key).includes(baseKey)) + .map(it => { + return { + text: it, + action: () => { + emitOrder([...currentOrders.value, { key: it, direction: '+' }]); + }, + }; + }); + os.contextMenu(menuItems, ev); +} + +function onRemoveSortOrderButtonClicked(order: SortOrder<T>) { + emitOrder(currentOrders.value.filter(it => it.key !== order.key)); +} + +function emitOrder(sortOrders: SortOrder<T>[]) { + emit('update', sortOrders); +} + +</script> + +<style module lang="scss"> +.sortOrderArea { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; +} + +.sortOrderAreaTags { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + flex-wrap: wrap; + gap: 8px; +} + +.sortOrderAddButton { + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + min-width: 2.0em; + min-height: 2.0em; + max-width: 2.0em; + max-height: 2.0em; + padding: 8px; + margin-left: auto; + border-radius: 9999px; + background-color: var(--MI_THEME-buttonBg); +} + +.sortOrderTag { + user-select: none; + cursor: pointer; +} +</style> diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index a32fd53c51..145de3b9d3 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only </details> <details v-if="note.poll"> <summary>{{ i18n.ts.poll }}</summary> - <MkPoll :noteId="note.id" :poll="note.poll"/> + <MkPoll :noteId="note.id" :poll="note.poll" :author="note.user" :emojiUrls="note.emojis"/> </details> <button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click.stop="collapsed = false"> <span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span> @@ -42,11 +42,11 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed, watch } from 'vue'; import * as Misskey from 'misskey-js'; import * as mfm from '@transfem-org/sfm-js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; import MkMediaList from '@/components/MkMediaList.vue'; import MkPoll from '@/components/MkPoll.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { shouldCollapsed } from '@@/js/collapsed.js'; import { defaultStore } from '@/store.js'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index c9c173aa35..56e8fcfa37 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -47,7 +47,7 @@ export type SuperMenuDef = { active?: boolean; action: (ev: MouseEvent) => void; } | { - type: 'link'; + type?: 'link'; to: string; icon?: string; text: string; diff --git a/packages/frontend/src/components/MkTagItem.stories.impl.ts b/packages/frontend/src/components/MkTagItem.stories.impl.ts new file mode 100644 index 0000000000..3f243ff651 --- /dev/null +++ b/packages/frontend/src/components/MkTagItem.stories.impl.ts @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import MkTagItem from './MkTagItem.vue'; + +export const Default = { + render(args) { + return { + components: { + MkTagItem: MkTagItem, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + click: action('click'), + exButtonClick: action('exButtonClick'), + }; + }, + }, + template: '<MkTagItem v-bind="props" v-on="events"></MkTagItem>', + }; + }, + args: { + content: 'name', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkTagItem>; + +export const Icon = { + ...Default, + args: { + ...Default.args, + iconClass: 'ti ti-arrow-up', + }, +} satisfies StoryObj<typeof MkTagItem>; + +export const ExButton = { + ...Default, + args: { + ...Default.args, + exButtonIconClass: 'ti ti-x', + }, +} satisfies StoryObj<typeof MkTagItem>; + +export const IconExButton = { + ...Default, + args: { + ...Default.args, + iconClass: 'ti ti-arrow-up', + exButtonIconClass: 'ti ti-x', + }, +} satisfies StoryObj<typeof MkTagItem>; diff --git a/packages/frontend/src/components/MkTagItem.vue b/packages/frontend/src/components/MkTagItem.vue new file mode 100644 index 0000000000..8b7460f3a3 --- /dev/null +++ b/packages/frontend/src/components/MkTagItem.vue @@ -0,0 +1,76 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root" @click="(ev) => emit('click', ev)"> + <span v-if="iconClass" :class="[$style.icon, iconClass]"></span> + <span :class="$style.content">{{ content }}</span> + <MkButton v-if="exButtonIconClass" :class="$style.exButton" @click="(ev) => emit('exButtonClick', ev)"> + <span :class="[$style.exButtonIcon, exButtonIconClass]"></span> + </MkButton> +</div> +</template> + +<script setup lang="ts"> +import MkButton from '@/components/MkButton.vue'; + +const emit = defineEmits<{ + (ev: 'click', payload: MouseEvent): void; + (ev: 'exButtonClick', payload: MouseEvent): void; +}>(); + +defineProps<{ + iconClass?: string; + content: string; + exButtonIconClass?: string +}>(); +</script> + +<style module lang="scss"> +$buttonSize : 1.8em; + +.root { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 9999px; + padding: 4px 6px; + gap: 3px; + + background-color: var(--MI_THEME-buttonBg); + + &:hover { + background-color: var(--MI_THEME-buttonHoverBg); + } +} + +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.70em; +} + +.exButton { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 9999px; + max-height: $buttonSize; + max-width: $buttonSize; + min-height: $buttonSize; + min-width: $buttonSize; + padding: 0; + box-sizing: border-box; + font-size: 0.65em; +} + +.exButtonIcon { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.80em; +} +</style> diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index 85d4666172..420146f80a 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header>{{ i18n.ts.selectUser }}</template> <div> <div :class="$style.form"> - <MkInput v-if="localOnly" v-model="username" :autofocus="true" @update:modelValue="search"> + <MkInput v-if="computedLocalOnly" v-model="username" :autofocus="true" @update:modelValue="search"> <template #label>{{ i18n.ts.username }}</template> <template #prefix>@</template> </MkInput> @@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef } from 'vue'; +import { onMounted, ref, computed, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import FormSplit from '@/components/form/split.vue'; @@ -70,6 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; import { host as currentHost, hostname } from '@@/js/config.js'; const emit = defineEmits<{ @@ -86,6 +87,8 @@ const props = withDefaults(defineProps<{ localOnly: false, }); +const computedLocalOnly = computed(() => props.localOnly || instance.federation === 'none'); + const username = ref(''); const host = ref(''); const users = ref<Misskey.entities.UserLite[]>([]); @@ -101,7 +104,7 @@ function search() { misskeyApi('users/search-by-username-and-host', { username: username.value, - host: props.localOnly ? '.' : host.value, + host: computedLocalOnly.value ? '.' : host.value, limit: 10, detail: false, }).then(_users => { @@ -143,7 +146,7 @@ onMounted(() => { }).then(foundUsers => { let _users = foundUsers; _users = _users.filter((u) => { - if (props.localOnly) { + if (computedLocalOnly.value) { return u.host == null; } else { return true; diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index 54f2ee655c..6d2a44e985 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -18,8 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- eslint-disable-next-line vue/no-v-html --> <div v-html="sanitizeHtml(instance.description) || i18n.ts.headlineMisskey"></div> </div> - <div v-if="instance.disableRegistration" :class="$style.mainWarn"> - <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> + <div v-if="instance.disableRegistration || instance.federation !== 'all'" :class="$style.mainWarn" class="_gaps_s"> + <MkInfo v-if="instance.disableRegistration" warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> + <MkInfo v-if="instance.federation === 'specified'" warn>{{ i18n.ts.federationSpecified }}</MkInfo> + <MkInfo v-else-if="instance.federation === 'none'" warn>{{ i18n.ts.federationDisabled }}</MkInfo> </div> <div v-if="instance.approvalRequiredForSignup" :class="$style.mainWarn"> <MkInfo warn>{{ i18n.ts.approvalRequiredToRegister }}</MkInfo> diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index b987283a65..3446e3d6e2 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <header :class="$style.editHeader"> <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select> <template #label>{{ i18n.ts.selectWidget }}</template> - <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option> + <option v-for="widget in _widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option> </MkSelect> <MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton> @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </Sortable> </template> - <component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> + <component :is="`widget-${widget.name}`" v-for="widget in _widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> </div> </template> @@ -50,13 +50,14 @@ export type DefaultStoredWidget = { </script> <script lang="ts" setup> -import { defineAsyncComponent, ref } from 'vue'; +import { defineAsyncComponent, ref, computed } from 'vue'; import { v4 as uuid } from 'uuid'; import MkSelect from '@/components/MkSelect.vue'; import MkButton from '@/components/MkButton.vue'; -import { widgets as widgetDefs } from '@/widgets/index.js'; +import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; import { isLink } from '@@/js/is-link.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -66,6 +67,16 @@ const props = defineProps<{ edit: boolean; }>(); +const _widgetDefs = computed(() => { + if (instance.federation === 'none') { + return widgetDefs.filter(x => !federationWidgets.includes(x)); + } else { + return widgetDefs; + } +}); + +const _widgets = computed(() => props.widgets.filter(x => _widgetDefs.value.includes(x.name))); + const emit = defineEmits<{ (ev: 'updateWidgets', widgets: Widget[]): void; (ev: 'addWidget', widget: Widget): void; diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue index 9a1ac3aca2..2f4141b901 100644 --- a/packages/frontend/src/components/global/MkAcct.vue +++ b/packages/frontend/src/components/global/MkAcct.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { toUnicode } from 'punycode/'; +import { toUnicode } from 'punycode.js'; import { host as hostRaw } from '@@/js/config.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 18c97b1bdb..1a424f349f 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -57,13 +57,16 @@ import { scrollToTop } from '@@/js/scroll.js'; import { globalEvents } from '@/events.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { PageHeaderItem } from '@/types/page-header.js'; +import type { PageHeaderItem } from '@/types/page-header.js'; +import type { PageMetadata } from '@/scripts/page-metadata.js'; const props = withDefaults(defineProps<{ + overridePageMetadata?: PageMetadata; tabs?: Tab[]; tab?: string; actions?: PageHeaderItem[] | null; thin?: boolean; + hideTitle?: boolean; displayMyAvatar?: boolean; displayBackButton?: boolean; }>(), { @@ -76,9 +79,10 @@ const emit = defineEmits<{ const displayBackButton = props.displayBackButton && history.state.key !== 'index' && history.length > 1 && inject('shouldBackButton', true); -const pageMetadata = injectReactiveMetadata(); +const injectedPageMetadata = injectReactiveMetadata(); +const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value); -const hideTitle = inject('shouldOmitHeaderTitle', false); +const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props.hideTitle); const thin_ = props.thin || inject('shouldHeaderThin', false); const el = shallowRef<HTMLElement | undefined>(undefined); @@ -87,7 +91,7 @@ const narrow = ref(false); const hasTabs = computed(() => props.tabs.length > 0); const hasActions = computed(() => props.actions && props.actions.length > 0); const show = computed(() => { - return !hideTitle || hasTabs.value || hasActions.value; + return !hideTitle.value || hasTabs.value || hasActions.value; }); const preventDrag = (ev: TouchEvent) => { diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 8cca47c1db..5196a63635 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; -import { toUnicode as decodePunycode } from 'punycode/'; +import { toUnicode as decodePunycode } from 'punycode.js'; import { url as local } from '@@/js/config.js'; import * as os from '@/os.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; diff --git a/packages/frontend/src/components/grid/MkCellTooltip.vue b/packages/frontend/src/components/grid/MkCellTooltip.vue new file mode 100644 index 0000000000..fd289c6cd9 --- /dev/null +++ b/packages/frontend/src/components/grid/MkCellTooltip.vue @@ -0,0 +1,35 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkTooltip ref="tooltip" :showing="showing" :targetElement="targetElement" :maxWidth="250" @closed="emit('closed')"> + <div :class="$style.root"> + {{ content }} + </div> +</MkTooltip> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import MkTooltip from '@/components/MkTooltip.vue'; + +defineProps<{ + showing: boolean; + content: string; + targetElement: HTMLElement; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); +</script> + +<style lang="scss" module> +.root { + font-size: 0.9em; + text-align: left; + text-wrap: normal; +} +</style> diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue new file mode 100644 index 0000000000..e473b7c1af --- /dev/null +++ b/packages/frontend/src/components/grid/MkDataCell.vue @@ -0,0 +1,418 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + v-if="cell.row.using" + ref="rootEl" + class="mk_grid_td" + :class="$style.cell" + :style="{ maxWidth: cellWidth, minWidth: cellWidth }" + :tabindex="-1" + data-grid-cell + :data-grid-cell-row="cell.row.index" + :data-grid-cell-col="cell.column.index" + @keydown="onCellKeyDown" + @dblclick.prevent="onCellDoubleClick" +> + <div + :class="[ + $style.root, + [(cell.violation.valid || cell.selected) ? {} : $style.error], + [cell.selected ? $style.selected : {}], + // 行ãŒé¸æŠžã•れã¦ã„ã‚‹ã¨ãã¯ç¯„å›²é¸æŠžè‰²ã®é©ç”¨ã‚’行å´ã«ä»»ã›ã‚‹ + [(cell.ranged && !cell.row.ranged) ? $style.ranged : {}], + [needsContentCentering ? $style.center : {}], + ]" + > + <div v-if="!editing" :class="[$style.contentArea]" :style="cellType === 'boolean' ? 'justify-content: center' : ''"> + <div ref="contentAreaEl" :class="$style.content"> + <div v-if="cellType === 'text'"> + {{ cell.value }} + </div> + <div v-if="cellType === 'number'"> + {{ cell.value }} + </div> + <div v-if="cellType === 'date'"> + {{ cell.value }} + </div> + <div v-else-if="cellType === 'boolean'"> + <div :class="[$style.bool, { + [$style.boolTrue]: cell.value === true, + 'ti ti-check': cell.value === true, + }]"></div> + </div> + <div v-else-if="cellType === 'image'"> + <img + :src="cell.value" + :alt="cell.value" + :class="$style.viewImage" + @load="emitContentSizeChanged" + /> + </div> + </div> + </div> + <div v-else ref="inputAreaEl" :class="$style.inputArea"> + <input + v-if="cellType === 'text'" + type="text" + :class="$style.editingInput" + :value="editingValue" + @input="onInputText" + @mousedown.stop + @contextmenu.stop + /> + <input + v-if="cellType === 'number'" + type="number" + :class="$style.editingInput" + :value="editingValue" + @input="onInputText" + @mousedown.stop + @contextmenu.stop + /> + <input + v-if="cellType === 'date'" + type="date" + :class="$style.editingInput" + :value="editingValue" + @input="onInputText" + @mousedown.stop + @contextmenu.stop + /> + </div> + </div> +</div> +</template> + +<script setup lang="ts"> +import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue'; +import { GridEventEmitter, Size } from '@/components/grid/grid.js'; +import { useTooltip } from '@/scripts/use-tooltip.js'; +import * as os from '@/os.js'; +import { CellValue, GridCell } from '@/components/grid/cell.js'; +import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js'; +import { GridRowSetting } from '@/components/grid/row.js'; + +const emit = defineEmits<{ + (ev: 'operation:beginEdit', sender: GridCell): void; + (ev: 'operation:endEdit', sender: GridCell): void; + (ev: 'change:value', sender: GridCell, newValue: CellValue): void; + (ev: 'change:contentSize', sender: GridCell, newSize: Size): void; +}>(); +const props = defineProps<{ + cell: GridCell, + rowSetting: GridRowSetting, + bus: GridEventEmitter, +}>(); + +const { cell, bus } = toRefs(props); + +const rootEl = shallowRef<InstanceType<typeof HTMLTableCellElement>>(); +const contentAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>(); +const inputAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>(); + +/** 値ãŒç·¨é›†ä¸ã‹ã©ã†ã‹ */ +const editing = ref<boolean>(false); +/** 編集ä¸ã®å€¤. {@link beginEditing}ã¨{@link endEditing}内ã€ãŠã‚ˆã³å„inputã‚¿ã‚°ã‚„ãã®ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯ã‹ã‚‰ã®æ“作ã®ã¿ã‚’想定ã™ã‚‹ */ +const editingValue = ref<CellValue>(undefined); + +const cellWidth = computed(() => cell.value.column.width); +const cellType = computed(() => cell.value.column.setting.type); +const needsContentCentering = computed(() => { + switch (cellType.value) { + case 'boolean': + return true; + default: + return false; + } +}); + +watch(() => [cell.value.value], () => { + // ä¸èº«ãŒã‚»ãƒƒãƒˆã•れãŸç›´å¾Œã¯ã‚µã‚¤ã‚ºãŒåˆ†ã‹ã‚‰ãªã„ã®ã§ã€æ¬¡ã®ã‚¿ã‚¤ãƒŸãƒ³ã‚°ã§æ›´æ–°ã™ã‚‹ + nextTick(emitContentSizeChanged); +}, { immediate: true }); + +watch(() => cell.value.selected, () => { + if (cell.value.selected) { + requestFocus(); + } +}); + +function onCellDoubleClick(ev: MouseEvent) { + switch (ev.type) { + case 'dblclick': { + beginEditing(ev.target as HTMLElement); + break; + } + } +} + +function onOutsideMouseDown(ev: MouseEvent) { + const isOutside = ev.target instanceof Node && !rootEl.value?.contains(ev.target); + if (isOutside || !equalCellAddress(cell.value.address, getCellAddress(ev.target as HTMLElement))) { + endEditing(true, false); + } +} + +function onCellKeyDown(ev: KeyboardEvent) { + if (!editing.value) { + ev.preventDefault(); + switch (ev.code) { + case 'NumpadEnter': + case 'Enter': + case 'F2': { + beginEditing(ev.target as HTMLElement); + break; + } + } + } else { + switch (ev.code) { + case 'Escape': { + endEditing(false, true); + break; + } + case 'NumpadEnter': + case 'Enter': { + if (!ev.isComposing) { + endEditing(true, true); + } + } + } + } +} + +function onInputText(ev: Event) { + editingValue.value = (ev.target as HTMLInputElement).value; +} + +function onForceRefreshContentSize() { + emitContentSizeChanged(); +} + +function registerOutsideMouseDown() { + unregisterOutsideMouseDown(); + addEventListener('mousedown', onOutsideMouseDown); +} + +function unregisterOutsideMouseDown() { + removeEventListener('mousedown', onOutsideMouseDown); +} + +async function beginEditing(target: HTMLElement) { + if (editing.value || !cell.value.selected || !cell.value.column.setting.editable) { + return; + } + + if (cell.value.column.setting.customValueEditor) { + emit('operation:beginEdit', cell.value); + const newValue = await cell.value.column.setting.customValueEditor( + cell.value.row, + cell.value.column, + cell.value.value, + target, + ); + emit('operation:endEdit', cell.value); + + if (newValue !== cell.value.value) { + emitValueChange(newValue); + } + + requestFocus(); + } else { + switch (cellType.value) { + case 'number': + case 'date': + case 'text': { + editingValue.value = cell.value.value; + editing.value = true; + registerOutsideMouseDown(); + emit('operation:beginEdit', cell.value); + + await nextTick(() => { + // inputã®å±•開後ã«ãƒ•ォーカスを当ã¦ãŸã„ + if (inputAreaEl.value) { + (inputAreaEl.value.querySelector('*') as HTMLElement).focus(); + } + }); + break; + } + case 'boolean': { + // ã¨ãã«ç‰¹æ®ŠãªUIã¯è¨ã‘ãšã€ãƒˆã‚°ãƒ«ã™ã‚‹ã ã‘ + emitValueChange(!cell.value.value); + break; + } + } + } +} + +function endEditing(applyValue: boolean, requireFocus: boolean) { + if (!editing.value) { + return; + } + + const newValue = editingValue.value; + editingValue.value = undefined; + + emit('operation:endEdit', cell.value); + unregisterOutsideMouseDown(); + + if (applyValue && newValue !== cell.value.value) { + emitValueChange(newValue); + } + + editing.value = false; + + if (requireFocus) { + requestFocus(); + } +} + +function requestFocus() { + nextTick(() => { + rootEl.value?.focus(); + }); +} + +function emitValueChange(newValue: CellValue) { + const _cell = cell.value; + emit('change:value', _cell, newValue); +} + +function emitContentSizeChanged() { + emit('change:contentSize', cell.value, { + width: contentAreaEl.value?.clientWidth ?? 0, + height: contentAreaEl.value?.clientHeight ?? 0, + }); +} + +useTooltip(rootEl, (showing) => { + if (cell.value.violation.valid) { + return; + } + + const content = cell.value.violation.violations.filter(it => !it.valid).map(it => it.result.message).join('\n'); + const result = os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), { + showing, + content, + targetElement: rootEl.value!, + }, { + closed: () => { + result.dispose(); + }, + }); +}); + +onMounted(() => { + bus.value.on('forceRefreshContentSize', onForceRefreshContentSize); +}); + +onUnmounted(() => { + bus.value.off('forceRefreshContentSize', onForceRefreshContentSize); +}); + +</script> + +<style module lang="scss"> +$cellHeight: 28px; + +.cell { + overflow: hidden; + white-space: nowrap; + height: $cellHeight; + max-height: $cellHeight; + min-height: $cellHeight; + cursor: cell; + + &:focus { + outline: none; + } +} + +.root { + display: flex; + flex-direction: row; + align-items: center; + box-sizing: border-box; + height: 100%; + + // selectedé©ç”¨æ™‚ã«ä¸èº«ãŒã‚ºãƒ¬ã¦ã—ã¾ã†ã®ã§ã€é€æ˜Žã®ç·šã‚’ã‚らã‹ã˜ã‚引ã„ã¦ãŠããŸã„ + border: solid 0.5px transparent; + + &.selected { + border: solid 0.5px var(--MI_THEME-accentLighten); + } + + &.ranged { + background-color: var(--MI_THEME-accentedBg); + } + + &.center { + justify-content: center; + } + + &.error { + border: solid 0.5px var(--MI_THEME-error); + } +} + +.contentArea, .inputArea { + display: flex; + align-items: center; + width: 100%; + max-width: 100%; +} + +.content { + display: inline-block; + padding: 0 8px; +} + +.viewImage { + width: auto; + max-height: $cellHeight; + height: $cellHeight; + object-fit: cover; +} + +.bool { + position: relative; + width: 18px; + height: 18px; + background: var(--MI_THEME-panel); + border: solid 2px var(--MI_THEME-divider); + border-radius: 4px; + box-sizing: border-box; + + &.boolTrue { + border-color: var(--MI_THEME-accent); + background: var(--MI_THEME-accent); + + &::before { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--MI_THEME-fgOnAccent); + font-size: 12px; + line-height: 18px; + } + } +} + +.editingInput { + padding: 0 8px; + width: 100%; + max-width: 100%; + box-sizing: border-box; + min-height: $cellHeight - 2; + max-height: $cellHeight - 2; + height: $cellHeight - 2; + outline: none; + border: none; + font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; +} + +</style> diff --git a/packages/frontend/src/components/grid/MkDataRow.vue b/packages/frontend/src/components/grid/MkDataRow.vue new file mode 100644 index 0000000000..280a14bc4a --- /dev/null +++ b/packages/frontend/src/components/grid/MkDataRow.vue @@ -0,0 +1,72 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + class="mk_grid_tr" + :class="[ + $style.row, + row.ranged ? $style.ranged : {}, + ...(row.additionalStyles ?? []).map(it => it.className ?? {}), + ]" + :style="[ + ...(row.additionalStyles ?? []).map(it => it.style ?? {}), + ]" + :data-grid-row="row.index" +> + <MkNumberCell + v-if="setting.showNumber" + :content="(row.index + 1).toString()" + :row="row" + /> + <MkDataCell + v-for="cell in cells" + :key="cell.address.col" + :vIf="cell.column.setting.type !== 'hidden'" + :cell="cell" + :rowSetting="setting" + :bus="bus" + @operation:beginEdit="(sender) => emit('operation:beginEdit', sender)" + @operation:endEdit="(sender) => emit('operation:endEdit', sender)" + @change:value="(sender, newValue) => emit('change:value', sender, newValue)" + @change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)" + /> +</div> +</template> + +<script setup lang="ts"> +import { GridEventEmitter, Size } from '@/components/grid/grid.js'; +import MkDataCell from '@/components/grid/MkDataCell.vue'; +import MkNumberCell from '@/components/grid/MkNumberCell.vue'; +import { CellValue, GridCell } from '@/components/grid/cell.js'; +import { GridRow, GridRowSetting } from '@/components/grid/row.js'; + +const emit = defineEmits<{ + (ev: 'operation:beginEdit', sender: GridCell): void; + (ev: 'operation:endEdit', sender: GridCell): void; + (ev: 'change:value', sender: GridCell, newValue: CellValue): void; + (ev: 'change:contentSize', sender: GridCell, newSize: Size): void; +}>(); +defineProps<{ + row: GridRow, + cells: GridCell[], + setting: GridRowSetting, + bus: GridEventEmitter, +}>(); + +</script> + +<style module lang="scss"> +.row { + display: flex; + flex-direction: row; + align-items: center; + width: fit-content; + + &.ranged { + background-color: var(--MI_THEME-accentedBg); + } +} +</style> diff --git a/packages/frontend/src/components/grid/MkGrid.stories.impl.ts b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts new file mode 100644 index 0000000000..5801012f15 --- /dev/null +++ b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts @@ -0,0 +1,223 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { ref } from 'vue'; +import { commonHandlers } from '../../../.storybook/mocks.js'; +import { boolean, choose, country, date, firstName, integer, lastName, text } from '../../../.storybook/fake-utils.js'; +import MkGrid from './MkGrid.vue'; +import { GridContext, GridEvent } from '@/components/grid/grid-event.js'; +import { DataSource, GridSetting } from '@/components/grid/grid.js'; +import { GridColumnSetting } from '@/components/grid/column.js'; + +function d(p: { + check?: boolean, + name?: string, + email?: string, + age?: number, + birthday?: string, + gender?: string, + country?: string, + reportCount?: number, + createdAt?: string, +}, seed: string) { + const prefix = text(10, seed); + + return { + check: p.check ?? boolean(seed), + name: p.name ?? `${firstName(seed)} ${lastName(seed)}`, + email: p.email ?? `${prefix}@example.com`, + age: p.age ?? integer(20, 80, seed), + birthday: date({}, seed).toISOString(), + gender: p.gender ?? choose(['male', 'female', 'other', 'unknown'], seed), + country: p.country ?? country(seed), + reportCount: p.reportCount ?? integer(0, 9999, seed), + createdAt: p.createdAt ?? date({}, seed).toISOString(), + }; +} + +const defaultCols: GridColumnSetting[] = [ + { bindTo: 'check', icon: 'ti-check', type: 'boolean', width: 50 }, + { bindTo: 'name', title: 'Name', type: 'text', width: 'auto' }, + { bindTo: 'email', title: 'Email', type: 'text', width: 'auto' }, + { bindTo: 'age', title: 'Age', type: 'number', width: 50 }, + { bindTo: 'birthday', title: 'Birthday', type: 'date', width: 'auto' }, + { bindTo: 'gender', title: 'Gender', type: 'text', width: 80 }, + { bindTo: 'country', title: 'Country', type: 'text', width: 120 }, + { bindTo: 'reportCount', title: 'ReportCount', type: 'number', width: 'auto' }, + { bindTo: 'createdAt', title: 'CreatedAt', type: 'date', width: 'auto' }, +]; + +function createArgs(overrides?: { settings?: Partial<GridSetting>, data?: DataSource[] }) { + const refData = ref<ReturnType<typeof d>[]>([]); + for (let i = 0; i < 100; i++) { + refData.value.push(d({}, i.toString())); + } + + return { + settings: { + row: overrides?.settings?.row, + cols: [ + ...defaultCols.filter(col => overrides?.settings?.cols?.every(c => c.bindTo !== col.bindTo) ?? true), + ...overrides?.settings?.cols ?? [], + ], + cells: overrides?.settings?.cells, + }, + data: refData.value, + }; +} + +function createRender(params: { settings: GridSetting, data: DataSource[] }) { + return { + render(args) { + return { + components: { + MkGrid, + }, + setup() { + return { + args, + }; + }, + data() { + return { + data: args.data, + }; + }, + computed: { + props() { + return { + ...args, + }; + }, + events() { + return { + event: (event: GridEvent, context: GridContext) => { + switch (event.type) { + case 'cell-value-change': { + args.data[event.row.index][event.column.setting.bindTo] = event.newValue; + } + } + }, + }; + }, + }, + template: '<div style="padding:20px"><MkGrid v-bind="props" v-on="events" /></div>', + }; + }, + args: { + ...params, + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + ], + }, + }, + } satisfies StoryObj<typeof MkGrid>; +} + +export const Default = createRender(createArgs()); + +export const NoNumber = createRender(createArgs({ + settings: { + row: { + showNumber: false, + }, + }, +})); + +export const NoSelectable = createRender(createArgs({ + settings: { + row: { + selectable: false, + }, + }, +})); + +export const Editable = createRender(createArgs({ + settings: { + cols: defaultCols.map(col => ({ ...col, editable: true })), + }, +})); + +export const AdditionalRowStyle = createRender(createArgs({ + settings: { + cols: defaultCols.map(col => ({ ...col, editable: true })), + row: { + styleRules: [ + { + condition: ({ row }) => AdditionalRowStyle.args.data[row.index].check as boolean, + applyStyle: { + style: { + backgroundColor: 'lightgray', + }, + }, + }, + ], + }, + }, +})); + +export const ContextMenu = createRender(createArgs({ + settings: { + cols: [ + { + bindTo: 'check', icon: 'ti-check', type: 'boolean', width: 50, contextMenuFactory: (col, context) => [ + { + type: 'button', + text: 'Check All', + action: () => { + for (const d of ContextMenu.args.data) { + d.check = true; + } + }, + }, + { + type: 'button', + text: 'Uncheck All', + action: () => { + for (const d of ContextMenu.args.data) { + d.check = false; + } + }, + }, + ], + }, + ], + row: { + contextMenuFactory: (row, context) => [ + { + type: 'button', + text: 'Delete', + action: () => { + const idxes = context.rangedRows.map(r => r.index); + const newData = ContextMenu.args.data.filter((d, i) => !idxes.includes(i)); + + ContextMenu.args.data.splice(0); + ContextMenu.args.data.push(...newData); + }, + }, + ], + }, + cells: { + contextMenuFactory: (col, row, value, context) => [ + { + type: 'button', + text: 'Delete', + action: () => { + for (const cell of context.rangedCells) { + ContextMenu.args.data[cell.row.index][cell.column.setting.bindTo] = undefined; + } + }, + }, + ], + }, + }, +})); diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue new file mode 100644 index 0000000000..4dbd4ebcae --- /dev/null +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -0,0 +1,1374 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + ref="rootEl" + class="mk_grid_border" + :class="[$style.grid, { + [$style.noOverflowHandling]: rootSetting.noOverflowStyle, + 'mk_grid_root_rounded': rootSetting.rounded, + 'mk_grid_root_border': rootSetting.outerBorder, + }]" + @mousedown.prevent="onMouseDown" + @keydown="onKeyDown" + @contextmenu.prevent.stop="onContextMenu" +> + <div class="mk_grid_thead"> + <MkHeaderRow + :columns="columns" + :gridSetting="rowSetting" + :bus="bus" + @operation:beginWidthChange="onHeaderCellWidthBeginChange" + @operation:endWidthChange="onHeaderCellWidthEndChange" + @operation:widthLargest="onHeaderCellWidthLargest" + @change:width="onHeaderCellChangeWidth" + @change:contentSize="onHeaderCellChangeContentSize" + /> + </div> + <div class="mk_grid_tbody"> + <MkDataRow + v-for="row in rows" + v-show="row.using" + :key="row.index" + :row="row" + :cells="cells[row.index].cells" + :setting="rowSetting" + :bus="bus" + :using="row.using" + :class="[lastLine === row.index ? 'last_row' : '']" + @operation:beginEdit="onCellEditBegin" + @operation:endEdit="onCellEditEnd" + @change:value="onChangeCellValue" + @change:contentSize="onChangeCellContentSize" + /> + </div> +</div> +</template> + +<script setup lang="ts"> +import { computed, onMounted, ref, toRefs, watch } from 'vue'; +import { DataSource, GridEventEmitter, GridSetting, GridState, Size } from '@/components/grid/grid.js'; +import MkDataRow from '@/components/grid/MkDataRow.vue'; +import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; +import { cellValidation } from '@/components/grid/cell-validators.js'; +import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell, resetCell } from '@/components/grid/cell.js'; +import { + copyGridDataToClipboard, + equalCellAddress, + getCellAddress, + getCellElement, + pasteToGridFromClipboard, + removeDataFromGrid, +} from '@/components/grid/grid-utils.js'; +import { MenuItem } from '@/types/menu.js'; +import * as os from '@/os.js'; +import { GridContext, GridEvent } from '@/components/grid/grid-event.js'; +import { createColumn, GridColumn } from '@/components/grid/column.js'; +import { createRow, defaultGridRowSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js'; +import { handleKeyEvent } from '@/scripts/key-event.js'; + +type RowHolder = { + row: GridRow, + cells: GridCell[], + origin: DataSource, +} + +const emit = defineEmits<{ + (ev: 'event', event: GridEvent, context: GridContext): void; +}>(); + +const props = defineProps<{ + settings: GridSetting; + data: DataSource[]; +}>(); + +const rootSetting: Required<GridSetting['root']> = { + noOverflowStyle: false, + rounded: true, + outerBorder: true, + ...props.settings.root, +}; + +// non-reactive +// eslint-disable-next-line vue/no-setup-props-reactivity-loss +const rowSetting: Required<GridRowSetting> = { + ...defaultGridRowSetting, + ...props.settings.row, +}; + +// non-reactive +// eslint-disable-next-line vue/no-setup-props-reactivity-loss +const columnSettings = props.settings.cols; + +// non-reactive +const cellSettings = props.settings.cells ?? {}; + +const { data } = toRefs(props); + +// #region Event Definitions +// region Event Definitions + +/** + * grid -> å„åコンãƒãƒ¼ãƒãƒ³ãƒˆã®ã‚¤ãƒ™ãƒ³ãƒˆçµŒè·¯ã‚’æ‹…ã†{@link GridEventEmitter}。ãŠã‚‚ã«propsã§ã®ä¼æ¬ãŒé›£ã—ã„ã‚¤ãƒ™ãƒ³ãƒˆã‚’ä¼æ¬ã™ã‚‹ãŸã‚ã«ä½¿ç”¨ã™ã‚‹ã€‚ + * åコンãƒãƒ¼ãƒãƒ³ãƒˆ -> gridã®ã‚¤ãƒ™ãƒ³ãƒˆã§ã¯åŽŸå‰‡ä½¿ç”¨ã›ãšã€{@link emit}を使用ã™ã‚‹ã€‚ + */ +const bus = new GridEventEmitter(); +/** + * テーブルコンãƒãƒ¼ãƒãƒ³ãƒˆã®ãƒªã‚µã‚¤ã‚ºã‚¤ãƒ™ãƒ³ãƒˆã‚’監視ã™ã‚‹ãŸã‚ã®{@link ResizeObserver}。 + * 表示切替を検知ã—ã€ã‚µã‚¤ã‚ºã®å†è¨ˆç®—è¦æ±‚を発行ã™ã‚‹ãŸã‚ã«ä½¿ç”¨ã™ã‚‹ï¼ˆãƒžã‚¦ãƒ³ãƒˆæ™‚ã«ã‚³ãƒ³ãƒ†ãƒ³ãƒ„ãŒè¡¨ç¤ºã•れã¦ã„ãªã„å ´åˆã€åˆæ‰‹ã®ã‚µã‚¤ã‚ºã®è‡ªå‹•è¨ˆç®—ãŒæ£å¸¸ã«åƒã‹ãªã„ãŸã‚) + * + * {@link setTimeout}を経由ã—ã¦ã„ã‚‹ç†ç”±ã¯ã€{@link onResize}ã®ä¸ã§ã‚µã‚¤ã‚ºå†è¨ˆç®—è¦æ±‚→サイズ変更ãŒç™ºç”Ÿã™ã‚‹ã¨ãƒ«ãƒ¼ãƒ—ã¨ã¿ãªã•れ〠+ * 「ResizeObserver loop completed with undelivered notifications.ã€ã¨ã„ã†è¦å‘ŠãŒç™ºç”Ÿã™ã‚‹ãŸã‚(å†è¨ˆç®—ãŒå®Œå…¨ã«çµ‚ã‚れã°é€šçŸ¥ã¯ç™ºç”Ÿã—ãªããªã‚‹ã®ã§å®Ÿéš›ã«ã¯ãƒ«ãƒ¼ãƒ—ã—ãªã„) + * + * @see {@link onResize} + */ +const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries))); + +const rootEl = ref<InstanceType<typeof HTMLTableElement>>(); +/** + * ã‚°ãƒªãƒƒãƒ‰ã®æœ€ã‚‚上ä½ã«ã‚る状態。 + */ +const state = ref<GridState>('normal'); +/** + * グリッドã®åˆ—定義。列定義ã®å…ƒã®è¨å®šå€¤ã¯éžãƒªã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã®ã§ã€åˆæœŸå€¤ã‚’生æˆã—ã¦ä»¥é™ã¯å¤‰æ›´ã—ãªã„。 + */ +const columns = ref<GridColumn[]>(columnSettings.map(createColumn)); +/** + * グリッドã®è¡Œå®šç¾©ã€‚propsã§å—ã‘å–ã£ãŸ{@link data}ã‚’ã‚‚ã¨ã«ã€{@link refreshData}ã§å†è¨ˆç®—ã•れる。 + */ +const rows = ref<GridRow[]>([]); +/** + * グリッドã®ã‚»ãƒ«å®šç¾©ã€‚propsã§å—ã‘å–ã£ãŸ{@link data}ã‚’ã‚‚ã¨ã«ã€{@link refreshData}ã§å†è¨ˆç®—ã•れる。 + */ +const cells = ref<RowHolder[]>([]); + +/** + * mousemoveイベントãŒç™ºç”Ÿã—ãŸéš›ã«ã€ã‚¤ãƒ™ãƒ³ãƒˆã‹ã‚‰å–å¾—ã—ãŸã‚»ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’ä¿æŒã™ã‚‹ãŸã‚ã®å¤‰æ•°ã€‚ + * セルアドレスãŒå¤‰ã‚ã£ãŸçž¬é–“ã«ã‚¤ãƒ™ãƒ³ãƒˆã‚’èµ·ã“ã—ãŸã„時ã®ãŸã‚ã«å‰å›žå€¤ã¨ã—ã¦ä½¿ç”¨ã™ã‚‹ã€‚ + */ +const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE); +/** + * 編集ä¸ã®ã‚»ãƒ«ã®ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’ä¿æŒã™ã‚‹ãŸã‚ã®å¤‰æ•°ã€‚ + */ +const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE); +/** + * 列ã®ç¯„å›²é¸æŠžã‚’ã™ã‚‹éš›ã®é–‹å§‹åœ°ç‚¹ã¨ãªã‚‹ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’ä¿æŒã™ã‚‹ãŸã‚ã®å¤‰æ•°ã€‚ + * ã“ã®é–‹å§‹åœ°ç‚¹ã‹ã‚‰ãƒžã‚¦ã‚¹ãŒå‹•ã„ãŸåœ°ç‚¹ã¾ã§ã®ç¯„å›²ã‚’é¸æŠžã™ã‚‹ã€‚ + */ +const firstSelectionColumnIdx = ref<number>(CELL_ADDRESS_NONE.col); +/** + * 行ã®ç¯„å›²é¸æŠžã‚’ã™ã‚‹éš›ã®é–‹å§‹åœ°ç‚¹ã¨ãªã‚‹ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’ä¿æŒã™ã‚‹ãŸã‚ã®å¤‰æ•°ã€‚ + * ã“ã®é–‹å§‹åœ°ç‚¹ã‹ã‚‰ãƒžã‚¦ã‚¹ãŒå‹•ã„ãŸåœ°ç‚¹ã¾ã§ã®ç¯„å›²ã‚’é¸æŠžã™ã‚‹ã€‚ + */ +const firstSelectionRowIdx = ref<number>(CELL_ADDRESS_NONE.row); + +/** + * é¸æŠžçŠ¶æ…‹ã®ã‚»ãƒ«ã‚’å–å¾—ã™ã‚‹ãŸã‚ã®è¨ˆç®—プãƒãƒ‘ãƒ†ã‚£ã€‚é¸æŠžçŠ¶æ…‹ã¨ã¯{@link GridCell.selected}ãŒtrueã®ã‚»ãƒ«ã®ã“ã¨ã€‚ + */ +const selectedCell = computed(() => { + const selected = cells.value.flatMap(it => it.cells).filter(it => it.selected); + return selected.length > 0 ? selected[0] : undefined; +}); +/** + * ç¯„å›²é¸æŠžçŠ¶æ…‹ã®ã‚»ãƒ«ã‚’å–å¾—ã™ã‚‹ãŸã‚ã®è¨ˆç®—プãƒãƒ‘ãƒ†ã‚£ã€‚ç¯„å›²é¸æŠžçŠ¶æ…‹ã¨ã¯{@link GridCell.ranged}ãŒtrueã®ã‚»ãƒ«ã®ã“ã¨ã€‚ + */ +const rangedCells = computed(() => cells.value.flatMap(it => it.cells).filter(it => it.ranged)); +/** + * ç¯„å›²é¸æŠžçŠ¶æ…‹ã®ã‚»ãƒ«ã®ç¯„囲をå–å¾—ã™ã‚‹ãŸã‚ã®è¨ˆç®—プãƒãƒ‘ティ。左上ã®ã‚»ãƒ«ç•ªåœ°ã¨å³ä¸‹ã®ã‚»ãƒ«ç•ªåœ°ã‚’計算ã™ã‚‹ã€‚ + */ +const rangedBounds = computed(() => { + const _cells = rangedCells.value; + const _cols = _cells.map(it => it.address.col); + const _rows = _cells.map(it => it.address.row); + + const leftTop = { + col: Math.min(..._cols), + row: Math.min(..._rows), + }; + const rightBottom = { + col: Math.max(..._cols), + row: Math.max(..._rows), + }; + + return { + leftTop, + rightBottom, + }; +}); +/** + * グリッドã®ä¸ã§ä½¿ç”¨å¯èƒ½ãªã‚»ãƒ«ã®ç¯„囲をå–å¾—ã™ã‚‹ãŸã‚ã®è¨ˆç®—プãƒãƒ‘ティ。左上ã®ã‚»ãƒ«ç•ªåœ°ã¨å³ä¸‹ã®ã‚»ãƒ«ç•ªåœ°ã‚’計算ã™ã‚‹ã€‚ + */ +const availableBounds = computed(() => { + const leftTop = { + col: 0, + row: 0, + }; + const rightBottom = { + col: Math.max(...columns.value.map(it => it.index)), + row: Math.max(...rows.value.filter(it => it.using).map(it => it.index)), + }; + return { leftTop, rightBottom }; +}); +/** + * ç¯„å›²é¸æŠžçŠ¶æ…‹ã®è¡Œã‚’å–å¾—ã™ã‚‹ãŸã‚ã®è¨ˆç®—プãƒãƒ‘ãƒ†ã‚£ã€‚ç¯„å›²é¸æŠžçŠ¶æ…‹ã¨ã¯{@link GridRow.ranged}ãŒtrueã®è¡Œã®ã“ã¨ã€‚ + */ +const rangedRows = computed(() => rows.value.filter(it => it.ranged)); + +const lastLine = computed(() => rows.value.filter(it => it.using).length - 1); + +// endregion +// #endregion + +watch(data, patchData, { deep: true }); + +if (_DEV_) { + watch(state, (value, oldValue) => { + console.log(`[grid][state] ${oldValue} -> ${value}`); + }); +} + +// #region Event Handlers +// region Event Handlers + +function onResize(entries: ResizeObserverEntry[]) { + if (entries.length !== 1 || entries[0].target !== rootEl.value) { + return; + } + + const contentRect = entries[0].contentRect; + if (_DEV_) { + console.log(`[grid][resize] contentRect: ${contentRect.width}x${contentRect.height}`); + } + + switch (state.value) { + case 'hidden': { + if (contentRect.width > 0 && contentRect.height > 0) { + // å…ˆã«çŠ¶æ…‹ã‚’å¤‰æ›´ã—ã¦ãŠãã€å†è¨ˆç®—è¦æ±‚ãŒè¤‡æ•°å›žèµ°ã‚‰ãªã„よã†ã«ã™ã‚‹ + state.value = 'normal'; + + // é¸æŠžçŠ¶æ…‹ãŒç‹‚ã†ã‹ã‚‚ã—れãªã„ã®ã§è§£é™¤ã—ã¦ãŠã + unSelectionRangeAll(); + + // å†è¨ˆç®—è¦æ±‚を発行。å„セルå´ã§æœ€ä½Žé™å¿…è¦ãªæ¨ªå¹…を算出ã—ã€emitã§è¿”ã—ã¦ãるよã†ã«ãªã£ã¦ã„ã‚‹ + bus.emit('forceRefreshContentSize'); + } + break; + } + default: { + if (contentRect.width === 0 || contentRect.height === 0) { + state.value = 'hidden'; + } + break; + } + } +} + +function onKeyDown(ev: KeyboardEvent) { + const { ctrlKey, shiftKey, code } = ev; + if (_DEV_) { + console.log(`[grid][key] ctrl: ${ctrlKey}, shift: ${shiftKey}, code: ${code}`); + } + + function updateSelectionRange(newBounds: { leftTop: CellAddress, rightBottom: CellAddress }) { + unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom); + expandCellRange(newBounds.leftTop, newBounds.rightBottom); + } + + switch (state.value) { + case 'normal': { + ev.preventDefault(); + ev.stopPropagation(); + + const selectedCellAddress = selectedCell.value?.address ?? CELL_ADDRESS_NONE; + const max = availableBounds.value; + const bounds = rangedBounds.value; + + handleKeyEvent(ev, [ + { + code: 'Delete', handler: () => { + if (rangedRows.value.length > 0) { + if (rowSetting.events.delete) { + rowSetting.events.delete(rangedRows.value); + } + } else { + const context = createContext(); + removeDataFromGrid(context, (cell) => { + emitCellValue(cell, undefined); + }); + } + }, + }, + { + code: 'KeyC', modifiers: ['Control'], handler: () => { + const context = createContext(); + copyGridDataToClipboard(data.value, context); + }, + }, + { + code: 'KeyV', modifiers: ['Control'], handler: async () => { + const _cells = cells.value; + const context = createContext(); + await pasteToGridFromClipboard(context, (row, col, parsedValue) => { + emitCellValue(_cells[row.index].cells[col.index], parsedValue); + }); + }, + }, + { + code: 'ArrowRight', modifiers: ['Control', 'Shift'], handler: () => { + updateSelectionRange({ + leftTop: { col: selectedCellAddress.col, row: bounds.leftTop.row }, + rightBottom: { col: max.rightBottom.col, row: bounds.rightBottom.row }, + }); + }, + }, + { + code: 'ArrowLeft', modifiers: ['Control', 'Shift'], handler: () => { + updateSelectionRange({ + leftTop: { col: max.leftTop.col, row: bounds.leftTop.row }, + rightBottom: { col: selectedCellAddress.col, row: bounds.rightBottom.row }, + }); + }, + }, + { + code: 'ArrowUp', modifiers: ['Control', 'Shift'], handler: () => { + updateSelectionRange({ + leftTop: { col: bounds.leftTop.col, row: max.leftTop.row }, + rightBottom: { col: bounds.rightBottom.col, row: selectedCellAddress.row }, + }); + }, + }, + { + code: 'ArrowDown', modifiers: ['Control', 'Shift'], handler: () => { + updateSelectionRange({ + leftTop: { col: bounds.leftTop.col, row: selectedCellAddress.row }, + rightBottom: { col: bounds.rightBottom.col, row: max.rightBottom.row }, + }); + }, + }, + { + code: 'ArrowRight', modifiers: ['Shift'], handler: () => { + updateSelectionRange({ + leftTop: { + col: bounds.leftTop.col < selectedCellAddress.col + ? bounds.leftTop.col + 1 + : selectedCellAddress.col, + row: bounds.leftTop.row, + }, + rightBottom: { + col: (bounds.rightBottom.col > selectedCellAddress.col || bounds.leftTop.col === selectedCellAddress.col) + ? bounds.rightBottom.col + 1 + : selectedCellAddress.col, + row: bounds.rightBottom.row, + }, + }); + }, + }, + { + code: 'ArrowLeft', modifiers: ['Shift'], handler: () => { + updateSelectionRange({ + leftTop: { + col: (bounds.leftTop.col < selectedCellAddress.col || bounds.rightBottom.col === selectedCellAddress.col) + ? bounds.leftTop.col - 1 + : selectedCellAddress.col, + row: bounds.leftTop.row, + }, + rightBottom: { + col: bounds.rightBottom.col > selectedCellAddress.col + ? bounds.rightBottom.col - 1 + : selectedCellAddress.col, + row: bounds.rightBottom.row, + }, + }); + }, + }, + { + code: 'ArrowUp', modifiers: ['Shift'], handler: () => { + updateSelectionRange({ + leftTop: { + col: bounds.leftTop.col, + row: (bounds.leftTop.row < selectedCellAddress.row || bounds.rightBottom.row === selectedCellAddress.row) + ? bounds.leftTop.row - 1 + : selectedCellAddress.row, + }, + rightBottom: { + col: bounds.rightBottom.col, + row: bounds.rightBottom.row > selectedCellAddress.row + ? bounds.rightBottom.row - 1 + : selectedCellAddress.row, + }, + }); + }, + }, + { + code: 'ArrowDown', modifiers: ['Shift'], handler: () => { + updateSelectionRange({ + leftTop: { + col: bounds.leftTop.col, + row: bounds.leftTop.row < selectedCellAddress.row + ? bounds.leftTop.row + 1 + : selectedCellAddress.row, + }, + rightBottom: { + col: bounds.rightBottom.col, + row: (bounds.rightBottom.row > selectedCellAddress.row || bounds.leftTop.row === selectedCellAddress.row) + ? bounds.rightBottom.row + 1 + : selectedCellAddress.row, + }, + }); + }, + }, + { + code: 'ArrowDown', handler: () => { + selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 }); + }, + }, + { + code: 'ArrowUp', handler: () => { + selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row - 1 }); + }, + }, + { + code: 'ArrowRight', handler: () => { + selectionCell({ col: selectedCellAddress.col + 1, row: selectedCellAddress.row }); + }, + }, + { + code: 'ArrowLeft', handler: () => { + selectionCell({ col: selectedCellAddress.col - 1, row: selectedCellAddress.row }); + }, + }, + ]); + + break; + } + } +} + +function onMouseDown(ev: MouseEvent) { + switch (ev.button) { + case 0: { + onLeftMouseDown(ev); + break; + } + case 2: { + onRightMouseDown(ev); + break; + } + } +} + +function onLeftMouseDown(ev: MouseEvent) { + const cellAddress = getCellAddress(ev.target as HTMLElement); + if (_DEV_) { + console.log(`[grid][mouse-left] state:${state.value}, button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); + } + + switch (state.value) { + case 'cellEditing': { + if (availableCellAddress(cellAddress) && !equalCellAddress(editingCellAddress.value, cellAddress)) { + selectionCell(cellAddress); + } + break; + } + case 'normal': { + if (availableCellAddress(cellAddress)) { + if (ev.shiftKey && selectedCell.value && !equalCellAddress(cellAddress, selectedCell.value.address)) { + const selectedCellAddress = selectedCell.value.address; + + const leftTop = { + col: Math.min(selectedCellAddress.col, cellAddress.col), + row: Math.min(selectedCellAddress.row, cellAddress.row), + }; + + const rightBottom = { + col: Math.max(selectedCellAddress.col, cellAddress.col), + row: Math.max(selectedCellAddress.row, cellAddress.row), + }; + + unSelectionRangeAll(); + expandCellRange(leftTop, rightBottom); + + cells.value[selectedCellAddress.row].cells[selectedCellAddress.col].selected = true; + } else { + selectionCell(cellAddress); + } + + previousCellAddress.value = cellAddress; + + registerMouseUp(); + registerMouseMove(); + state.value = 'cellSelecting'; + } else if (isColumnHeaderCellAddress(cellAddress)) { + if (ev.shiftKey) { + const rangedColumnIndexes = rangedCells.value.map(it => it.address.col); + const targetColumnIndexes = [cellAddress.col, ...rangedColumnIndexes]; + unSelectionRangeAll(); + + const leftTop = { + col: Math.min(...targetColumnIndexes), + row: 0, + }; + + const rightBottom = { + col: Math.max(...targetColumnIndexes), + row: cells.value.length - 1, + }; + + expandCellRange(leftTop, rightBottom); + + if (rangedColumnIndexes.length === 0) { + firstSelectionColumnIdx.value = cellAddress.col; + } else { + if (cellAddress.col > Math.min(...rangedColumnIndexes)) { + firstSelectionColumnIdx.value = Math.min(...rangedColumnIndexes); + } else { + firstSelectionColumnIdx.value = Math.max(...rangedColumnIndexes); + } + } + } else { + unSelectionRangeAll(); + + const colCells = cells.value.map(row => row.cells[cellAddress.col]); + selectionRange(...colCells.map(cell => cell.address)); + + firstSelectionColumnIdx.value = cellAddress.col; + } + + registerMouseUp(); + registerMouseMove(); + previousCellAddress.value = cellAddress; + state.value = 'colSelecting'; + + // フォーカスを当ã¦ãªã„ã¨ã‚ãƒ¼ã‚¤ãƒ™ãƒ³ãƒˆãŒæ‹¾ãˆãªã„ã®ã§ + getCellElement(ev.target as HTMLElement)?.focus(); + } else if (isRowNumberCellAddress(cellAddress)) { + if (ev.shiftKey) { + const rangedRowIndexes = rangedRows.value.map(it => it.index); + const targetRowIndexes = [cellAddress.row, ...rangedRowIndexes]; + unSelectionRangeAll(); + + const leftTop = { + col: 0, + row: Math.min(...targetRowIndexes), + }; + + const rightBottom = { + col: Math.min(...cells.value.map(it => it.cells.length - 1)), + row: Math.max(...targetRowIndexes), + }; + + expandCellRange(leftTop, rightBottom); + expandRowRange(Math.min(...targetRowIndexes), Math.max(...targetRowIndexes)); + + if (rangedRowIndexes.length === 0) { + firstSelectionRowIdx.value = cellAddress.row; + } else { + if (cellAddress.col > Math.min(...rangedRowIndexes)) { + firstSelectionRowIdx.value = Math.min(...rangedRowIndexes); + } else { + firstSelectionRowIdx.value = Math.max(...rangedRowIndexes); + } + } + } else { + unSelectionRangeAll(); + const rowCells = cells.value[cellAddress.row].cells; + selectionRange(...rowCells.map(cell => cell.address)); + expandRowRange(cellAddress.row, cellAddress.row); + + firstSelectionRowIdx.value = cellAddress.row; + } + + registerMouseUp(); + registerMouseMove(); + previousCellAddress.value = cellAddress; + state.value = 'rowSelecting'; + + // フォーカスを当ã¦ãªã„ã¨ã‚ãƒ¼ã‚¤ãƒ™ãƒ³ãƒˆãŒæ‹¾ãˆãªã„ã®ã§ + getCellElement(ev.target as HTMLElement)?.focus(); + } + break; + } + } +} + +function onRightMouseDown(ev: MouseEvent) { + const cellAddress = getCellAddress(ev.target as HTMLElement); + if (_DEV_) { + console.log(`[grid][mouse-right] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); + } + + switch (state.value) { + case 'normal': { + if (!availableCellAddress(cellAddress)) { + return; + } + + const _rangedCells = [...rangedCells.value]; + if (!_rangedCells.some(it => equalCellAddress(it.address, cellAddress))) { + // ç¯„å›²é¸æŠžå¤–ã‚’å³ã‚¯ãƒªãƒƒã‚¯ã—ãŸå ´åˆã¯ã€ç¯„å›²é¸æŠžã‚’è§£é™¤ï¼ˆç¯„å›²é¸æŠžå†…ã§ã‚れã°ç¯„å›²é¸æŠžã‚’ç¶æŒã™ã‚‹ï¼‰ + selectionCell(cellAddress); + } + + break; + } + } +} + +function onMouseMove(ev: MouseEvent) { + ev.preventDefault(); + + const targetCellAddress = getCellAddress(ev.target as HTMLElement); + if (equalCellAddress(previousCellAddress.value, targetCellAddress)) { + // セルãŒå¤‰ã‚ã‚‹ã¾ã§ã‚¤ãƒ™ãƒ³ãƒˆã‚’èµ·ã“ã—ãŸããªã„ + return; + } + + if (_DEV_) { + console.log(`[grid][mouse-move] button: ${ev.button}, cell: ${targetCellAddress.row}x${targetCellAddress.col}`); + } + + switch (state.value) { + case 'cellSelecting': { + const selectedCellAddress = selectedCell.value?.address; + if (!availableCellAddress(targetCellAddress) || !selectedCellAddress) { + // æ£ã—ã„セル範囲ã§ã¯ãªã„ + return; + } + + const leftTop = { + col: Math.min(targetCellAddress.col, selectedCellAddress.col), + row: Math.min(targetCellAddress.row, selectedCellAddress.row), + }; + + const rightBottom = { + col: Math.max(targetCellAddress.col, selectedCellAddress.col), + row: Math.max(targetCellAddress.row, selectedCellAddress.row), + }; + + // 範囲外ã®ã‚»ãƒ«ã¯é¸æŠžè§£é™¤ã—ã€ç¯„囲内ã®ã‚»ãƒ«ã¯é¸æŠžçŠ¶æ…‹ã«ã™ã‚‹ + unSelectionOutOfRange(leftTop, rightBottom); + expandCellRange(leftTop, rightBottom); + previousCellAddress.value = targetCellAddress; + + break; + } + case 'colSelecting': { + if (!isColumnHeaderCellAddress(targetCellAddress) || previousCellAddress.value.col === targetCellAddress.col) { + // セルãŒå¤‰ã‚ã‚‹ã¾ã§ã‚¤ãƒ™ãƒ³ãƒˆã‚’èµ·ã“ã—ãŸããªã„ + return; + } + + const leftTop = { + col: Math.min(targetCellAddress.col, firstSelectionColumnIdx.value), + row: 0, + }; + + const rightBottom = { + col: Math.max(targetCellAddress.col, firstSelectionColumnIdx.value), + row: cells.value.length - 1, + }; + + // 範囲外ã®ã‚»ãƒ«ã¯é¸æŠžè§£é™¤ã—ã€ç¯„囲内ã®ã‚»ãƒ«ã¯é¸æŠžçŠ¶æ…‹ã«ã™ã‚‹ + unSelectionOutOfRange(leftTop, rightBottom); + expandCellRange(leftTop, rightBottom); + previousCellAddress.value = targetCellAddress; + + // フォーカスを当ã¦ãªã„ã¨ã‚ãƒ¼ã‚¤ãƒ™ãƒ³ãƒˆãŒæ‹¾ãˆãªã„ã®ã§ + getCellElement(ev.target as HTMLElement)?.focus(); + + break; + } + case 'rowSelecting': { + if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) { + // セルãŒå¤‰ã‚ã‚‹ã¾ã§ã‚¤ãƒ™ãƒ³ãƒˆã‚’èµ·ã“ã—ãŸããªã„ + return; + } + + const leftTop = { + col: 0, + row: Math.min(targetCellAddress.row, firstSelectionRowIdx.value), + }; + + const rightBottom = { + col: Math.min(...cells.value.map(it => it.cells.length - 1)), + row: Math.max(targetCellAddress.row, firstSelectionRowIdx.value), + }; + + // 範囲外ã®ã‚»ãƒ«ã¯é¸æŠžè§£é™¤ã—ã€ç¯„囲内ã®ã‚»ãƒ«ã¯é¸æŠžçŠ¶æ…‹ã«ã™ã‚‹ + unSelectionOutOfRange(leftTop, rightBottom); + expandCellRange(leftTop, rightBottom); + + // è¡Œã‚‚åŒæ§˜ã« + const rangedRowIndexes = [rows.value[targetCellAddress.row].index, ...rangedRows.value.map(it => it.index)]; + expandRowRange(Math.min(...rangedRowIndexes), Math.max(...rangedRowIndexes)); + + previousCellAddress.value = targetCellAddress; + + // フォーカスを当ã¦ãªã„ã¨ã‚ãƒ¼ã‚¤ãƒ™ãƒ³ãƒˆãŒæ‹¾ãˆãªã„ã®ã§ + getCellElement(ev.target as HTMLElement)?.focus(); + + break; + } + } +} + +function onMouseUp(ev: MouseEvent) { + ev.preventDefault(); + switch (state.value) { + case 'rowSelecting': + case 'colSelecting': + case 'cellSelecting': { + unregisterMouseUp(); + unregisterMouseMove(); + state.value = 'normal'; + previousCellAddress.value = CELL_ADDRESS_NONE; + break; + } + } +} + +function onContextMenu(ev: MouseEvent) { + const cellAddress = getCellAddress(ev.target as HTMLElement); + if (_DEV_) { + console.log(`[grid][context-menu] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); + } + + const context = createContext(); + const menuItems = Array.of<MenuItem>(); + switch (true) { + // 通常セルã®ã‚³ãƒ³ãƒ†ã‚ã‚¹ãƒˆãƒ¡ãƒ‹ãƒ¥ãƒ¼ä½œæˆ + case availableCellAddress(cellAddress): { + const cell = cells.value[cellAddress.row].cells[cellAddress.col]; + if (cell.setting.contextMenuFactory) { + menuItems.push(...cell.setting.contextMenuFactory(cell.column, cell.row, cell.value, context)); + } + break; + } + // 列ヘッダセルã®ã‚³ãƒ³ãƒ†ã‚ã‚¹ãƒˆãƒ¡ãƒ‹ãƒ¥ãƒ¼ä½œæˆ + case isColumnHeaderCellAddress(cellAddress): { + const col = columns.value[cellAddress.col]; + if (col.setting.contextMenuFactory) { + menuItems.push(...col.setting.contextMenuFactory(col, context)); + } + break; + } + // 行ヘッダセルã®ã‚³ãƒ³ãƒ†ã‚ã‚¹ãƒˆãƒ¡ãƒ‹ãƒ¥ãƒ¼ä½œæˆ + case isRowNumberCellAddress(cellAddress): { + const row = rows.value[cellAddress.row]; + if (row.setting.contextMenuFactory) { + menuItems.push(...row.setting.contextMenuFactory(row, context)); + } + break; + } + } + + if (menuItems.length > 0) { + os.contextMenu(menuItems, ev); + } +} + +function onCellEditBegin(sender: GridCell) { + state.value = 'cellEditing'; + editingCellAddress.value = sender.address; + for (const cell of cells.value.flatMap(it => it.cells)) { + if (cell.address.col !== sender.address.col || cell.address.row !== sender.address.row) { + // 編集状態ã¨ãªã£ãŸã‚»ãƒ«ä»¥å¤–ã¯å…¨éƒ¨é¸æŠžè§£é™¤ + cell.selected = false; + } + } +} + +function onCellEditEnd() { + editingCellAddress.value = CELL_ADDRESS_NONE; + state.value = 'normal'; +} + +function onChangeCellValue(sender: GridCell, newValue: CellValue) { + applyRowRules([sender]); + emitCellValue(sender, newValue); +} + +function onChangeCellContentSize(sender: GridCell, contentSize: Size) { + const _cells = cells.value; + if (_cells.length > sender.address.row && _cells[sender.address.row].cells.length > sender.address.col) { + const currentSize = _cells[sender.address.row].cells[sender.address.col].contentSize; + if (currentSize.width !== contentSize.width || currentSize.height !== contentSize.height) { + // 通常セルã®ã‚»ãƒ«å¹…ãŒç¢ºå®šã—ãŸã‚‰ã€ãã®ã‚µã‚¤ã‚ºã‚’ä¿æŒã—ã¦ãŠã(内容ã«å¼•ã£å¼µã‚‰ã‚Œã¦æƒ³å®šã‚ˆã‚Šã‚‚大ãã„セルサイズã«ãªã‚‰ãªã„よã†ã«ã™ã‚‹ãŸã‚ã®CSS作æˆã«ä½¿ç”¨ï¼‰ + _cells[sender.address.row].cells[sender.address.col].contentSize = contentSize; + + if (sender.column.setting.width === 'auto') { + calcLargestCellWidth(sender.column); + } + } + } +} + +function onHeaderCellWidthBeginChange() { + switch (state.value) { + case 'normal': { + state.value = 'colResizing'; + break; + } + } +} + +function onHeaderCellWidthEndChange() { + switch (state.value) { + case 'colResizing': { + state.value = 'normal'; + break; + } + } +} + +function onHeaderCellChangeWidth(sender: GridColumn, width: string) { + switch (state.value) { + case 'colResizing': { + const column = columns.value[sender.index]; + column.width = width; + break; + } + } +} + +function onHeaderCellChangeContentSize(sender: GridColumn, newSize: Size) { + switch (state.value) { + case 'normal': { + const currentSize = columns.value[sender.index].contentSize; + if (currentSize.width !== newSize.width || currentSize.height !== newSize.height) { + // ヘッダセルã®ã‚»ãƒ«å¹…ãŒç¢ºå®šã—ãŸã‚‰ã€ãã®ã‚µã‚¤ã‚ºã‚’ä¿æŒã—ã¦ãŠã(内容ã«å¼•ã£å¼µã‚‰ã‚Œã¦æƒ³å®šã‚ˆã‚Šã‚‚大ãã„セルサイズã«ãªã‚‰ãªã„よã†ã«ã™ã‚‹ãŸã‚ã®CSS作æˆã«ä½¿ç”¨ï¼‰ + columns.value[sender.index].contentSize = newSize; + + if (sender.setting.width === 'auto') { + calcLargestCellWidth(sender); + } + } + break; + } + } +} + +function onHeaderCellWidthLargest(sender: GridColumn) { + switch (state.value) { + case 'normal': { + calcLargestCellWidth(sender); + break; + } + } +} + +// endregion +// #endregion + +// #region Methods +// region Methods + +/** + * カラム内ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を表示ã—ãã‚‹ãŸã‚ã«å¿…è¦ãªæ¨ªå¹…ã¨ã€å„セルã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を表示ã—ãã‚‹ãŸã‚ã«å¿…è¦ãªæ¨ªå¹…を比較ã—ã€å¤§ãã„æ–¹ã‚’åˆ—å…¨ä½“ã®æ¨ªå¹…ã¨ã—ã¦æŽ¡ç”¨ã™ã‚‹ã€‚ + */ +function calcLargestCellWidth(column: GridColumn) { + const _cells = cells.value; + const largestColumnWidth = columns.value[column.index].contentSize.width; + + const largestCellWidth = (_cells.length > 0) + ? _cells + .map(row => row.cells[column.index]) + .reduce( + (acc, value) => Math.max(acc, value.contentSize.width), + 0, + ) + : 0; + + if (_DEV_) { + console.log(`[grid][calc-largest] idx:${column.setting.bindTo}, col:${largestColumnWidth}, cell:${largestCellWidth}`); + } + + column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`; +} + +/** + * {@link emit}を使用ã—ã¦ã‚¤ãƒ™ãƒ³ãƒˆã‚’発行ã™ã‚‹ã€‚ + */ +function emitGridEvent(ev: GridEvent) { + const currentState: GridContext = { + selectedCell: selectedCell.value, + rangedCells: rangedCells.value, + rangedRows: rangedRows.value, + randedBounds: rangedBounds.value, + availableBounds: availableBounds.value, + state: state.value, + rows: rows.value, + columns: columns.value, + }; + + emit( + 'event', + ev, + currentState, + ); +} + +/** + * 親コンãƒãƒ¼ãƒãƒ³ãƒˆã«æ–°ã—ã„値を通知ã™ã‚‹ã€‚ + * æ–°ã—ã„値ã¯ã€ã‚¤ãƒ™ãƒ³ãƒˆé€šçŸ¥â†’元データã¸ã®åæ˜ â†’å†è¨ˆç®—(ãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³å«ã‚€ï¼‰â†’å†æç”»ã®æµã‚Œã§åæ˜ ã•れる。 + */ +function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) { + const cellAddress = 'address' in sender ? sender.address : sender; + const cell = cells.value[cellAddress.row].cells[cellAddress.col]; + + emitGridEvent({ + type: 'cell-value-change', + column: cell.column, + row: cell.row, + oldValue: cell.value, + newValue: newValue, + }); + + if (_DEV_) { + console.log(`[grid][cell-value] row:${cell.row}, col:${cell.column.index}, value:${newValue}`); + } +} + +/** + * {@link target}ã®ã‚»ãƒ«ã‚’é¸æŠžçŠ¶æ…‹ã«ã™ã‚‹ã€‚ + * ãã®éš›ã€{@link target}以外ã®è¡ŒãŠã‚ˆã³ã‚»ãƒ«ã®ç¯„å›²é¸æŠžçŠ¶æ…‹ã‚’è§£é™¤ã™ã‚‹ã€‚ + */ +function selectionCell(target: CellAddress) { + if (!availableCellAddress(target)) { + return; + } + + unSelectionRangeAll(); + + const _cells = cells.value; + _cells[target.row].cells[target.col].selected = true; + _cells[target.row].cells[target.col].ranged = true; +} + +/** + * {@link targets}ã®ã‚»ãƒ«ã‚’ç¯„å›²é¸æŠžçŠ¶æ…‹ã«ã™ã‚‹ã€‚ + */ +function selectionRange(...targets: CellAddress[]) { + const _cells = cells.value; + for (const target of targets) { + const row = _cells[target.row]; + if (row.row.using) { + row.cells[target.col].ranged = true; + } + } +} + +/** + * 行ãŠã‚ˆã³ã‚»ãƒ«ã®ç¯„å›²é¸æŠžçŠ¶æ…‹ã‚’ã™ã¹ã¦è§£é™¤ã™ã‚‹ã€‚ + */ +function unSelectionRangeAll() { + const _cells = rangedCells.value; + for (const cell of _cells) { + cell.selected = false; + cell.ranged = false; + } + + const _rows = rows.value.filter(it => it.using); + for (const row of _rows) { + row.ranged = false; + } +} + +/** + * {@link leftTop}ã‹ã‚‰{@link rightBottom}ã®ç¯„囲外ã«ã‚ã‚‹ã‚»ãƒ«ã‚’ç¯„å›²é¸æŠžçŠ¶æ…‹ã‹ã‚‰å¤–ã™ã€‚ + */ +function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) { + const safeBounds = getSafeAddressBounds({ leftTop, rightBottom }); + + const _cells = rangedCells.value; + for (const cell of _cells) { + const outOfRangeCol = cell.address.col < safeBounds.leftTop.col || cell.address.col > safeBounds.rightBottom.col; + const outOfRangeRow = cell.address.row < safeBounds.leftTop.row || cell.address.row > safeBounds.rightBottom.row; + if (outOfRangeCol || outOfRangeRow) { + cell.ranged = false; + } + } + + const outOfRangeRows = rows.value.filter((_, index) => index < safeBounds.leftTop.row || index > safeBounds.rightBottom.row); + for (const row of outOfRangeRows) { + row.ranged = false; + } +} + +/** + * {@link leftTop}ã‹ã‚‰{@link rightBottom}ã®ç¯„囲内ã«ã‚ã‚‹ã‚»ãƒ«ã‚’ç¯„å›²é¸æŠžçŠ¶æ…‹ã«ã™ã‚‹ã€‚ + */ +function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) { + const safeBounds = getSafeAddressBounds({ leftTop, rightBottom }); + const targetRows = cells.value.slice(safeBounds.leftTop.row, safeBounds.rightBottom.row + 1); + for (const row of targetRows) { + for (const cell of row.cells.slice(safeBounds.leftTop.col, safeBounds.rightBottom.col + 1)) { + cell.ranged = true; + } + } +} + +/** + * {@link top}ã‹ã‚‰{@link bottom}ã¾ã§ã®è¡Œã‚’ç¯„å›²é¸æŠžçŠ¶æ…‹ã«ã™ã‚‹ã€‚ + */ +function expandRowRange(top: number, bottom: number) { + if (!rowSetting.selectable) { + return; + } + + const targetRows = rows.value.slice(top, bottom + 1); + for (const row of targetRows) { + row.ranged = true; + } +} + +/** + * ç‰¹å®šã®æ¡ä»¶ä¸‹ã§ã®ã¿é©ç”¨ã•れるCSSã‚’åæ˜ ã™ã‚‹ã€‚ + */ +function applyRowRules(targetCells: GridCell[]) { + const _rows = rows.value; + const targetRowIdxes = [...new Set(targetCells.map(it => it.address.row))]; + const rowGroups = Array.of<{ row: GridRow, cells: GridCell[] }>(); + for (const rowIdx of targetRowIdxes) { + const rowGroup = targetCells.filter(it => it.address.row === rowIdx); + rowGroups.push({ row: _rows[rowIdx], cells: rowGroup }); + } + + const _cells = cells.value; + for (const group of rowGroups.filter(it => it.row.using)) { + const row = group.row; + const targetCols = group.cells.map(it => it.column); + const rowCells = _cells[group.row.index].cells; + + const newStyles = rowSetting.styleRules + .filter(it => it.condition({ row, targetCols, cells: rowCells })) + .map(it => it.applyStyle); + + if (JSON.stringify(newStyles) !== JSON.stringify(row.additionalStyles)) { + row.additionalStyles = newStyles; + } + } +} + +function availableCellAddress(cellAddress: CellAddress): boolean { + const safeBounds = availableBounds.value; + return cellAddress.row >= safeBounds.leftTop.row && + cellAddress.col >= safeBounds.leftTop.col && + cellAddress.row <= safeBounds.rightBottom.row && + cellAddress.col <= safeBounds.rightBottom.col; +} + +function isColumnHeaderCellAddress(cellAddress: CellAddress): boolean { + return cellAddress.row === -1 && cellAddress.col >= 0; +} + +function isRowNumberCellAddress(cellAddress: CellAddress): boolean { + return cellAddress.row >= 0 && cellAddress.col === -1; +} + +function getSafeAddressBounds( + bounds: { leftTop: CellAddress, rightBottom: CellAddress }, +): { leftTop: CellAddress, rightBottom: CellAddress } { + const available = availableBounds.value; + + const safeLeftTop = { + col: Math.max(bounds.leftTop.col, available.leftTop.col), + row: Math.max(bounds.leftTop.row, available.leftTop.row), + }; + const safeRightBottom = { + col: Math.min(bounds.rightBottom.col, available.rightBottom.col), + row: Math.min(bounds.rightBottom.row, available.rightBottom.row), + }; + + return { leftTop: safeLeftTop, rightBottom: safeRightBottom }; +} + +function registerMouseMove() { + unregisterMouseMove(); + addEventListener('mousemove', onMouseMove); +} + +function unregisterMouseMove() { + removeEventListener('mousemove', onMouseMove); +} + +function registerMouseUp() { + unregisterMouseUp(); + addEventListener('mouseup', onMouseUp); +} + +function unregisterMouseUp() { + removeEventListener('mouseup', onMouseUp); +} + +function createContext(): GridContext { + return { + selectedCell: selectedCell.value, + rangedCells: rangedCells.value, + rangedRows: rangedRows.value, + randedBounds: rangedBounds.value, + availableBounds: availableBounds.value, + state: state.value, + rows: rows.value, + columns: columns.value, + }; +} + +function refreshData() { + if (_DEV_) { + console.log('[grid][refresh-data][begin]'); + } + + // データを元ã«è¡Œãƒ»åˆ—・セルを作æˆã™ã‚‹ã€‚ + // 行ã¯å…ƒãƒ‡ãƒ¼ã‚¿ã®é…列ã®é•·ã•ã«å¿œã˜ã¦ä½œæˆã™ã‚‹ãŒã€æœ€ä½Žé™ã®è¡Œæ•°ã¯è¨å®šã«ã‚ˆã£ã¦æ±ºã¾ã‚‹ã€‚ + // 行数ãŒå¤‰ã‚ã‚‹ãŸã³ã«éƒ½åº¦ãƒ¬ãƒ³ãƒ€ãƒªãƒ³ã‚°ã™ã‚‹ã¨ãƒ‘フォーマンスãŒã‚¤ãƒžã‚¤ãƒãªã®ã§ã€ã‚らã‹ã˜ã‚多ã‚ã«ã‚»ãƒ«ã‚’用æ„ã—ã¦ãŠããŸã‚ã®æŽªç½®ã€‚ + const _data: DataSource[] = data.value; + const _rows: GridRow[] = (_data.length > rowSetting.minimumDefinitionCount) + ? _data.map((_, index) => createRow(index, true, rowSetting)) + : Array.from({ length: rowSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length, rowSetting)); + const _cols: GridColumn[] = columns.value; + + // 行・列ã®å®šç¾©ã‹ã‚‰ã€å…ƒãƒ‡ãƒ¼ã‚¿ã®é…列より値をå–å¾—ã—ã¦ã‚»ãƒ«ã‚’作æˆã™ã‚‹ã€‚ + // 行・列ã®å®šç¾©ã¯ãれãžã‚Œã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’æŒã£ã¦ãŠã‚Šã€ãã®ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã¯å…ƒãƒ‡ãƒ¼ã‚¿ã®é…列番地ã«å¯¾å¿œã—ã¦ã„る。 + const _cells: RowHolder[] = _rows.map(row => { + const newCells = row.using + ? _cols.map(col => createCell(col, row, _data[row.index][col.setting.bindTo], cellSettings)) + : _cols.map(col => createCell(col, row, undefined, cellSettings)); + + return { row, cells: newCells, origin: _data[row.index] }; + }); + + rows.value = _rows; + cells.value = _cells; + + const allCells = _cells.filter(it => it.row.using).flatMap(it => it.cells); + for (const cell of allCells) { + cell.violation = cellValidation(allCells, cell, cell.value); + } + + applyRowRules(allCells); + + if (_DEV_) { + console.log('[grid][refresh-data][end]'); + } +} + +/** + * セル値を部分更新ã™ã‚‹ã€‚ã“ã®é–¢æ•°ã¯ã€å¤–éƒ¨èµ·å› ã§ãƒ‡ãƒ¼ã‚¿ãŒå¤‰æ›´ã•れãŸå ´åˆã«å‘¼ã°ã‚Œã‚‹ã€‚ + * + * å¤–éƒ¨èµ·å› ã§ãƒ‡ãƒ¼ã‚¿ãŒå¤‰æ›´ã•れãŸå ´åˆã¯{@link data}ã®å€¤ãŒå¤‰æ›´ã•れるãŒã€ä½•処ã®ç•ªåœ°ãŒã©ã®ã‚ˆã†ã«å¤‰ã‚ã£ãŸã®ã‹ã¾ã§ã¯æ¤œçŸ¥ã§ããªã„。 + * セルをã™ã¹ã¦ä½œã‚Šç›´ã›ã°ã„ã„ãŒã€ãã®æ‰‹æ³•ã ã¨ä»¥ä¸‹ã®ãƒ‡ãƒ¡ãƒªãƒƒãƒˆãŒã‚る。 + * - æç”»è² è·ãŒã‹ã‹ã‚‹ + * - å„ã‚»ãƒ«ãŒæŒã¤å€‹åˆ¥ã®çŠ¶æ…‹ï¼ˆé¸æŠžä¸çŠ¶æ…‹ã‚„ãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³çµæžœãªã©ï¼‰ãŒå¤±ã‚れる + * + * ãã“ã§ã€æ–°ã—ã„値ã¨ã‚»ãƒ«ãŒæŒã¤å€¤ã‚’çªãåˆã‚ã›ã€å¤‰æ›´ãŒã‚ã£ãŸå ´åˆã®ã¿å€¤ã‚’æ›´æ–°ã—ã€ã‚»ãƒ«ãã®ã‚‚ã®ã¯ä½¿ã„ã¾ã‚ã—ã¤ã¤å€¤ã‚’最新化ã™ã‚‹ã€‚ + */ +function patchData(newItems: DataSource[]) { + if (_DEV_) { + console.log('[grid][patch-data][begin]'); + } + + const _cols = columns.value; + + if (rows.value.length < newItems.length) { + const newRows = Array.of<GridRow>(); + const newCells = Array.of<RowHolder>(); + + // 未使用ã®è¡Œã‚’å«ã‚ã¦ã‚‚足りãªã„ã®ã§æ–°ã—ã„è¡Œã‚’è¿½åŠ ã™ã‚‹ + for (let rowIdx = rows.value.length; rowIdx < newItems.length; rowIdx++) { + const newRow = createRow(rowIdx, true, rowSetting); + newRows.push(newRow); + newCells.push({ + row: newRow, + cells: _cols.map(col => createCell(col, newRow, newItems[rowIdx][col.setting.bindTo], cellSettings)), + origin: newItems[rowIdx], + }); + } + + rows.value.push(...newRows); + cells.value.push(...newCells); + + applyRowRules(newCells.flatMap(it => it.cells)); + } + + // 行数ã®ä¸Šé™ãŒæ¬²ã—ã„å ´åˆã¯ã“ã“ã«è¨ã‘ã¦ã‚‚ã„ã„ã‹ã‚‚ã—れãªã„ + + const usingRows = rows.value.filter(it => it.using); + if (usingRows.length > newItems.length) { + // è¡Œæ•°ãŒæ¸›ã£ã¦ã„ã‚‹ã®ã§å¤ã„行をクリアã™ã‚‹ï¼ˆå†ãƒžã‚¦ãƒ³ãƒˆãƒ»å†ãƒ¬ãƒ³ãƒ€ãƒªãƒ³ã‚°ãŒé‡ã„ã®ã§è¦ç´ ãã®ã‚‚ã®ã¯æ¶ˆã•ãªã„) + for (let rowIdx = newItems.length; rowIdx < usingRows.length; rowIdx++) { + resetRow(rows.value[rowIdx]); + for (let colIdx = 0; colIdx < _cols.length; colIdx++) { + const holder = cells.value[rowIdx]; + holder.origin = {}; + resetCell(holder.cells[colIdx]); + } + } + } + + // æ–°ã—ã„å€¤ã¨æ—¢ã«è¨å®šã•れã¦ã„ãŸå€¤ã‚’入れ替ãˆã‚‹ + const changedCells = Array.of<GridCell>(); + for (let rowIdx = 0; rowIdx < newItems.length; rowIdx++) { + const holder = cells.value[rowIdx]; + holder.row.using = true; + + const oldCells = holder.cells; + const newItem = newItems[rowIdx]; + for (let colIdx = 0; colIdx < oldCells.length; colIdx++) { + const _col = columns.value[colIdx]; + + const oldCell = oldCells[colIdx]; + const newValue = newItem[_col.setting.bindTo]; + if (oldCell.value !== newValue) { + oldCell.value = _col.setting.valueTransformer + ? _col.setting.valueTransformer(holder.row, _col, newValue) + : newValue; + changedCells.push(oldCell); + } + } + } + + if (changedCells.length > 0) { + const allCells = cells.value.slice(0, newItems.length).flatMap(it => it.cells); + for (const cell of allCells) { + cell.violation = cellValidation(allCells, cell, cell.value); + } + + applyRowRules(changedCells); + + // ã‚»ãƒ«å€¤ãŒæ›¸ãæ›ã‚ã£ã¦ãŠã‚Šã€ãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³ã®çµæžœã‚‚変ã‚ã£ã¦ã„ã‚‹ã®ã§å¤–部ã«é€šçŸ¥ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ + emitGridEvent({ + type: 'cell-validation', + all: cells.value + .filter(it => it.row.using) + .flatMap(it => it.cells) + .map(it => it.violation) + .filter(it => !it.valid), + }); + } + + if (_DEV_) { + console.log('[grid][patch-data][end]'); + } +} + +// endregion +// #endregion + +onMounted(() => { + state.value = 'normal'; + + const bindToList = columnSettings.map(it => it.bindTo); + if (new Set(bindToList).size !== columnSettings.length) { + // å–å¾—å…ƒã®ãƒ—ãƒãƒ‘ティåé‡è¤‡ã¯è¨±å®¹ã—ãŸããªã„ + throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`); + } + + if (rootEl.value) { + resizeObserver.observe(rootEl.value); + + // åˆæœŸè¡¨ç¤ºæ™‚ã«ã‚³ãƒ³ãƒ†ãƒ³ãƒ„ãŒè¡¨ç¤ºã•れã¦ã„ãªã„å ´åˆã¯hidden状態ã«ã—ã¦ãŠã。 + // コンテンツ表示時ã«resizeイベントãŒç™ºç”Ÿã™ã‚‹ãŒã€ãã®ã¨ãã«hidden状態ã«ã—ã¦ãŠã‹ãªã„ã¨ã‚µã‚¤ã‚ºã®å†è¨ˆç®—ãŒèµ°ã‚‰ãªã„ã®ã§ + const bounds = rootEl.value.getBoundingClientRect(); + if (bounds.width === 0 || bounds.height === 0) { + state.value = 'hidden'; + } + } + + refreshData(); +}); +</script> + +<style module lang="scss"> +.grid { + font-size: 90%; + overflow-x: scroll; + // firefoxã ã¨ã‚¹ã‚¯ãƒãƒ¼ãƒ«ãƒãƒ¼ãŒã‚»ãƒ«ã«é‡ãªã£ã¦è¦‹ã¥ã‚‰ããªã£ã¦ã—ã¾ã†ã®ã§ã‚¹ãƒšãƒ¼ã‚¹ã‚’空ã‘ã¦ãŠã + padding-bottom: 8px; + + &.noOverflowHandling { + overflow-x: revert; + padding-bottom: 0; + } +} +</style> + +<style lang="scss"> +$borderSetting: solid 0.5px var(--MI_THEME-divider); + +// é…下コンãƒãƒ¼ãƒãƒ³ãƒˆã‚’å«ã‚ã¦ä¸€æ‹¬ã—ã¦ã‚³ãƒ³ãƒˆãƒãƒ¼ãƒ«ã™ã‚‹ãŸã‚ã€scopedã‚‚moduleも使用ã§ããªã„ +.mk_grid_border { + --rootBorderSetting: none; + --borderRadius: 0; + + border-spacing: 0; + + &.mk_grid_root_border { + --rootBorderSetting: #{$borderSetting}; + } + + &.mk_grid_root_rounded { + --borderRadius: var(--MI-radius); + } + + .mk_grid_thead { + .mk_grid_tr { + .mk_grid_th { + border-left: $borderSetting; + border-top: var(--rootBorderSetting); + + &:first-child { + // 左上セル + border-left: var(--rootBorderSetting); + border-top-left-radius: var(--borderRadius); + } + + &:last-child { + // å³ä¸Šã‚»ãƒ« + border-top-right-radius: var(--borderRadius); + border-right: var(--rootBorderSetting); + } + } + } + } + + .mk_grid_tbody { + .mk_grid_tr { + .mk_grid_td, .mk_grid_th { + border-left: $borderSetting; + border-top: $borderSetting; + + &:first-child { + // 左端ã®åˆ— + border-left: var(--rootBorderSetting); + } + + &:last-child { + // 一番å³ç«¯ã®åˆ— + border-right: var(--rootBorderSetting); + } + } + } + + .last_row { + .mk_grid_td, .mk_grid_th { + // 一番下ã®è¡Œ + border-bottom: var(--rootBorderSetting); + + &:first-child { + // 左下セル + border-bottom-left-radius: var(--borderRadius); + } + + &:last-child { + // å³ä¸‹ã‚»ãƒ« + border-bottom-right-radius: var(--borderRadius); + } + } + } + } +} +</style> diff --git a/packages/frontend/src/components/grid/MkHeaderCell.vue b/packages/frontend/src/components/grid/MkHeaderCell.vue new file mode 100644 index 0000000000..aecfe7eaa3 --- /dev/null +++ b/packages/frontend/src/components/grid/MkHeaderCell.vue @@ -0,0 +1,216 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + ref="rootEl" + class="mk_grid_th" + :class="$style.cell" + :style="[{ maxWidth: column.width, minWidth: column.width, width: column.width }]" + data-grid-cell + :data-grid-cell-row="-1" + :data-grid-cell-col="column.index" +> + <div :class="$style.root"> + <div :class="$style.left"></div> + <div :class="$style.wrapper"> + <div ref="contentEl" :class="$style.contentArea"> + <span v-if="column.setting.icon" class="ti" :class="column.setting.icon" style="line-height: normal"></span> + <span v-else>{{ text }}</span> + </div> + </div> + <div + :class="$style.right" + @mousedown="onHandleMouseDown" + @dblclick="onHandleDoubleClick" + ></div> + </div> +</div> +</template> + +<script setup lang="ts"> +import { computed, nextTick, onMounted, onUnmounted, ref, toRefs, watch } from 'vue'; +import { GridEventEmitter, Size } from '@/components/grid/grid.js'; +import { GridColumn } from '@/components/grid/column.js'; + +const emit = defineEmits<{ + (ev: 'operation:beginWidthChange', sender: GridColumn): void; + (ev: 'operation:endWidthChange', sender: GridColumn): void; + (ev: 'operation:widthLargest', sender: GridColumn): void; + (ev: 'change:width', sender: GridColumn, width: string): void; + (ev: 'change:contentSize', sender: GridColumn, newSize: Size): void; +}>(); +const props = defineProps<{ + column: GridColumn, + bus: GridEventEmitter, +}>(); + +const { column, bus } = toRefs(props); + +const rootEl = ref<InstanceType<typeof HTMLTableCellElement>>(); +const contentEl = ref<InstanceType<typeof HTMLDivElement>>(); + +const resizing = ref<boolean>(false); + +const text = computed(() => { + const result = column.value.setting.title ?? column.value.setting.bindTo; + return result.length > 0 ? result : ' '; +}); + +watch(column, () => { + // ä¸èº«ãŒã‚»ãƒƒãƒˆã•れãŸç›´å¾Œã¯ã‚µã‚¤ã‚ºãŒåˆ†ã‹ã‚‰ãªã„ã®ã§ã€æ¬¡ã®ã‚¿ã‚¤ãƒŸãƒ³ã‚°ã§æ›´æ–°ã™ã‚‹ + nextTick(emitContentSizeChanged); +}, { immediate: true }); + +function onHandleDoubleClick(ev: MouseEvent) { + switch (ev.type) { + case 'dblclick': { + emit('operation:widthLargest', column.value); + break; + } + } +} + +function onHandleMouseDown(ev: MouseEvent) { + switch (ev.type) { + case 'mousedown': { + if (!resizing.value) { + registerHandleMouseUp(); + registerHandleMouseMove(); + resizing.value = true; + emit('operation:beginWidthChange', column.value); + } + break; + } + } +} + +function onHandleMouseMove(ev: MouseEvent) { + if (!rootEl.value) { + // 型ガード + return; + } + + switch (ev.type) { + case 'mousemove': { + if (resizing.value) { + const bounds = rootEl.value.getBoundingClientRect(); + const clientWidth = rootEl.value.clientWidth; + const clientRight = bounds.left + clientWidth; + const nextWidth = clientWidth + (ev.clientX - clientRight); + emit('change:width', column.value, `${nextWidth}px`); + } + break; + } + } +} + +function onHandleMouseUp(ev: MouseEvent) { + switch (ev.type) { + case 'mouseup': { + if (resizing.value) { + unregisterHandleMouseUp(); + unregisterHandleMouseMove(); + resizing.value = false; + emit('operation:endWidthChange', column.value); + } + break; + } + } +} + +function onForceRefreshContentSize() { + emitContentSizeChanged(); +} + +function registerHandleMouseMove() { + unregisterHandleMouseMove(); + addEventListener('mousemove', onHandleMouseMove); +} + +function unregisterHandleMouseMove() { + removeEventListener('mousemove', onHandleMouseMove); +} + +function registerHandleMouseUp() { + unregisterHandleMouseUp(); + addEventListener('mouseup', onHandleMouseUp); +} + +function unregisterHandleMouseUp() { + removeEventListener('mouseup', onHandleMouseUp); +} + +function emitContentSizeChanged() { + const clientWidth = contentEl.value?.clientWidth ?? 0; + const clientHeight = contentEl.value?.clientHeight ?? 0; + emit('change:contentSize', column.value, { + // ãƒãƒ¼ã®æ¨ªå¹…も考慮ã—ãŸã„ã®ã§ã€+3px + width: clientWidth + 3 + 3, + height: clientHeight, + }); +} + +onMounted(() => { + bus.value.on('forceRefreshContentSize', onForceRefreshContentSize); +}); + +onUnmounted(() => { + bus.value.off('forceRefreshContentSize', onForceRefreshContentSize); +}); + +</script> + +<style module lang="scss"> +$handleWidth: 5px; +$cellHeight: 28px; + +.cell { + cursor: pointer; +} + +.root { + display: flex; + flex-direction: row; + height: $cellHeight; + max-height: $cellHeight; + min-height: $cellHeight; + + .wrapper { + flex: 1; + display: flex; + flex-direction: row; + overflow: hidden; + justify-content: center; + } + + .contentArea { + display: flex; + padding: 6px 4px; + box-sizing: border-box; + overflow: hidden; + white-space: nowrap; + text-align: center; + } + + .left { + // rightã®ã¶ã‚“ã ã‘ズレるã®ã§ãれを相殺ã™ã‚‹ãŸã‚ã®ãƒã‚¬ãƒ†ã‚£ãƒ–マージン + margin-left: -$handleWidth; + margin-right: auto; + width: $handleWidth; + min-width: $handleWidth; + } + + .right { + margin-left: auto; + // 判定を罫線ã®ä¸Šã«é‡ããŸã„ã®ã§ãƒã‚¬ãƒ†ã‚£ãƒ–マージンを使ㆠ+ margin-right: -$handleWidth; + width: $handleWidth; + min-width: $handleWidth; + cursor: w-resize; + z-index: 1; + } +} +</style> diff --git a/packages/frontend/src/components/grid/MkHeaderRow.vue b/packages/frontend/src/components/grid/MkHeaderRow.vue new file mode 100644 index 0000000000..8affa08fd5 --- /dev/null +++ b/packages/frontend/src/components/grid/MkHeaderRow.vue @@ -0,0 +1,60 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + class="mk_grid_tr" + :class="$style.root" + :data-grid-row="-1" +> + <MkNumberCell + v-if="gridSetting.showNumber" + content="#" + :top="true" + /> + <MkHeaderCell + v-for="column in columns" + :key="column.index" + :column="column" + :bus="bus" + @operation:beginWidthChange="(sender) => emit('operation:beginWidthChange', sender)" + @operation:endWidthChange="(sender) => emit('operation:endWidthChange', sender)" + @operation:widthLargest="(sender) => emit('operation:widthLargest', sender)" + @change:width="(sender, width) => emit('change:width', sender, width)" + @change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)" + /> +</div> +</template> + +<script setup lang="ts"> +import { GridEventEmitter, Size } from '@/components/grid/grid.js'; +import MkHeaderCell from '@/components/grid/MkHeaderCell.vue'; +import MkNumberCell from '@/components/grid/MkNumberCell.vue'; +import { GridColumn } from '@/components/grid/column.js'; +import { GridRowSetting } from '@/components/grid/row.js'; + +const emit = defineEmits<{ + (ev: 'operation:beginWidthChange', sender: GridColumn): void; + (ev: 'operation:endWidthChange', sender: GridColumn): void; + (ev: 'operation:widthLargest', sender: GridColumn): void; + (ev: 'operation:selectionColumn', sender: GridColumn): void; + (ev: 'change:width', sender: GridColumn, width: string): void; + (ev: 'change:contentSize', sender: GridColumn, newSize: Size): void; +}>(); + +defineProps<{ + columns: GridColumn[], + gridSetting: GridRowSetting, + bus: GridEventEmitter, +}>(); +</script> + +<style module lang="scss"> +.root { + display: flex; + flex-direction: row; + align-items: center; +} +</style> diff --git a/packages/frontend/src/components/grid/MkNumberCell.vue b/packages/frontend/src/components/grid/MkNumberCell.vue new file mode 100644 index 0000000000..674bba96bc --- /dev/null +++ b/packages/frontend/src/components/grid/MkNumberCell.vue @@ -0,0 +1,61 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + class="mk_grid_th" + :class="[$style.cell]" + :tabindex="-1" + data-grid-cell + :data-grid-cell-row="row?.index ?? -1" + :data-grid-cell-col="-1" +> + <div :class="[$style.root]"> + {{ content }} + </div> +</div> +</template> + +<script setup lang="ts"> + +import { GridRow } from '@/components/grid/row.js'; + +defineProps<{ + content: string, + row?: GridRow, +}>(); + +</script> + +<style module lang="scss"> +$cellHeight: 28px; +$cellWidth: 34px; + +.cell { + overflow: hidden; + white-space: nowrap; + height: $cellHeight; + max-height: $cellHeight; + min-height: $cellHeight; + min-width: $cellWidth; + width: $cellWidth; + cursor: pointer; +} + +.root { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + box-sizing: border-box; + padding: 0 8px; + height: 100%; + border: solid 0.5px transparent; + + &.selected { + background-color: var(--MI_THEME-accentedBg); + } +} +</style> diff --git a/packages/frontend/src/components/grid/cell-validators.ts b/packages/frontend/src/components/grid/cell-validators.ts new file mode 100644 index 0000000000..949cab2ec6 --- /dev/null +++ b/packages/frontend/src/components/grid/cell-validators.ts @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { CellValue, GridCell } from '@/components/grid/cell.js'; +import { GridColumn } from '@/components/grid/column.js'; +import { GridRow } from '@/components/grid/row.js'; +import { i18n } from '@/i18n.js'; + +export type ValidatorParams = { + column: GridColumn; + row: GridRow; + value: CellValue; + allCells: GridCell[]; +}; + +export type ValidatorResult = { + valid: boolean; + message?: string; +} + +export type GridCellValidator = { + name?: string; + ignoreViolation?: boolean; + validate: (params: ValidatorParams) => ValidatorResult; +} + +export type ValidateViolation = { + valid: boolean; + params: ValidatorParams; + violations: ValidateViolationItem[]; +} + +export type ValidateViolationItem = { + valid: boolean; + validator: GridCellValidator; + result: ValidatorResult; +} + +export function cellValidation(allCells: GridCell[], cell: GridCell, newValue: CellValue): ValidateViolation { + const { column, row } = cell; + const validators = column.setting.validators ?? []; + + const params: ValidatorParams = { + column, + row, + value: newValue, + allCells, + }; + + const violations: ValidateViolationItem[] = validators.map(validator => { + const result = validator.validate(params); + return { + valid: result.valid, + validator, + result, + }; + }); + + return { + valid: violations.every(v => v.result.valid), + params, + violations, + }; +} + +class ValidatorPreset { + required(): GridCellValidator { + return { + name: 'required', + validate: ({ value }): ValidatorResult => { + return { + valid: value !== null && value !== undefined && value !== '', + message: i18n.ts._gridComponent._error.requiredValue, + }; + }, + }; + } + + regex(pattern: RegExp): GridCellValidator { + return { + name: 'regex', + validate: ({ value }): ValidatorResult => { + return { + valid: (typeof value !== 'string') || pattern.test(value.toString() ?? ''), + message: i18n.tsx._gridComponent._error.patternNotMatch({ pattern: pattern.source }), + }; + }, + }; + } + + unique(): GridCellValidator { + return { + name: 'unique', + validate: ({ column, row, value, allCells }): ValidatorResult => { + const bindTo = column.setting.bindTo; + const isUnique = allCells + .filter(it => it.column.setting.bindTo === bindTo && it.row.index !== row.index) + .every(cell => cell.value !== value); + return { + valid: isUnique, + message: i18n.ts._gridComponent._error.notUnique, + }; + }, + }; + } +} + +export const validators = new ValidatorPreset(); diff --git a/packages/frontend/src/components/grid/cell.ts b/packages/frontend/src/components/grid/cell.ts new file mode 100644 index 0000000000..71b7a3e3f1 --- /dev/null +++ b/packages/frontend/src/components/grid/cell.ts @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ValidateViolation } from '@/components/grid/cell-validators.js'; +import { Size } from '@/components/grid/grid.js'; +import { GridColumn } from '@/components/grid/column.js'; +import { GridRow } from '@/components/grid/row.js'; +import { MenuItem } from '@/types/menu.js'; +import { GridContext } from '@/components/grid/grid-event.js'; + +export type CellValue = string | boolean | number | undefined | null | Array<unknown> | NonNullable<unknown>; + +export type CellAddress = { + row: number; + col: number; +} + +export const CELL_ADDRESS_NONE: CellAddress = { + row: -1, + col: -1, +}; + +export type GridCell = { + address: CellAddress; + value: CellValue; + column: GridColumn; + row: GridRow; + selected: boolean; + ranged: boolean; + contentSize: Size; + setting: GridCellSetting; + violation: ValidateViolation; +} + +export type GridCellContextMenuFactory = (col: GridColumn, row: GridRow, value: CellValue, context: GridContext) => MenuItem[]; + +export type GridCellSetting = { + contextMenuFactory?: GridCellContextMenuFactory; +} + +export function createCell( + column: GridColumn, + row: GridRow, + value: CellValue, + setting: GridCellSetting, +): GridCell { + const newValue = (row.using && column.setting.valueTransformer) + ? column.setting.valueTransformer(row, column, value) + : value; + + return { + address: { row: row.index, col: column.index }, + value: newValue, + column, + row, + selected: false, + ranged: false, + contentSize: { width: 0, height: 0 }, + violation: { + valid: true, + params: { + column, + row, + value, + allCells: [], + }, + violations: [], + }, + setting, + }; +} + +export function resetCell(cell: GridCell): void { + cell.selected = false; + cell.ranged = false; + cell.violation = { + valid: true, + params: { + column: cell.column, + row: cell.row, + value: cell.value, + allCells: [], + }, + violations: [], + }; +} diff --git a/packages/frontend/src/components/grid/column.ts b/packages/frontend/src/components/grid/column.ts new file mode 100644 index 0000000000..2f505756fe --- /dev/null +++ b/packages/frontend/src/components/grid/column.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { GridCellValidator } from '@/components/grid/cell-validators.js'; +import { Size, SizeStyle } from '@/components/grid/grid.js'; +import { calcCellWidth } from '@/components/grid/grid-utils.js'; +import { CellValue, GridCell } from '@/components/grid/cell.js'; +import { GridRow } from '@/components/grid/row.js'; +import { MenuItem } from '@/types/menu.js'; +import { GridContext } from '@/components/grid/grid-event.js'; + +export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image' | 'hidden'; + +export type CustomValueEditor = (row: GridRow, col: GridColumn, value: CellValue, cellElement: HTMLElement) => Promise<CellValue>; +export type CellValueTransformer = (row: GridRow, col: GridColumn, value: CellValue) => CellValue; +export type GridColumnContextMenuFactory = (col: GridColumn, context: GridContext) => MenuItem[]; + +export type GridColumnSetting = { + bindTo: string; + title?: string; + icon?: string; + type: ColumnType; + width: SizeStyle; + editable?: boolean; + validators?: GridCellValidator[]; + customValueEditor?: CustomValueEditor; + valueTransformer?: CellValueTransformer; + contextMenuFactory?: GridColumnContextMenuFactory; + events?: { + copy?: (value: CellValue) => string; + paste?: (text: string) => CellValue; + delete?: (cell: GridCell, context: GridContext) => void; + } +}; + +export type GridColumn = { + index: number; + setting: GridColumnSetting; + width: string; + contentSize: Size; +} + +export function createColumn(setting: GridColumnSetting, index: number): GridColumn { + return { + index, + setting, + width: calcCellWidth(setting.width), + contentSize: { width: 0, height: 0 }, + }; +} + diff --git a/packages/frontend/src/components/grid/grid-event.ts b/packages/frontend/src/components/grid/grid-event.ts new file mode 100644 index 0000000000..074b72b956 --- /dev/null +++ b/packages/frontend/src/components/grid/grid-event.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; +import { GridState } from '@/components/grid/grid.js'; +import { ValidateViolation } from '@/components/grid/cell-validators.js'; +import { GridColumn } from '@/components/grid/column.js'; +import { GridRow } from '@/components/grid/row.js'; + +export type GridContext = { + selectedCell?: GridCell; + rangedCells: GridCell[]; + rangedRows: GridRow[]; + randedBounds: { + leftTop: CellAddress; + rightBottom: CellAddress; + }; + availableBounds: { + leftTop: CellAddress; + rightBottom: CellAddress; + }; + state: GridState; + rows: GridRow[]; + columns: GridColumn[]; +}; + +export type GridEvent = + GridCellValueChangeEvent | + GridCellValidationEvent + ; + +export type GridCellValueChangeEvent = { + type: 'cell-value-change'; + column: GridColumn; + row: GridRow; + oldValue: CellValue; + newValue: CellValue; +}; + +export type GridCellValidationEvent = { + type: 'cell-validation'; + violation?: ValidateViolation; + all: ValidateViolation[]; +}; diff --git a/packages/frontend/src/components/grid/grid-utils.ts b/packages/frontend/src/components/grid/grid-utils.ts new file mode 100644 index 0000000000..a45bc88926 --- /dev/null +++ b/packages/frontend/src/components/grid/grid-utils.ts @@ -0,0 +1,215 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { isRef, Ref } from 'vue'; +import { DataSource, SizeStyle } from '@/components/grid/grid.js'; +import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; +import { GridRow } from '@/components/grid/row.js'; +import { GridContext } from '@/components/grid/grid-event.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { GridColumn, GridColumnSetting } from '@/components/grid/column.js'; + +export function isCellElement(elem: HTMLElement): boolean { + return elem.hasAttribute('data-grid-cell'); +} + +export function isRowElement(elem: HTMLElement): boolean { + return elem.hasAttribute('data-grid-row'); +} + +export function calcCellWidth(widthSetting: SizeStyle): string { + switch (widthSetting) { + case undefined: + case 'auto': { + return 'auto'; + } + default: { + return `${widthSetting}px`; + } + } +} + +function getCellRowByAttribute(elem: HTMLElement): number { + const row = elem.getAttribute('data-grid-cell-row'); + if (row === null) { + throw new Error('data-grid-cell-row attribute not found'); + } + return Number(row); +} + +function getCellColByAttribute(elem: HTMLElement): number { + const col = elem.getAttribute('data-grid-cell-col'); + if (col === null) { + throw new Error('data-grid-cell-col attribute not found'); + } + return Number(col); +} + +export function getCellAddress(elem: HTMLElement, parentNodeCount = 10): CellAddress { + let node = elem; + for (let i = 0; i < parentNodeCount; i++) { + if (!node.parentElement) { + break; + } + + if (isCellElement(node) && isRowElement(node.parentElement)) { + const row = getCellRowByAttribute(node); + const col = getCellColByAttribute(node); + + return { row, col }; + } + + node = node.parentElement; + } + + return CELL_ADDRESS_NONE; +} + +export function getCellElement(elem: HTMLElement, parentNodeCount = 10): HTMLElement | null { + let node = elem; + for (let i = 0; i < parentNodeCount; i++) { + if (isCellElement(node)) { + return node; + } + + if (!node.parentElement) { + break; + } + + node = node.parentElement; + } + + return null; +} + +export function equalCellAddress(a: CellAddress, b: CellAddress): boolean { + return a.row === b.row && a.col === b.col; +} + +/** + * グリッドã®é¸æŠžç¯„囲ã®å†…容をタブ区切り形å¼ãƒ†ã‚ストã«å¤‰æ›ã—ã¦ã‚¯ãƒªãƒƒãƒ—ボードã«ã‚³ãƒ”ーã™ã‚‹ã€‚ + */ +export function copyGridDataToClipboard( + gridItems: Ref<DataSource[]> | DataSource[], + context: GridContext, +) { + const items = isRef(gridItems) ? gridItems.value : gridItems; + const lines = Array.of<string>(); + const bounds = context.randedBounds; + + for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { + const rowItems = Array.of<string>(); + for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) { + const { bindTo, events } = context.columns[col].setting; + const value = items[row][bindTo]; + const transformValue = events?.copy + ? events.copy(value) + : typeof value === 'object' || Array.isArray(value) + ? JSON.stringify(value) + : value?.toString() ?? ''; + rowItems.push(transformValue); + } + lines.push(rowItems.join('\t')); + } + + const text = lines.join('\n'); + copyToClipboard(text); + + if (_DEV_) { + console.log(`Copied to clipboard: ${text}`); + } +} + +/** + * クリップボードã‹ã‚‰ã‚¿ãƒ–区切りテã‚ストã¨ã—ã¦å€¤ã‚’èªã¿å–りã€ã‚°ãƒªãƒƒãƒ‰ã®é¸æŠžç¯„囲ã«è²¼ã‚Šä»˜ã‘ã‚‹ãŸã‚ã®ãƒ¦ãƒ¼ãƒ†ã‚£ãƒªãƒ†ã‚£é–¢æ•°ã€‚ + * …ã¨è¨€ã„ã¤ã¤ã‚‚ã€ä½¿ç”¨ç®‡æ‰€ã«ã‚ˆã‚Šåæ˜ æ–¹æ³•ã«å·®ãŒã‚ã‚‹ãŸã‚æ›´æ–°æ“作ã¯ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯é–¢æ•°ã«ä»»ã›ã¦ã„る。 + */ +export async function pasteToGridFromClipboard( + context: GridContext, + callback: (row: GridRow, col: GridColumn, parsedValue: CellValue) => void, +) { + function parseValue(value: string, setting: GridColumnSetting): CellValue { + if (setting.events?.paste) { + return setting.events.paste(value); + } else { + switch (setting.type) { + case 'number': { + return Number(value); + } + case 'boolean': { + return value === 'true'; + } + default: { + return value; + } + } + } + } + + const clipBoardText = await navigator.clipboard.readText(); + if (_DEV_) { + console.log(`Paste from clipboard: ${clipBoardText}`); + } + + const bounds = context.randedBounds; + const lines = clipBoardText.replace(/\r/g, '') + .split('\n') + .map(it => it.split('\t')); + + if (lines.length === 1 && lines[0].length === 1) { + // å˜ç‹¬æ–‡å—列ã®å ´åˆã¯é¸æŠžç¯„囲全体ã«åŒã˜ãƒ†ã‚ストを貼り付ã‘ã‚‹ + const ranges = context.rangedCells; + for (const cell of ranges) { + if (cell.column.setting.editable) { + callback(cell.row, cell.column, parseValue(lines[0][0], cell.column.setting)); + } + } + } else { + // è¡¨å½¢å¼æ–‡å—列ã®å ´åˆã¯è¡¨å½¢å¼ã«ãƒ‘ースã—ã€é¸æŠžç¯„囲ã«åˆã†ã‚ˆã†ã«è²¼ã‚Šä»˜ã‘ã‚‹ + const offsetRow = bounds.leftTop.row; + const offsetCol = bounds.leftTop.col; + const { columns, rows } = context; + for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { + const rowIdx = row - offsetRow; + if (lines.length <= rowIdx) { + // クリップボードã‹ã‚‰èªã‚“ã 二次元é…åˆ—ã‚ˆã‚Šã‚‚é¸æŠžç¯„å›²ã®æ–¹ãŒå¤§ãã„å ´åˆã€è²¼ã‚Šä»˜ã‘æ“作を打ã¡åˆ‡ã‚‹ + break; + } + + const items = lines[rowIdx]; + for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) { + const colIdx = col - offsetCol; + if (items.length <= colIdx) { + // クリップボードã‹ã‚‰èªã‚“ã 二次元é…åˆ—ã‚ˆã‚Šã‚‚é¸æŠžç¯„å›²ã®æ–¹ãŒå¤§ãã„å ´åˆã€è²¼ã‚Šä»˜ã‘æ“作を打ã¡åˆ‡ã‚‹ + break; + } + + if (columns[col].setting.editable) { + callback(rows[row], columns[col], parseValue(items[colIdx], columns[col].setting)); + } + } + } + } +} + +/** + * グリッドã®é¸æŠžç¯„囲ã«ã‚るデータを削除ã™ã‚‹ãŸã‚ã®ãƒ¦ãƒ¼ãƒ†ã‚£ãƒªãƒ†ã‚£é–¢æ•°ã€‚ + * …ã¨è¨€ã„ã¤ã¤ã‚‚ã€ä½¿ç”¨ç®‡æ‰€ã«ã‚ˆã‚Šåæ˜ æ–¹æ³•ã«å·®ãŒã‚ã‚‹ãŸã‚æ›´æ–°æ“作ã¯ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯é–¢æ•°ã«ä»»ã›ã¦ã„る。 + */ +export function removeDataFromGrid( + context: GridContext, + callback: (cell: GridCell) => void, +) { + for (const cell of context.rangedCells) { + const { editable, events } = cell.column.setting; + if (editable) { + if (events?.delete) { + events.delete(cell, context); + } else { + callback(cell); + } + } + } +} diff --git a/packages/frontend/src/components/grid/grid.ts b/packages/frontend/src/components/grid/grid.ts new file mode 100644 index 0000000000..b82e12b304 --- /dev/null +++ b/packages/frontend/src/components/grid/grid.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import { CellValue, GridCellSetting } from '@/components/grid/cell.js'; +import { GridColumnSetting } from '@/components/grid/column.js'; +import { GridRowSetting } from '@/components/grid/row.js'; + +export type GridSetting = { + root?: { + noOverflowStyle?: boolean; + rounded?: boolean; + outerBorder?: boolean; + }; + row?: GridRowSetting; + cols: GridColumnSetting[]; + cells?: GridCellSetting; +}; + +export type DataSource = Record<string, CellValue>; + +export type GridState = + 'normal' | + 'cellSelecting' | + 'cellEditing' | + 'colResizing' | + 'colSelecting' | + 'rowSelecting' | + 'hidden' + ; + +export type Size = { + width: number; + height: number; +} + +export type SizeStyle = number | 'auto' | undefined; + +export type AdditionalStyle = { + className?: string; + style?: Record<string, string | number>; +} + +export class GridEventEmitter extends EventEmitter<{ + 'forceRefreshContentSize': void; +}> { +} diff --git a/packages/frontend/src/components/grid/row.ts b/packages/frontend/src/components/grid/row.ts new file mode 100644 index 0000000000..e0a317c9d3 --- /dev/null +++ b/packages/frontend/src/components/grid/row.ts @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { AdditionalStyle } from '@/components/grid/grid.js'; +import { GridCell } from '@/components/grid/cell.js'; +import { GridColumn } from '@/components/grid/column.js'; +import { MenuItem } from '@/types/menu.js'; +import { GridContext } from '@/components/grid/grid-event.js'; + +export const defaultGridRowSetting: Required<GridRowSetting> = { + showNumber: true, + selectable: true, + minimumDefinitionCount: 100, + styleRules: [], + contextMenuFactory: () => [], + events: {}, +}; + +export type GridRowStyleRuleConditionParams = { + row: GridRow, + targetCols: GridColumn[], + cells: GridCell[] +}; + +export type GridRowStyleRule = { + condition: (params: GridRowStyleRuleConditionParams) => boolean; + applyStyle: AdditionalStyle; +} + +export type GridRowContextMenuFactory = (row: GridRow, context: GridContext) => MenuItem[]; + +export type GridRowSetting = { + showNumber?: boolean; + selectable?: boolean; + minimumDefinitionCount?: number; + styleRules?: GridRowStyleRule[]; + contextMenuFactory?: GridRowContextMenuFactory; + events?: { + delete?: (rows: GridRow[]) => void; + } +} + +export type GridRow = { + index: number; + ranged: boolean; + using: boolean; + setting: GridRowSetting; + additionalStyles: AdditionalStyle[]; +} + +export function createRow(index: number, using: boolean, setting: GridRowSetting): GridRow { + return { + index, + ranged: false, + using: using, + setting, + additionalStyles: [], + }; +} + +export function resetRow(row: GridRow): void { + row.ranged = false; + row.using = false; + row.additionalStyles = []; +} + diff --git a/packages/frontend/src/components/hook/useLoading.ts b/packages/frontend/src/components/hook/useLoading.ts new file mode 100644 index 0000000000..6c6ff6ae0d --- /dev/null +++ b/packages/frontend/src/components/hook/useLoading.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { computed, h, ref } from 'vue'; +import MkLoading from '@/components/global/MkLoading.vue'; + +export const useLoading = (props?: { + static?: boolean; + inline?: boolean; + colored?: boolean; + mini?: boolean; + em?: boolean; +}) => { + const showingCnt = ref(0); + + const show = () => { + showingCnt.value++; + }; + + const close = (force?: boolean) => { + if (force) { + showingCnt.value = 0; + } else { + showingCnt.value = Math.max(0, showingCnt.value - 1); + } + }; + + const scope = <T>(fn: () => T) => { + show(); + + const result = fn(); + if (result instanceof Promise) { + return result.finally(() => close()); + } else { + close(); + return result; + } + }; + + const showing = computed(() => showingCnt.value > 0); + const component = computed(() => showing.value ? h(MkLoading, props) : null); + + return { + show, + close, + scope, + component, + showing, + }; +}; diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html index f9ce113687..e69de29bb2 100644 --- a/packages/frontend/src/index.html +++ b/packages/frontend/src/index.html @@ -1,39 +0,0 @@ -<!-- - SPDX-FileCopyrightText: syuilo and misskey-project - SPDX-License-Identifier: AGPL-3.0-only ---> - -<!-- - 開発モードã®viteã¯ã“ã®ãƒ•ァイルを起点ã«ã‚µãƒ¼ãƒãƒ¼ã‚’èµ·å‹•ã—ã¾ã™ã€‚ - ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã«æ›¸ã‹ã‚ŒãŸ [t]js ã®ãƒªãƒ³ã‚¯ã¨ (s)cssã®ãƒªãƒ³ã‚¯ã¨ã€ãã®ä¾å˜é–¢ä¿‚ã«ã‚るファイルã¯ãƒ“ルドã•れã¾ã™ ---> - -<!DOCTYPE html> -<html> -<head> - <meta charset="UTF-8" /> - <title>[DEV] Loading...</title> - <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> - <meta - http-equiv="Content-Security-Policy" - content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/; - worker-src 'self' blob:; - script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh https://cdn.jsdelivr.net https://raw.esm.sh; - style-src 'self' 'unsafe-inline'; - img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com; - media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; - connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org https://api.friendlycaptcha.com https://raw.esm.sh; - frame-src *;" - /> - <meta property="og:site_name" content="[DEV BUILD] Sharkey" /> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta name="theme-color-orig" content="#86b300"> - <link rel='stylesheet' href='/assets/phosphor-icons/bold/style.css'> - <link rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css'> -</head> - -<body> -<div id="sharkey_app"></div> -<script type="module" src="./_dev_boot_.ts"></script> -</body> -</html> diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index a81f67aef3..e33c121145 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -11,6 +11,7 @@ import * as Misskey from 'misskey-js'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/scripts/form.js'; import type { MenuItem } from '@/types/menu.js'; +import type { PostFormProps } from '@/types/post-form.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -28,15 +29,15 @@ import { pleaseLogin } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; import { focusParent } from '@/scripts/focus.js'; -import type { PostFormProps } from '@/types/post-form.js'; export const openingWindowsCount = ref(0); +export type ApiWithDialogCustomErrors = Record<string, { title?: string; text: string; }>; export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>( endpoint: E, data: P, token?: string | null | undefined, - customErrors?: Record<string, { title?: string; text: string; }>, + customErrors?: ApiWithDialogCustomErrors, ) => { const promise = misskeyApi(endpoint, data, token); promiseDialog(promise, null, async (err) => { @@ -610,6 +611,27 @@ export async function selectDriveFolder(multiple: boolean): Promise<Misskey.enti }); } +export async function selectRole(params: { + initialRoleIds?: string[], + title?: string, + infoMessage?: string, + publicOnly?: boolean, +}): Promise< + { canceled: true; result: undefined; } | + { canceled: false; result: Misskey.entities.Role[] } +> { + return new Promise((resolve) => { + popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, { + done: roles => { + resolve({ canceled: false, result: roles }); + }, + close: () => { + resolve({ canceled: true, result: undefined }); + }, + }, 'dispose'); + }); +} + export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> { return new Promise(resolve => { const { dispose } = popup(MkEmojiPickerDialog, { diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/packages/frontend/src/pages/about-misskey.vue diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index f35cbe8d5a..1f36589a49 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> <XEmojis/> </MkSpacer> - <MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20"> + <MkSpacer v-else-if="instance.federation !== 'none' && tab === 'federation'" :contentMax="1000" :marginMin="20"> <XFederation/> </MkSpacer> <MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20"> @@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, defineAsyncComponent, ref, watch } from 'vue'; +import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -51,22 +52,34 @@ watch(tab, () => { const headerActions = computed(() => []); -const headerTabs = computed(() => [{ - key: 'overview', - title: i18n.ts.overview, -}, { - key: 'emojis', - title: i18n.ts.customEmojis, - icon: 'ph-smiley ph-bold ph-lg', -}, { - key: 'federation', - title: i18n.ts.federation, - icon: 'ti ti-whirl', -}, { - key: 'charts', - title: i18n.ts.charts, - icon: 'ti ti-chart-line', -}]); +const headerTabs = computed(() => { + const items = []; + + items.push({ + key: 'overview', + title: i18n.ts.overview, + }, { + key: 'emojis', + title: i18n.ts.customEmojis, + icon: 'ph-smiley ph-bold ph-lg', + }); + + if (instance.federation !== 'none') { + items.push({ + key: 'federation', + title: i18n.ts.federation, + icon: 'ti ti-whirl', + }); + } + + items.push({ + key: 'charts', + title: i18n.ts.charts, + icon: 'ti ti-chart-line', + }); + + return items; +}); definePageMetadata(() => ({ title: i18n.ts.instanceInfo, diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 2f6dac8097..23c5a0f9b7 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -14,13 +14,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-else-if="botProtectionForm.savedState.provider === 'fc'" #suffix>FriendlyCaptcha</template> <template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template> <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> - <template v-if="botProtectionForm.modified.value" #footer> - <MkFormFooter :form="botProtectionForm"/> + <template #footer> + <MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/> </template> <div class="_gaps_m"> <MkRadios v-model="botProtectionForm.state.provider"> - <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> + <option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> <option value="hcaptcha">hCaptcha</option> <option value="mcaptcha">mCaptcha</option> <option value="recaptcha">reCAPTCHA</option> @@ -30,71 +30,126 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRadios> <template v-if="botProtectionForm.state.provider === 'hcaptcha'"> - <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey"> + <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> </MkInput> - <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey"> + <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> </MkInput> - <FormSlot> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> + <FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey"> + <template #label>{{ i18n.ts._captcha.verify }}</template> + <MkCaptcha + v-model="captchaResult" + provider="hcaptcha" + :sitekey="botProtectionForm.state.hcaptchaSiteKey" + :secretKey="botProtectionForm.state.hcaptchaSecretKey" + /> </FormSlot> + <MkInfo> + <div :class="$style.captchaInfoMsg"> + <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div> + <div> + <span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a> + </div> + </div> + </MkInfo> </template> + <template v-else-if="botProtectionForm.state.provider === 'mcaptcha'"> - <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey"> + <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> </MkInput> - <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey"> + <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> </MkInput> - <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl"> + <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce> <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> </MkInput> <FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl"> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"/> + <template #label>{{ i18n.ts._captcha.verify }}</template> + <MkCaptcha + v-model="captchaResult" + provider="mcaptcha" + :sitekey="botProtectionForm.state.mcaptchaSiteKey" + :secretKey="botProtectionForm.state.mcaptchaSecretKey" + :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl" + /> </FormSlot> </template> + <template v-else-if="botProtectionForm.state.provider === 'recaptcha'"> - <MkInput v-model="botProtectionForm.state.recaptchaSiteKey"> + <MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.recaptchaSiteKey }}</template> </MkInput> - <MkInput v-model="botProtectionForm.state.recaptchaSecretKey"> + <MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.recaptchaSecretKey }}</template> </MkInput> <FormSlot v-if="botProtectionForm.state.recaptchaSiteKey"> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="recaptcha" :sitekey="botProtectionForm.state.recaptchaSiteKey"/> + <template #label>{{ i18n.ts._captcha.verify }}</template> + <MkCaptcha + v-model="captchaResult" + provider="recaptcha" + :sitekey="botProtectionForm.state.recaptchaSiteKey" + :secretKey="botProtectionForm.state.recaptchaSecretKey" + /> </FormSlot> + <MkInfo> + <div :class="$style.captchaInfoMsg"> + <div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div> + <div> + <span>ref: </span> + <a + href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do" + target="_blank" + >reCAPTCHA FAQ</a> + </div> + </div> + </MkInfo> </template> + <template v-else-if="botProtectionForm.state.provider === 'turnstile'"> - <MkInput v-model="botProtectionForm.state.turnstileSiteKey"> + <MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.turnstileSiteKey }}</template> </MkInput> - <MkInput v-model="botProtectionForm.state.turnstileSecretKey"> + <MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.turnstileSecretKey }}</template> </MkInput> - <FormSlot> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/> + <FormSlot v-if="botProtectionForm.state.turnstileSiteKey"> + <template #label>{{ i18n.ts._captcha.verify }}</template> + <MkCaptcha + v-model="captchaResult" + provider="turnstile" + :sitekey="botProtectionForm.state.turnstileSiteKey" + :secretKey="botProtectionForm.state.turnstileSecretKey" + /> </FormSlot> + <MkInfo> + <div :class="$style.captchaInfoMsg"> + <div> + {{ i18n.ts._captcha.testSiteKeyMessage }} + </div> + <div> + <span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a> + </div> + </div> + </MkInfo> </template> + <template v-else-if="botProtectionForm.state.provider === 'fc'"> - <MkInput v-model="botProtectionForm.state.fcSiteKey"> + <MkInput v-model="botProtectionForm.state.fcSiteKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> </MkInput> - <MkInput v-model="botProtectionForm.state.fcSecretKey"> + <MkInput v-model="botProtectionForm.state.fcSecretKey" debounce> <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> </MkInput> @@ -102,12 +157,32 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.preview }}</template> <MkCaptcha provider="fc" :sitekey="botProtectionForm.state.fcSiteKey"/> </FormSlot> + <FormSlot v-if="botProtectionForm.state.fcSiteKey"> + <template #label>{{ i18n.ts._captcha.verify }}</template> + <MkCaptcha + v-model="captchaResult" + provider="fc" + :sitekey="botProtectionForm.state.fcSiteKey" + :secretKey="botProtectionForm.state.fcSecretKey" + /> + </FormSlot> + <MkInfo> + <div :class="$style.captchaInfoMsg"> + <div> + {{ i18n.ts._captcha.testSiteKeyMessage }} + </div> + <div> + <span>ref: </span><a href="https://docs.friendlycaptcha.com/#/installation?id=_3-verifying-the-captcha-solution-on-the-server" target="_blank">FriendlyCaptcha Docs</a> + </div> + </div> + </MkInfo> </template> + <template v-else-if="botProtectionForm.state.provider === 'testcaptcha'"> <MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo> <FormSlot> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="testcaptcha"/> + <template #label>{{ i18n.ts._captcha.verify }}</template> + <MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/> </FormSlot> </template> </div> @@ -115,7 +190,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, ref } from 'vue'; +import { computed, defineAsyncComponent, ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; import MkRadios from '@/components/MkRadios.vue'; import MkInput from '@/components/MkInput.vue'; import FormSlot from '@/components/form/slot.vue'; @@ -127,56 +203,113 @@ import { useForm } from '@/scripts/use-form.js'; import MkFormFooter from '@/components/MkFormFooter.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInfo from '@/components/MkInfo.vue'; +import { ApiWithDialogCustomErrors } from '@/os.js'; const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); -const meta = await misskeyApi('admin/meta'); +const errorHandler: ApiWithDialogCustomErrors = { + // 検証リクエストãã®ã‚‚ã®ã«å¤±æ•— + '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd': { + title: i18n.ts._captcha._error._requestFailed.title, + text: i18n.ts._captcha._error._requestFailed.text, + }, + // 検証リクエストã®çµæžœãŒä¸æ£ + 'c41c067f-24f3-4150-84b2-b5a3ae8c2214': { + title: i18n.ts._captcha._error._verificationFailed.title, + text: i18n.ts._captcha._error._verificationFailed.text, + }, + // 䏿˜Žãªã‚¨ãƒ©ãƒ¼ + 'f868d509-e257-42a9-99c1-42614b031a97': { + title: i18n.ts._captcha._error._unknown.title, + text: i18n.ts._captcha._error._unknown.text, + }, +}; + +const captchaResult = ref<string | null>(null); +const meta = await misskeyApi('admin/captcha/current'); const botProtectionForm = useForm({ - provider: meta.enableHcaptcha - ? 'hcaptcha' - : meta.enableRecaptcha - ? 'recaptcha' - : meta.enableTurnstile - ? 'turnstile' - : meta.enableMcaptcha - ? 'mcaptcha' - : meta.enableFC - ? 'fc' - : meta.enableTestcaptcha - ? 'testcaptcha' - : null, - hcaptchaSiteKey: meta.hcaptchaSiteKey, - hcaptchaSecretKey: meta.hcaptchaSecretKey, - mcaptchaSiteKey: meta.mcaptchaSiteKey, - mcaptchaSecretKey: meta.mcaptchaSecretKey, - mcaptchaInstanceUrl: meta.mcaptchaInstanceUrl, - recaptchaSiteKey: meta.recaptchaSiteKey, - recaptchaSecretKey: meta.recaptchaSecretKey, - turnstileSiteKey: meta.turnstileSiteKey, - turnstileSecretKey: meta.turnstileSecretKey, - fcSiteKey: meta.fcSiteKey, - fcSecretKey: meta.fcSecretKey, + provider: meta.provider, + hcaptchaSiteKey: meta.hcaptcha.siteKey, + hcaptchaSecretKey: meta.hcaptcha.secretKey, + mcaptchaSiteKey: meta.mcaptcha.siteKey, + mcaptchaSecretKey: meta.mcaptcha.secretKey, + mcaptchaInstanceUrl: meta.mcaptcha.instanceUrl, + recaptchaSiteKey: meta.recaptcha.siteKey, + recaptchaSecretKey: meta.recaptcha.secretKey, + turnstileSiteKey: meta.turnstile.siteKey, + turnstileSecretKey: meta.turnstile.secretKey, + fcSiteKey: meta.fc.siteKey, + fcSecretKey: meta.fc.secretKey, }, async (state) => { - await os.apiWithDialog('admin/update-meta', { - enableHcaptcha: state.provider === 'hcaptcha', - hcaptchaSiteKey: state.hcaptchaSiteKey, - hcaptchaSecretKey: state.hcaptchaSecretKey, - enableMcaptcha: state.provider === 'mcaptcha', - mcaptchaSiteKey: state.mcaptchaSiteKey, - mcaptchaSecretKey: state.mcaptchaSecretKey, - mcaptchaInstanceUrl: state.mcaptchaInstanceUrl, - enableRecaptcha: state.provider === 'recaptcha', - recaptchaSiteKey: state.recaptchaSiteKey, - recaptchaSecretKey: state.recaptchaSecretKey, - enableTurnstile: state.provider === 'turnstile', - turnstileSiteKey: state.turnstileSiteKey, - turnstileSecretKey: state.turnstileSecretKey, - enableFC: state.provider === 'fc', - fcSiteKey: state.fcSiteKey, - fcSecretKey: state.fcSecretKey, - enableTestcaptcha: state.provider === 'testcaptcha', - }); - fetchInstance(true); + const provider = state.provider; + if (provider === 'none') { + await os.apiWithDialog( + 'admin/captcha/save', + { provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'] }, + undefined, + errorHandler, + ); + } else { + const sitekey = provider === 'hcaptcha' + ? state.hcaptchaSiteKey + : provider === 'mcaptcha' + ? state.mcaptchaSiteKey + : provider === 'recaptcha' + ? state.recaptchaSiteKey + : provider === 'turnstile' + ? state.turnstileSiteKey + : provider === 'fc' + ? state.fcSiteKey + : null; + const secret = provider === 'hcaptcha' + ? state.hcaptchaSecretKey + : provider === 'mcaptcha' + ? state.mcaptchaSecretKey + : provider === 'recaptcha' + ? state.recaptchaSecretKey + : provider === 'turnstile' + ? state.turnstileSecretKey + : provider === 'fc' + ? state.fcSecretKey + : null; + + await os.apiWithDialog( + 'admin/captcha/save', + { + provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'], + sitekey: sitekey, + secret: secret, + instanceUrl: state.mcaptchaInstanceUrl, + captchaResult: captchaResult.value, + }, + undefined, + errorHandler, + ); + } + + await fetchInstance(true); }); + +watch(botProtectionForm.state, () => { + captchaResult.value = null; +}); + +const canSaving = computed((): boolean => { + return (botProtectionForm.state.provider === 'none') || + (botProtectionForm.state.provider === 'hcaptcha' && !!captchaResult.value) || + (botProtectionForm.state.provider === 'mcaptcha' && !!captchaResult.value) || + (botProtectionForm.state.provider === 'recaptcha' && !!captchaResult.value) || + (botProtectionForm.state.provider === 'turnstile' && !!captchaResult.value) || + (botProtectionForm.state.provider === 'testcaptcha' && !!captchaResult.value); +}); + </script> + +<style lang="scss" module> +.captchaInfoMsg { + display: flex; + flex-direction: column; + gap: 8px; +} +</style> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts new file mode 100644 index 0000000000..141ab858d3 --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type RequestLogItem = { + failed: boolean; + url: string; + name: string; + error?: string; +}; + +export const gridSortOrderKeys = [ + 'name', + 'category', + 'aliases', + 'type', + 'license', + 'host', + 'uri', + 'publicUrl', + 'isSensitive', + 'localOnly', + 'updatedAt', +] as const satisfies string[]; + +export type GridSortOrderKey = typeof gridSortOrderKeys[number]; + +export function emptyStrToUndefined(value: string | null) { + return value ? value : undefined; +} + +export function emptyStrToNull(value: string) { + return value === '' ? null : value; +} + +export function emptyStrToEmptyArray(value: string) { + return value === '' ? [] : value.split(' ').map(it => it.trim()); +} + +export function roleIdsParser(text: string): { id: string, name: string }[] { + // idã¨nameã®ãƒšã‚¢é…列をJSONã§å—ã‘å–る。ãれ以外ã®å½¢å¼ã¯è¨±å®¹ã—ãªã„ + try { + const obj = JSON.parse(text); + if (!Array.isArray(obj)) { + return []; + } + if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) { + return []; + } + + return obj.map(it => ({ id: it.id, name: it.name })); + } catch (ex) { + console.warn(ex); + return []; + } +} diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue new file mode 100644 index 0000000000..4b145db0ed --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue @@ -0,0 +1,39 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkWindow + ref="uiWindow" + :initialWidth="400" + :initialHeight="500" + :canResize="true" + @closed="emit('closed')" +> + <template #header> + <i class="ti ti-notes" style="margin-right: 0.5em;"></i> {{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }} + </template> + <MkSpacer> + <XRegisterLogs :logs="logs"/> + </MkSpacer> +</MkWindow> +</template> + +<script setup lang="ts"> +import MkWindow from '@/components/MkWindow.vue'; +import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; + +import { i18n } from '@/i18n.js'; + +import type { RequestLogItem } from './custom-emojis-manager.impl.js'; + +defineProps<{ + logs: RequestLogItem[]; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); + +</script> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue new file mode 100644 index 0000000000..ae43507d66 --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue @@ -0,0 +1,213 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkWindow + ref="uiWindow" + :initialWidth="400" + :initialHeight="500" + :canResize="true" + @closed="emit('closed')" +> + <template #header> + <i class="ti ti-search" style="margin-right: 0.5em;"></i> {{ i18n.ts.search }} + </template> + <div :class="$style.root"> + <MkSpacer> + <div class="_gaps"> + <div class="_gaps_s"> + <MkInput + v-model="model.name" + type="search" + autocapitalize="off" + > + <template #label>name</template> + </MkInput> + <MkInput + v-model="model.category" + type="search" + autocapitalize="off" + > + <template #label>category</template> + </MkInput> + <MkInput + v-model="model.aliases" + type="search" + autocapitalize="off" + > + <template #label>aliases</template> + </MkInput> + + <MkInput + v-model="model.type" + type="search" + autocapitalize="off" + > + <template #label>type</template> + </MkInput> + <MkInput + v-model="model.license" + type="search" + autocapitalize="off" + > + <template #label>license</template> + </MkInput> + <MkSelect + v-model="model.sensitive" + > + <template #label>sensitive</template> + <option :value="null">-</option> + <option :value="true">true</option> + <option :value="false">false</option> + </MkSelect> + + <MkSelect + v-model="model.localOnly" + > + <template #label>localOnly</template> + <option :value="null">-</option> + <option :value="true">true</option> + <option :value="false">false</option> + </MkSelect> + <MkInput + v-model="model.updatedAtFrom" + type="date" + autocapitalize="off" + > + <template #label>updatedAt(from)</template> + </MkInput> + <MkInput + v-model="model.updatedAtTo" + type="date" + autocapitalize="off" + > + <template #label>updatedAt(to)</template> + </MkInput> + + <MkInput + v-model="queryRolesText" + type="text" + readonly + autocapitalize="off" + @click="onQueryRolesEditClicked" + > + <template #label>role</template> + <template #suffix><i class="ti ti-pencil"></i></template> + </MkInput> + </div> + <MkFolder :spacerMax="8" :spacerMin="8"> + <template #icon><i class="ti ti-arrows-sort"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template> + <MkSortOrderEditor + :baseOrderKeyNames="gridSortOrderKeys" + :currentOrders="sortOrders" + @update="onSortOrderUpdate" + /> + </MkFolder> + </div> + </MkSpacer> + <div :class="$style.footerActions"> + <MkButton primary @click="onSearchRequest"> + {{ i18n.ts.search }} + </MkButton> + <MkButton @click="onQueryResetButtonClicked"> + {{ i18n.ts.reset }} + </MkButton> + </div> + </div> +</MkWindow> +</template> + +<script setup lang="ts"> +import { computed, ref, watch } from 'vue'; +import MkWindow from '@/components/MkWindow.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; + +import { + gridSortOrderKeys, +} from './custom-emojis-manager.impl.js'; + +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; + +import type { EmojiSearchQuery } from './custom-emojis-manager.local.list.vue'; +import type { SortOrder } from '@/components/MkSortOrderEditor.define.js'; +import type { GridSortOrderKey } from './custom-emojis-manager.impl.js'; + +const props = defineProps<{ + query: EmojiSearchQuery; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; + (ev: 'queryUpdated', query: EmojiSearchQuery): void; + (ev: 'sortOrderUpdated', orders: SortOrder<GridSortOrderKey>[]): void; + (ev: 'search'): void; +}>(); + +const model = ref<EmojiSearchQuery>(props.query); +const queryRolesText = computed(() => model.value.roles.map(it => it.name).join(',')); + +watch(model, () => { + emit('queryUpdated', model.value); +}, { deep: true }); + +const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]); + +function onSortOrderUpdate(orders: SortOrder<GridSortOrderKey>[]) { + sortOrders.value = orders; + emit('sortOrderUpdated', orders); +} + +function onSearchRequest() { + emit('search'); +} + +function onQueryResetButtonClicked() { + model.value.name = ''; + model.value.category = ''; + model.value.aliases = ''; + model.value.type = ''; + model.value.license = ''; + model.value.sensitive = null; + model.value.localOnly = null; + model.value.updatedAtFrom = ''; + model.value.updatedAtTo = ''; + sortOrders.value = []; +} + +async function onQueryRolesEditClicked() { + const result = await os.selectRole({ + initialRoleIds: model.value.roles.map(it => it.id), + title: i18n.ts._customEmojisManager._local._list.dialogSelectRoleTitle, + publicOnly: true, + }); + if (result.canceled) { + return; + } + + model.value.roles = result.result; +} +</script> + +<style module> +.root { + position: relative; +} + +.footerActions { + position: sticky; + bottom: 0; + padding: var(--MI-margin); + background-color: var(--MI_THEME-bg); + display: flex; + gap: 8px; + z-index: 1; +} +</style> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue new file mode 100644 index 0000000000..c4ea3b93e3 --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue @@ -0,0 +1,660 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header> + <MkPageHeader :overridePageMetadata="headerPageMetadata" :actions="headerActions"/> + </template> + <template #default> + <div class="_gaps" :class="$style.main"> + <component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/> + <template v-else> + <div v-if="gridItems.length === 0" style="text-align: center"> + {{ i18n.ts._customEmojisManager._local._list.emojisNothing }} + </div> + + <template v-else> + <div :class="$style.grid"> + <MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/> + </div> + </template> + </template> + </div> + </template> + + <template #footer> + <div v-if="gridItems.length > 0" :class="$style.footer"> + <div :class="$style.left"> + <MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked"> + {{ i18n.ts.delete }} ({{ deleteItemsCount }}) + </MkButton> + </div> + + <div :class="$style.center"> + <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/> + </div> + + <div :class="$style.right"> + <MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked"> + {{ i18n.ts.update }} ({{ updatedItemsCount }}) + </MkButton> + <MkButton @click="onGridResetButtonClicked">{{ i18n.ts.reset }}</MkButton> + </div> + </div> + </template> +</MkStickyContainer> +</template> + +<script lang="ts"> +import type { SortOrder } from '@/components/MkSortOrderEditor.define.js'; +import type { GridSortOrderKey } from './custom-emojis-manager.impl.js'; + +export type EmojiSearchQuery = { + name: string | null; + category: string | null; + aliases: string | null; + type: string | null; + license: string | null; + updatedAtFrom: string | null; + updatedAtTo: string | null; + sensitive: string | null; + localOnly: string | null; + roles: { id: string, name: string }[]; + sortOrders: SortOrder<GridSortOrderKey>[]; + limit: number; +}; +</script> + +<script setup lang="ts"> +import { computed, defineAsyncComponent, onMounted, ref, nextTick, useCssModule } from 'vue'; +import * as Misskey from 'misskey-js'; +import * as os from '@/os.js'; +import { + emptyStrToEmptyArray, + emptyStrToNull, + emptyStrToUndefined, + RequestLogItem, + roleIdsParser, +} from '@/pages/admin/custom-emojis-manager.impl.js'; +import MkGrid from '@/components/grid/MkGrid.vue'; +import { i18n } from '@/i18n.js'; +import MkButton from '@/components/MkButton.vue'; +import { validators } from '@/components/grid/cell-validators.js'; +import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import MkPagingButtons from '@/components/MkPagingButtons.vue'; +import { GridSetting } from '@/components/grid/grid.js'; +import { selectFile } from '@/scripts/select-file.js'; +import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js'; +import { useLoading } from "@/components/hook/useLoading.js"; + +type GridItem = { + checked: boolean; + id: string; + url: string; + name: string; + host: string; + category: string; + aliases: string; + license: string; + isSensitive: boolean; + localOnly: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[]; + fileId?: string; + updatedAt: string | null; + publicUrl?: string | null; + originalUrl?: string | null; + type: string | null; +} + +function setupGrid(): GridSetting { + const $style = useCssModule(); + + const required = validators.required(); + const regex = validators.regex(/^[a-zA-Z0-9_]+$/); + const unique = validators.unique(); + return { + root: { + noOverflowStyle: true, + rounded: false, + outerBorder: false, + }, + row: { + showNumber: true, + selectable: true, + // グリッドã®è¡Œæ•°ã‚’ã‚らã‹ã˜ã‚100行確ä¿ã™ã‚‹ + minimumDefinitionCount: 100, + styleRules: [ + { + // åˆæœŸå€¤ã‹ã‚‰å¤‰ã‚ã£ã¦ã„ãŸã‚‰èƒŒæ™¯è‰²ã‚’変更 + condition: ({ row }) => JSON.stringify(gridItems.value[row.index]) !== JSON.stringify(originGridItems.value[row.index]), + applyStyle: { className: $style.changedRow }, + }, + { + // ãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³ã«å¼•ã£ã‹ã‹ã£ã¦ã„ãŸã‚‰èƒŒæ™¯è‰²ã‚’変更 + condition: ({ cells }) => cells.some(it => !it.violation.valid), + applyStyle: { className: $style.violationRow }, + }, + ], + // 行ã®ã‚³ãƒ³ãƒ†ã‚ストメニューè¨å®š + contextMenuFactory: (row, context) => { + return [ + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.copySelectionRows, + icon: 'ti ti-copy', + action: () => copyGridDataToClipboard(gridItems, context), + }, + { + type: 'button', + text: i18n.ts._customEmojisManager._local._list.markAsDeleteTargetRows, + icon: 'ti ti-trash', + action: () => { + for (const rangedRow of context.rangedRows) { + gridItems.value[rangedRow.index].checked = true; + } + }, + }, + ]; + }, + events: { + delete(rows) { + // 行削除時ã¯å…ƒãƒ‡ãƒ¼ã‚¿ã®è¡Œã‚’消ã•ãšã€å‰Šé™¤å¯¾è±¡ã¨ã—ã¦ãƒžãƒ¼ã‚¯ã™ã‚‹ã®ã¿ã«ã™ã‚‹ + for (const row of rows) { + gridItems.value[row.index].checked = true; + } + }, + }, + }, + cols: [ + { bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 }, + { + bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required], + async customValueEditor(row, col, value, cellElement) { + const file = await selectFile(cellElement); + gridItems.value[row.index].url = file.url; + gridItems.value[row.index].fileId = file.id; + + return file.url; + }, + }, + { + bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, + validators: [required, regex, unique], + }, + { bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 }, + { bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 }, + { bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 }, + { bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 }, + { bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 }, + { + bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140, + valueTransformer(row) { + // ãƒãƒƒã‚¯ã‚¨ãƒ³ãƒ‰ã‹ã‚‰ã‹ã‚‰ã¯IDã¨åå‰ã®ãƒšã‚¢é…列ã§å—ã‘å–ã‚‹ãŒã€è¡¨ç¤ºã«IDãŒã‚ã‚‹ã¨ç…©é›‘ãªã®ã§åå‰ã ã‘ã«ã™ã‚‹ + return gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction + .map((it) => it.name) + .join(','); + }, + async customValueEditor(row) { + // ID直記入ã¯ä½“é¨“çš„ã«æœ€æ‚ªãªã®ã§ãƒ¢ãƒ¼ãƒ€ãƒ«ã‚’使ã£ã¦å…¥åŠ›ã™ã‚‹ + const current = gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction; + const result = await os.selectRole({ + initialRoleIds: current.map(it => it.id), + title: i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction, + infoMessage: i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription, + publicOnly: true, + }); + if (result.canceled) { + return current; + } + + const transform = result.result.map(it => ({ id: it.id, name: it.name })); + gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = transform; + + return transform; + }, + events: { + paste: roleIdsParser, + delete(cell) { + // デフォルトã¯undefinedã«ãªã‚‹ãŒã€ã“ã®ãƒ—ãƒãƒ‘ティã¯ç©ºé…列ã«ã—ãŸã„ + gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = []; + }, + }, + }, + { bindTo: 'type', type: 'text', editable: false, width: 90 }, + { bindTo: 'updatedAt', type: 'text', editable: false, width: 'auto' }, + { bindTo: 'publicUrl', type: 'text', editable: false, width: 180 }, + { bindTo: 'originalUrl', type: 'text', editable: false, width: 180 }, + ], + cells: { + // セルã®ã‚³ãƒ³ãƒ†ã‚ストメニューè¨å®š + contextMenuFactory(col, row, value, context) { + return [ + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.copySelectionRanges, + icon: 'ti ti-copy', + action: () => { + return copyGridDataToClipboard(gridItems, context); + }, + }, + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.deleteSelectionRanges, + icon: 'ti ti-trash', + action: () => { + removeDataFromGrid(context, (cell) => { + gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined; + }); + }, + }, + { + type: 'button', + text: i18n.ts._customEmojisManager._local._list.markAsDeleteTargetRanges, + icon: 'ti ti-trash', + action: () => { + for (const rowIdx of [...new Set(context.rangedCells.map(it => it.row.index)).values()]) { + gridItems.value[rowIdx].checked = true; + } + }, + }, + ]; + }, + }, + }; +} + +const loadingHandler = useLoading(); + +const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]); +const allPages = ref<number>(0); +const currentPage = ref<number>(0); + +const searchQuery = ref<EmojiSearchQuery>({ + name: null, + category: null, + aliases: null, + type: null, + license: null, + updatedAtFrom: null, + updatedAtTo: null, + sensitive: null, + localOnly: null, + roles: [], + sortOrders: [], + limit: 25, +}); +let searchWindowOpening = false; + +const previousQuery = ref<string | undefined>(undefined); +const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]); +const requestLogs = ref<RequestLogItem[]>([]); + +const gridItems = ref<GridItem[]>([]); +const originGridItems = ref<GridItem[]>([]); +const updateButtonDisabled = ref<boolean>(false); + +const updatedItemsCount = computed(() => { + return gridItems.value.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(originGridItems.value[idx])).length; +}); +const deleteItemsCount = computed(() => gridItems.value.filter(it => it.checked).length); + +async function onUpdateButtonClicked() { + const _items = gridItems.value; + const _originItems = originGridItems.value; + if (_items.length !== _originItems.length) { + throw new Error('The number of items has been changed. Please refresh the page and try again.'); + } + + const updatedItems = _items.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(_originItems[idx])); + if (updatedItems.length === 0) { + await os.alert({ + type: 'info', + text: i18n.ts._customEmojisManager._local._list.alertUpdateEmojisNothingDescription, + }); + return; + } + + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.tsx._customEmojisManager._local._list.confirmUpdateEmojisDescription({ count: updatedItems.length }), + }); + if (canceled) { + return; + } + + const action = () => { + return updatedItems.map(item => + misskeyApi( + 'admin/emoji/update', + { + // eslint-disable-next-line + id: item.id!, + name: item.name, + category: emptyStrToNull(item.category), + aliases: emptyStrToEmptyArray(item.aliases), + license: emptyStrToNull(item.license), + isSensitive: item.isSensitive, + localOnly: item.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id), + fileId: item.fileId, + }) + .then(() => ({ item, success: true, err: undefined })) + .catch(err => ({ item, success: false, err })), + ); + }; + + const result = await os.promiseDialog(Promise.all(action())); + const failedItems = result.filter(it => !it.success); + + if (failedItems.length > 0) { + await os.alert({ + type: 'error', + title: i18n.ts.somethingHappened, + text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription, + }); + } + + requestLogs.value = result.map(it => ({ + failed: !it.success, + url: it.item.url, + name: it.item.name, + error: it.err ? JSON.stringify(it.err) : undefined, + })); + + await refreshCustomEmojis(); +} + +async function onDeleteButtonClicked() { + const _items = gridItems.value; + const _originItems = originGridItems.value; + if (_items.length !== _originItems.length) { + throw new Error('The number of items has been changed. Please refresh the page and try again.'); + } + + const deleteItems = _items.filter((it) => it.checked); + if (deleteItems.length === 0) { + await os.alert({ + type: 'info', + text: i18n.ts._customEmojisManager._local._list.alertDeleteEmojisNothingDescription, + }); + return; + } + + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.tsx._customEmojisManager._local._list.confirmDeleteEmojisDescription({ count: deleteItems.length }), + }); + if (canceled) { + return; + } + + async function action() { + const deleteIds = deleteItems.map(it => it.id!); + await misskeyApi('admin/emoji/delete-bulk', { ids: deleteIds }); + } + + await os.promiseDialog( + action(), + ); +} + +async function onGridResetButtonClicked() { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts.resetAreYouSure, + text: i18n.ts._customEmojisManager._local._list.confirmResetDescription, + }); + + if (canceled) return; + + refreshGridItems(); +} + +async function onSearchRequest() { + await refreshCustomEmojis(); +} + +async function onPageChanged(pageNumber: number) { + if (updatedItemsCount.value > 0) { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts._customEmojisManager._local._list.confirmMovePage, + text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption, + }); + if (canceled) return; + } + + currentPage.value = pageNumber; + await nextTick(); + refreshCustomEmojis(); +} + +function onGridEvent(event: GridEvent) { + switch (event.type) { + case 'cell-validation': + onGridCellValidation(event); + break; + case 'cell-value-change': + onGridCellValueChange(event); + break; + } +} + +function onGridCellValidation(event: GridCellValidationEvent) { + updateButtonDisabled.value = event.all.filter(it => !it.valid).length > 0; +} + +function onGridCellValueChange(event: GridCellValueChangeEvent) { + const { row, column, newValue } = event; + if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) { + gridItems.value[row.index][column.setting.bindTo] = newValue; + } +} + +async function refreshCustomEmojis() { + const limit = searchQuery.value.limit; + + const query: Misskey.entities.V2AdminEmojiListRequest['query'] = { + name: emptyStrToUndefined(searchQuery.value.name), + type: emptyStrToUndefined(searchQuery.value.type), + aliases: emptyStrToUndefined(searchQuery.value.aliases), + category: emptyStrToUndefined(searchQuery.value.category), + license: emptyStrToUndefined(searchQuery.value.license), + isSensitive: searchQuery.value.sensitive ? Boolean(searchQuery.value.sensitive).valueOf() : undefined, + localOnly: searchQuery.value.localOnly ? Boolean(searchQuery.value.localOnly).valueOf() : undefined, + updatedAtFrom: emptyStrToUndefined(searchQuery.value.updatedAtFrom), + updatedAtTo: emptyStrToUndefined(searchQuery.value.updatedAtTo), + roleIds: searchQuery.value.roles.map(it => it.id), + hostType: 'local', + }; + + if (JSON.stringify(query) !== previousQuery.value) { + currentPage.value = 1; + } + + const result = await loadingHandler.scope(() => misskeyApi('v2/admin/emoji/list', { + query: query, + limit: limit, + page: currentPage.value, + sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}` as any), + })); + + customEmojis.value = result.emojis; + allPages.value = result.allPages; + + previousQuery.value = JSON.stringify(query); + + refreshGridItems(); +} + +function refreshGridItems() { + gridItems.value = customEmojis.value.map(it => ({ + checked: false, + id: it.id, + fileId: undefined, + url: it.publicUrl, + name: it.name, + host: it.host ?? '', + category: it.category ?? '', + aliases: it.aliases.join(','), + license: it.license ?? '', + isSensitive: it.isSensitive, + localOnly: it.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction, + updatedAt: it.updatedAt, + publicUrl: it.publicUrl, + originalUrl: it.originalUrl, + type: it.type, + })); + originGridItems.value = JSON.parse(JSON.stringify(gridItems.value)); +} + +onMounted(async () => { + await refreshCustomEmojis(); +}); + +const headerPageMetadata = computed(() => ({ + title: i18n.ts._customEmojisManager._local.tabTitleList, + icon: 'ti ti-icons', +})); + +const headerActions = computed(() => [{ + icon: 'ti ti-search', + text: i18n.ts.search, + handler: () => { + if (searchWindowOpening) return; + searchWindowOpening = true; + const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.search.vue')), { + query: searchQuery.value, + }, { + queryUpdated: (query: EmojiSearchQuery) => { + searchQuery.value = query; + }, + sortOrderUpdated: (orders: SortOrder<GridSortOrderKey>[]) => { + sortOrders.value = orders; + }, + search: () => { + onSearchRequest(); + }, + closed: () => { + dispose(); + searchWindowOpening = false; + }, + }); + }, +}, { + icon: 'ti ti-list-numbers', + text: i18n.ts._customEmojisManager._gridCommon.searchLimit, + handler: (ev: MouseEvent) => { + async function changeSearchLimit(to: number) { + if (updatedItemsCount.value > 0) { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts._customEmojisManager._local._list.confirmChangeView, + text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption, + }); + if (canceled) return; + } + + searchQuery.value.limit = to; + refreshCustomEmojis(); + } + + os.popupMenu([{ + type: 'radioOption', + text: '25', + active: computed(() => searchQuery.value.limit === 25), + action: () => changeSearchLimit(25), + }, { + type: 'radioOption', + text: '50', + active: computed(() => searchQuery.value.limit === 50), + action: () => changeSearchLimit(50), + }, { + type: 'radioOption', + text: '100', + active: computed(() => searchQuery.value.limit === 100), + action: () => changeSearchLimit(100), + }], ev.currentTarget ?? ev.target); + }, +}, { + icon: 'ti ti-notes', + text: i18n.ts._customEmojisManager._gridCommon.registrationLogs, + handler: () => { + const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.logs.vue')), { + logs: requestLogs.value, + }, { + closed: () => { + dispose(); + }, + }); + } +}]); +</script> + +<style module lang="scss"> +.violationRow { + background-color: var(--MI_THEME-infoWarnBg); +} + +.changedRow { + background-color: var(--MI_THEME-infoBg); +} + +.editedRow { + background-color: var(--MI_THEME-infoBg); +} + +.main { + height: calc(100vh - var(--MI-stickyTop) - var(--MI-stickyBottom)); + overflow: scroll; +} + +.grid { + width: max-content; + border-bottom: 1px solid var(--MI_THEME-divider); +} + +.footer { + background-color: var(--MI_THEME-bg); + + padding: var(--MI-margin); + border-top: 1px solid var(--MI_THEME-divider); + + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + + & .left { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + } + + & .center { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + } + + & .right { + display: flex; + align-items: center; + justify-content: flex-end; + flex-direction: row; + gap: 8px; + } +} + +.divider { + margin: 8px 0; + border-top: solid 0.5px var(--MI_THEME-divider); +} + +</style> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue new file mode 100644 index 0000000000..cc8b625cd5 --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue @@ -0,0 +1,481 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps"> + <MkFolder> + <template #icon><i class="ti ti-settings"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._local._register.uploadSettingTitle }}</template> + <template #caption>{{ i18n.ts._customEmojisManager._local._register.uploadSettingDescription }}</template> + + <div class="_gaps"> + <MkSelect v-model="selectedFolderId"> + <template #label>{{ i18n.ts.uploadFolder }}</template> + <option v-for="folder in uploadFolders" :key="folder.id" :value="folder.id"> + {{ folder.name }} + </option> + </MkSelect> + + <MkSwitch v-model="keepOriginalUploading"> + <template #label>{{ i18n.ts.keepOriginalUploading }}</template> + <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> + </MkSwitch> + + <MkSwitch v-model="directoryToCategory"> + <template #label>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryLabel }}</template> + <template #caption>{{ i18n.ts._customEmojisManager._local._register.directoryToCategoryCaption }}</template> + </MkSwitch> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-notes"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template> + <template #caption> + {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }} + </template> + <XRegisterLogs :logs="requestLogs"/> + </MkFolder> + + <div + :class="[$style.uploadBox, [isDragOver ? $style.dragOver : {}]]" + @dragover.prevent="isDragOver = true" + @dragleave.prevent="isDragOver = false" + @drop.prevent.stop="onDrop" + > + <div style="margin-top: 1em"> + {{ i18n.ts._customEmojisManager._local._register.emojiInputAreaCaption }} + </div> + <ul> + <li>{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList1 }}</li> + <li><a @click.prevent="onFileSelectClicked">{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList2 }}</a></li> + <li><a @click.prevent="onDriveSelectClicked">{{ i18n.ts._customEmojisManager._local._register.emojiInputAreaList3 }}</a></li> + </ul> + </div> + + <div v-if="gridItems.length > 0" :class="$style.gridArea"> + <MkGrid + :data="gridItems" + :settings="setupGrid()" + @event="onGridEvent" + /> + </div> + + <div v-if="gridItems.length > 0" :class="$style.footer"> + <MkButton primary :disabled="registerButtonDisabled" @click="onRegistryClicked"> + {{ i18n.ts.registration }} + </MkButton> + <MkButton @click="onClearClicked"> + {{ i18n.ts.clear }} + </MkButton> + </div> +</div> +</template> + +<script setup lang="ts"> +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import * as Misskey from 'misskey-js'; +import { onMounted, ref, useCssModule } from 'vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { + emptyStrToEmptyArray, + emptyStrToNull, + RequestLogItem, + roleIdsParser, +} from '@/pages/admin/custom-emojis-manager.impl.js'; +import MkGrid from '@/components/grid/MkGrid.vue'; +import { i18n } from '@/i18n.js'; +import MkSelect from '@/components/MkSelect.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { defaultStore } from '@/store.js'; +import MkFolder from '@/components/MkFolder.vue'; +import MkButton from '@/components/MkButton.vue'; +import * as os from '@/os.js'; +import { validators } from '@/components/grid/cell-validators.js'; +import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js'; +import { uploadFile } from '@/scripts/upload.js'; +import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; +import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js'; +import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; +import { GridSetting } from '@/components/grid/grid.js'; +import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; +import { GridRow } from '@/components/grid/row.js'; + +const MAXIMUM_EMOJI_REGISTER_COUNT = 100; + +type FolderItem = { + id?: string; + name: string; +}; + +type GridItem = { + fileId: string; + url: string; + name: string; + host: string; + category: string; + aliases: string; + license: string; + isSensitive: boolean; + localOnly: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[]; + type: string | null; +} + +function setupGrid(): GridSetting { + const $style = useCssModule(); + + const required = validators.required(); + const regex = validators.regex(/^[a-zA-Z0-9_]+$/); + const unique = validators.unique(); + + function removeRows(rows: GridRow[]) { + const idxes = [...new Set(rows.map(it => it.index))]; + gridItems.value = gridItems.value.filter((_, i) => !idxes.includes(i)); + } + + return { + row: { + showNumber: true, + selectable: true, + minimumDefinitionCount: 100, + styleRules: [ + { + // 1ã¤ã§ã‚‚ãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³ã‚¨ãƒ©ãƒ¼ãŒã‚れã°è¡Œå…¨ä½“をエラー表示ã™ã‚‹ + condition: ({ cells }) => cells.some(it => !it.violation.valid), + applyStyle: { className: $style.violationRow }, + }, + ], + // 行ã®ã‚³ãƒ³ãƒ†ã‚ストメニューè¨å®š + contextMenuFactory: (row, context) => { + return [ + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.copySelectionRows, + icon: 'ti ti-copy', + action: () => copyGridDataToClipboard(gridItems, context), + }, + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.deleteSelectionRows, + icon: 'ti ti-trash', + action: () => removeRows(context.rangedRows), + }, + ]; + }, + events: { + delete(rows) { + removeRows(rows); + }, + }, + }, + cols: [ + { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] }, + { + bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, + validators: [required, regex, unique], + }, + { bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 }, + { bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 }, + { bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 }, + { bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 }, + { bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 }, + { + bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140, + valueTransformer: (row) => { + // ãƒãƒƒã‚¯ã‚¨ãƒ³ãƒ‰ã‹ã‚‰ã‹ã‚‰ã¯IDã¨åå‰ã®ãƒšã‚¢é…列ã§å—ã‘å–ã‚‹ãŒã€è¡¨ç¤ºã«IDãŒã‚ã‚‹ã¨ç…©é›‘ãªã®ã§åå‰ã ã‘ã«ã™ã‚‹ + return gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction + .map((it) => it.name) + .join(','); + }, + customValueEditor: async (row) => { + // ID直記入ã¯ä½“é¨“çš„ã«æœ€æ‚ªãªã®ã§ãƒ¢ãƒ¼ãƒ€ãƒ«ã‚’使ã£ã¦å…¥åŠ›ã™ã‚‹ + const current = gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction; + const result = await os.selectRole({ + initialRoleIds: current.map(it => it.id), + title: i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction, + infoMessage: i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription, + publicOnly: true, + }); + if (result.canceled) { + return current; + } + + const transform = result.result.map(it => ({ id: it.id, name: it.name })); + gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = transform; + + return transform; + }, + events: { + paste: roleIdsParser, + delete(cell) { + // デフォルトã¯undefinedã«ãªã‚‹ãŒã€ã“ã®ãƒ—ãƒãƒ‘ティã¯ç©ºé…列ã«ã—ãŸã„ + gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = []; + }, + }, + }, + { bindTo: 'type', type: 'text', editable: false, width: 90 }, + ], + cells: { + // セルã®ã‚³ãƒ³ãƒ†ã‚ストメニューè¨å®š + contextMenuFactory: (col, row, value, context) => { + return [ + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.copySelectionRanges, + icon: 'ti ti-copy', + action: () => copyGridDataToClipboard(gridItems, context), + }, + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.deleteSelectionRanges, + icon: 'ti ti-trash', + action: () => removeRows(context.rangedCells.map(it => it.row)), + }, + ]; + }, + }, + }; +} + +const uploadFolders = ref<FolderItem[]>([]); +const gridItems = ref<GridItem[]>([]); +const selectedFolderId = ref(defaultStore.state.uploadFolder); +const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading); +const directoryToCategory = ref<boolean>(false); +const registerButtonDisabled = ref<boolean>(false); +const requestLogs = ref<RequestLogItem[]>([]); +const isDragOver = ref<boolean>(false); + +async function onRegistryClicked() { + const dialogSelection = await os.confirm({ + type: 'info', + text: i18n.tsx._customEmojisManager._local._register.confirmRegisterEmojisDescription({ count: MAXIMUM_EMOJI_REGISTER_COUNT }), + }); + + if (dialogSelection.canceled) { + return; + } + + const items = gridItems.value; + const upload = () => { + return items.slice(0, MAXIMUM_EMOJI_REGISTER_COUNT) + .map(item => + misskeyApi( + 'admin/emoji/add', { + name: item.name, + category: emptyStrToNull(item.category), + aliases: emptyStrToEmptyArray(item.aliases), + license: emptyStrToNull(item.license), + isSensitive: item.isSensitive, + localOnly: item.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id), + fileId: item.fileId!, + }) + .then(() => ({ item, success: true, err: undefined })) + .catch(err => ({ item, success: false, err })), + ); + }; + + const result = await os.promiseDialog(Promise.all(upload())); + const failedItems = result.filter(it => !it.success); + + if (failedItems.length > 0) { + await os.alert({ + type: 'error', + title: i18n.ts.somethingHappened, + text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription, + }); + } + + requestLogs.value = result.map(it => ({ + failed: !it.success, + url: it.item.url, + name: it.item.name, + error: it.err ? JSON.stringify(it.err) : undefined, + })); + + // ç™»éŒ²ã«æˆåŠŸã—ãŸã‚‚ã®ã¯ä¸€è¦§ã‹ã‚‰é™¤ã + const successItems = result.filter(it => it.success).map(it => it.item); + gridItems.value = gridItems.value.filter(it => !successItems.includes(it)); +} + +async function onClearClicked() { + const result = await os.confirm({ + type: 'warning', + text: i18n.ts._customEmojisManager._local._register.confirmClearEmojisDescription, + }); + + if (!result.canceled) { + gridItems.value = []; + } +} + +async function onDrop(ev: DragEvent) { + isDragOver.value = false; + + const droppedFiles = await extractDroppedItems(ev).then(it => flattenDroppedFiles(it)); + const confirm = await os.confirm({ + type: 'info', + text: i18n.tsx._customEmojisManager._local._register.confirmUploadEmojisDescription({ count: droppedFiles.length }), + }); + if (confirm.canceled) { + return; + } + + const uploadedItems = Array.of<{ droppedFile: DroppedFile, driveFile: Misskey.entities.DriveFile }>(); + try { + uploadedItems.push( + ...await os.promiseDialog( + Promise.all( + droppedFiles.map(async (it) => ({ + droppedFile: it, + driveFile: await uploadFile( + it.file, + selectedFolderId.value, + it.file.name.replace(/\.[^.]+$/, ''), + keepOriginalUploading.value, + ), + }), + ), + ), + () => { + }, + () => { + }, + ), + ); + } catch (err) { + // ダイアãƒã‚°ã¯å…±é€šéƒ¨å“å´ã§å‡ºã¦ã„ã‚‹ã¯ãšãªã®ã§ä½•ã‚‚ã—ãªã„ + return; + } + + const items = uploadedItems.map(({ droppedFile, driveFile }) => { + const item = fromDriveFile(driveFile); + if (directoryToCategory.value) { + item.category = droppedFile.path + .replace(/^\//, '') + .replace(/\/[^/]+$/, '') + .replace(droppedFile.file.name, ''); + } + return item; + }); + + gridItems.value.push(...items); +} + +async function onFileSelectClicked() { + const driveFiles = await chooseFileFromPc( + true, + { + uploadFolder: selectedFolderId.value, + keepOriginal: keepOriginalUploading.value, + // æ‹¡å¼µåã¯æ¶ˆã™ + nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''), + }, + ); + + gridItems.value.push(...driveFiles.map(fromDriveFile)); +} + +async function onDriveSelectClicked() { + const driveFiles = await chooseFileFromDrive(true); + gridItems.value.push(...driveFiles.map(fromDriveFile)); +} + +function onGridEvent(event: GridEvent) { + switch (event.type) { + case 'cell-validation': + onGridCellValidation(event); + break; + case 'cell-value-change': + onGridCellValueChange(event); + break; + } +} + +function onGridCellValidation(event: GridCellValidationEvent) { + registerButtonDisabled.value = event.all.filter(it => !it.valid).length > 0; +} + +function onGridCellValueChange(event: GridCellValueChangeEvent) { + const { row, column, newValue } = event; + if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) { + gridItems.value[row.index][column.setting.bindTo] = newValue; + } +} + +function fromDriveFile(it: Misskey.entities.DriveFile): GridItem { + return { + fileId: it.id, + url: it.url, + name: it.name.replace(/(\.[a-zA-Z0-9]+)+$/, ''), + host: '', + category: '', + aliases: '', + license: '', + isSensitive: it.isSensitive, + localOnly: false, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], + type: it.type, + }; +} + +async function refreshUploadFolders() { + const result = await misskeyApi('drive/folders', {}); + uploadFolders.value = Array.of<FolderItem>({ name: '-' }, ...result); +} + +onMounted(async () => { + await refreshUploadFolders(); +}); +</script> + +<style module lang="scss"> +.violationRow { + background-color: var(--MI_THEME-infoWarnBg); +} + +.uploadBox { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: auto; + border: 0.5px dotted var(--MI_THEME-accentedBg); + border-radius: var(--MI-radius); + background-color: var(--MI_THEME-accentedBg); + box-sizing: border-box; + + &.dragOver { + cursor: copy; + } +} + +.gridArea { + padding-top: 8px; + padding-bottom: 8px; +} + +.footer { + background-color: var(--MI_THEME-bg); + + position: sticky; + left:0; + bottom:0; + z-index: 1; + // stickyã§è¿½å¾“ã•ã›ã‚‹éƒ½åˆä¸Šã€ãƒ•ッター自身ã§paddingã‚’æŒã¤å¿…è¦ãŒã‚ã‚‹ãŸã‚ã€è¦ªè¦ç´ ã§ç”»ä¸€çš„ã«æŒ‡å®šã—ã¦ã„る分をãƒã‚¬ãƒ†ã‚£ãƒ–マージンã§ç›¸æ®ºã—ã¦ã„ã‚‹ + margin-top: calc(var(--MI-margin) * -1); + margin-bottom: calc(var(--MI-margin) * -1); + padding-top: var(--MI-margin); + padding-bottom: var(--MI-margin); + + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} +</style> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue new file mode 100644 index 0000000000..6e7e7e53e3 --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue @@ -0,0 +1,35 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header> + <MkPageHeader v-model:tab="headerTab" :tabs="headerTabs" hideTitle thin/> + </template> + <XListComponent v-if="headerTab === 'list'" key="localList"/> + <MkSpacer v-else key="localRegister"> + <XRegisterComponent/> + </MkSpacer> +</MkStickyContainer> +</template> + +<script setup lang="ts"> +import { ref, computed } from 'vue'; +import { i18n } from '@/i18n.js'; +import XListComponent from '@/pages/admin/custom-emojis-manager.local.list.vue'; +import XRegisterComponent from '@/pages/admin/custom-emojis-manager.local.register.vue'; + +type PageMode = 'list' | 'register'; + +const headerTab = ref<PageMode>('list'); + +const headerTabs = computed(() => [{ + key: 'list', + title: i18n.ts._customEmojisManager._local.tabTitleList, +}, { + key: 'register', + title: i18n.ts._customEmojisManager._local.tabTitleRegister, +}]); +</script> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue new file mode 100644 index 0000000000..eef55a9f7e --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue @@ -0,0 +1,88 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <div v-if="logs.length > 0" style="display:flex; flex-direction: column; overflow-y: scroll; gap: 16px;"> + <MkSwitch v-model="showingSuccessLogs"> + <template #label>{{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}</template> + </MkSwitch> + <div> + <div v-if="filteredLogs.length > 0"> + <MkGrid + :data="filteredLogs" + :settings="setupGrid()" + /> + </div> + <div v-else> + {{ i18n.ts._customEmojisManager._logs.failureLogNothing }} + </div> + </div> + </div> + <div v-else> + {{ i18n.ts._customEmojisManager._logs.logNothing }} + </div> +</div> +</template> + +<script setup lang="ts"> +import { computed, ref, toRefs } from 'vue'; +import { i18n } from '@/i18n.js'; +import MkGrid from '@/components/grid/MkGrid.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; + +import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; +import type { GridSetting } from '@/components/grid/grid.js'; + +function setupGrid(): GridSetting { + return { + row: { + showNumber: false, + selectable: false, + contextMenuFactory: (row, context) => { + return [ + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.copySelectionRows, + icon: 'ti ti-copy', + action: () => copyGridDataToClipboard(logs, context), + }, + ]; + }, + }, + cols: [ + { bindTo: 'failed', title: 'failed', type: 'boolean', editable: false, width: 50 }, + { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' }, + { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 140 }, + { bindTo: 'error', title: 'log', type: 'text', editable: false, width: 'auto' }, + ], + cells: { + contextMenuFactory: (col, row, value, context) => { + return [ + { + type: 'button', + text: i18n.ts._customEmojisManager._gridCommon.copySelectionRanges, + icon: 'ti ti-copy', + action: () => copyGridDataToClipboard(logs, context), + }, + ]; + }, + }, + }; +} + +const props = defineProps<{ + logs: RequestLogItem[]; +}>(); + +const { logs } = toRefs(props); +const showingSuccessLogs = ref<boolean>(false); + +const filteredLogs = computed(() => { + const forceShowing = showingSuccessLogs.value; + return logs.value.filter((log) => forceShowing || log.failed); +}); +</script> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue new file mode 100644 index 0000000000..eecf8d7390 --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue @@ -0,0 +1,503 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #default> + <div :class="$style.root" class="_gaps"> + <MkFolder> + <template #icon><i class="ti ti-search"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchSettings }}</template> + <template #caption> + {{ i18n.ts._customEmojisManager._gridCommon.searchSettingCaption }} + </template> + + <div class="_gaps"> + <div :class="[[spMode ? $style.searchAreaSp : $style.searchArea]]"> + <MkInput + v-model="queryName" + type="search" + autocapitalize="off" + :class="[$style.col1, $style.row1]" + @enter="onSearchRequest" + > + <template #label>name</template> + </MkInput> + <MkInput + v-model="queryHost" + type="search" + autocapitalize="off" + :class="[$style.col2, $style.row1]" + @enter="onSearchRequest" + > + <template #label>host</template> + </MkInput> + <MkInput + v-model="queryLicense" + type="search" + autocapitalize="off" + :class="[$style.col3, $style.row1]" + @enter="onSearchRequest" + > + <template #label>license</template> + </MkInput> + + <MkInput + v-model="queryUri" + type="search" + autocapitalize="off" + :class="[$style.col1, $style.row2]" + @enter="onSearchRequest" + > + <template #label>uri</template> + </MkInput> + <MkInput + v-model="queryPublicUrl" + type="search" + autocapitalize="off" + :class="[$style.col2, $style.row2]" + @enter="onSearchRequest" + > + <template #label>publicUrl</template> + </MkInput> + </div> + + <hr> + + <MkFolder :spacerMax="8" :spacerMin="8"> + <template #icon><i class="ti ti-arrows-sort"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template> + <MkSortOrderEditor + :baseOrderKeyNames="gridSortOrderKeys" + :currentOrders="sortOrders" + @update="onSortOrderUpdate" + /> + </MkFolder> + + <MkInput + v-model="queryLimit" + type="number" + :max="100" + > + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchLimit }}</template> + </MkInput> + + <div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]"> + <MkButton primary @click="onSearchRequest"> + {{ i18n.ts.search }} + </MkButton> + <MkButton @click="onQueryResetButtonClicked"> + {{ i18n.ts.reset }} + </MkButton> + </div> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-notes"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template> + <template #caption> + {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }} + </template> + <XRegisterLogs :logs="requestLogs"/> + </MkFolder> + + <component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/> + <template v-else> + <div v-if="gridItems.length === 0" style="text-align: center"> + {{ i18n.ts._customEmojisManager._local._list.emojisNothing }} + </div> + + <template v-else> + <div v-if="gridItems.length > 0" :class="$style.gridArea"> + <MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/> + </div> + + <div :class="$style.footer"> + <div> + <!-- レイアウト調整用ã®ã‚¹ãƒšãƒ¼ã‚¹ --> + </div> + + <div :class="$style.center"> + <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/> + </div> + + <div :class="$style.right"> + <MkButton primary @click="onImportClicked"> + {{ + i18n.ts._customEmojisManager._remote.importEmojisButton + }} ({{ checkedItemsCount }}) + </MkButton> + </div> + </div> + </template> + </template> + </div> + </template> +</MkStickyContainer> +</template> + +<script setup lang="ts"> +import { computed, onMounted, ref, useCssModule } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkGrid from '@/components/grid/MkGrid.vue'; +import { + emptyStrToUndefined, + GridSortOrderKey, + gridSortOrderKeys, + RequestLogItem, +} from '@/pages/admin/custom-emojis-manager.impl.js'; +import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; +import MkFolder from '@/components/MkFolder.vue'; +import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; +import * as os from '@/os.js'; +import { GridSetting } from '@/components/grid/grid.js'; +import { deviceKind } from '@/scripts/device-kind.js'; +import MkPagingButtons from '@/components/MkPagingButtons.vue'; +import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; +import { SortOrder } from '@/components/MkSortOrderEditor.define.js'; +import { useLoading } from '@/components/hook/useLoading.js'; + +type GridItem = { + checked: boolean; + id: string; + url: string; + name: string; + host: string; +} + +function setupGrid(): GridSetting { + const $style = useCssModule(); + + return { + row: { + // グリッドã®è¡Œæ•°ã‚’ã‚らã‹ã˜ã‚100行確ä¿ã™ã‚‹ + minimumDefinitionCount: 100, + styleRules: [ + { + // ãƒã‚§ãƒƒã‚¯ã•れãŸã‚‰èƒŒæ™¯è‰²ã‚’変ãˆã‚‹ + condition: ({ row }) => gridItems.value[row.index].checked, + applyStyle: { className: $style.changedRow }, + }, + ], + contextMenuFactory: (row, context) => { + return [ + { + type: 'button', + text: i18n.ts._customEmojisManager._remote.importSelectionRows, + icon: 'ti ti-download', + action: async () => { + const targets = context.rangedRows.map(it => gridItems.value[it.index]); + await importEmojis(targets); + }, + }, + ]; + }, + }, + cols: [ + { bindTo: 'checked', icon: 'ti-download', type: 'boolean', editable: true, width: 34 }, + { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' }, + { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' }, + { bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' }, + { bindTo: 'license', title: 'license', type: 'text', editable: false, width: 200 }, + { bindTo: 'uri', title: 'uri', type: 'text', editable: false, width: 'auto' }, + { bindTo: 'publicUrl', title: 'publicUrl', type: 'text', editable: false, width: 'auto' }, + ], + cells: { + contextMenuFactory: (col, row, value, context) => { + return [ + { + type: 'button', + text: i18n.ts._customEmojisManager._remote.selectionRowDetail, + icon: 'ti ti-info-circle', + action: async () => { + const target = customEmojis.value[row.index]; + const { dispose } = os.popup(MkRemoteEmojiEditDialog, { + emoji: { + id: target.id, + name: target.name, + host: target.host!, + license: target.license, + url: target.publicUrl, + }, + }, { + done: () => { + dispose(); + }, + closed: () => { + dispose(); + }, + }); + }, + }, + { + type: 'button', + text: i18n.ts._customEmojisManager._remote.importSelectionRangesRows, + icon: 'ti ti-download', + action: async () => { + const targets = context.rangedCells.map(it => gridItems.value[it.row.index]); + await importEmojis(targets); + }, + }, + ]; + }, + }, + }; +} + +const loadingHandler = useLoading(); + +const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]); +const allPages = ref<number>(0); +const currentPage = ref<number>(0); + +const queryName = ref<string | null>(null); +const queryHost = ref<string | null>(null); +const queryLicense = ref<string | null>(null); +const queryUri = ref<string | null>(null); +const queryPublicUrl = ref<string | null>(null); +const queryLimit = ref<number>(25); +const previousQuery = ref<string | undefined>(undefined); +const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]); +const requestLogs = ref<RequestLogItem[]>([]); + +const gridItems = ref<GridItem[]>([]); + +const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind)); +const checkedItemsCount = computed(() => gridItems.value.filter(it => it.checked).length); + +function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) { + sortOrders.value = _sortOrders; +} + +async function onSearchRequest() { + await refreshCustomEmojis(); +} + +function onQueryResetButtonClicked() { + queryName.value = null; + queryHost.value = null; + queryLicense.value = null; + queryUri.value = null; + queryPublicUrl.value = null; +} + +async function onPageChanged(pageNumber: number) { + currentPage.value = pageNumber; + await refreshCustomEmojis(); +} + +async function onImportClicked() { + const targets = gridItems.value.filter(it => it.checked); + await importEmojis(targets); +} + +function onGridEvent(event: GridEvent) { + switch (event.type) { + case 'cell-value-change': + onGridCellValueChange(event); + break; + } +} + +function onGridCellValueChange(event: GridCellValueChangeEvent) { + const { row, column, newValue } = event; + if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) { + gridItems.value[row.index][column.setting.bindTo] = newValue; + } +} + +async function importEmojis(targets: GridItem[]) { + const confirm = await os.confirm({ + type: 'info', + title: i18n.ts._customEmojisManager._remote.confirmImportEmojisTitle, + text: i18n.tsx._customEmojisManager._remote.confirmImportEmojisDescription({ count: targets.length }), + }); + + if (confirm.canceled) { + return; + } + + const result = await os.promiseDialog( + Promise.all( + targets.map(item => + misskeyApi( + 'admin/emoji/copy', + { + emojiId: item.id!, + }) + .then(() => ({ item, success: true, err: undefined })) + .catch(err => ({ item, success: false, err })), + ), + ), + ); + const failedItems = result.filter(it => !it.success); + + if (failedItems.length > 0) { + await os.alert({ + type: 'error', + title: i18n.ts.somethingHappened, + text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription, + }); + } + + requestLogs.value = result.map(it => ({ + failed: !it.success, + url: it.item.url, + name: it.item.name, + error: it.err ? JSON.stringify(it.err) : undefined, + })); + + await refreshCustomEmojis(); +} + +async function refreshCustomEmojis() { + const query: Misskey.entities.V2AdminEmojiListRequest['query'] = { + name: emptyStrToUndefined(queryName.value), + host: emptyStrToUndefined(queryHost.value), + license: emptyStrToUndefined(queryLicense.value), + uri: emptyStrToUndefined(queryUri.value), + publicUrl: emptyStrToUndefined(queryPublicUrl.value), + hostType: 'remote', + }; + + if (JSON.stringify(query) !== previousQuery.value) { + currentPage.value = 1; + } + + const result = await loadingHandler.scope(() => misskeyApi('v2/admin/emoji/list', { + limit: queryLimit.value, + query: query, + page: currentPage.value, + sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}`) as never[], + })); + + customEmojis.value = result.emojis; + allPages.value = result.allPages; + previousQuery.value = JSON.stringify(query); + gridItems.value = customEmojis.value.map(it => ({ + checked: false, + id: it.id, + url: it.publicUrl, + name: it.name, + license: it.license, + host: it.host!, + })); +} + +onMounted(async () => { + await refreshCustomEmojis(); +}); +</script> + +<style module lang="scss"> +.row1 { + grid-row: 1 / 2; +} + +.row2 { + grid-row: 2 / 3; +} + +.col1 { + grid-column: 1 / 2; +} + +.col2 { + grid-column: 2 / 3; +} + +.col3 { + grid-column: 3 / 4; +} + +.root { + padding: 16px; +} + +.changedRow { + background-color: var(--MI_THEME-infoBg); +} + +.searchArea { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 16px; +} + +.searchButtons { + display: flex; + justify-content: flex-end; + align-items: flex-end; + gap: 8px; +} + +.searchButtonsSp { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; +} + +.searchAreaSp { + display: flex; + flex-direction: column; + gap: 8px; +} + +.gridArea { + padding-top: 8px; + padding-bottom: 8px; +} + +.pages { + display: flex; + justify-content: center; + align-items: center; + + button { + background-color: var(--MI_THEME-buttonBg); + border-radius: 9999px; + border: none; + margin: 0 4px; + padding: 8px; + } +} + +.footer { + background-color: var(--MI_THEME-bg); + + position: sticky; + left:0; + bottom:0; + z-index: 1; + // stickyã§è¿½å¾“ã•ã›ã‚‹éƒ½åˆä¸Šã€ãƒ•ッター自身ã§paddingã‚’æŒã¤å¿…è¦ãŒã‚ã‚‹ãŸã‚ã€è¦ªè¦ç´ ã§ç”»ä¸€çš„ã«æŒ‡å®šã—ã¦ã„る分をãƒã‚¬ãƒ†ã‚£ãƒ–マージンã§ç›¸æ®ºã—ã¦ã„ã‚‹ + margin-top: calc(var(--MI-margin) * -1); + margin-bottom: calc(var(--MI-margin) * -1); + padding-top: var(--MI-margin); + padding-bottom: var(--MI-margin); + + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + + & .center { + display: flex; + justify-content: center; + align-items: center; + } + + & .right { + display: flex; + justify-content: flex-end; + align-items: center; + } +} +</style> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts new file mode 100644 index 0000000000..f62304277a --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.stories.impl.ts @@ -0,0 +1,160 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { delay, http, HttpResponse } from 'msw'; +import { StoryObj } from '@storybook/vue3'; +import { entities } from 'misskey-js'; +import { commonHandlers } from '../../../.storybook/mocks.js'; +import { emoji } from '../../../.storybook/fakes.js'; +import { fakeId } from '../../../.storybook/fake-utils.js'; +import custom_emojis_manager2 from './custom-emojis-manager2.vue'; + +function createRender(params: { + emojis: entities.EmojiDetailedAdmin[]; +}) { + const storedEmojis: entities.EmojiDetailedAdmin[] = [...params.emojis]; + const storedDriveFiles: entities.DriveFile[] = []; + + return { + render(args) { + return { + components: { + custom_emojis_manager2, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<custom_emojis_manager2 v-bind="props" />', + }; + }, + args: { + + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/v2/admin/emoji/list', async ({ request }) => { + await delay(100); + + const bodyStream = request.body as ReadableStream; + const body = await new Response(bodyStream).json() as entities.V2AdminEmojiListRequest; + + const emojis = storedEmojis; + const limit = body.limit ?? 10; + const page = body.page ?? 1; + const result = emojis.slice((page - 1) * limit, page * limit); + + return HttpResponse.json({ + emojis: result, + count: Math.min(emojis.length, limit), + allCount: emojis.length, + allPages: Math.ceil(emojis.length / limit), + }); + }), + http.post('/api/drive/folders', () => { + return HttpResponse.json([]); + }), + http.post('/api/drive/files', () => { + return HttpResponse.json(storedDriveFiles); + }), + http.post('/api/drive/files/create', async ({ request }) => { + const data = await request.formData(); + const file = data.get('file'); + if (!file || !(file instanceof File)) { + return HttpResponse.json({ error: 'file is required' }, { + status: 400, + }); + } + + // FIXME: ファイルã®ãƒã‚¤ãƒŠãƒªã«0xEF 0xBF 0xBDãŒæ··å…¥ã—ã¦ã—ã¾ã„ã€ã†ã¾ãç”»åƒãƒ•ァイルã¨ã—ã¦è¡¨ç¤ºã§ããªã„å•題ãŒã‚ã‚‹ + const base64 = await new Promise<string>((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.readAsDataURL(new Blob([file], { type: 'image/webp' })); + }); + + const driveFile: entities.DriveFile = { + id: fakeId(file.name), + createdAt: new Date().toISOString(), + name: file.name, + type: file.type, + md5: '', + size: file.size, + isSensitive: false, + blurhash: null, + properties: {}, + url: base64, + thumbnailUrl: null, + comment: null, + folderId: null, + folder: null, + userId: null, + user: null, + }; + + storedDriveFiles.push(driveFile); + + return HttpResponse.json(driveFile); + }), + http.post('api/admin/emoji/add', async ({ request }) => { + await delay(100); + + const bodyStream = request.body as ReadableStream; + const body = await new Response(bodyStream).json() as entities.AdminEmojiAddRequest; + + const fileId = body.fileId; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const file = storedDriveFiles.find(f => f.id === fileId)!; + + const em = emoji({ + id: fakeId(file.name), + name: body.name, + publicUrl: file.url, + originalUrl: file.url, + type: file.type, + aliases: body.aliases, + category: body.category ?? undefined, + license: body.license ?? undefined, + localOnly: body.localOnly, + isSensitive: body.isSensitive, + }); + storedEmojis.push(em); + + return HttpResponse.json(null); + }), + ], + }, + }, + } satisfies StoryObj<typeof custom_emojis_manager2>; +} + +export const Default = createRender({ + emojis: [], +}); + +export const List10 = createRender({ + emojis: Array.from({ length: 10 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())), +}); + +export const List100 = createRender({ + emojis: Array.from({ length: 100 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())), +}); + +export const List1000 = createRender({ + emojis: Array.from({ length: 1000 }, (_, i) => emoji({ name: `emoji_${i}` }, i.toString())), +}); diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue new file mode 100644 index 0000000000..fb930064ff --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue @@ -0,0 +1,51 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <MkStickyContainer> + <template #header> + <MkPageHeader v-model:tab="headerTab" :tabs="headerTabs"/> + </template> + <XGridLocalComponent v-if="headerTab === 'local'" :class="$style.local"/> + <XGridRemoteComponent v-else/> + </MkStickyContainer> +</div> +</template> + +<script setup lang="ts"> +import { computed, ref } from 'vue'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import XGridLocalComponent from '@/pages/admin/custom-emojis-manager.local.vue'; +import XGridRemoteComponent from '@/pages/admin/custom-emojis-manager.remote.vue'; +import MkPageHeader from '@/components/global/MkPageHeader.vue'; +import MkStickyContainer from '@/components/global/MkStickyContainer.vue'; + +type PageMode = 'local' | 'remote'; + +const headerTab = ref<PageMode>('local'); + +const headerTabs = computed(() => [{ + key: 'local', + title: i18n.ts.local, +}, { + key: 'remote', + title: i18n.ts.remote, +}]); + +definePageMetadata(computed(() => ({ + title: i18n.ts.customEmojis, + icon: 'ti ti-icons', + needWideArea: true, +}))); +</script> + +<style lang="css" module> +.local { + height: calc(100dvh - var(--MI-stickyTop) - var(--MI-stickyBottom)); + overflow: clip; +} +</style> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 6cdf0eda7a..cbd0d12dcc 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -35,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; import MkSuperMenu from '@/components/MkSuperMenu.vue'; +import type { SuperMenuDef } from '@/components/MkSuperMenu.vue'; import MkInfo from '@/components/MkInfo.vue'; import { instance } from '@/instance.js'; import { lookup } from '@/scripts/lookup.js'; @@ -56,7 +57,7 @@ const indexInfo = { provide('shouldOmitHeaderTitle', false); -const INFO = ref(indexInfo); +const INFO = ref<PageMetadata>(indexInfo); const childInfo = ref<null | PageMetadata>(null); const narrow = ref(false); const view = ref(null); @@ -91,7 +92,7 @@ const ro = new ResizeObserver((entries, observer) => { narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; }); -const menuDef = computed(() => [{ +const menuDef = computed<SuperMenuDef[]>(() => [{ title: i18n.ts.quickAction, items: [{ type: 'button', @@ -99,7 +100,7 @@ const menuDef = computed(() => [{ text: i18n.ts.lookup, action: adminLookup, }, ...(instance.disableRegistration ? [{ - type: 'button', + type: 'button' as const, icon: 'ti ti-user-plus', text: i18n.ts.createInviteCode, action: invite, @@ -137,6 +138,11 @@ const menuDef = computed(() => [{ to: '/admin/emojis', active: currentPage.value?.route.name === 'emojis', }, { + icon: 'ti ti-icons', + text: i18n.ts.customEmojis + '(beta)', + to: '/admin/emojis2', + active: currentPage.value?.route.name === 'emojis2', + }, { icon: 'ti ti-sparkles', text: i18n.ts.avatarDecorations, to: '/admin/avatar-decorations', @@ -343,12 +349,14 @@ defineExpose({ height: 100%; > .nav { + position: sticky; + top: 0; width: 32%; max-width: 280px; box-sizing: border-box; border-right: solid 0.5px var(--MI_THEME-divider); overflow: auto; - height: 100%; + height: 100dvh; } > .main { diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 5d896db98c..6bab594d36 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -641,7 +641,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="role.policies.avatarDecorationLimit.useDefault" :readonly="readonly"> <template #label>{{ i18n.ts._role.useBaseValue }}</template> </MkSwitch> - <MkInput v-model="role.policies.avatarDecorationLimit.value" type="number" :min="0"> + <MkInput v-model="role.policies.avatarDecorationLimit.value" type="number" :min="0" :max="16" @update:modelValue="updateAvatarDecorationLimit"> <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template> </MkInput> <MkRange v-model="role.policies.avatarDecorationLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> @@ -757,6 +757,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, ref, computed } from 'vue'; import { throttle } from 'throttle-debounce'; +import { ROLE_POLICIES } from '@@/js/const.js'; import RolesEditorFormula from './RolesEditorFormula.vue'; import MkInput from '@/components/MkInput.vue'; import MkColorInput from '@/components/MkColorInput.vue'; @@ -767,7 +768,6 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRange from '@/components/MkRange.vue'; import FormSlot from '@/components/form/slot.vue'; import { i18n } from '@/i18n.js'; -import { ROLE_POLICIES } from '@@/js/const.js'; import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; @@ -793,6 +793,12 @@ for (const ROLE_POLICY of ROLE_POLICIES) { } } +function updateAvatarDecorationLimit(value: string | number) { + const numValue = Number(value); + const limited = Math.min(16, Math.max(0, numValue)); + role.value.policies.avatarDecorationLimit.value = limited; +} + const rolePermission = computed({ get: () => role.value.isAdministrator ? 'administrator' : role.value.isModerator ? 'moderator' : 'normal', set: (val) => { diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 036f18fe0d..f67b1cd582 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -239,7 +239,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="matchQuery([i18n.ts._role._options.avatarDecorationLimit, 'avatarDecorationLimit'])"> <template #label>{{ i18n.ts._role._options.avatarDecorationLimit }}</template> <template #suffix>{{ policies.avatarDecorationLimit }}</template> - <MkInput v-model="policies.avatarDecorationLimit" type="number" :min="0"> + <MkInput v-model="avatarDecorationLimit" type="number" :min="0" :max="16" @update:modelValue="updateAvatarDecorationLimit"> </MkInput> </MkFolder> @@ -334,6 +334,17 @@ for (const ROLE_POLICY of ROLE_POLICIES) { policies[ROLE_POLICY] = instance.policies[ROLE_POLICY]; } +const avatarDecorationLimit = computed({ + get: () => Math.min(16, Math.max(0, policies.avatarDecorationLimit)), + set: (value) => { + policies.avatarDecorationLimit = Math.min(Number(value), 16); + }, +}); + +function updateAvatarDecorationLimit(value: string | number) { + avatarDecorationLimit.value = Number(value); +} + function matchQuery(keywords: string[]): boolean { if (baseRoleQ.value.trim().length === 0) return true; return keywords.some(keyword => keyword.toLowerCase().includes(baseRoleQ.value.toLowerCase())); diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 9cd2546312..fb99379a0a 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNotes :pagination="featuredPagination"/> </div> <div v-else-if="tab === 'search'" key="search"> - <div class="_gaps"> + <div v-if="notesSearchAvailable" class="_gaps"> <div> <MkInput v-model="searchQuery" @enter="search()"> <template #prefix><i class="ti ti-search"></i></template> @@ -54,6 +54,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/> </div> + <div v-else> + <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> + </div> </div> </MkHorizontalSwipe> </MkSpacer> @@ -94,6 +97,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import { PageHeaderItem } from '@/types/page-header.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { notesSearchAvailable } from '@/scripts/check-permissions.js'; import { miLocalStorage } from '@/local-storage.js'; import { useRouter } from '@/router/supplier.js'; import { deepMerge } from '@/scripts/merge.js'; diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index bde1650754..6830c1ace4 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :contentMax="700"> + <MkSpacer :contentMax="1200"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> - <div v-if="tab === 'search'" key="search"> + <div v-if="tab === 'search'" key="search" :class="$style.searchRoot"> <div class="_gaps"> <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> <template #prefix><i class="ti ti-search"></i></template> @@ -27,23 +27,31 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="tab === 'featured'" key="featured"> <MkPagination v-slot="{items}" :pagination="featuredPagination"> - <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> + <div :class="$style.root"> + <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/> + </div> </MkPagination> </div> <div v-else-if="tab === 'favorites'" key="favorites"> <MkPagination v-slot="{items}" :pagination="favoritesPagination"> - <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> + <div :class="$style.root"> + <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/> + </div> </MkPagination> </div> <div v-else-if="tab === 'following'" key="following"> <MkPagination v-slot="{items}" :pagination="followingPagination"> - <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> + <div :class="$style.root"> + <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/> + </div> </MkPagination> </div> <div v-else-if="tab === 'owned'" key="owned"> <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> <MkPagination v-slot="{items}" :pagination="ownedPagination"> - <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> + <div :class="$style.root"> + <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/> + </div> </MkPagination> </div> </MkHorizontalSwipe> @@ -85,6 +93,7 @@ onMounted(() => { const featuredPagination = { endpoint: 'channels/featured' as const, + limit: 10, noPaging: true, }; const favoritesPagination = { @@ -157,3 +166,17 @@ definePageMetadata(() => ({ icon: 'ti ti-device-tv', })); </script> + +<style lang="scss" module> +.searchRoot { + width: 100%; + max-width: 700px; + margin: 0 auto; +} + +.root { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: var(--MI-margin); +} +</style> diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 716cd9a73f..240f395e04 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -46,9 +46,10 @@ import { clipsCache } from '@/cache.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { genEmbedCode } from '@/scripts/get-embed-code.js'; -import { getServerContext } from '@/server-context.js'; +import { assertServerContext, serverContext } from '@/server-context.js'; -const CTX_CLIP = getServerContext('clip'); +// contextã¯éžãƒã‚°ã‚¤ãƒ³çŠ¶æ…‹ã®æƒ…å ±ã—ã‹ãªã„ãŸã‚ãƒã‚°ã‚¤ãƒ³æ™‚ã¯åˆ©ç”¨ã§ããªã„ +const CTX_CLIP = !$i && assertServerContext(serverContext, 'clip') ? serverContext.clip : null; const props = defineProps<{ clipId: string, diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 850c1c5eb0..107a0d760c 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #default="{items}"> <div class="ldhfsamy"> <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> - <img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/> + <img :src="emoji.url" class="img" :alt="emoji.name"/> <div class="body"> <div class="name _monospace">{{ emoji.name }}</div> <div class="info">{{ emoji.category }}</div> @@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #default="{items}"> <div class="ldhfsamy"> <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> - <img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" class="img" :alt="emoji.name"/> + <img :src="getProxiedImageUrl(emoji.url, 'emoji')" class="img" :alt="emoji.name"/> <div class="body"> <div class="name _monospace">{{ emoji.name }}</div> <div class="info">{{ emoji.host }}</div> @@ -78,11 +78,13 @@ import { computed, defineAsyncComponent, ref, shallowRef } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkPagination from '@/components/MkPagination.vue'; +import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSplit from '@/components/form/split.vue'; import { selectFile } from '@/scripts/select-file.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; +import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -161,6 +163,19 @@ const edit = (emoji) => { }); }; +const detailRemoteEmoji = (emoji) => { + const { dispose } = os.popup(MkRemoteEmojiEditDialog, { + emoji: emoji, + }, { + done: () => { + dispose(); + }, + closed: () => { + dispose(); + }, + }); +}; + const importEmoji = (emoji) => { os.apiWithDialog('admin/emoji/copy', { emojiId: emoji.id, @@ -171,13 +186,15 @@ const remoteMenu = (emoji, ev: MouseEvent) => { os.popupMenu([{ type: 'label', text: ':' + emoji.name + ':', - }, - { + }, { + text: i18n.ts.details, + icon: 'ti ti-info-circle', + action: () => { detailRemoteEmoji(emoji); }, + }, { text: i18n.ts.import, icon: 'ti ti-plus', action: () => { importEmoji(emoji); }, - }, - { + }, { text: i18n.ts.delete, icon: 'ph-trash ph-bold ph-lg', action: () => { diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index d3e9ca0dcf..c8e6dfb05a 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -118,7 +118,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); }, { immediate: true }); -const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); +const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null); async function changeImage(ev: Event) { file.value = await selectFile(ev.currentTarget ?? ev.target, null); diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index 2550100a42..0d2c6217d4 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkSpacer :contentMax="1200"> - <MkTab v-model="origin" style="margin-bottom: var(--MI-margin);"> + <MkTab v-if="instance.federation !== 'none'" v-model="origin" style="margin-bottom: var(--MI-margin);"> <option value="local">{{ i18n.ts.local }}</option> <option value="remote">{{ i18n.ts.remote }}</option> </MkTab> @@ -69,6 +69,7 @@ import MkUserList from '@/components/MkUserList.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; +import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index e85d2c29c1..ab060587c5 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -59,18 +59,18 @@ async function onAccept(token: string) { name: props.name, iconUrl: props.icon, permission: _permissions.value, - }, token).catch(() => { + }, token).then(() => { + if (props.callback && props.callback !== '') { + const cbUrl = new URL(props.callback); + if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url'); + cbUrl.searchParams.set('session', props.session); + location.href = cbUrl.toString(); + } else { + authRoot.value?.showUI('success'); + } + }).catch(() => { authRoot.value?.showUI('failed'); }); - - if (props.callback && props.callback !== '') { - const cbUrl = new URL(props.callback); - if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url'); - cbUrl.searchParams.set('session', props.session); - location.href = cbUrl.toString(); - } else { - authRoot.value?.showUI('success'); - } } function onDeny() { @@ -117,5 +117,6 @@ definePageMetadata(() => ({ border-radius: var(--MI-radius); background-color: var(--MI_THEME-panel); overflow-x: scroll; + white-space: nowrap; } </style> diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 737b0eea4c..b70bff052a 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -50,6 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { host } from '@@/js/config.js'; import type { Paging } from '@/components/MkPagination.vue'; import MkNotes from '@/components/MkNotes.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; @@ -61,9 +62,11 @@ import { dateString } from '@/filters/date.js'; import MkClipPreview from '@/components/MkClipPreview.vue'; import { defaultStore } from '@/store.js'; import { pleaseLogin } from '@/scripts/please-login.js'; -import { getServerContext } from '@/server-context.js'; +import { serverContext, assertServerContext } from '@/server-context.js'; +import { $i } from '@/account.js'; -const CTX_NOTE = getServerContext('note'); +// contextã¯éžãƒã‚°ã‚¤ãƒ³çŠ¶æ…‹ã®æƒ…å ±ã—ã‹ãªã„ãŸã‚ãƒã‚°ã‚¤ãƒ³æ™‚ã¯åˆ©ç”¨ã§ããªã„ +const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null; const MkNoteDetailed = defineAsyncComponent(() => (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteDetailed.vue') : @@ -146,7 +149,12 @@ function fetchNote() { }).catch(err => { if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') { pleaseLogin({ + path: '/', message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor, + openOnRemote: { + type: 'lookup', + url: `https://${host}/notes/${props.noteId}`, + }, }); } error.value = err; diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index d64537d289..73b2839a44 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -13,16 +13,20 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header>{{ i18n.ts.options }}</template> <div class="_gaps_m"> - <MkRadios v-model="hostSelect"> - <template #label>{{ i18n.ts.host }}</template> - <option value="all" default>{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option> - </MkRadios> - <MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search"> - <template #prefix><i class="ti ti-server"></i></template> - </MkInput> + <template v-if="instance.federation !== 'none'"> + <MkRadios v-model="hostSelect"> + <template #label>{{ i18n.ts.host }}</template> + <option value="all" default>{{ i18n.ts.all }}</option> + <option value="local">{{ i18n.ts.local }}</option> + <option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option> + </MkRadios> + <MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search"> + <template #prefix><i class="ti ti-server"></i></template> + </MkInput> + </template> + <MkSwitch v-model="order">Sort by newest to oldest</MkSwitch> + <MkSelect v-model="filetype" small> <template #label>File Type</template> <option :value="null">None</option> @@ -114,7 +118,7 @@ setHostSelectWithInput(hostInput.value, undefined); watch(hostInput, setHostSelectWithInput); const searchHost = computed(() => { - if (hostSelect.value === 'local') return '.'; + if (hostSelect.value === 'local' || instance.federation === 'none') return '.'; if (hostSelect.value === 'specified') return hostInput.value; return null; }); diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index a355c0eeaa..772ee91d63 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search"> <template #prefix><i class="ti ti-search"></i></template> </MkInput> - <MkRadios v-model="searchOrigin" @update:modelValue="search()"> + <MkRadios v-if="instance.federation !== 'none'" v-model="searchOrigin" @update:modelValue="search()"> <option value="combined">{{ i18n.ts.all }}</option> <option value="local">{{ i18n.ts.local }}</option> <option value="remote">{{ i18n.ts.remote }}</option> @@ -33,6 +33,7 @@ import MkInput from '@/components/MkInput.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; import * as os from '@/os.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -118,7 +119,7 @@ async function search() { limit: 10, params: { query: query, - origin: searchOrigin.value, + origin: instance.federation === 'none' ? 'local' : searchOrigin.value, }, }; diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index 97e960675f..c2588736b3 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -12,7 +12,16 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton> </div> - <MkUserCardMini v-for="user in accounts" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/> + <template v-for="[id, user] in accounts"> + <MkUserCardMini v-if="user != null" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/> + <button v-else v-panel class="_button" :class="$style.unknownUser" @click="menu(id, $event)"> + <div :class="$style.unknownUserAvatarMock"><i class="ti ti-user-question"></i></div> + <div> + <div :class="$style.unknownUserTitle">{{ i18n.ts.unknown }}</div> + <div :class="$style.unknownUserSub">ID: <span class="_monospace">{{ id }}</span></div> + </div> + </button> + </template> </div> </FormSuspense> </div> @@ -29,9 +38,10 @@ import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWith import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import { MenuItem } from '@/types/menu'; const storedAccounts = ref<{ id: string, token: string }[] | null>(null); -const accounts = ref<Misskey.entities.UserDetailed[]>([]); +const accounts = ref(new Map<string, Misskey.entities.UserDetailed | null>()); const init = async () => { getAccounts().then(accounts => { @@ -41,21 +51,35 @@ const init = async () => { userIds: storedAccounts.value.map(x => x.id), }); }).then(response => { - accounts.value = response; + if (storedAccounts.value == null) return; + accounts.value = new Map(storedAccounts.value.map(x => [x.id, response.find((y: Misskey.entities.UserDetailed) => y.id === x.id) ?? null])); }); }; -function menu(account: Misskey.entities.UserDetailed, ev: MouseEvent) { - os.popupMenu([{ - text: i18n.ts.switch, - icon: 'ti ti-switch-horizontal', - action: () => switchAccount(account), - }, { - text: i18n.ts.logout, - icon: 'ti ti-trash', - danger: true, - action: () => removeAccount(account), - }], ev.currentTarget ?? ev.target); +function menu(account: Misskey.entities.UserDetailed | string, ev: MouseEvent) { + let menu: MenuItem[]; + + if (typeof account === 'string') { + menu = [{ + text: i18n.ts.logout, + icon: 'ti ti-trash', + danger: true, + action: () => removeAccount(account), + }]; + } else { + menu = [{ + text: i18n.ts.switch, + icon: 'ti ti-switch-horizontal', + action: () => switchAccount(account.id), + }, { + text: i18n.ts.logout, + icon: 'ti ti-trash', + danger: true, + action: () => removeAccount(account.id), + }]; + } + + os.popupMenu(menu, ev.currentTarget ?? ev.target); } function addAccount(ev: MouseEvent) { @@ -68,9 +92,9 @@ function addAccount(ev: MouseEvent) { }], ev.currentTarget ?? ev.target); } -async function removeAccount(account: Misskey.entities.UserDetailed) { - await _removeAccount(account.id); - accounts.value = accounts.value.filter(x => x.id !== account.id); +async function removeAccount(id: string) { + await _removeAccount(id); + accounts.value.delete(id); } function addExistingAccount() { @@ -90,9 +114,9 @@ function createAccount() { }); } -async function switchAccount(account: Misskey.entities.UserDetailed) { +async function switchAccount(id: string) { const fetchedAccounts = await getAccounts(); - const token = fetchedAccounts.find(x => x.id === account.id)!.token; + const token = fetchedAccounts.find(x => x.id === id)!.token; switchAccountWithToken(token); } @@ -112,6 +136,49 @@ definePageMetadata(() => ({ <style lang="scss" module> .user { - cursor: pointer; + cursor: pointer; +} + +.unknownUser { + display: flex; + align-items: center; + text-align: start; + padding: 16px; + background: var(--MI_THEME-panel); + border-radius: 8px; + font-size: 0.9em; +} + +.unknownUserAvatarMock { + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + font-size: 16px; + margin-right: 12px; + background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%); + color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%); + border-radius: 50%; +} + +.unknownUserTitle { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 18px; +} + +.unknownUserSub { + display: block; + width: 100%; + font-size: 95%; + opacity: 0.7; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 16px; } </style> diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index cb0451c8b4..f8a0575a77 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -106,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch> </div> - <MkSelect v-model="instanceTicker"> + <MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker"> <template #label>{{ i18n.ts.instanceTicker }}</template> <option value="none">{{ i18n.ts._instanceTicker.none }}</option> <option value="remote">{{ i18n.ts._instanceTicker.remote }}</option> @@ -357,6 +357,7 @@ import MkInfo from '@/components/MkInfo.vue'; import { searchEngineMap } from '@/scripts/search-engine-map.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; +import { instance } from '@/instance.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 552b4ee028..b7bf8c5dc1 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -43,7 +43,7 @@ const indexInfo = { icon: 'ti ti-settings', hideHeader: true, }; -const INFO = ref(indexInfo); +const INFO = ref<PageMetadata>(indexInfo); const el = shallowRef<HTMLElement | null>(null); const childInfo = ref<null | PageMetadata>(null); diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 82aeb6063f..d6ee45e074 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -9,17 +9,24 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ph-envelope ph-bold ph-lg"></i></template> <template #label>{{ i18n.ts.wordMute }}</template> - <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/> + <div class="_gaps_m"> + <MkInfo>{{ i18n.ts.wordMuteDescription }}</MkInfo> + <MkSwitch v-model="showSoftWordMutedWord">{{ i18n.ts.showMutedWord }}</MkSwitch> + <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/> + </div> </MkFolder> <MkFolder> <template #icon><i class="ph-x-square ph-bold ph-lg"></i></template> <template #label>{{ i18n.ts.hardWordMute }}</template> - <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/> + <div class="_gaps_m"> + <MkInfo>{{ i18n.ts.hardWordMuteDescription }}</MkInfo> + <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/> + </div> </MkFolder> - <MkFolder> + <MkFolder v-if="instance.federation !== 'none'"> <template #icon><i class="ti ti-planet-off"></i></template> <template #label>{{ i18n.ts.instanceMute }}</template> @@ -126,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, watch } from 'vue'; import XInstanceMute from './mute-block.instance-mute.vue'; import XWordMute from './mute-block.word-mute.vue'; import MkPagination from '@/components/MkPagination.vue'; @@ -135,9 +142,13 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import * as os from '@/os.js'; -import { infoImageUrl } from '@/instance.js'; +import { instance, infoImageUrl } from '@/instance.js'; import { signinRequired } from '@/account.js'; +import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { defaultStore } from '@/store'; +import { reloadAsk } from '@/scripts/reload-ask.js'; const $i = signinRequired(); @@ -160,6 +171,14 @@ const expandedRenoteMuteItems = ref([]); const expandedMuteItems = ref([]); const expandedBlockItems = ref([]); +const showSoftWordMutedWord = computed(defaultStore.makeGetterSetter('showSoftWordMutedWord')); + +watch([ + showSoftWordMutedWord, +], async () => { + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); +}); + async function unrenoteMute(user, ev) { os.popupMenu([{ text: i18n.ts.renoteUnmute, diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 790f9e44e2..ce4c229a3a 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption> <div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div> <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div> - <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div> + <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div> </template> </MkSwitch> @@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption> <div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div> - <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div> + <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div> </template> </FormSlot> @@ -129,7 +129,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption> <div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div> - <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div> + <div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div> </template> </FormSlot> </div> @@ -171,6 +171,7 @@ import MkFolder from '@/components/MkFolder.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; import { signinRequired } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import FormSlot from '@/components/form/slot.vue'; @@ -224,7 +225,7 @@ watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => { }); async function update_requireSigninToViewContents(value: boolean) { - if (value) { + if (value === true && instance.federation !== 'none') { const { canceled } = await os.confirm({ type: 'warning', text: i18n.ts.acknowledgeNotesAndEnable, diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue index 67943524ef..140b6beb14 100644 --- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSelect v-model="statusbar.type" placeholder="Please select"> <template #label>{{ i18n.ts.type }}</template> <option value="rss">RSS</option> - <option value="federation">Federation</option> + <option v-if="instance.federation !== 'none'" value="federation">Federation</option> <option value="userList">User list timeline</option> </MkSelect> @@ -96,6 +96,7 @@ import MkButton from '@/components/MkButton.vue'; import MkRange from '@/components/MkRange.vue'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; const props = defineProps<{ diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index a530f4b5d6..e49d6af470 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, ref, computed } from 'vue'; -import { toUnicode } from 'punycode/'; +import { toUnicode } from 'punycode.js'; import tinycolor from 'tinycolor2'; import { v4 as uuid } from 'uuid'; import JSON5 from 'json5'; diff --git a/packages/frontend/src/pages/user/files.vue b/packages/frontend/src/pages/user/files.vue new file mode 100644 index 0000000000..b6c7c1c777 --- /dev/null +++ b/packages/frontend/src/pages/user/files.vue @@ -0,0 +1,56 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> + <MkSpacer :contentMax="1100"> + <div :class="$style.root"> + <MkPagination v-slot="{items}" :pagination="pagination"> + <div :class="$style.stream"> + <MkNoteMediaGrid v-for="note in items" :note="note" square/> + </div> + </MkPagination> + </div> + </MkSpacer> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; + +import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue'; +import MkPagination from '@/components/MkPagination.vue'; + +const props = defineProps<{ + user: Misskey.entities.UserDetailed; +}>(); + +const pagination = { + endpoint: 'users/notes' as const, + limit: 15, + params: computed(() => ({ + userId: props.user.id, + withFiles: true, + })), +}; +</script> + +<style lang="scss" module> +.root { + padding: 8px; +} + +.stream { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: var(--MI-marginHalf); +} + +@media screen and (min-width: 600px) { + .stream { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + } + +} +</style> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 5565555ca4..645c3b3c3c 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -138,7 +138,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-if="user.pinnedNotes.length === 0 && $i?.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo> <template v-if="narrow"> <MkLazy> - <XFiles :key="user.id" :user="user" :collapsed="true"/> + <XFiles :key="user.id" :user="user" :collapsed="true" @unfold="emit('unfoldFiles')"/> </MkLazy> <MkLazy> <XActivity :key="user.id" :user="user" :collapsed="true"/> @@ -180,7 +180,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;"> - <XFiles :key="user.id" :user="user"/> + <XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/> <XActivity :key="user.id" :user="user"/> <XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user"/> </div> @@ -242,7 +242,6 @@ function calcAge(birthdate: string): number { const XFiles = defineAsyncComponent(() => import('./index.files.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); const XListenBrainz = defineAsyncComponent(() => import('./index.listenbrainz.vue')); -//const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed; @@ -252,6 +251,10 @@ const props = withDefaults(defineProps<{ disableNotes: false, }); +const emit = defineEmits<{ + (ev: 'unfoldFiles'): void; +}>(); + const router = useRouter(); const user = ref(props.user); diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue index 7fe90da865..44e35e3479 100644 --- a/packages/frontend/src/pages/user/index.files.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -4,30 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkContainer :max-height="300" :foldable="true" :expanded="!collapsed"> +<MkContainer :max-height="300" :foldable="true" :expanded="!collapsed" :onUnfold="unfoldContainer"> <template #icon><i class="ti ti-photo"></i></template> <template #header>{{ i18n.ts.files }}</template> <div :class="$style.root"> <MkLoading v-if="fetching"/> - <div v-if="!fetching && files.length > 0" :class="$style.stream"> - <template v-for="file in files" :key="file.note.id + file.file.id"> - <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)"> - <!-- TODO: ç”»åƒä»¥å¤–ã®ãƒ•ァイルã«å¯¾å¿œ --> - <ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/> - <div :class="$style.sensitive"> - <div> - <div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div> - <div>{{ i18n.ts.clickToShow }}</div> - </div> - </div> - </div> - <MkA v-else :class="$style.img" :to="notePage(file.note)"> - <!-- TODO: ç”»åƒä»¥å¤–ã®ãƒ•ァイルã«å¯¾å¿œ --> - <ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/> - </MkA> - </template> + <div v-if="!fetching && notes.length > 0" :class="$style.stream"> + <MkNoteMediaGrid v-for="note in notes" :note="note"/> </div> - <p v-if="!fetching && files.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p> + <p v-if="!fetching && notes.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p> </div> </MkContainer> </template> @@ -35,13 +20,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { notePage } from '@/filters/note.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; -import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; +import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue'; const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed; @@ -50,33 +32,25 @@ const props = withDefaults(defineProps<{ collapsed: false, }); +const emit = defineEmits<{ + (ev: 'unfold'): void; +}>(); + const fetching = ref(true); -const files = ref<{ - note: Misskey.entities.Note; - file: Misskey.entities.DriveFile; -}[]>([]); -const showingFiles = ref<string[]>([]); +const notes = ref<Misskey.entities.Note[]>([]); -function thumbnail(image: Misskey.entities.DriveFile): string { - return defaultStore.state.disableShowingAnimatedImages - ? getStaticImageUrl(image.url) - : image.thumbnailUrl; +function unfoldContainer(): boolean { + emit('unfold'); + return false; } onMounted(() => { misskeyApi('users/notes', { userId: props.user.id, withFiles: true, - limit: 15, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - files.value.push({ - note, - file, - }); - } - } + limit: 10, + }).then(_notes => { + notes.value = _notes; fetching.value = false; }); }); diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index a35250bf5f..ba02559d68 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -9,10 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <div v-if="user"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> - <XHome v-if="tab === 'home'" key="home" :user="user"/> + <XHome v-if="tab === 'home'" key="home" :user="user" @unfoldFiles="() => { tab = 'files'; }"/> <MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800" style="padding-top: 0"> <XTimeline :user="user"/> </MkSpacer> + <XFiles v-else-if="tab === 'files'" :user="user"/> <XActivity v-else-if="tab === 'activity'" key="activity" :user="user"/> <XAchievements v-else-if="tab === 'achievements'" key="achievements" :user="user"/> <XReactions v-else-if="tab === 'reactions'" key="reactions" :user="user"/> @@ -39,10 +40,11 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; -import { getServerContext } from '@/server-context.js'; +import { serverContext, assertServerContext } from '@/server-context.js'; const XHome = defineAsyncComponent(() => import('./home.vue')); const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); +const XFiles = defineAsyncComponent(() => import('./files.vue')); const XActivity = defineAsyncComponent(() => import('./activity.vue')); const XAchievements = defineAsyncComponent(() => import('./achievements.vue')); const XReactions = defineAsyncComponent(() => import('./reactions.vue')); @@ -53,7 +55,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue')); const XGallery = defineAsyncComponent(() => import('./gallery.vue')); const XRaw = defineAsyncComponent(() => import('./raw.vue')); -const CTX_USER = getServerContext('user'); +// contextã¯éžãƒã‚°ã‚¤ãƒ³çŠ¶æ…‹ã®æƒ…å ±ã—ã‹ãªã„ãŸã‚ãƒã‚°ã‚¤ãƒ³æ™‚ã¯åˆ©ç”¨ã§ããªã„ +const CTX_USER = !$i && assertServerContext(serverContext, 'user') ? serverContext.user : null; const props = withDefaults(defineProps<{ acct: string; @@ -103,6 +106,10 @@ const headerTabs = computed(() => user.value ? [{ title: i18n.ts.notes, icon: 'ti ti-pencil', }, { + key: 'files', + title: i18n.ts.files, + icon: 'ti ti-photo', +}, { key: 'activity', title: i18n.ts.activity, icon: 'ti ti-chart-line', diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index f1842255e0..c5731bd2a9 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -53,12 +53,14 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string if (!instance.iconUrl) { return ''; } + return getProxiedImageUrl(instance.iconUrl, 'preview'); } misskeyApiGet('federation/instances', { sort: '+pubSub', limit: 20, + blocked: 'false', }).then(_instances => { instances.value = _instances; }); diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index c7637a1db9..74e1482ce0 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -391,6 +391,10 @@ const routes: RouteDef[] = [{ name: 'emojis', component: page(() => import('@/pages/custom-emojis-manager.vue')), }, { + path: '/emojis2', + name: 'emojis2', + component: page(() => import('@/pages/admin/custom-emojis-manager2.vue')), + }, { path: '/avatar-decorations', name: 'avatarDecorations', component: page(() => import('@/pages/avatar-decorations.vue')), diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 46aed49330..e203c51bba 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -3,14 +3,24 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { utils, values } from '@syuilo/aiscript'; +import { errors, utils, values } from '@syuilo/aiscript'; import * as Misskey from 'misskey-js'; +import { url, lang } from '@@/js/config.js'; +import { assertStringAndIsIn } from './common.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { url, lang } from '@@/js/config.js'; + +const DIALOG_TYPES = [ + 'error', + 'info', + 'success', + 'warning', + 'waiting', + 'question', +] as const; export function aiScriptReadline(q: string): Promise<string> { return new Promise(ok => { @@ -22,15 +32,20 @@ export function aiScriptReadline(q: string): Promise<string> { }); } -export function createAiScriptEnv(opts) { +export function createAiScriptEnv(opts: { storageKey: string, token?: string }) { return { USER_ID: $i ? values.STR($i.id) : values.NULL, - USER_NAME: $i ? values.STR($i.name) : values.NULL, + USER_NAME: $i?.name ? values.STR($i.name) : values.NULL, USER_USERNAME: $i ? values.STR($i.username) : values.NULL, CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value), LOCALE: values.STR(lang), SERVER_URL: values.STR(url), 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { + utils.assertString(title); + utils.assertString(text); + if (type != null) { + assertStringAndIsIn(type, DIALOG_TYPES); + } await os.alert({ type: type ? type.value : 'info', title: title.value, @@ -39,6 +54,11 @@ export function createAiScriptEnv(opts) { return values.NULL; }), 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { + utils.assertString(title); + utils.assertString(text); + if (type != null) { + assertStringAndIsIn(type, DIALOG_TYPES); + } const confirm = await os.confirm({ type: type ? type.value : 'question', title: title.value, @@ -48,14 +68,20 @@ export function createAiScriptEnv(opts) { }), 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { utils.assertString(ep); - if (ep.value.includes('://')) throw new Error('invalid endpoint'); + if (ep.value.includes('://')) { + throw new errors.AiScriptRuntimeError('invalid endpoint'); + } if (token) { utils.assertString(token); // ãƒã‚°ãŒã‚れã°undefinedã‚‚ã‚り得るãŸã‚念ã®ãŸã‚ if (typeof token.value !== 'string') throw new Error('invalid token'); } const actualToken: string|null = token?.value ?? opts.token ?? null; - return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => { + if (param == null) { + throw new errors.AiScriptRuntimeError('expected param'); + } + utils.assertObject(param); + return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => { return utils.jsToVal(res); }, err => { return values.ERROR('request_failed', utils.jsToVal(err)); @@ -75,12 +101,18 @@ export function createAiScriptEnv(opts) { */ 'Mk:save': values.FN_NATIVE(([key, value]) => { utils.assertString(key); + utils.expectAny(value); miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value))); return values.NULL; }), 'Mk:load': values.FN_NATIVE(([key]) => { utils.assertString(key); - return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`))); + return utils.jsToVal(miLocalStorage.getItemAsJson(`aiscript:${opts.storageKey}:${key.value}`) ?? null); + }), + 'Mk:remove': values.FN_NATIVE(([key]) => { + utils.assertString(key); + miLocalStorage.removeItem(`aiscript:${opts.storageKey}:${key.value}`); + return values.NULL; }), 'Mk:url': values.FN_NATIVE(() => { return values.STR(window.location.href); diff --git a/packages/frontend/src/scripts/aiscript/common.ts b/packages/frontend/src/scripts/aiscript/common.ts new file mode 100644 index 0000000000..de6fa1d633 --- /dev/null +++ b/packages/frontend/src/scripts/aiscript/common.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { errors, utils, type values } from '@syuilo/aiscript'; + +export function assertStringAndIsIn<A extends readonly string[]>(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } { + utils.assertString(value); + const str = value.value; + if (!expects.includes(str)) { + const expected = expects.map((expect) => `"${expect}"`).join(', '); + throw new errors.AiScriptRuntimeError(`"${value.value}" is not in ${expected}`); + } +} diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 2b386bebb8..ca92b27ff5 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -7,6 +7,15 @@ import { utils, values } from '@syuilo/aiscript'; import { v4 as uuid } from 'uuid'; import { ref, Ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { assertStringAndIsIn } from './common.js'; + +const ALIGNS = ['left', 'center', 'right'] as const; +const FONTS = ['serif', 'sans-serif', 'monospace'] as const; +const BORDER_STYLES = ['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] as const; + +type Align = (typeof ALIGNS)[number]; +type Font = (typeof FONTS)[number]; +type BorderStyle = (typeof BORDER_STYLES)[number]; export type AsUiComponentBase = { id: string; @@ -21,13 +30,13 @@ export type AsUiRoot = AsUiComponentBase & { export type AsUiContainer = AsUiComponentBase & { type: 'container'; children?: AsUiComponent['id'][]; - align?: 'left' | 'center' | 'right'; + align?: Align; bgColor?: string; fgColor?: string; - font?: 'serif' | 'sans-serif' | 'monospace'; + font?: Font; borderWidth?: number; borderColor?: string; - borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset'; + borderStyle?: BorderStyle; borderRadius?: number; padding?: number; rounded?: boolean; @@ -40,7 +49,7 @@ export type AsUiText = AsUiComponentBase & { size?: number; bold?: boolean; color?: string; - font?: 'serif' | 'sans-serif' | 'monospace'; + font?: Font; }; export type AsUiMfm = AsUiComponentBase & { @@ -49,14 +58,14 @@ export type AsUiMfm = AsUiComponentBase & { size?: number; bold?: boolean; color?: string; - font?: 'serif' | 'sans-serif' | 'monospace'; - onClickEv?: (evId: string) => void + font?: Font; + onClickEv?: (evId: string) => Promise<void>; }; export type AsUiButton = AsUiComponentBase & { type: 'button'; text?: string; - onClick?: () => void; + onClick?: () => Promise<void>; primary?: boolean; rounded?: boolean; disabled?: boolean; @@ -69,7 +78,7 @@ export type AsUiButtons = AsUiComponentBase & { export type AsUiSwitch = AsUiComponentBase & { type: 'switch'; - onChange?: (v: boolean) => void; + onChange?: (v: boolean) => Promise<void>; default?: boolean; label?: string; caption?: string; @@ -77,7 +86,7 @@ export type AsUiSwitch = AsUiComponentBase & { export type AsUiTextarea = AsUiComponentBase & { type: 'textarea'; - onInput?: (v: string) => void; + onInput?: (v: string) => Promise<void>; default?: string; label?: string; caption?: string; @@ -85,7 +94,7 @@ export type AsUiTextarea = AsUiComponentBase & { export type AsUiTextInput = AsUiComponentBase & { type: 'textInput'; - onInput?: (v: string) => void; + onInput?: (v: string) => Promise<void>; default?: string; label?: string; caption?: string; @@ -93,7 +102,7 @@ export type AsUiTextInput = AsUiComponentBase & { export type AsUiNumberInput = AsUiComponentBase & { type: 'numberInput'; - onInput?: (v: number) => void; + onInput?: (v: number) => Promise<void>; default?: number; label?: string; caption?: string; @@ -105,7 +114,7 @@ export type AsUiSelect = AsUiComponentBase & { text: string; value: string; }[]; - onChange?: (v: string) => void; + onChange?: (v: string) => Promise<void>; default?: string; label?: string; caption?: string; @@ -140,11 +149,15 @@ export type AsUiPostForm = AsUiComponentBase & { export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm; +type Options<T extends AsUiComponent> = T extends AsUiButtons + ? Omit<T, 'id' | 'type' | 'buttons'> & { 'buttons'?: Options<AsUiButton>[] } + : Omit<T, 'id' | 'type'>; + export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) { // TODO } -function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 'type'> { +function getRootOptions(def: values.Value | undefined): Options<AsUiRoot> { utils.assertObject(def); const children = def.value.get('children'); @@ -153,30 +166,32 @@ function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 't return { children: children.value.map(v => { utils.assertObject(v); - return v.value.get('id').value; + const id = v.value.get('id'); + utils.assertString(id); + return id.value; }), }; } -function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, 'id' | 'type'> { +function getContainerOptions(def: values.Value | undefined): Options<AsUiContainer> { utils.assertObject(def); const children = def.value.get('children'); if (children) utils.assertArray(children); const align = def.value.get('align'); - if (align) utils.assertString(align); + if (align) assertStringAndIsIn(align, ALIGNS); const bgColor = def.value.get('bgColor'); if (bgColor) utils.assertString(bgColor); const fgColor = def.value.get('fgColor'); if (fgColor) utils.assertString(fgColor); const font = def.value.get('font'); - if (font) utils.assertString(font); + if (font) assertStringAndIsIn(font, FONTS); const borderWidth = def.value.get('borderWidth'); if (borderWidth) utils.assertNumber(borderWidth); const borderColor = def.value.get('borderColor'); if (borderColor) utils.assertString(borderColor); const borderStyle = def.value.get('borderStyle'); - if (borderStyle) utils.assertString(borderStyle); + if (borderStyle) assertStringAndIsIn(borderStyle, BORDER_STYLES); const borderRadius = def.value.get('borderRadius'); if (borderRadius) utils.assertNumber(borderRadius); const padding = def.value.get('padding'); @@ -189,7 +204,9 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, return { children: children ? children.value.map(v => { utils.assertObject(v); - return v.value.get('id').value; + const id = v.value.get('id'); + utils.assertString(id); + return id.value; }) : [], align: align?.value, fgColor: fgColor?.value, @@ -205,7 +222,7 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, }; } -function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 'type'> { +function getTextOptions(def: values.Value | undefined): Options<AsUiText> { utils.assertObject(def); const text = def.value.get('text'); @@ -217,7 +234,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't const color = def.value.get('color'); if (color) utils.assertString(color); const font = def.value.get('font'); - if (font) utils.assertString(font); + if (font) assertStringAndIsIn(font, FONTS); return { text: text?.value, @@ -228,7 +245,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't }; } -function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> { +function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiMfm> { utils.assertObject(def); const text = def.value.get('text'); @@ -240,7 +257,7 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg const color = def.value.get('color'); if (color) utils.assertString(color); const font = def.value.get('font'); - if (font) utils.assertString(font); + if (font) assertStringAndIsIn(font, FONTS); const onClickEv = def.value.get('onClickEv'); if (onClickEv) utils.assertFunction(onClickEv); @@ -250,13 +267,13 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg bold: bold?.value, color: color?.value, font: font?.value, - onClickEv: (evId: string) => { - if (onClickEv) call(onClickEv, [values.STR(evId)]); + onClickEv: async (evId: string) => { + if (onClickEv) await call(onClickEv, [values.STR(evId)]); }, }; } -function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextInput, 'id' | 'type'> { +function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextInput> { utils.assertObject(def); const onInput = def.value.get('onInput'); @@ -269,8 +286,8 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF if (caption) utils.assertString(caption); return { - onInput: (v) => { - if (onInput) call(onInput, [utils.jsToVal(v)]); + onInput: async (v) => { + if (onInput) await call(onInput, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -278,7 +295,7 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF }; } -function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextarea, 'id' | 'type'> { +function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextarea> { utils.assertObject(def); const onInput = def.value.get('onInput'); @@ -291,8 +308,8 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn if (caption) utils.assertString(caption); return { - onInput: (v) => { - if (onInput) call(onInput, [utils.jsToVal(v)]); + onInput: async (v) => { + if (onInput) await call(onInput, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -300,7 +317,7 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn }; } -function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiNumberInput, 'id' | 'type'> { +function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiNumberInput> { utils.assertObject(def); const onInput = def.value.get('onInput'); @@ -313,8 +330,8 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values. if (caption) utils.assertString(caption); return { - onInput: (v) => { - if (onInput) call(onInput, [utils.jsToVal(v)]); + onInput: async (v) => { + if (onInput) await call(onInput, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -322,7 +339,7 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values. }; } -function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButton, 'id' | 'type'> { +function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButton> { utils.assertObject(def); const text = def.value.get('text'); @@ -338,8 +355,8 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, return { text: text?.value, - onClick: () => { - if (onClick) call(onClick, []); + onClick: async () => { + if (onClick) await call(onClick, []); }, primary: primary?.value, rounded: rounded?.value, @@ -347,7 +364,7 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, }; } -function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButtons, 'id' | 'type'> { +function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButtons> { utils.assertObject(def); const buttons = def.value.get('buttons'); @@ -369,8 +386,8 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, return { text: text.value, - onClick: () => { - call(onClick, []); + onClick: async () => { + await call(onClick, []); }, primary: primary?.value, rounded: rounded?.value, @@ -380,7 +397,7 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, }; } -function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSwitch, 'id' | 'type'> { +function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSwitch> { utils.assertObject(def); const onChange = def.value.get('onChange'); @@ -393,8 +410,8 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, if (caption) utils.assertString(caption); return { - onChange: (v) => { - if (onChange) call(onChange, [utils.jsToVal(v)]); + onChange: async (v) => { + if (onChange) await call(onChange, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -402,7 +419,7 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, }; } -function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSelect, 'id' | 'type'> { +function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSelect> { utils.assertObject(def); const items = def.value.get('items'); @@ -428,8 +445,8 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, value: value ? value.value : text.value, }; }) : [], - onChange: (v) => { - if (onChange) call(onChange, [utils.jsToVal(v)]); + onChange: async (v) => { + if (onChange) await call(onChange, [utils.jsToVal(v)]); }, default: defaultValue?.value, label: label?.value, @@ -437,7 +454,7 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, }; } -function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id' | 'type'> { +function getFolderOptions(def: values.Value | undefined): Options<AsUiFolder> { utils.assertObject(def); const children = def.value.get('children'); @@ -450,7 +467,9 @@ function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id' return { children: children ? children.value.map(v => { utils.assertObject(v); - return v.value.get('id').value; + const id = v.value.get('id'); + utils.assertString(id); + return id.value; }) : [], title: title?.value ?? '', opened: opened?.value ?? true, @@ -475,7 +494,7 @@ function getPostFormProps(form: values.VObj): PostFormPropsForAsUi { }; } -function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostFormButton, 'id' | 'type'> { +function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostFormButton> { utils.assertObject(def); const text = def.value.get('text'); @@ -497,7 +516,7 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu }; } -function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostForm, 'id' | 'type'> { +function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostForm> { utils.assertObject(def); const form = def.value.get('form'); @@ -511,18 +530,26 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn } export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) { + type OptionsConverter<T extends AsUiComponent, C> = (def: values.Value | undefined, call: C) => Options<T>; + const instances = {}; - function createComponentInstance(type: AsUiComponent['type'], def: values.Value | undefined, id: values.Value | undefined, getOptions: (def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) => any, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) { + function createComponentInstance<T extends AsUiComponent, C>( + type: T['type'], + def: values.Value | undefined, + id: values.Value | undefined, + getOptions: OptionsConverter<T, C>, + call: C, + ) { if (id) utils.assertString(id); const _id = id?.value ?? uuid(); const component = ref({ ...getOptions(def, call), type, id: _id, - }); + } as T); components.push(component); - const instance = values.OBJ(new Map([ + const instance = values.OBJ(new Map<string, values.Value>([ ['id', values.STR(_id)], ['update', values.FN_NATIVE(([def], opts) => { utils.assertObject(def); @@ -547,7 +574,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R 'Ui:patch': values.FN_NATIVE(([id, val], opts) => { utils.assertString(id); utils.assertArray(val); - patch(id.value, val.value, opts.call); + // patch(id.value, val.value, opts.call); // TODO }), 'Ui:get': values.FN_NATIVE(([id], opts) => { @@ -566,7 +593,9 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R rootComponent.value.children = children.value.map(v => { utils.assertObject(v); - return v.value.get('id').value; + const id = v.value.get('id'); + utils.assertString(id); + return id.value; }); }), diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts index d942402ffc..8a3a6bf6db 100644 --- a/packages/frontend/src/scripts/autocomplete.ts +++ b/packages/frontend/src/scripts/autocomplete.ts @@ -5,7 +5,7 @@ import { nextTick, Ref, ref, defineAsyncComponent } from 'vue'; import getCaretCoordinates from 'textarea-caret'; -import { toASCII } from 'punycode/'; +import { toASCII } from 'punycode.js'; import { popup } from '@/os.js'; export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam'; diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts index 6525c207f7..194ef0f420 100644 --- a/packages/frontend/src/scripts/check-word-mute.ts +++ b/packages/frontend/src/scripts/check-word-mute.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean { +export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false { // 自分自身 if (me && (note.userId === me.id)) return false; @@ -13,7 +13,7 @@ export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities. if (text === '') return false; - const matched = mutedWords.some(filter => { + const matched = mutedWords.filter(filter => { if (Array.isArray(filter)) { // Clean up const filteredFilter = filter.filter(keyword => keyword !== ''); @@ -36,7 +36,7 @@ export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities. } }); - if (matched) return true; + if (matched.length > 0) return matched; } return false; diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index 6710d9826e..4d57dcd944 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { createHighlighterCore, loadWasm } from 'shiki/core'; +import { createHighlighterCore } from 'shiki/core'; +import { createOnigurumaEngine } from 'shiki/engine/oniguruma'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; @@ -60,8 +61,6 @@ export async function getHighlighter(): Promise<HighlighterCore> { } async function initHighlighter() { - await loadWasm(import('shiki/onig.wasm?init')); - // テーマã®é‡è¤‡ã‚’消㙠const themes = unique([ darkPlus, @@ -70,6 +69,7 @@ async function initHighlighter() { const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); const highlighter = await createHighlighterCore({ + engine: createOnigurumaEngine(() => import('shiki/onig.wasm?init')), themes, langs: [ ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), diff --git a/packages/frontend/src/scripts/file-drop.ts b/packages/frontend/src/scripts/file-drop.ts new file mode 100644 index 0000000000..c2e863c0dc --- /dev/null +++ b/packages/frontend/src/scripts/file-drop.ts @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type DroppedItem = DroppedFile | DroppedDirectory; + +export type DroppedFile = { + isFile: true; + path: string; + file: File; +}; + +export type DroppedDirectory = { + isFile: false; + path: string; + children: DroppedItem[]; +} + +export async function extractDroppedItems(ev: DragEvent): Promise<DroppedItem[]> { + const dropItems = ev.dataTransfer?.items; + if (!dropItems || dropItems.length === 0) { + return []; + } + + const apiTestItem = dropItems[0]; + if ('webkitGetAsEntry' in apiTestItem) { + return readDataTransferItems(dropItems); + } else { + // webkitGetAsEntryã«å¯¾å¿œã—ã¦ã„ãªã„å ´åˆã¯filesã‹ã‚‰å–å¾—ã™ã‚‹ï¼ˆãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã®ã‚µãƒãƒ¼ãƒˆã¯å‡ºæ¥ãªã„) + const dropFiles = ev.dataTransfer.files; + if (dropFiles.length === 0) { + return []; + } + + const droppedFiles = Array.of<DroppedFile>(); + for (let i = 0; i < dropFiles.length; i++) { + const file = dropFiles.item(i); + if (file) { + droppedFiles.push({ + isFile: true, + path: file.name, + file, + }); + } + } + + return droppedFiles; + } +} + +/** + * ドラッグ&ドãƒãƒƒãƒ—ã•れãŸãƒ•ァイルã®ãƒªã‚¹ãƒˆã‹ã‚‰ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªæ§‹é€ ã¨ãƒ•ァイルã¸ã®å‚照({@link File})をå–å¾—ã™ã‚‹ã€‚ + */ +export async function readDataTransferItems(itemList: DataTransferItemList): Promise<DroppedItem[]> { + async function readEntry(entry: FileSystemEntry): Promise<DroppedItem> { + if (entry.isFile) { + return { + isFile: true, + path: entry.fullPath, + file: await readFile(entry as FileSystemFileEntry), + }; + } else { + return { + isFile: false, + path: entry.fullPath, + children: await readDirectory(entry as FileSystemDirectoryEntry), + }; + } + } + + function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise<File> { + return new Promise((resolve, reject) => { + fileSystemFileEntry.file(resolve, reject); + }); + } + + function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> { + return new Promise(async (resolve) => { + const allEntries = Array.of<FileSystemEntry>(); + const reader = fileSystemDirectoryEntry.createReader(); + while (true) { + const entries = await new Promise<FileSystemEntry[]>((res, rej) => reader.readEntries(res, rej)); + if (entries.length === 0) { + break; + } + allEntries.push(...entries); + } + + resolve(await Promise.all(allEntries.map(readEntry))); + }); + } + + // 扱ã„ã«ãã„ã®ã§é…列ã«å¤‰æ› + const items = Array.of<DataTransferItem>(); + for (let i = 0; i < itemList.length; i++) { + items.push(itemList[i]); + } + + return Promise.all( + items + .map(it => it.webkitGetAsEntry()) + .filter(it => it) + .map(it => readEntry(it!)), + ); +} + +/** + * {@link DroppedItem}ã®ãƒªã‚¹ãƒˆã‹ã‚‰ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’å†å¸°çš„ã«æ¤œç´¢ã—ã€ãƒ•ァイルã®ãƒªã‚¹ãƒˆã‚’å–å¾—ã™ã‚‹ã€‚ + */ +export function flattenDroppedFiles(items: DroppedItem[]): DroppedFile[] { + const result = Array.of<DroppedFile>(); + for (const item of items) { + if (item.isFile) { + result.push(item); + } else { + result.push(...flattenDroppedFiles(item.children)); + } + } + return result; +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index c56fd185b6..9112daf49f 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -5,19 +5,19 @@ import { defineAsyncComponent, Ref, ShallowRef } from 'vue'; import * as Misskey from 'misskey-js'; +import { url } from '@@/js/config.js'; import { claimAchievement } from './achievements.js'; +import type { MenuItem } from '@/types/menu.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@@/js/config.js'; import { defaultStore, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache, favoritedChannelsCache } from '@/cache.js'; -import type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; @@ -205,7 +205,7 @@ export function getNoteMenu(props: { noteId: appearNote.id, }); - if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) { + if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) { claimAchievement('noteDeletedWithin1min'); } }); @@ -224,7 +224,7 @@ export function getNoteMenu(props: { os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel }); - if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) { + if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) { claimAchievement('noteDeletedWithin1min'); } }); @@ -259,11 +259,6 @@ export function getNoteMenu(props: { os.success(); } - function copyLink(): void { - copyToClipboard(`${url}/notes/${appearNote.id}`); - os.success(); - } - function togglePin(pin: boolean): void { os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { noteId: appearNote.id, @@ -347,6 +342,13 @@ export function getNoteMenu(props: { getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') ); menuItems.push({ + icon: 'ti ti-link', + text: i18n.ts.copyRemoteLink, + action: () => { + copyToClipboard(appearNote.url ?? appearNote.uri); + os.success(); + }, + }, { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { @@ -508,6 +510,13 @@ export function getNoteMenu(props: { getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') ); menuItems.push({ + icon: 'ti ti-link', + text: i18n.ts.copyRemoteLink, + action: () => { + copyToClipboard(appearNote.url ?? appearNote.uri); + os.success(); + }, + }, { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 090cffe203..2fbdaf5d3c 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { toUnicode } from 'punycode'; +import { toUnicode } from 'punycode.js'; import { defineAsyncComponent, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/scripts/key-event.ts b/packages/frontend/src/scripts/key-event.ts new file mode 100644 index 0000000000..a72776d48c --- /dev/null +++ b/packages/frontend/src/scripts/key-event.ts @@ -0,0 +1,153 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * {@link KeyboardEvent.code} ã®å€¤ã‚’è¡¨ã™æ–‡å—列。ä¸è¶³åˆ†ã¯é©å®œè¿½åŠ ã™ã‚‹ + * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values + */ +export type KeyCode = + | 'Backspace' + | 'Tab' + | 'Enter' + | 'Shift' + | 'Control' + | 'Alt' + | 'Pause' + | 'CapsLock' + | 'Escape' + | 'Space' + | 'PageUp' + | 'PageDown' + | 'End' + | 'Home' + | 'ArrowLeft' + | 'ArrowUp' + | 'ArrowRight' + | 'ArrowDown' + | 'Insert' + | 'Delete' + | 'Digit0' + | 'Digit1' + | 'Digit2' + | 'Digit3' + | 'Digit4' + | 'Digit5' + | 'Digit6' + | 'Digit7' + | 'Digit8' + | 'Digit9' + | 'KeyA' + | 'KeyB' + | 'KeyC' + | 'KeyD' + | 'KeyE' + | 'KeyF' + | 'KeyG' + | 'KeyH' + | 'KeyI' + | 'KeyJ' + | 'KeyK' + | 'KeyL' + | 'KeyM' + | 'KeyN' + | 'KeyO' + | 'KeyP' + | 'KeyQ' + | 'KeyR' + | 'KeyS' + | 'KeyT' + | 'KeyU' + | 'KeyV' + | 'KeyW' + | 'KeyX' + | 'KeyY' + | 'KeyZ' + | 'MetaLeft' + | 'MetaRight' + | 'ContextMenu' + | 'F1' + | 'F2' + | 'F3' + | 'F4' + | 'F5' + | 'F6' + | 'F7' + | 'F8' + | 'F9' + | 'F10' + | 'F11' + | 'F12' + | 'NumLock' + | 'ScrollLock' + | 'Semicolon' + | 'Equal' + | 'Comma' + | 'Minus' + | 'Period' + | 'Slash' + | 'Backquote' + | 'BracketLeft' + | 'Backslash' + | 'BracketRight' + | 'Quote' + | 'Meta' + | 'AltGraph' + ; + +/** + * 修飾ã‚ãƒ¼ã‚’è¡¨ã™æ–‡å—列。ä¸è¶³åˆ†ã¯é©å®œè¿½åŠ ã™ã‚‹ã€‚ + */ +export type KeyModifier = + | 'Shift' + | 'Control' + | 'Alt' + | 'Meta' + ; + +/** + * 押下ã•れãŸã‚ー以外ã®çŠ¶æ…‹ã‚’è¡¨ã™æ–‡å—列。ä¸è¶³åˆ†ã¯é©å®œè¿½åŠ ã™ã‚‹ã€‚ + */ +export type KeyState = + | 'composing' + | 'repeat' + ; + +export type KeyEventHandler = { + modifiers?: KeyModifier[]; + states?: KeyState[]; + code: KeyCode | 'any'; + handler: (event: KeyboardEvent) => void; +} + +export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) { + function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) { + if (modifiers) { + return modifiers.every(modifier => ev.getModifierState(modifier)); + } + return true; + } + + function checkState(ev: KeyboardEvent, states?: KeyState[]) { + if (states) { + return states.every(state => ev.getModifierState(state)); + } + return true; + } + + let hit = false; + for (const handler of handlers.filter(it => it.code === event.code)) { + if (checkModifier(event, handler.modifiers) && checkState(event, handler.states)) { + handler.handler(event); + hit = true; + break; + } + } + + if (!hit) { + for (const handler of handlers.filter(it => it.code === 'any')) { + handler.handler(event); + } + } +} diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts index e20b23f166..54ec2ce39b 100644 --- a/packages/frontend/src/scripts/lookup.ts +++ b/packages/frontend/src/scripts/lookup.ts @@ -33,7 +33,43 @@ export async function lookup(router?: Router) { uri: query, }); - os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); + os.promiseDialog(promise, null, (err) => { + let title = i18n.ts.somethingHappened; + let text = err.message + '\n' + err.id; + + switch (err.id) { + case '974b799e-1a29-4889-b706-18d4dd93e266': + title = i18n.ts._remoteLookupErrors._federationNotAllowed.title; + text = i18n.ts._remoteLookupErrors._federationNotAllowed.description; + break; + case '1a5eab56-e47b-48c2-8d5e-217b897d70db': + title = i18n.ts._remoteLookupErrors._uriInvalid.title; + text = i18n.ts._remoteLookupErrors._uriInvalid.description; + break; + case '81b539cf-4f57-4b29-bc98-032c33c0792e': + title = i18n.ts._remoteLookupErrors._requestFailed.title; + text = i18n.ts._remoteLookupErrors._requestFailed.description; + break; + case '70193c39-54f3-4813-82f0-70a680f7495b': + title = i18n.ts._remoteLookupErrors._responseInvalid.title; + text = i18n.ts._remoteLookupErrors._responseInvalid.description; + break; + case 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a': + title = i18n.ts._remoteLookupErrors._responseInvalid.title; + text = i18n.ts._remoteLookupErrors._responseInvalidIdHostNotMatch.description; + break; + case 'dc94d745-1262-4e63-a17d-fecaa57efc82': + title = i18n.ts._remoteLookupErrors._noSuchObject.title; + text = i18n.ts._remoteLookupErrors._noSuchObject.description; + break; + } + + os.alert({ + type: 'error', + title, + text, + }); + }, i18n.ts.fetchingAsApObject); const res = await promise; diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 89fdda0cbb..004b6d42a4 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -7,10 +7,10 @@ import { deepClone } from './clone.js'; import type { Cloneable } from './clone.js'; export type DeepPartial<T> = { - [P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P]; + [P in keyof T]?: T[P] extends Record<PropertyKey, unknown> ? DeepPartial<T[P]> : T[P]; }; -function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> { +function isPureObject(value: unknown): value is Record<PropertyKey, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -18,14 +18,14 @@ function isPureObject(value: unknown): value is Record<string | number | symbol, * valueã«ãªã„ã‚ーをdefã‹ã‚‰ã‚‚らã†ï¼ˆå†å¸°çš„)\ * nullã¯ãã®ã¾ã¾ã€undefinedã¯defã®å€¤ **/ -export function deepMerge<X extends object>(value: DeepPartial<X>, def: X): X { +export function deepMerge<X extends Record<PropertyKey, unknown>>(value: DeepPartial<X>, def: X): X { if (isPureObject(value) && isPureObject(def)) { const result = deepClone(value as Cloneable) as X; for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) { result[k] = v; } else if (isPureObject(v) && isPureObject(result[k])) { - const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<string | number | symbol, unknown>>; + const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<PropertyKey, unknown>>; result[k] = deepMerge<typeof v>(child, v); } } diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts index e7a92e2d5c..dc07ad477b 100644 --- a/packages/frontend/src/scripts/misskey-api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -9,12 +9,24 @@ import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); +export type Endpoint = keyof Misskey.Endpoints; + +export type Request<E extends Endpoint> = Misskey.Endpoints[E]['req']; + +export type AnyRequest<E extends Endpoint | (string & unknown)> = + (E extends Endpoint ? Request<E> : never) | object; + +export type Response<E extends Endpoint | (string & unknown), P extends AnyRequest<E>> = + E extends Endpoint + ? P extends Request<E> ? Misskey.api.SwitchCaseResponseType<E, P> : never + : object; + // Implements Misskey.api.ApiClient.request export function misskeyApi< ResT = void, - E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, - P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], - _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, + E extends Endpoint | NonNullable<string> = Endpoint, + P extends AnyRequest<E> = E extends Endpoint ? Request<E> : never, + _ResT = ResT extends void ? Response<E, P> : ResT, >( endpoint: E, data: P & { i?: string | null; } = {} as any, diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts index 43dcf11936..a8a330eb6d 100644 --- a/packages/frontend/src/scripts/please-login.ts +++ b/packages/frontend/src/scripts/please-login.ts @@ -5,6 +5,7 @@ import { defineAsyncComponent } from 'vue'; import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { popup } from '@/os.js'; @@ -51,10 +52,17 @@ export function pleaseLogin(opts: { } = {}) { if ($i) return; + let _openOnRemote: OpenOnRemoteOptions | undefined = undefined; + + // 連åˆã§ãã‚‹å ´åˆã¨ã€ï¼ˆé€£åˆãŒã§ããªãã¦ã‚‚)共有ã™ã‚‹å ´åˆã¯å¤–部連æºã‚ªãƒ—ションをè¨å®š + if (opts.openOnRemote != null && (instance.federation !== 'none' || opts.openOnRemote.type === 'share')) { + _openOnRemote = opts.openOnRemote; + } + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { autoSet: true, - message: opts.message ?? (opts.openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired), - openOnRemote: opts.openOnRemote, + message: opts.message ?? (_openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired), + openOnRemote: _openOnRemote, }, { cancelled: () => { if (opts.path) { diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index b037aa8acc..c25b4d73bd 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -12,14 +12,28 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { uploadFile } from '@/scripts/upload.js'; -export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise<Misskey.entities.DriveFile[]> { +export function chooseFileFromPc( + multiple: boolean, + options?: { + uploadFolder?: string | null; + keepOriginal?: boolean; + nameConverter?: (file: File) => string | undefined; + }, +): Promise<Misskey.entities.DriveFile[]> { + const uploadFolder = options?.uploadFolder ?? defaultStore.state.uploadFolder; + const keepOriginal = options?.keepOriginal ?? defaultStore.state.keepOriginalUploading; + const nameConverter = options?.nameConverter ?? (() => undefined); + return new Promise((res, rej) => { const input = document.createElement('input'); input.type = 'file'; input.multiple = multiple; input.onchange = () => { if (!input.files) return res([]); - const promises = Array.from(input.files, file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal)); + const promises = Array.from( + input.files, + file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal), + ); Promise.all(promises).then(driveFiles => { res(driveFiles); @@ -94,7 +108,7 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul }, { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)), + action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)), }, { text: i18n.ts.fromDrive, icon: 'ti ti-cloud', diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 05f82fce7d..2008afe045 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -93,6 +93,10 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ctx == null) { ctx = new AudioContext(); + + window.addEventListener('beforeunload', () => { + ctx.close(); + }); } if (options?.useCache ?? true) { if (cache.has(url)) { diff --git a/packages/frontend/src/server-context.ts b/packages/frontend/src/server-context.ts index aa44a10290..e79d3fa314 100644 --- a/packages/frontend/src/server-context.ts +++ b/packages/frontend/src/server-context.ts @@ -2,22 +2,20 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ + import * as Misskey from 'misskey-js'; -import { $i } from '@/account.js'; const providedContextEl = document.getElementById('misskey_clientCtx'); export type ServerContext = { clip?: Misskey.entities.Clip; note?: Misskey.entities.Note; - user?: Misskey.entities.UserLite; + user?: Misskey.entities.UserDetailed; } | null; export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null; -export function getServerContext<K extends keyof NonNullable<ServerContext>>(entity: K): Required<Pick<NonNullable<ServerContext>, K>> | null { - // contextã¯éžãƒã‚°ã‚¤ãƒ³çŠ¶æ…‹ã®æƒ…å ±ã—ã‹ãªã„ãŸã‚ãƒã‚°ã‚¤ãƒ³æ™‚ã¯åˆ©ç”¨ã§ããªã„ - if ($i) return null; - - return serverContext ? (serverContext[entity] ?? null) : null; +export function assertServerContext<K extends keyof NonNullable<ServerContext>>(ctx: ServerContext, entity: K): ctx is Required<Pick<NonNullable<ServerContext>, K>> { + if (ctx == null) return false; + return entity in ctx && ctx[entity] != null; } diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index c34e0bbf48..69fcef32c2 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -10,11 +10,11 @@ import lightTheme from '@@/themes/l-cherry.json5'; import darkTheme from '@@/themes/d-ice.json5'; import { searchEngineMap } from './scripts/search-engine-map.js'; import type { SoundType } from '@/scripts/sound.js'; +import type { Ast } from '@syuilo/aiscript'; import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; import { defaultFollowingFeedState } from '@/scripts/following-feed-utils.js'; import { Storage } from '@/pizzax.js'; -import type { Ast } from '@syuilo/aiscript'; interface PostFormAction { title: string, @@ -561,6 +561,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, + showSoftWordMutedWord: { + where: 'device', + default: false, + }, sound_masterVolume: { where: 'device', diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 8355ae3061..6ec85686db 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -56,12 +56,18 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.customEmojis, icon: 'ph-smiley ph-bold ph-lg', to: '/about#emojis', - }, { - type: 'link', - text: i18n.ts.federation, - icon: 'ti ti-whirl', - to: '/about#federation', - }, { + }); + + if (instance.federation !== 'none') { + menuItems.push({ + type: 'link', + text: i18n.ts.federation, + icon: 'ti ti-whirl', + to: '/about#federation', + }); + } + + menuItems.push({ type: 'link', text: i18n.ts.charts, icon: 'ti ti-chart-line', @@ -134,7 +140,7 @@ export function openInstanceMenu(ev: MouseEvent) { }); } - if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) { + if (instance.impressumUrl != null || instance.tosUrl != null || instance.privacyPolicyUrl != null || nstance.donationUrl != null) { menuItems.push({ type: 'divider' }); } diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 5cc0e52f77..062a8faf3f 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </component> </template> <div :class="$style.divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin"> + <MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin"> <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> </MkA> <button class="_button" :class="$style.item" @click="more"> @@ -48,10 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> </div> <div :class="$style.bottom"> - <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post"> + <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="() => { os.post(); }"> <i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span> </button> - <button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu"> + <button v-if="$i != null" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu"> <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/> </button> </div> @@ -83,8 +83,12 @@ import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; +import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; -const iconOnly = ref(false); +const forceIconOnly = ref(window.innerWidth <= 1279); +const iconOnly = computed(() => { + return forceIconOnly.value || (defaultStore.reactiveState.menuDisplay.value === 'sideIcon'); +}); const menu = computed(() => defaultStore.state.menu); const otherMenuItemIndicated = computed(() => { @@ -95,14 +99,10 @@ const otherMenuItemIndicated = computed(() => { return false; }); -const forceIconOnly = window.innerWidth <= 1279; - function calcViewState() { - iconOnly.value = forceIconOnly || (defaultStore.state.menuDisplay === 'sideIcon'); + forceIconOnly.value = window.innerWidth <= 1279; } -calcViewState(); - window.addEventListener('resize', calcViewState); watch(defaultStore.reactiveState.menuDisplay, () => { @@ -120,8 +120,10 @@ function openAccountMenu(ev: MouseEvent) { } function more(ev: MouseEvent) { + const target = getHTMLElementOrNull(ev.currentTarget ?? ev.target); + if (!target) return; const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { - src: ev.currentTarget ?? ev.target, + src: target, }, { closed: () => dispose(), }); diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index 5f9a938017..ed881bef22 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <span :class="$style.name">{{ x.name }}</span> <XRss v-if="x.type === 'rss'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url" :shuffle="x.props.shuffle"/> - <XFederation v-else-if="x.type === 'federation'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/> + <XFederation v-else-if="x.type === 'federation' && instance.federation !== 'none'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/> <XUserList v-else-if="x.type === 'userList'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :userListId="x.props.userListId"/> </div> </div> @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent } from 'vue'; +import { instance } from '@/instance.js'; import { defaultStore } from '@/store.js'; const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue')); const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue')); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index c552b65318..e8c71f61cf 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XWidgets/> </div> - <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> + <button v-if="!isDesktop && !pageMetadata?.needWideArea && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> <div v-if="isMobile" ref="navFooter" :class="$style.nav"> <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts index 29e4558f1e..37742f8c2e 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -37,6 +37,12 @@ export default function(app: App) { app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue'))); } +// 連åˆé–¢é€£ã®ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆï¼ˆé€£åˆç„¡åŠ¹æ™‚ã«éš ã™ï¼‰ +export const federationWidgets = [ + 'federation', + 'instanceCloud', +]; + export const widgets = [ 'profile', 'instanceInfo', @@ -52,8 +58,6 @@ export const widgets = [ 'photos', 'digitalClock', 'unixClock', - 'federation', - 'instanceCloud', 'postForm', 'slideshow', 'serverMetric', @@ -67,4 +71,6 @@ export const widgets = [ 'clicker', 'search', 'birthdayFollowings', + + ...federationWidgets, ]; diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts new file mode 100644 index 0000000000..2a15a74249 --- /dev/null +++ b/packages/frontend/test/aiscript/api.test.ts @@ -0,0 +1,401 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { miLocalStorage } from '@/local-storage.js'; +import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { errors, Interpreter, Parser, values } from '@syuilo/aiscript'; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, + vi +} from 'vitest'; + +async function exe(script: string): Promise<values.Value[]> { + const outputs: values.Value[] = []; + const interpreter = new Interpreter( + createAiScriptEnv({ storageKey: 'widget' }), + { + in: aiScriptReadline, + out: (value) => { + outputs.push(value); + } + } + ); + const ast = Parser.parse(script); + await interpreter.exec(ast); + return outputs; +} + +let $iMock = vi.hoisted<Partial<typeof import('@/account.js').$i> | null >( + () => null +); + +vi.mock('@/account.js', () => { + return { + get $i() { + return $iMock; + }, + }; +}); + +const osMock = vi.hoisted(() => { + return { + inputText: vi.fn(), + alert: vi.fn(), + confirm: vi.fn(), + }; +}); + +vi.mock('@/os.js', () => { + return osMock; +}); + +const misskeyApiMock = vi.hoisted(() => vi.fn()); + +vi.mock('@/scripts/misskey-api.js', () => { + return { misskeyApi: misskeyApiMock }; +}); + +describe('AiScript common API', () => { + afterAll(() => { + vi.unstubAllGlobals(); + }); + + describe('readline', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test.sequential('ok', async () => { + osMock.inputText.mockImplementationOnce(async ({ title }) => { + expect(title).toBe('question'); + return { + canceled: false, + result: 'Hello', + }; + }); + const [res] = await exe(` + <: readline('question') + `); + expect(res).toStrictEqual(values.STR('Hello')); + expect(osMock.inputText).toHaveBeenCalledOnce(); + }); + + test.sequential('cancelled', async () => { + osMock.inputText.mockImplementationOnce(async ({ title }) => { + expect(title).toBe('question'); + return { + canceled: true, + result: undefined, + }; + }); + const [res] = await exe(` + <: readline('question') + `); + expect(res).toStrictEqual(values.STR('')); + expect(osMock.inputText).toHaveBeenCalledOnce(); + }); + }); + + describe('user constants', () => { + describe.sequential('logged in', () => { + beforeAll(() => { + $iMock = { + id: 'xxxxxxxx', + name: 'è—', + username: 'ai', + }; + }); + + test.concurrent('USER_ID', async () => { + const [res] = await exe(` + <: USER_ID + `); + expect(res).toStrictEqual(values.STR('xxxxxxxx')); + }); + + test.concurrent('USER_NAME', async () => { + const [res] = await exe(` + <: USER_NAME + `); + expect(res).toStrictEqual(values.STR('è—')); + }); + + test.concurrent('USER_USERNAME', async () => { + const [res] = await exe(` + <: USER_USERNAME + `); + expect(res).toStrictEqual(values.STR('ai')); + }); + }); + + describe.sequential('not logged in', () => { + beforeAll(() => { + $iMock = null; + }); + + test.concurrent('USER_ID', async () => { + const [res] = await exe(` + <: USER_ID + `); + expect(res).toStrictEqual(values.NULL); + }); + + test.concurrent('USER_NAME', async () => { + const [res] = await exe(` + <: USER_NAME + `); + expect(res).toStrictEqual(values.NULL); + }); + + test.concurrent('USER_USERNAME', async () => { + const [res] = await exe(` + <: USER_USERNAME + `); + expect(res).toStrictEqual(values.NULL); + }); + }); + }); + + describe('dialog', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test.sequential('ok', async () => { + osMock.alert.mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('success'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + }); + const [res] = await exe(` + <: Mk:dialog('Hello', 'world', 'success') + `); + expect(res).toStrictEqual(values.NULL); + expect(osMock.alert).toHaveBeenCalledOnce(); + }); + + test.sequential('omit type', async () => { + osMock.alert.mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('info'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + }); + const [res] = await exe(` + <: Mk:dialog('Hello', 'world') + `); + expect(res).toStrictEqual(values.NULL); + expect(osMock.alert).toHaveBeenCalledOnce(); + }); + + test.sequential('invalid type', async () => { + await expect(() => exe(` + <: Mk:dialog('Hello', 'world', 'invalid') + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + expect(osMock.alert).not.toHaveBeenCalled(); + }); + }); + + describe('confirm', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test.sequential('ok', async () => { + osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('success'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + return { canceled: false }; + }); + const [res] = await exe(` + <: Mk:confirm('Hello', 'world', 'success') + `); + expect(res).toStrictEqual(values.TRUE); + expect(osMock.confirm).toHaveBeenCalledOnce(); + }); + + test.sequential('omit type', async () => { + osMock.confirm + .mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('question'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + return { canceled: false }; + }); + const [res] = await exe(` + <: Mk:confirm('Hello', 'world') + `); + expect(res).toStrictEqual(values.TRUE); + expect(osMock.confirm).toHaveBeenCalledOnce(); + }); + + test.sequential('canceled', async () => { + osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => { + expect(type).toBe('question'); + expect(title).toBe('Hello'); + expect(text).toBe('world'); + return { canceled: true }; + }); + const [res] = await exe(` + <: Mk:confirm('Hello', 'world') + `); + expect(res).toStrictEqual(values.FALSE); + expect(osMock.confirm).toHaveBeenCalledOnce(); + }); + + test.sequential('invalid type', async () => { + const confirm = osMock.confirm; + await expect(() => exe(` + <: Mk:confirm('Hello', 'world', 'invalid') + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + expect(confirm).not.toHaveBeenCalled(); + }); + }); + + describe('api', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test.sequential('successful', async () => { + misskeyApiMock.mockImplementationOnce( + async (endpoint, data, token) => { + expect(endpoint).toBe('ping'); + expect(data).toStrictEqual({}); + expect(token).toBeNull(); + return { pong: 1735657200000 }; + } + ); + const [res] = await exe(` + <: Mk:api('ping', {}) + `); + expect(res).toStrictEqual(values.OBJ(new Map([ + ['pong', values.NUM(1735657200000)], + ]))); + expect(misskeyApiMock).toHaveBeenCalledOnce(); + }); + + test.sequential('with token', async () => { + misskeyApiMock.mockImplementationOnce( + async (endpoint, data, token) => { + expect(endpoint).toBe('ping'); + expect(data).toStrictEqual({}); + expect(token).toStrictEqual('xxxxxxxx'); + return { pong: 1735657200000 }; + } + ); + const [res] = await exe(` + <: Mk:api('ping', {}, 'xxxxxxxx') + `); + expect(res).toStrictEqual(values.OBJ(new Map([ + ['pong', values.NUM(1735657200000 )], + ]))); + expect(misskeyApiMock).toHaveBeenCalledOnce(); + }); + + test.sequential('request failed', async () => { + misskeyApiMock.mockRejectedValueOnce('Not Found'); + const [res] = await exe(` + <: Mk:api('this/endpoint/should/not/be/found', {}) + `); + expect(res).toStrictEqual( + values.ERROR('request_failed', values.STR('Not Found')) + ); + expect(misskeyApiMock).toHaveBeenCalledOnce(); + }); + + test.sequential('invalid endpoint', async () => { + await expect(() => exe(` + Mk:api('https://example.com/api/ping', {}) + `)).rejects.toStrictEqual( + new errors.AiScriptRuntimeError('invalid endpoint'), + ); + expect(misskeyApiMock).not.toHaveBeenCalled(); + }); + + test.sequential('missing param', async () => { + await expect(() => exe(` + Mk:api('ping') + `)).rejects.toStrictEqual( + new errors.AiScriptRuntimeError('expected param'), + ); + expect(misskeyApiMock).not.toHaveBeenCalled(); + }); + }); + + describe('save and load', () => { + beforeEach(() => { + miLocalStorage.removeItem('aiscript:widget:key'); + }); + + afterEach(() => { + miLocalStorage.removeItem('aiscript:widget:key'); + }); + + test.sequential('successful', async () => { + const [res] = await exe(` + Mk:save('key', 'value') + <: Mk:load('key') + `); + expect(miLocalStorage.getItem('aiscript:widget:key')).toBe('"value"'); + expect(res).toStrictEqual(values.STR('value')); + }); + + test.sequential('missing value to save', async () => { + await expect(() => exe(` + Mk:save('key') + `)).rejects.toStrictEqual( + new errors.AiScriptRuntimeError('Expect anything, but got nothing.'), + ); + }); + + test.sequential('not value found to load', async () => { + const [res] = await exe(` + <: Mk:load('key') + `); + expect(res).toStrictEqual(values.NULL); + }); + + test.sequential('remove existing', async () => { + const res = await exe(` + Mk:save('key', 'value') + <: Mk:load('key') + <: Mk:remove('key') + <: Mk:load('key') + `); + expect(res).toStrictEqual([values.STR('value'), values.NULL, values.NULL]); + }); + + test.sequential('remove nothing', async () => { + const res = await exe(` + <: Mk:load('key') + <: Mk:remove('key') + <: Mk:load('key') + `); + expect(res).toStrictEqual([values.NULL, values.NULL, values.NULL]); + }); + }); + + test.concurrent('url', async () => { + vi.stubGlobal('location', { href: 'https://example.com/' }); + const [res] = await exe(` + <: Mk:url() + `); + expect(res).toStrictEqual(values.STR('https://example.com/')); + }); + + test.concurrent('nyaize', async () => { + const [res] = await exe(` + <: Mk:nyaize('ãª') + `); + expect(res).toStrictEqual(values.STR('ã«ã‚ƒ')); + }); +}); diff --git a/packages/frontend/test/aiscript/common.test.ts b/packages/frontend/test/aiscript/common.test.ts new file mode 100644 index 0000000000..acc48826ea --- /dev/null +++ b/packages/frontend/test/aiscript/common.test.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { assertStringAndIsIn } from "@/scripts/aiscript/common.js"; +import { values } from "@syuilo/aiscript"; +import { describe, expect, test } from "vitest"; + +describe('AiScript common script', () => { + test('assertStringAndIsIn', () => { + expect( + () => assertStringAndIsIn(values.STR('a'), ['a', 'b']) + ).not.toThrow(); + expect( + () => assertStringAndIsIn(values.STR('c'), ['a', 'b']) + ).toThrow('"c" is not in "a", "b"'); + expect(() => assertStringAndIsIn( + values.STR('invalid'), + ['left', 'center', 'right'] + )).toThrow('"invalid" is not in "left", "center", "right"'); + }); +}); diff --git a/packages/frontend/test/aiscript/ui.test.ts b/packages/frontend/test/aiscript/ui.test.ts new file mode 100644 index 0000000000..5f77edbb49 --- /dev/null +++ b/packages/frontend/test/aiscript/ui.test.ts @@ -0,0 +1,825 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { registerAsUiLib } from '@/scripts/aiscript/ui.js'; +import { errors, Interpreter, Parser, values } from '@syuilo/aiscript'; +import { describe, expect, test } from 'vitest'; +import { type Ref, ref } from 'vue'; +import type { + AsUiButton, + AsUiButtons, + AsUiComponent, + AsUiMfm, + AsUiNumberInput, + AsUiRoot, + AsUiSelect, + AsUiSwitch, + AsUiText, + AsUiTextarea, + AsUiTextInput, +} from '@/scripts/aiscript/ui.js'; + +type ExeResult = { + root: AsUiRoot; + get: (id: string) => AsUiComponent; + outputs: values.Value[]; +} +async function exe(script: string): Promise<ExeResult> { + const rootRef = ref<AsUiRoot>(); + const componentRefs = ref<Ref<AsUiComponent>[]>([]); + const outputs: values.Value[] = []; + + const interpreter = new Interpreter( + registerAsUiLib(componentRefs.value, (root) => { + rootRef.value = root.value; + }), + { + out: (value) => { + outputs.push(value); + } + } + ); + const ast = Parser.parse(script); + await interpreter.exec(ast); + + const root = rootRef.value; + if (root === undefined) { + expect.unreachable('root must not be undefined'); + } + const components = componentRefs.value.map( + (componentRef) => componentRef.value, + ); + expect(root).toBe(components[0]); + expect(root.type).toBe('root'); + const get = (id: string) => { + const component = componentRefs.value.find( + (componentRef) => componentRef.value.id === id, + ); + if (component === undefined) { + expect.unreachable(`component "${id}" is not defined`); + } + return component.value; + }; + return { root, get, outputs }; +} + +describe('AiScript UI API', () => { + test.concurrent('root', async () => { + const { root } = await exe(''); + expect(root.children).toStrictEqual([]); + }); + + describe('get', () => { + test.concurrent('some', async () => { + const { outputs } = await exe(` + Ui:C:text({}, 'id') + <: Ui:get('id') + `); + const output = outputs[0] as values.VObj; + expect(output.type).toBe('obj'); + expect(output.value.size).toBe(2); + expect(output.value.get('id')).toStrictEqual(values.STR('id')); + expect(output.value.get('update')!.type).toBe('fn'); + }); + + test.concurrent('none', async () => { + const { outputs } = await exe(` + <: Ui:get('id') + `); + expect(outputs).toStrictEqual([values.NULL]); + }); + }); + + describe('update', () => { + test.concurrent('normal', async () => { + const { get } = await exe(` + let text = Ui:C:text({ text: 'a' }, 'id') + text.update({ text: 'b' }) + `); + const text = get('id') as AsUiText; + expect(text.text).toBe('b'); + }); + + test.concurrent('skip unknown key', async () => { + const { get } = await exe(` + let text = Ui:C:text({ text: 'a' }, 'id') + text.update({ + text: 'b' + unknown: null + }) + `); + const text = get('id') as AsUiText; + expect(text.text).toBe('b'); + expect('unknown' in text).toBeFalsy(); + }); + }); + + describe('container', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let text = Ui:C:text({ + text: 'text' + }, 'id1') + let container = Ui:C:container({ + children: [text] + align: 'left' + bgColor: '#fff' + fgColor: '#000' + font: 'sans-serif' + borderWidth: 1 + borderColor: '#f00' + borderStyle: 'hidden' + borderRadius: 2 + padding: 3 + rounded: true + hidden: false + }, 'id2') + Ui:render([container]) + `); + expect(root.children).toStrictEqual(['id2']); + expect(get('id2')).toStrictEqual({ + type: 'container', + id: 'id2', + children: ['id1'], + align: 'left', + bgColor: '#fff', + fgColor: '#000', + font: 'sans-serif', + borderColor: '#f00', + borderWidth: 1, + borderStyle: 'hidden', + borderRadius: 2, + padding: 3, + rounded: true, + hidden: false, + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:container({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'container', + id: 'id', + children: [], + align: undefined, + fgColor: undefined, + bgColor: undefined, + font: undefined, + borderWidth: undefined, + borderColor: undefined, + borderStyle: undefined, + borderRadius: undefined, + padding: undefined, + rounded: undefined, + hidden: undefined, + }); + }); + + test.concurrent('invalid children', async () => { + await expect(() => exe(` + Ui:C:container({ + children: 0 + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + + test.concurrent('invalid align', async () => { + await expect(() => exe(` + Ui:C:container({ + align: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + + test.concurrent('invalid font', async () => { + await expect(() => exe(` + Ui:C:container({ + font: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + + test.concurrent('invalid borderStyle', async () => { + await expect(() => exe(` + Ui:C:container({ + borderStyle: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + }); + + describe('text', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let text = Ui:C:text({ + text: 'a' + size: 1 + bold: true + color: '#000' + font: 'sans-serif' + }, 'id') + Ui:render([text]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'text', + id: 'id', + text: 'a', + size: 1, + bold: true, + color: '#000', + font: 'sans-serif', + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:text({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'text', + id: 'id', + text: undefined, + size: undefined, + bold: undefined, + color: undefined, + font: undefined, + }); + }); + + test.concurrent('invalid font', async () => { + await expect(() => exe(` + Ui:C:text({ + font: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + }); + + describe('mfm', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let mfm = Ui:C:mfm({ + text: 'text' + size: 1 + bold: true + color: '#000' + font: 'sans-serif' + onClickEv: print + }, 'id') + Ui:render([mfm]) + `); + expect(root.children).toStrictEqual(['id']); + const { onClickEv, ...mfm } = get('id') as AsUiMfm; + expect(mfm).toStrictEqual({ + type: 'mfm', + id: 'id', + text: 'text', + size: 1, + bold: true, + color: '#000', + font: 'sans-serif', + }); + await onClickEv!('a'); + expect(outputs).toStrictEqual([values.STR('a')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:mfm({}, 'id') + `); + const { onClickEv, ...mfm } = get('id') as AsUiMfm; + expect(onClickEv).toBeTypeOf('function'); + expect(mfm).toStrictEqual({ + type: 'mfm', + id: 'id', + text: undefined, + size: undefined, + bold: undefined, + color: undefined, + font: undefined, + }); + }); + + test.concurrent('invalid font', async () => { + await expect(() => exe(` + Ui:C:mfm({ + font: 'invalid' + }) + `)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError); + }); + }); + + describe('textInput', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let text_input = Ui:C:textInput({ + onInput: print + default: 'a' + label: 'b' + caption: 'c' + }, 'id') + Ui:render([text_input]) + `); + expect(root.children).toStrictEqual(['id']); + const { onInput, ...textInput } = get('id') as AsUiTextInput; + expect(textInput).toStrictEqual({ + type: 'textInput', + id: 'id', + default: 'a', + label: 'b', + caption: 'c', + }); + await onInput!('d'); + expect(outputs).toStrictEqual([values.STR('d')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:textInput({}, 'id') + `); + const { onInput, ...textInput } = get('id') as AsUiTextInput; + expect(onInput).toBeTypeOf('function'); + expect(textInput).toStrictEqual({ + type: 'textInput', + id: 'id', + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('textarea', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let textarea = Ui:C:textarea({ + onInput: print + default: 'a' + label: 'b' + caption: 'c' + }, 'id') + Ui:render([textarea]) + `); + expect(root.children).toStrictEqual(['id']); + const { onInput, ...textarea } = get('id') as AsUiTextarea; + expect(textarea).toStrictEqual({ + type: 'textarea', + id: 'id', + default: 'a', + label: 'b', + caption: 'c', + }); + await onInput!('d'); + expect(outputs).toStrictEqual([values.STR('d')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:textarea({}, 'id') + `); + const { onInput, ...textarea } = get('id') as AsUiTextarea; + expect(onInput).toBeTypeOf('function'); + expect(textarea).toStrictEqual({ + type: 'textarea', + id: 'id', + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('numberInput', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let number_input = Ui:C:numberInput({ + onInput: print + default: 1 + label: 'a' + caption: 'b' + }, 'id') + Ui:render([number_input]) + `); + expect(root.children).toStrictEqual(['id']); + const { onInput, ...numberInput } = get('id') as AsUiNumberInput; + expect(numberInput).toStrictEqual({ + type: 'numberInput', + id: 'id', + default: 1, + label: 'a', + caption: 'b', + }); + await onInput!(2); + expect(outputs).toStrictEqual([values.NUM(2)]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:numberInput({}, 'id') + `); + const { onInput, ...numberInput } = get('id') as AsUiNumberInput; + expect(onInput).toBeTypeOf('function'); + expect(numberInput).toStrictEqual({ + type: 'numberInput', + id: 'id', + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('button', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let button = Ui:C:button({ + text: 'a' + onClick: @() { <: 'clicked' } + primary: true + rounded: false + disabled: false + }, 'id') + Ui:render([button]) + `); + expect(root.children).toStrictEqual(['id']); + const { onClick, ...button } = get('id') as AsUiButton; + expect(button).toStrictEqual({ + type: 'button', + id: 'id', + text: 'a', + primary: true, + rounded: false, + disabled: false, + }); + await onClick!(); + expect(outputs).toStrictEqual([values.STR('clicked')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:button({}, 'id') + `); + const { onClick, ...button } = get('id') as AsUiButton; + expect(onClick).toBeTypeOf('function'); + expect(button).toStrictEqual({ + type: 'button', + id: 'id', + text: undefined, + primary: undefined, + rounded: undefined, + disabled: undefined, + }); + }); + }); + + describe('buttons', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let buttons = Ui:C:buttons({ + buttons: [] + }, 'id') + Ui:render([buttons]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'buttons', + id: 'id', + buttons: [], + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:buttons({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'buttons', + id: 'id', + buttons: [], + }); + }); + + test.concurrent('some buttons', async () => { + const { root, get, outputs } = await exe(` + let buttons = Ui:C:buttons({ + buttons: [ + { + text: 'a' + onClick: @() { <: 'clicked a' } + primary: true + rounded: false + disabled: false + } + { + text: 'b' + onClick: @() { <: 'clicked b' } + primary: true + rounded: false + disabled: false + } + ] + }, 'id') + Ui:render([buttons]) + `); + expect(root.children).toStrictEqual(['id']); + const { buttons, ...buttonsOptions } = get('id') as AsUiButtons; + expect(buttonsOptions).toStrictEqual({ + type: 'buttons', + id: 'id', + }); + expect(buttons!.length).toBe(2); + const { onClick: onClickA, ...buttonA } = buttons![0]; + expect(buttonA).toStrictEqual({ + text: 'a', + primary: true, + rounded: false, + disabled: false, + }); + const { onClick: onClickB, ...buttonB } = buttons![1]; + expect(buttonB).toStrictEqual({ + text: 'b', + primary: true, + rounded: false, + disabled: false, + }); + await onClickA!(); + await onClickB!(); + expect(outputs).toStrictEqual( + [values.STR('clicked a'), values.STR('clicked b')] + ); + }); + }); + + describe('switch', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let switch = Ui:C:switch({ + onChange: print + default: false + label: 'a' + caption: 'b' + }, 'id') + Ui:render([switch]) + `); + expect(root.children).toStrictEqual(['id']); + const { onChange, ...switchOptions } = get('id') as AsUiSwitch; + expect(switchOptions).toStrictEqual({ + type: 'switch', + id: 'id', + default: false, + label: 'a', + caption: 'b', + }); + await onChange!(true); + expect(outputs).toStrictEqual([values.TRUE]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:switch({}, 'id') + `); + const { onChange, ...switchOptions } = get('id') as AsUiSwitch; + expect(onChange).toBeTypeOf('function'); + expect(switchOptions).toStrictEqual({ + type: 'switch', + id: 'id', + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('select', () => { + test.concurrent('all options', async () => { + const { root, get, outputs } = await exe(` + let select = Ui:C:select({ + items: [ + { text: 'A', value: 'a' } + { text: 'B', value: 'b' } + ] + onChange: print + default: 'a' + label: 'c' + caption: 'd' + }, 'id') + Ui:render([select]) + `); + expect(root.children).toStrictEqual(['id']); + const { onChange, ...select } = get('id') as AsUiSelect; + expect(select).toStrictEqual({ + type: 'select', + id: 'id', + items: [ + { text: 'A', value: 'a' }, + { text: 'B', value: 'b' }, + ], + default: 'a', + label: 'c', + caption: 'd', + }); + await onChange!('b'); + expect(outputs).toStrictEqual([values.STR('b')]); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:select({}, 'id') + `); + const { onChange, ...select } = get('id') as AsUiSelect; + expect(onChange).toBeTypeOf('function'); + expect(select).toStrictEqual({ + type: 'select', + id: 'id', + items: [], + default: undefined, + label: undefined, + caption: undefined, + }); + }); + + test.concurrent('omit item values', async () => { + const { get } = await exe(` + let select = Ui:C:select({ + items: [ + { text: 'A' } + { text: 'B' } + ] + }, 'id') + `); + const { onChange, ...select } = get('id') as AsUiSelect; + expect(onChange).toBeTypeOf('function'); + expect(select).toStrictEqual({ + type: 'select', + id: 'id', + items: [ + { text: 'A', value: 'A' }, + { text: 'B', value: 'B' }, + ], + default: undefined, + label: undefined, + caption: undefined, + }); + }); + }); + + describe('folder', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let folder = Ui:C:folder({ + children: [] + title: 'a' + opened: true + }, 'id') + Ui:render([folder]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'folder', + id: 'id', + children: [], + title: 'a', + opened: true, + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:folder({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'folder', + id: 'id', + children: [], + title: '', + opened: true, + }); + }); + + test.concurrent('some children', async () => { + const { get } = await exe(` + let text = Ui:C:text({ + text: 'text' + }, 'id1') + Ui:C:folder({ + children: [text] + }, 'id2') + `); + expect(get('id2')).toStrictEqual({ + type: 'folder', + id: 'id2', + children: ['id1'], + title: '', + opened: true, + }); + }); + }); + + describe('postFormButton', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let post_form_button = Ui:C:postFormButton({ + text: 'a' + primary: true + rounded: false + form: { + text: 'b' + cw: 'c' + visibility: 'public' + localOnly: true + } + }, 'id') + Ui:render([post_form_button]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'postFormButton', + id: 'id', + text: 'a', + primary: true, + rounded: false, + form: { + text: 'b', + cw: 'c', + visibility: 'public', + localOnly: true, + }, + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:postFormButton({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'postFormButton', + id: 'id', + text: undefined, + primary: undefined, + rounded: undefined, + form: { text: '' }, + }); + }); + }); + + describe('postForm', () => { + test.concurrent('all options', async () => { + const { root, get } = await exe(` + let post_form = Ui:C:postForm({ + form: { + text: 'a' + cw: 'b' + visibility: 'public' + localOnly: true + } + }, 'id') + Ui:render([post_form]) + `); + expect(root.children).toStrictEqual(['id']); + expect(get('id')).toStrictEqual({ + type: 'postForm', + id: 'id', + form: { + text: 'a', + cw: 'b', + visibility: 'public', + localOnly: true, + }, + }); + }); + + test.concurrent('minimum options', async () => { + const { get } = await exe(` + Ui:C:postForm({}, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'postForm', + id: 'id', + form: { text: '' }, + }); + }); + + test.concurrent('minimum options for form', async () => { + const { get } = await exe(` + Ui:C:postForm({ + form: { text: '' } + }, 'id') + `); + expect(get('id')).toStrictEqual({ + type: 'postForm', + id: 'id', + form: { + text: '', + cw: undefined, + visibility: undefined, + localOnly: undefined, + }, + }); + }); + }); +}); diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index bbf9d653cf..169bd5029c 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -10,8 +10,8 @@ "declaration": false, "sourceMap": false, "target": "ES2022", - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "ES2022", + "moduleResolution": "Bundler", "removeComments": false, "noLib": false, "strict": true, diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts deleted file mode 100644 index d588f83138..0000000000 --- a/packages/frontend/vite.config.local-dev.ts +++ /dev/null @@ -1,102 +0,0 @@ -import dns from 'dns'; -import { readFile } from 'node:fs/promises'; -import type { IncomingMessage } from 'node:http'; -import { defineConfig } from 'vite'; -import type { UserConfig } from 'vite'; -import * as yaml from 'js-yaml'; -import locales from '../../locales/index.js'; -import { getConfig } from './vite.config.js'; - -dns.setDefaultResultOrder('ipv4first'); - -const defaultConfig = getConfig(); - -const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8')); - -const httpUrl = `http://localhost:${port}/`; -const websocketUrl = `ws://localhost:${port}/`; -const embedUrl = `http://localhost:5174/`; - -// activitypubリクエストã¯Proxyを通ã—ã€ãれ以外ã¯Viteã®é–‹ç™ºã‚µãƒ¼ãƒãƒ¼ã‚’返㙠-function varyHandler(req: IncomingMessage) { - if (req.headers.accept?.includes('application/activity+json')) { - return null; - } - return '/index.html'; -} - -const devConfig: UserConfig = { - // 基本ã®è¨å®šã¯ vite.config.js ã‹ã‚‰å¼•ãç¶™ã - ...defaultConfig, - root: 'src', - publicDir: '../assets', - base: './', - server: { - host: 'localhost', - port: 5173, - proxy: { - '/api': { - changeOrigin: true, - target: httpUrl, - }, - '/assets': httpUrl, - '/static-assets': httpUrl, - '/client-assets': httpUrl, - '/files': httpUrl, - '/twemoji': httpUrl, - '/fluent-emoji': httpUrl, - '/tossface': httpUrl, - '/sw.js': httpUrl, - '/streaming': { - target: websocketUrl, - ws: true, - }, - '/favicon.ico': httpUrl, - '/robots.txt': httpUrl, - '/embed.js': httpUrl, - '/embed': { - target: embedUrl, - ws: true, - }, - '/identicon': { - target: httpUrl, - rewrite(path) { - return path.replace('@localhost:5173', ''); - }, - }, - '/url': httpUrl, - '/proxy': httpUrl, - '/_info_card_': httpUrl, - '/bios': httpUrl, - '/cli': httpUrl, - '/inbox': httpUrl, - '/emoji/': httpUrl, - '/notes': { - target: httpUrl, - bypass: varyHandler, - }, - '/users': { - target: httpUrl, - bypass: varyHandler, - }, - '/.well-known': { - target: httpUrl, - }, - }, - }, - build: { - ...defaultConfig.build, - rollupOptions: { - ...defaultConfig.build?.rollupOptions, - input: 'index.html', - }, - }, - - define: { - ...defaultConfig.define, - _LANGS_FULL_: JSON.stringify(Object.entries(locales)), - }, -}; - -export default defineConfig(({ command, mode }) => devConfig); - diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 2ba63c010f..51940486bd 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -3,6 +3,9 @@ import pluginReplace from '@rollup/plugin-replace'; import pluginVue from '@vitejs/plugin-vue'; import { type UserConfig, defineConfig } from 'vite'; import { localesVersion } from '../../locales/version.js'; +import * as yaml from 'js-yaml'; +import { promises as fsp } from 'fs'; + import locales from '../../locales/index.js'; import meta from '../../package.json'; import packageInfo from './package.json' with { type: 'json' }; @@ -10,6 +13,9 @@ import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-modul import pluginJson5 from './vite.json5.js'; import { pluginReplaceIcons } from './vite.replaceIcons.js'; +const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null; +const host = url ? (new URL(url)).hostname : undefined; + const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue', '.wasm']; /** @@ -76,7 +82,14 @@ export function getConfig(): UserConfig { base: '/vite/', server: { + host, port: 5173, + hmr: { + // ãƒãƒƒã‚¯ã‚¨ãƒ³ãƒ‰çµŒç”±ã§ã®èµ·å‹•時ã€Viteã¯5173経由ã§ã‚¢ã‚»ãƒƒãƒˆã‚’å‚ç…§ã—ã¦ã„ã‚‹ã¨æ€ã„込んã§ã„ã‚‹ãŒå®Ÿéš›ã¯3000ã‹ã‚‰é…ä¿¡ã•れる + // ãã®ãŸã‚ã€ãƒãƒƒã‚¯ã‚¨ãƒ³ãƒ‰ã®WSサーãƒãƒ¼ã«HMRã®WSリクエストãŒå¸åŽã•れã¦ã—ã¾ã„ã€æ£ã—ãHMRãŒæ©Ÿèƒ½ã—ãªã„ + // クライアントå´ã®WSãƒãƒ¼ãƒˆã‚’Viteサーãƒãƒ¼ã®ãƒãƒ¼ãƒˆã«å¼·åˆ¶ã•ã›ã‚‹ã“ã¨ã§ã€æ£ã—ãHMRãŒæ©Ÿèƒ½ã™ã‚‹ã‚ˆã†ã«ãªã‚‹ + clientPort: 5173, + }, headers: { // ãªã‚“ã‹åйã‹ãªã„ 'X-Frame-Options': 'DENY', }, diff --git a/packages/misskey-bubble-game/build.js b/packages/misskey-bubble-game/build.js index a80b71646f..5d534cc6fd 100644 --- a/packages/misskey-bubble-game/build.js +++ b/packages/misskey-bubble-game/build.js @@ -23,10 +23,14 @@ const options = { sourcemap: 'linked', }; +const args = process.argv.slice(2).map(arg => arg.toLowerCase()); + // builté…下をã™ã¹ã¦å‰Šé™¤ã™ã‚‹ -fs.rmSync('./built', { recursive: true, force: true }); +if (!args.includes('--no-clean')) { + fs.rmSync('./built', { recursive: true, force: true }); +} -if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { +if (args.includes('--watch')) { await watchSrc(); } else { await buildSrc(); diff --git a/packages/misskey-js/LICENSE b/packages/misskey-js/LICENSE index 63762b85d8..16352625db 100644 --- a/packages/misskey-js/LICENSE +++ b/packages/misskey-js/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2024 syuilo and other contributors +Copyright (c) 2021-2025 syuilo and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/misskey-js/api-extractor.json b/packages/misskey-js/api-extractor.json index a95281a6d5..627a245a49 100644 --- a/packages/misskey-js/api-extractor.json +++ b/packages/misskey-js/api-extractor.json @@ -62,6 +62,8 @@ */ "bundledPackages": [], + "newlineKind": "lf", + /** * Determines how the TypeScript compiler engine will be invoked by API Extractor. */ diff --git a/packages/misskey-js/build.js b/packages/misskey-js/build.js index a80b71646f..b794592815 100644 --- a/packages/misskey-js/build.js +++ b/packages/misskey-js/build.js @@ -24,9 +24,14 @@ const options = { }; // builté…下をã™ã¹ã¦å‰Šé™¤ã™ã‚‹ -fs.rmSync('./built', { recursive: true, force: true }); +const args = process.argv.slice(2).map(arg => arg.toLowerCase()); -if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { +// builté…下をã™ã¹ã¦å‰Šé™¤ã™ã‚‹ +if (!args.includes('--no-clean')) { + fs.rmSync('./built', { recursive: true, force: true }); +} + +if (args.includes('--watch')) { await watchSrc(); } else { await buildSrc(); diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 8ae9791d7f..1b4675b938 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2025.1.0-dev", + "version": "2025.2.0-dev", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts index ed1282957f..e663d712a7 100644 --- a/packages/misskey-js/src/api.ts +++ b/packages/misskey-js/src/api.ts @@ -44,7 +44,7 @@ export class APIClient { credential?: APIClient['credential']; fetch?: APIClient['fetch'] | null | undefined; }) { - this.origin = opts.origin; + this.origin = opts.origin.replace(/\/$/, ''); this.credential = opts.credential; // ãƒã‚¤ãƒ†ã‚£ãƒ–関数をãã®ã¾ã¾å¤‰æ•°ã«ä»£å…¥ã—ã¦ä½¿ãŠã†ã¨ã™ã‚‹ã¨Chromiumã§ã¯Illegal invocationエラーãŒç™ºç”Ÿã™ã‚‹ãŸã‚〠// 環境ã§å®Ÿè£…ã•れã¦ã„ã‚‹fetchを使ã†å ´åˆã¯ç„¡å関数ã§ãƒ©ãƒƒãƒ—ã—ã¦ä½¿ç”¨ã™ã‚‹ diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index ccb513b7f9..faabf10c43 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -5,7 +5,7 @@ declare module '../api.js' { export interface APIClient { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:meta* */ request<E extends 'admin/meta', P extends Endpoints[E]['req']>( @@ -16,7 +16,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-user-reports* */ request<E extends 'admin/abuse-user-reports', P extends Endpoints[E]['req']>( @@ -27,7 +27,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient* */ @@ -39,7 +39,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *read:admin:abuse-report:notification-recipient* */ @@ -51,7 +51,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* */ @@ -63,7 +63,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* */ @@ -75,7 +75,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* */ @@ -87,7 +87,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'admin/accounts/create', P extends Endpoints[E]['req']>( @@ -98,7 +98,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:account* */ request<E extends 'admin/accounts/delete', P extends Endpoints[E]['req']>( @@ -109,7 +109,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:account* */ request<E extends 'admin/accounts/find-by-email', P extends Endpoints[E]['req']>( @@ -120,7 +120,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ request<E extends 'admin/ad/create', P extends Endpoints[E]['req']>( @@ -131,7 +131,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ request<E extends 'admin/ad/delete', P extends Endpoints[E]['req']>( @@ -142,7 +142,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:ad* */ request<E extends 'admin/ad/list', P extends Endpoints[E]['req']>( @@ -153,7 +153,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ request<E extends 'admin/ad/update', P extends Endpoints[E]['req']>( @@ -164,7 +164,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ request<E extends 'admin/announcements/create', P extends Endpoints[E]['req']>( @@ -175,7 +175,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ request<E extends 'admin/announcements/delete', P extends Endpoints[E]['req']>( @@ -186,7 +186,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:announcements* */ request<E extends 'admin/announcements/list', P extends Endpoints[E]['req']>( @@ -197,7 +197,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ request<E extends 'admin/announcements/update', P extends Endpoints[E]['req']>( @@ -208,7 +208,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ request<E extends 'admin/avatar-decorations/create', P extends Endpoints[E]['req']>( @@ -219,7 +219,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ request<E extends 'admin/avatar-decorations/delete', P extends Endpoints[E]['req']>( @@ -230,7 +230,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:avatar-decorations* */ request<E extends 'admin/avatar-decorations/list', P extends Endpoints[E]['req']>( @@ -241,7 +241,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ request<E extends 'admin/avatar-decorations/update', P extends Endpoints[E]['req']>( @@ -252,7 +252,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user* */ request<E extends 'admin/delete-all-files-of-a-user', P extends Endpoints[E]['req']>( @@ -263,7 +263,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar* */ request<E extends 'admin/unset-user-avatar', P extends Endpoints[E]['req']>( @@ -274,7 +274,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner* */ request<E extends 'admin/unset-user-banner', P extends Endpoints[E]['req']>( @@ -285,7 +285,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:drive* */ request<E extends 'admin/drive/clean-remote-files', P extends Endpoints[E]['req']>( @@ -296,7 +296,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:drive* */ request<E extends 'admin/drive/cleanup', P extends Endpoints[E]['req']>( @@ -307,7 +307,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:drive* */ request<E extends 'admin/drive/files', P extends Endpoints[E]['req']>( @@ -318,7 +318,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:drive* */ request<E extends 'admin/drive/show-file', P extends Endpoints[E]['req']>( @@ -329,7 +329,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/add-aliases-bulk', P extends Endpoints[E]['req']>( @@ -340,7 +340,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/add', P extends Endpoints[E]['req']>( @@ -351,7 +351,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/copy', P extends Endpoints[E]['req']>( @@ -362,7 +362,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/delete-bulk', P extends Endpoints[E]['req']>( @@ -373,7 +373,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/delete', P extends Endpoints[E]['req']>( @@ -384,7 +384,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -396,7 +396,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ request<E extends 'admin/emoji/list-remote', P extends Endpoints[E]['req']>( @@ -407,7 +407,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ request<E extends 'admin/emoji/list', P extends Endpoints[E]['req']>( @@ -418,7 +418,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/remove-aliases-bulk', P extends Endpoints[E]['req']>( @@ -429,7 +429,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/set-aliases-bulk', P extends Endpoints[E]['req']>( @@ -440,7 +440,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/set-category-bulk', P extends Endpoints[E]['req']>( @@ -451,7 +451,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/set-license-bulk', P extends Endpoints[E]['req']>( @@ -462,7 +462,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:emoji* */ request<E extends 'admin/emoji/update', P extends Endpoints[E]['req']>( @@ -473,7 +473,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ request<E extends 'admin/federation/delete-all-files', P extends Endpoints[E]['req']>( @@ -484,7 +484,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ request<E extends 'admin/federation/refresh-remote-instance-metadata', P extends Endpoints[E]['req']>( @@ -495,7 +495,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ request<E extends 'admin/federation/remove-all-following', P extends Endpoints[E]['req']>( @@ -506,7 +506,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:federation* */ request<E extends 'admin/federation/update-instance', P extends Endpoints[E]['req']>( @@ -517,7 +517,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:index-stats* */ request<E extends 'admin/get-index-stats', P extends Endpoints[E]['req']>( @@ -528,7 +528,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:table-stats* */ request<E extends 'admin/get-table-stats', P extends Endpoints[E]['req']>( @@ -539,7 +539,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:user-ips* */ request<E extends 'admin/get-user-ips', P extends Endpoints[E]['req']>( @@ -550,7 +550,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:invite-codes* */ request<E extends 'admin/invite/create', P extends Endpoints[E]['req']>( @@ -561,7 +561,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:invite-codes* */ request<E extends 'admin/invite/list', P extends Endpoints[E]['req']>( @@ -572,7 +572,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:promo* */ request<E extends 'admin/promo/create', P extends Endpoints[E]['req']>( @@ -583,7 +583,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ request<E extends 'admin/queue/clear', P extends Endpoints[E]['req']>( @@ -594,7 +594,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:queue* */ request<E extends 'admin/queue/deliver-delayed', P extends Endpoints[E]['req']>( @@ -605,7 +605,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:queue* */ request<E extends 'admin/queue/inbox-delayed', P extends Endpoints[E]['req']>( @@ -616,7 +616,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:queue* */ request<E extends 'admin/queue/promote', P extends Endpoints[E]['req']>( @@ -627,7 +627,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:emoji* */ request<E extends 'admin/queue/stats', P extends Endpoints[E]['req']>( @@ -638,7 +638,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:relays* */ request<E extends 'admin/relays/add', P extends Endpoints[E]['req']>( @@ -649,7 +649,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:relays* */ request<E extends 'admin/relays/list', P extends Endpoints[E]['req']>( @@ -660,7 +660,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:relays* */ request<E extends 'admin/relays/remove', P extends Endpoints[E]['req']>( @@ -671,7 +671,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:reset-password* */ request<E extends 'admin/reset-password', P extends Endpoints[E]['req']>( @@ -682,7 +682,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report* */ request<E extends 'admin/resolve-abuse-user-report', P extends Endpoints[E]['req']>( @@ -693,7 +693,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report* */ request<E extends 'admin/forward-abuse-user-report', P extends Endpoints[E]['req']>( @@ -704,7 +704,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report* */ request<E extends 'admin/update-abuse-user-report', P extends Endpoints[E]['req']>( @@ -715,7 +715,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:send-email* */ request<E extends 'admin/send-email', P extends Endpoints[E]['req']>( @@ -726,7 +726,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:server-info* */ request<E extends 'admin/server-info', P extends Endpoints[E]['req']>( @@ -737,7 +737,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:show-moderation-log* */ request<E extends 'admin/show-moderation-logs', P extends Endpoints[E]['req']>( @@ -748,7 +748,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:show-user* */ request<E extends 'admin/show-user', P extends Endpoints[E]['req']>( @@ -759,7 +759,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:show-user* */ request<E extends 'admin/show-users', P extends Endpoints[E]['req']>( @@ -770,7 +770,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:nsfw-user* */ request<E extends 'admin/nsfw-user', P extends Endpoints[E]['req']>( @@ -781,7 +781,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:unnsfw-user* */ request<E extends 'admin/unnsfw-user', P extends Endpoints[E]['req']>( @@ -792,7 +792,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:silence-user* */ request<E extends 'admin/silence-user', P extends Endpoints[E]['req']>( @@ -803,7 +803,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:unsilence-user* */ request<E extends 'admin/unsilence-user', P extends Endpoints[E]['req']>( @@ -814,7 +814,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user* */ request<E extends 'admin/suspend-user', P extends Endpoints[E]['req']>( @@ -825,7 +825,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:approve-user* */ request<E extends 'admin/approve-user', P extends Endpoints[E]['req']>( @@ -836,7 +836,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user* */ request<E extends 'admin/decline-user', P extends Endpoints[E]['req']>( @@ -847,7 +847,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:unsuspend-user* */ request<E extends 'admin/unsuspend-user', P extends Endpoints[E]['req']>( @@ -858,7 +858,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:meta* */ request<E extends 'admin/update-meta', P extends Endpoints[E]['req']>( @@ -869,7 +869,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:delete-account* */ request<E extends 'admin/delete-account', P extends Endpoints[E]['req']>( @@ -880,7 +880,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:user-note* */ request<E extends 'admin/update-user-note', P extends Endpoints[E]['req']>( @@ -891,7 +891,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ request<E extends 'admin/roles/create', P extends Endpoints[E]['req']>( @@ -902,7 +902,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ request<E extends 'admin/roles/delete', P extends Endpoints[E]['req']>( @@ -913,7 +913,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:roles* */ request<E extends 'admin/roles/list', P extends Endpoints[E]['req']>( @@ -924,7 +924,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:admin:roles* */ request<E extends 'admin/roles/show', P extends Endpoints[E]['req']>( @@ -935,7 +935,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ request<E extends 'admin/roles/update', P extends Endpoints[E]['req']>( @@ -946,7 +946,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ request<E extends 'admin/roles/assign', P extends Endpoints[E]['req']>( @@ -957,7 +957,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ request<E extends 'admin/roles/unassign', P extends Endpoints[E]['req']>( @@ -968,7 +968,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ request<E extends 'admin/roles/update-default-policies', P extends Endpoints[E]['req']>( @@ -979,7 +979,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* / **Permission**: *read:admin:roles* */ request<E extends 'admin/roles/users', P extends Endpoints[E]['req']>( @@ -990,7 +990,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ @@ -1002,7 +1002,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ @@ -1014,7 +1014,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ @@ -1026,7 +1026,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ @@ -1038,7 +1038,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ @@ -1050,7 +1050,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook* */ @@ -1062,7 +1062,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'announcements', P extends Endpoints[E]['req']>( @@ -1073,7 +1073,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'announcements/show', P extends Endpoints[E]['req']>( @@ -1084,7 +1084,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'antennas/create', P extends Endpoints[E]['req']>( @@ -1095,7 +1095,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'antennas/delete', P extends Endpoints[E]['req']>( @@ -1106,7 +1106,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'antennas/list', P extends Endpoints[E]['req']>( @@ -1117,7 +1117,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'antennas/notes', P extends Endpoints[E]['req']>( @@ -1128,7 +1128,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'antennas/show', P extends Endpoints[E]['req']>( @@ -1139,7 +1139,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'antennas/update', P extends Endpoints[E]['req']>( @@ -1150,7 +1150,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:federation* */ request<E extends 'ap/get', P extends Endpoints[E]['req']>( @@ -1161,7 +1161,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'ap/show', P extends Endpoints[E]['req']>( @@ -1172,7 +1172,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'app/create', P extends Endpoints[E]['req']>( @@ -1183,7 +1183,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'app/show', P extends Endpoints[E]['req']>( @@ -1194,7 +1194,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -1206,7 +1206,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'auth/session/generate', P extends Endpoints[E]['req']>( @@ -1217,7 +1217,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'auth/session/show', P extends Endpoints[E]['req']>( @@ -1228,7 +1228,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'auth/session/userkey', P extends Endpoints[E]['req']>( @@ -1239,7 +1239,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:blocks* */ request<E extends 'blocking/create', P extends Endpoints[E]['req']>( @@ -1250,7 +1250,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:blocks* */ request<E extends 'blocking/delete', P extends Endpoints[E]['req']>( @@ -1261,7 +1261,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:blocks* */ request<E extends 'blocking/list', P extends Endpoints[E]['req']>( @@ -1272,7 +1272,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:channels* */ request<E extends 'channels/create', P extends Endpoints[E]['req']>( @@ -1283,7 +1283,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'channels/featured', P extends Endpoints[E]['req']>( @@ -1294,7 +1294,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:channels* */ request<E extends 'channels/follow', P extends Endpoints[E]['req']>( @@ -1305,7 +1305,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:channels* */ request<E extends 'channels/followed', P extends Endpoints[E]['req']>( @@ -1316,7 +1316,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:channels* */ request<E extends 'channels/owned', P extends Endpoints[E]['req']>( @@ -1327,7 +1327,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'channels/show', P extends Endpoints[E]['req']>( @@ -1338,7 +1338,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'channels/timeline', P extends Endpoints[E]['req']>( @@ -1349,7 +1349,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:channels* */ request<E extends 'channels/unfollow', P extends Endpoints[E]['req']>( @@ -1360,7 +1360,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:channels* */ request<E extends 'channels/update', P extends Endpoints[E]['req']>( @@ -1371,7 +1371,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:channels* */ request<E extends 'channels/favorite', P extends Endpoints[E]['req']>( @@ -1382,7 +1382,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:channels* */ request<E extends 'channels/unfavorite', P extends Endpoints[E]['req']>( @@ -1393,7 +1393,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:channels* */ request<E extends 'channels/my-favorites', P extends Endpoints[E]['req']>( @@ -1404,7 +1404,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'channels/search', P extends Endpoints[E]['req']>( @@ -1415,7 +1415,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/active-users', P extends Endpoints[E]['req']>( @@ -1426,7 +1426,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/ap-request', P extends Endpoints[E]['req']>( @@ -1437,7 +1437,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/drive', P extends Endpoints[E]['req']>( @@ -1448,7 +1448,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/federation', P extends Endpoints[E]['req']>( @@ -1459,7 +1459,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/instance', P extends Endpoints[E]['req']>( @@ -1470,7 +1470,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/notes', P extends Endpoints[E]['req']>( @@ -1481,7 +1481,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/user/drive', P extends Endpoints[E]['req']>( @@ -1492,7 +1492,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/user/following', P extends Endpoints[E]['req']>( @@ -1503,7 +1503,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/user/notes', P extends Endpoints[E]['req']>( @@ -1514,7 +1514,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/user/pv', P extends Endpoints[E]['req']>( @@ -1525,7 +1525,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/user/reactions', P extends Endpoints[E]['req']>( @@ -1536,7 +1536,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'charts/users', P extends Endpoints[E]['req']>( @@ -1547,7 +1547,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'clips/add-note', P extends Endpoints[E]['req']>( @@ -1558,7 +1558,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'clips/remove-note', P extends Endpoints[E]['req']>( @@ -1569,7 +1569,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'clips/create', P extends Endpoints[E]['req']>( @@ -1580,7 +1580,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'clips/delete', P extends Endpoints[E]['req']>( @@ -1591,7 +1591,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'clips/list', P extends Endpoints[E]['req']>( @@ -1602,7 +1602,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* / **Permission**: *read:account* */ request<E extends 'clips/notes', P extends Endpoints[E]['req']>( @@ -1613,7 +1613,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* / **Permission**: *read:account* */ request<E extends 'clips/show', P extends Endpoints[E]['req']>( @@ -1624,7 +1624,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'clips/update', P extends Endpoints[E]['req']>( @@ -1635,7 +1635,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* */ request<E extends 'clips/favorite', P extends Endpoints[E]['req']>( @@ -1646,7 +1646,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:clip-favorite* */ request<E extends 'clips/unfavorite', P extends Endpoints[E]['req']>( @@ -1657,7 +1657,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:clip-favorite* */ request<E extends 'clips/my-favorites', P extends Endpoints[E]['req']>( @@ -1668,7 +1668,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive', P extends Endpoints[E]['req']>( @@ -1679,7 +1679,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/files', P extends Endpoints[E]['req']>( @@ -1690,7 +1690,7 @@ declare module '../api.js' { /** * Find the notes to which the given file is attached. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/files/attached-notes', P extends Endpoints[E]['req']>( @@ -1701,7 +1701,7 @@ declare module '../api.js' { /** * Check if a given file exists. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/files/check-existence', P extends Endpoints[E]['req']>( @@ -1712,7 +1712,7 @@ declare module '../api.js' { /** * Upload a new drive file. - * + * * **Credential required**: *Yes* / **Permission**: *write:drive* */ request<E extends 'drive/files/create', P extends Endpoints[E]['req']>( @@ -1723,7 +1723,7 @@ declare module '../api.js' { /** * Delete an existing drive file. - * + * * **Credential required**: *Yes* / **Permission**: *write:drive* */ request<E extends 'drive/files/delete', P extends Endpoints[E]['req']>( @@ -1734,7 +1734,7 @@ declare module '../api.js' { /** * Search for a drive file by a hash of the contents. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/files/find-by-hash', P extends Endpoints[E]['req']>( @@ -1745,7 +1745,7 @@ declare module '../api.js' { /** * Search for a drive file by the given parameters. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/files/find', P extends Endpoints[E]['req']>( @@ -1756,7 +1756,7 @@ declare module '../api.js' { /** * Show the properties of a drive file. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/files/show', P extends Endpoints[E]['req']>( @@ -1767,7 +1767,7 @@ declare module '../api.js' { /** * Update the properties of a drive file. - * + * * **Credential required**: *Yes* / **Permission**: *write:drive* */ request<E extends 'drive/files/update', P extends Endpoints[E]['req']>( @@ -1778,7 +1778,7 @@ declare module '../api.js' { /** * Request the server to download a new drive file from the specified URL. - * + * * **Credential required**: *Yes* / **Permission**: *write:drive* */ request<E extends 'drive/files/upload-from-url', P extends Endpoints[E]['req']>( @@ -1789,7 +1789,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/folders', P extends Endpoints[E]['req']>( @@ -1800,7 +1800,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:drive* */ request<E extends 'drive/folders/create', P extends Endpoints[E]['req']>( @@ -1811,7 +1811,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:drive* */ request<E extends 'drive/folders/delete', P extends Endpoints[E]['req']>( @@ -1822,7 +1822,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/folders/find', P extends Endpoints[E]['req']>( @@ -1833,7 +1833,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/folders/show', P extends Endpoints[E]['req']>( @@ -1844,7 +1844,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:drive* */ request<E extends 'drive/folders/update', P extends Endpoints[E]['req']>( @@ -1855,7 +1855,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:drive* */ request<E extends 'drive/stream', P extends Endpoints[E]['req']>( @@ -1866,7 +1866,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'email-address/available', P extends Endpoints[E]['req']>( @@ -1877,7 +1877,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'endpoint', P extends Endpoints[E]['req']>( @@ -1888,7 +1888,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'endpoints', P extends Endpoints[E]['req']>( @@ -1899,7 +1899,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -1911,7 +1911,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'federation/followers', P extends Endpoints[E]['req']>( @@ -1922,7 +1922,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'federation/following', P extends Endpoints[E]['req']>( @@ -1933,7 +1933,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'federation/instances', P extends Endpoints[E]['req']>( @@ -1944,7 +1944,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'federation/show-instance', P extends Endpoints[E]['req']>( @@ -1955,7 +1955,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'federation/update-remote-user', P extends Endpoints[E]['req']>( @@ -1966,7 +1966,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'federation/users', P extends Endpoints[E]['req']>( @@ -1977,7 +1977,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'federation/stats', P extends Endpoints[E]['req']>( @@ -1988,7 +1988,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:following* */ request<E extends 'following/create', P extends Endpoints[E]['req']>( @@ -1999,7 +1999,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:following* */ request<E extends 'following/delete', P extends Endpoints[E]['req']>( @@ -2010,7 +2010,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:following* */ request<E extends 'following/update', P extends Endpoints[E]['req']>( @@ -2021,7 +2021,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:following* */ request<E extends 'following/update-all', P extends Endpoints[E]['req']>( @@ -2032,7 +2032,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:following* */ request<E extends 'following/invalidate', P extends Endpoints[E]['req']>( @@ -2043,7 +2043,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:following* */ request<E extends 'following/requests/accept', P extends Endpoints[E]['req']>( @@ -2054,7 +2054,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:following* */ request<E extends 'following/requests/cancel', P extends Endpoints[E]['req']>( @@ -2065,7 +2065,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:following* */ request<E extends 'following/requests/list', P extends Endpoints[E]['req']>( @@ -2076,7 +2076,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:following* */ request<E extends 'following/requests/sent', P extends Endpoints[E]['req']>( @@ -2087,7 +2087,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:following* */ request<E extends 'following/requests/reject', P extends Endpoints[E]['req']>( @@ -2098,7 +2098,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'gallery/featured', P extends Endpoints[E]['req']>( @@ -2109,7 +2109,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'gallery/popular', P extends Endpoints[E]['req']>( @@ -2120,7 +2120,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'gallery/posts', P extends Endpoints[E]['req']>( @@ -2131,7 +2131,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ request<E extends 'gallery/posts/create', P extends Endpoints[E]['req']>( @@ -2142,7 +2142,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ request<E extends 'gallery/posts/delete', P extends Endpoints[E]['req']>( @@ -2153,7 +2153,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* */ request<E extends 'gallery/posts/like', P extends Endpoints[E]['req']>( @@ -2164,7 +2164,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'gallery/posts/show', P extends Endpoints[E]['req']>( @@ -2175,7 +2175,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:gallery-likes* */ request<E extends 'gallery/posts/unlike', P extends Endpoints[E]['req']>( @@ -2186,7 +2186,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:gallery* */ request<E extends 'gallery/posts/update', P extends Endpoints[E]['req']>( @@ -2197,7 +2197,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'get-online-users-count', P extends Endpoints[E]['req']>( @@ -2208,7 +2208,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'get-avatar-decorations', P extends Endpoints[E]['req']>( @@ -2219,7 +2219,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'hashtags/list', P extends Endpoints[E]['req']>( @@ -2230,7 +2230,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'hashtags/search', P extends Endpoints[E]['req']>( @@ -2241,7 +2241,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'hashtags/show', P extends Endpoints[E]['req']>( @@ -2252,7 +2252,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'hashtags/trend', P extends Endpoints[E]['req']>( @@ -2263,7 +2263,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'hashtags/users', P extends Endpoints[E]['req']>( @@ -2274,7 +2274,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i', P extends Endpoints[E]['req']>( @@ -2285,7 +2285,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2297,7 +2297,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2309,7 +2309,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2321,7 +2321,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2333,7 +2333,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2345,7 +2345,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2357,7 +2357,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2369,7 +2369,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2381,7 +2381,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2393,7 +2393,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2405,7 +2405,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/claim-achievement', P extends Endpoints[E]['req']>( @@ -2416,7 +2416,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2428,7 +2428,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2440,7 +2440,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2452,7 +2452,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2464,7 +2464,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2476,7 +2476,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2488,7 +2488,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2500,7 +2500,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2512,7 +2512,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2524,7 +2524,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2536,7 +2536,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2548,7 +2548,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:favorites* */ request<E extends 'i/favorites', P extends Endpoints[E]['req']>( @@ -2559,7 +2559,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:gallery-likes* */ request<E extends 'i/gallery/likes', P extends Endpoints[E]['req']>( @@ -2570,7 +2570,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:gallery* */ request<E extends 'i/gallery/posts', P extends Endpoints[E]['req']>( @@ -2581,7 +2581,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2593,7 +2593,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2605,7 +2605,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2617,7 +2617,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2629,7 +2629,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2641,7 +2641,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2653,7 +2653,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:notifications* */ request<E extends 'i/notifications', P extends Endpoints[E]['req']>( @@ -2664,7 +2664,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:notifications* */ request<E extends 'i/notifications-grouped', P extends Endpoints[E]['req']>( @@ -2675,7 +2675,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:page-likes* */ request<E extends 'i/page-likes', P extends Endpoints[E]['req']>( @@ -2686,7 +2686,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:pages* */ request<E extends 'i/pages', P extends Endpoints[E]['req']>( @@ -2697,7 +2697,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/pin', P extends Endpoints[E]['req']>( @@ -2708,7 +2708,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/read-all-unread-notes', P extends Endpoints[E]['req']>( @@ -2719,7 +2719,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/read-announcement', P extends Endpoints[E]['req']>( @@ -2730,7 +2730,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2742,7 +2742,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i/registry/get-all', P extends Endpoints[E]['req']>( @@ -2753,7 +2753,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i/registry/get-unsecure', P extends Endpoints[E]['req']>( @@ -2764,7 +2764,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i/registry/get-detail', P extends Endpoints[E]['req']>( @@ -2775,7 +2775,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i/registry/get', P extends Endpoints[E]['req']>( @@ -2786,7 +2786,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i/registry/keys-with-type', P extends Endpoints[E]['req']>( @@ -2797,7 +2797,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i/registry/keys', P extends Endpoints[E]['req']>( @@ -2808,7 +2808,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/registry/remove', P extends Endpoints[E]['req']>( @@ -2819,7 +2819,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2831,7 +2831,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/registry/set', P extends Endpoints[E]['req']>( @@ -2842,7 +2842,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2854,7 +2854,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2866,7 +2866,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/unpin', P extends Endpoints[E]['req']>( @@ -2877,7 +2877,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2889,7 +2889,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/update', P extends Endpoints[E]['req']>( @@ -2900,7 +2900,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -2912,7 +2912,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/webhooks/create', P extends Endpoints[E]['req']>( @@ -2923,7 +2923,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i/webhooks/list', P extends Endpoints[E]['req']>( @@ -2934,7 +2934,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'i/webhooks/show', P extends Endpoints[E]['req']>( @@ -2945,7 +2945,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/webhooks/update', P extends Endpoints[E]['req']>( @@ -2956,7 +2956,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'i/webhooks/delete', P extends Endpoints[E]['req']>( @@ -2967,7 +2967,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* / **Permission**: *read:account* */ @@ -2979,7 +2979,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:invite-codes* */ request<E extends 'invite/create', P extends Endpoints[E]['req']>( @@ -2990,7 +2990,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:invite-codes* */ request<E extends 'invite/delete', P extends Endpoints[E]['req']>( @@ -3001,7 +3001,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:invite-codes* */ request<E extends 'invite/list', P extends Endpoints[E]['req']>( @@ -3012,7 +3012,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:invite-codes* */ request<E extends 'invite/limit', P extends Endpoints[E]['req']>( @@ -3023,7 +3023,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'meta', P extends Endpoints[E]['req']>( @@ -3034,7 +3034,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'emojis', P extends Endpoints[E]['req']>( @@ -3045,7 +3045,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'emoji', P extends Endpoints[E]['req']>( @@ -3056,7 +3056,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -3068,7 +3068,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ request<E extends 'mute/create', P extends Endpoints[E]['req']>( @@ -3079,7 +3079,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ request<E extends 'mute/delete', P extends Endpoints[E]['req']>( @@ -3090,7 +3090,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:mutes* */ request<E extends 'mute/list', P extends Endpoints[E]['req']>( @@ -3101,7 +3101,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ request<E extends 'renote-mute/create', P extends Endpoints[E]['req']>( @@ -3112,7 +3112,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:mutes* */ request<E extends 'renote-mute/delete', P extends Endpoints[E]['req']>( @@ -3123,7 +3123,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:mutes* */ request<E extends 'renote-mute/list', P extends Endpoints[E]['req']>( @@ -3134,7 +3134,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'my/apps', P extends Endpoints[E]['req']>( @@ -3145,7 +3145,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes', P extends Endpoints[E]['req']>( @@ -3156,7 +3156,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/children', P extends Endpoints[E]['req']>( @@ -3167,7 +3167,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/clips', P extends Endpoints[E]['req']>( @@ -3178,7 +3178,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/conversation', P extends Endpoints[E]['req']>( @@ -3189,7 +3189,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notes* */ request<E extends 'notes/create', P extends Endpoints[E]['req']>( @@ -3200,7 +3200,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notes* */ request<E extends 'notes/delete', P extends Endpoints[E]['req']>( @@ -3211,7 +3211,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:favorites* */ request<E extends 'notes/favorites/create', P extends Endpoints[E]['req']>( @@ -3222,7 +3222,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:favorites* */ request<E extends 'notes/favorites/delete', P extends Endpoints[E]['req']>( @@ -3233,7 +3233,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/featured', P extends Endpoints[E]['req']>( @@ -3244,7 +3244,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/following', P extends Endpoints[E]['req']>( @@ -3255,7 +3255,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/global-timeline', P extends Endpoints[E]['req']>( @@ -3266,7 +3266,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/bubble-timeline', P extends Endpoints[E]['req']>( @@ -3277,7 +3277,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/hybrid-timeline', P extends Endpoints[E]['req']>( @@ -3288,7 +3288,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/local-timeline', P extends Endpoints[E]['req']>( @@ -3299,7 +3299,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/mentions', P extends Endpoints[E]['req']>( @@ -3310,7 +3310,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/polls/recommendation', P extends Endpoints[E]['req']>( @@ -3321,7 +3321,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:votes* */ request<E extends 'notes/polls/vote', P extends Endpoints[E]['req']>( @@ -3332,7 +3332,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:federation* */ request<E extends 'notes/polls/refresh', P extends Endpoints[E]['req']>( @@ -3343,7 +3343,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/reactions', P extends Endpoints[E]['req']>( @@ -3354,7 +3354,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:reactions* */ request<E extends 'notes/reactions/create', P extends Endpoints[E]['req']>( @@ -3365,7 +3365,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:reactions* */ request<E extends 'notes/reactions/delete', P extends Endpoints[E]['req']>( @@ -3376,7 +3376,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:reactions* */ request<E extends 'notes/like', P extends Endpoints[E]['req']>( @@ -3387,7 +3387,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/renotes', P extends Endpoints[E]['req']>( @@ -3398,7 +3398,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/replies', P extends Endpoints[E]['req']>( @@ -3409,7 +3409,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* */ request<E extends 'notes/schedule/create', P extends Endpoints[E]['req']>( @@ -3420,7 +3420,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* */ request<E extends 'notes/schedule/delete', P extends Endpoints[E]['req']>( @@ -3431,7 +3431,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* */ request<E extends 'notes/schedule/list', P extends Endpoints[E]['req']>( @@ -3442,7 +3442,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/search-by-tag', P extends Endpoints[E]['req']>( @@ -3453,7 +3453,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/search', P extends Endpoints[E]['req']>( @@ -3464,7 +3464,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/show', P extends Endpoints[E]['req']>( @@ -3475,7 +3475,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/state', P extends Endpoints[E]['req']>( @@ -3486,7 +3486,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'notes/thread-muting/create', P extends Endpoints[E]['req']>( @@ -3497,7 +3497,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'notes/thread-muting/delete', P extends Endpoints[E]['req']>( @@ -3508,7 +3508,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/timeline', P extends Endpoints[E]['req']>( @@ -3519,7 +3519,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/translate', P extends Endpoints[E]['req']>( @@ -3530,7 +3530,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notes* */ request<E extends 'notes/unrenote', P extends Endpoints[E]['req']>( @@ -3541,7 +3541,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'notes/user-list-timeline', P extends Endpoints[E]['req']>( @@ -3552,7 +3552,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notes* */ request<E extends 'notes/edit', P extends Endpoints[E]['req']>( @@ -3563,7 +3563,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'notes/versions', P extends Endpoints[E]['req']>( @@ -3574,7 +3574,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ request<E extends 'notifications/create', P extends Endpoints[E]['req']>( @@ -3585,7 +3585,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ request<E extends 'notifications/flush', P extends Endpoints[E]['req']>( @@ -3596,7 +3596,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ request<E extends 'notifications/mark-all-as-read', P extends Endpoints[E]['req']>( @@ -3607,7 +3607,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:notifications* */ request<E extends 'notifications/test-notification', P extends Endpoints[E]['req']>( @@ -3618,7 +3618,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -3630,7 +3630,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:pages* */ request<E extends 'pages/create', P extends Endpoints[E]['req']>( @@ -3641,7 +3641,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:pages* */ request<E extends 'pages/delete', P extends Endpoints[E]['req']>( @@ -3652,7 +3652,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'pages/featured', P extends Endpoints[E]['req']>( @@ -3663,7 +3663,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:page-likes* */ request<E extends 'pages/like', P extends Endpoints[E]['req']>( @@ -3674,7 +3674,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'pages/show', P extends Endpoints[E]['req']>( @@ -3685,7 +3685,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:page-likes* */ request<E extends 'pages/unlike', P extends Endpoints[E]['req']>( @@ -3696,7 +3696,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:pages* */ request<E extends 'pages/update', P extends Endpoints[E]['req']>( @@ -3707,7 +3707,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:flash* */ request<E extends 'flash/create', P extends Endpoints[E]['req']>( @@ -3718,7 +3718,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:flash* */ request<E extends 'flash/delete', P extends Endpoints[E]['req']>( @@ -3729,7 +3729,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'flash/featured', P extends Endpoints[E]['req']>( @@ -3740,7 +3740,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:flash-likes* */ request<E extends 'flash/like', P extends Endpoints[E]['req']>( @@ -3751,7 +3751,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'flash/show', P extends Endpoints[E]['req']>( @@ -3762,7 +3762,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:flash-likes* */ request<E extends 'flash/unlike', P extends Endpoints[E]['req']>( @@ -3773,7 +3773,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:flash* */ request<E extends 'flash/update', P extends Endpoints[E]['req']>( @@ -3784,7 +3784,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:flash* */ request<E extends 'flash/my', P extends Endpoints[E]['req']>( @@ -3795,7 +3795,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:flash-likes* */ request<E extends 'flash/my-likes', P extends Endpoints[E]['req']>( @@ -3806,7 +3806,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'ping', P extends Endpoints[E]['req']>( @@ -3817,7 +3817,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'pinned-users', P extends Endpoints[E]['req']>( @@ -3828,7 +3828,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'promo/read', P extends Endpoints[E]['req']>( @@ -3839,7 +3839,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'roles/list', P extends Endpoints[E]['req']>( @@ -3850,7 +3850,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'roles/show', P extends Endpoints[E]['req']>( @@ -3861,7 +3861,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'roles/users', P extends Endpoints[E]['req']>( @@ -3872,7 +3872,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'roles/notes', P extends Endpoints[E]['req']>( @@ -3883,7 +3883,7 @@ declare module '../api.js' { /** * Request a users password to be reset. - * + * * **Credential required**: *No* */ request<E extends 'request-reset-password', P extends Endpoints[E]['req']>( @@ -3894,7 +3894,7 @@ declare module '../api.js' { /** * Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis. - * + * * **Credential required**: *No* */ request<E extends 'reset-db', P extends Endpoints[E]['req']>( @@ -3905,7 +3905,7 @@ declare module '../api.js' { /** * Complete the password reset that was previously requested. - * + * * **Credential required**: *No* */ request<E extends 'reset-password', P extends Endpoints[E]['req']>( @@ -3916,7 +3916,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'server-info', P extends Endpoints[E]['req']>( @@ -3927,7 +3927,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'stats', P extends Endpoints[E]['req']>( @@ -3938,7 +3938,7 @@ declare module '../api.js' { /** * Check push notification registration exists. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -3950,7 +3950,7 @@ declare module '../api.js' { /** * Update push notification registration. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -3962,7 +3962,7 @@ declare module '../api.js' { /** * Register to receive push notifications. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -3974,7 +3974,7 @@ declare module '../api.js' { /** * Unregister from receiving push notifications. - * + * * **Credential required**: *No* */ request<E extends 'sw/unregister', P extends Endpoints[E]['req']>( @@ -3985,7 +3985,7 @@ declare module '../api.js' { /** * Endpoint for testing input validation. - * + * * **Credential required**: *No* */ request<E extends 'test', P extends Endpoints[E]['req']>( @@ -3996,7 +3996,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'username/available', P extends Endpoints[E]['req']>( @@ -4007,7 +4007,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'users', P extends Endpoints[E]['req']>( @@ -4018,7 +4018,7 @@ declare module '../api.js' { /** * Show all clips this user owns. - * + * * **Credential required**: *No* */ request<E extends 'users/clips', P extends Endpoints[E]['req']>( @@ -4029,7 +4029,7 @@ declare module '../api.js' { /** * Show everyone that follows this user. - * + * * **Credential required**: *No* */ request<E extends 'users/followers', P extends Endpoints[E]['req']>( @@ -4040,7 +4040,7 @@ declare module '../api.js' { /** * Show everyone that this user is following. - * + * * **Credential required**: *No* */ request<E extends 'users/following', P extends Endpoints[E]['req']>( @@ -4051,7 +4051,7 @@ declare module '../api.js' { /** * Show all gallery posts by the given user. - * + * * **Credential required**: *No* */ request<E extends 'users/gallery/posts', P extends Endpoints[E]['req']>( @@ -4062,7 +4062,7 @@ declare module '../api.js' { /** * Get a list of other users that the specified user frequently replies to. - * + * * **Credential required**: *No* */ request<E extends 'users/get-frequently-replied-users', P extends Endpoints[E]['req']>( @@ -4073,7 +4073,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'users/featured-notes', P extends Endpoints[E]['req']>( @@ -4084,7 +4084,7 @@ declare module '../api.js' { /** * Create a new list of users. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/create', P extends Endpoints[E]['req']>( @@ -4095,7 +4095,7 @@ declare module '../api.js' { /** * Delete an existing list of users. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/delete', P extends Endpoints[E]['req']>( @@ -4106,7 +4106,7 @@ declare module '../api.js' { /** * Show all lists that the authenticated user has created. - * + * * **Credential required**: *No* / **Permission**: *read:account* */ request<E extends 'users/lists/list', P extends Endpoints[E]['req']>( @@ -4117,7 +4117,7 @@ declare module '../api.js' { /** * Remove a user from a list. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/pull', P extends Endpoints[E]['req']>( @@ -4128,7 +4128,7 @@ declare module '../api.js' { /** * Add a user to an existing list. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/push', P extends Endpoints[E]['req']>( @@ -4139,7 +4139,7 @@ declare module '../api.js' { /** * Show the properties of a list. - * + * * **Credential required**: *No* / **Permission**: *read:account* */ request<E extends 'users/lists/show', P extends Endpoints[E]['req']>( @@ -4150,7 +4150,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/favorite', P extends Endpoints[E]['req']>( @@ -4161,7 +4161,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/unfavorite', P extends Endpoints[E]['req']>( @@ -4172,7 +4172,7 @@ declare module '../api.js' { /** * Update the properties of a list. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/update', P extends Endpoints[E]['req']>( @@ -4183,7 +4183,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/create-from-public', P extends Endpoints[E]['req']>( @@ -4194,7 +4194,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/lists/update-membership', P extends Endpoints[E]['req']>( @@ -4205,7 +4205,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* / **Permission**: *read:account* */ request<E extends 'users/lists/get-memberships', P extends Endpoints[E]['req']>( @@ -4216,7 +4216,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'users/notes', P extends Endpoints[E]['req']>( @@ -4227,7 +4227,7 @@ declare module '../api.js' { /** * Show all pages this user created. - * + * * **Credential required**: *No* */ request<E extends 'users/pages', P extends Endpoints[E]['req']>( @@ -4238,7 +4238,7 @@ declare module '../api.js' { /** * Show all flashs this user created. - * + * * **Credential required**: *No* */ request<E extends 'users/flashs', P extends Endpoints[E]['req']>( @@ -4249,7 +4249,7 @@ declare module '../api.js' { /** * Show all reactions this user made. - * + * * **Credential required**: *No* */ request<E extends 'users/reactions', P extends Endpoints[E]['req']>( @@ -4260,7 +4260,7 @@ declare module '../api.js' { /** * Show users that the authenticated user might be interested to follow. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'users/recommendation', P extends Endpoints[E]['req']>( @@ -4271,7 +4271,7 @@ declare module '../api.js' { /** * Show the different kinds of relations between the authenticated user and the specified user(s). - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'users/relation', P extends Endpoints[E]['req']>( @@ -4282,7 +4282,7 @@ declare module '../api.js' { /** * File a report. - * + * * **Credential required**: *Yes* / **Permission**: *write:report-abuse* */ request<E extends 'users/report-abuse', P extends Endpoints[E]['req']>( @@ -4293,7 +4293,7 @@ declare module '../api.js' { /** * Search for a user by username and/or host. - * + * * **Credential required**: *No* */ request<E extends 'users/search-by-username-and-host', P extends Endpoints[E]['req']>( @@ -4304,7 +4304,7 @@ declare module '../api.js' { /** * Search for users. - * + * * **Credential required**: *No* */ request<E extends 'users/search', P extends Endpoints[E]['req']>( @@ -4315,7 +4315,7 @@ declare module '../api.js' { /** * Show the properties of a user. - * + * * **Credential required**: *No* */ request<E extends 'users/show', P extends Endpoints[E]['req']>( @@ -4326,7 +4326,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'users/achievements', P extends Endpoints[E]['req']>( @@ -4337,7 +4337,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'users/update-memo', P extends Endpoints[E]['req']>( @@ -4348,7 +4348,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'fetch-rss', P extends Endpoints[E]['req']>( @@ -4359,7 +4359,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. * **Credential required**: *Yes* */ @@ -4371,7 +4371,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'retention', P extends Endpoints[E]['req']>( @@ -4382,7 +4382,7 @@ declare module '../api.js' { /** * Get Sharkey Sponsors or Instance Sponsors - * + * * **Credential required**: *No* */ request<E extends 'sponsors', P extends Endpoints[E]['req']>( @@ -4393,7 +4393,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'bubble-game/register', P extends Endpoints[E]['req']>( @@ -4404,7 +4404,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'bubble-game/ranking', P extends Endpoints[E]['req']>( @@ -4415,7 +4415,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'reversi/cancel-match', P extends Endpoints[E]['req']>( @@ -4426,7 +4426,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'reversi/games', P extends Endpoints[E]['req']>( @@ -4437,7 +4437,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'reversi/match', P extends Endpoints[E]['req']>( @@ -4448,7 +4448,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'reversi/invitations', P extends Endpoints[E]['req']>( @@ -4459,7 +4459,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'reversi/show-game', P extends Endpoints[E]['req']>( @@ -4470,7 +4470,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *Yes* / **Permission**: *write:account* */ request<E extends 'reversi/surrender', P extends Endpoints[E]['req']>( @@ -4481,7 +4481,7 @@ declare module '../api.js' { /** * No description provided. - * + * * **Credential required**: *No* */ request<E extends 'reversi/verify', P extends Endpoints[E]['req']>( diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 9166bb701f..61c9ed1260 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -4,18 +4,17 @@ import { operations } from './types.js'; export type EmptyRequest = Record<string, unknown> | undefined; export type EmptyResponse = Record<string, unknown> | undefined; -export type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json']; -export type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json']; -export type AdminAbuseUserReportsResponse = operations['admin___abuse-user-reports']['responses']['200']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json']; export type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json']; export type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json']; export type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json']; export type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json']; -export type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json']; -export type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json']; export type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json']; export type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json']; -export type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json']; +export type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json']; +export type AdminAbuseUserReportsResponse = operations['admin___abuse-user-reports']['responses']['200']['content']['application/json']; export type AdminAccountsCreateRequest = operations['admin___accounts___create']['requestBody']['content']['application/json']; export type AdminAccountsCreateResponse = operations['admin___accounts___create']['responses']['200']['content']['application/json']; export type AdminAccountsDeleteRequest = operations['admin___accounts___delete']['requestBody']['content']['application/json']; @@ -33,31 +32,34 @@ export type AdminAnnouncementsDeleteRequest = operations['admin___announcements_ export type AdminAnnouncementsListRequest = operations['admin___announcements___list']['requestBody']['content']['application/json']; export type AdminAnnouncementsListResponse = operations['admin___announcements___list']['responses']['200']['content']['application/json']; export type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json']; +export type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json']; export type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json']; export type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json']; export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json']; export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json']; export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json']; export type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json']; +export type AdminCaptchaCurrentResponse = operations['admin___captcha___current']['responses']['200']['content']['application/json']; +export type AdminCaptchaSaveRequest = operations['admin___captcha___save']['requestBody']['content']['application/json']; +export type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json']; +export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json']; export type AdminDeleteAllFilesOfAUserRequest = operations['admin___delete-all-files-of-a-user']['requestBody']['content']['application/json']; -export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json']; -export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json']; export type AdminDriveFilesRequest = operations['admin___drive___files']['requestBody']['content']['application/json']; export type AdminDriveFilesResponse = operations['admin___drive___files']['responses']['200']['content']['application/json']; export type AdminDriveShowFileRequest = operations['admin___drive___show-file']['requestBody']['content']['application/json']; export type AdminDriveShowFileResponse = operations['admin___drive___show-file']['responses']['200']['content']['application/json']; -export type AdminEmojiAddAliasesBulkRequest = operations['admin___emoji___add-aliases-bulk']['requestBody']['content']['application/json']; export type AdminEmojiAddRequest = operations['admin___emoji___add']['requestBody']['content']['application/json']; export type AdminEmojiAddResponse = operations['admin___emoji___add']['responses']['200']['content']['application/json']; +export type AdminEmojiAddAliasesBulkRequest = operations['admin___emoji___add-aliases-bulk']['requestBody']['content']['application/json']; export type AdminEmojiCopyRequest = operations['admin___emoji___copy']['requestBody']['content']['application/json']; export type AdminEmojiCopyResponse = operations['admin___emoji___copy']['responses']['200']['content']['application/json']; -export type AdminEmojiDeleteBulkRequest = operations['admin___emoji___delete-bulk']['requestBody']['content']['application/json']; export type AdminEmojiDeleteRequest = operations['admin___emoji___delete']['requestBody']['content']['application/json']; +export type AdminEmojiDeleteBulkRequest = operations['admin___emoji___delete-bulk']['requestBody']['content']['application/json']; export type AdminEmojiImportZipRequest = operations['admin___emoji___import-zip']['requestBody']['content']['application/json']; -export type AdminEmojiListRemoteRequest = operations['admin___emoji___list-remote']['requestBody']['content']['application/json']; -export type AdminEmojiListRemoteResponse = operations['admin___emoji___list-remote']['responses']['200']['content']['application/json']; export type AdminEmojiListRequest = operations['admin___emoji___list']['requestBody']['content']['application/json']; export type AdminEmojiListResponse = operations['admin___emoji___list']['responses']['200']['content']['application/json']; +export type AdminEmojiListRemoteRequest = operations['admin___emoji___list-remote']['requestBody']['content']['application/json']; +export type AdminEmojiListRemoteResponse = operations['admin___emoji___list-remote']['responses']['200']['content']['application/json']; export type AdminEmojiRemoveAliasesBulkRequest = operations['admin___emoji___remove-aliases-bulk']['requestBody']['content']['application/json']; export type AdminEmojiSetAliasesBulkRequest = operations['admin___emoji___set-aliases-bulk']['requestBody']['content']['application/json']; export type AdminEmojiSetCategoryBulkRequest = operations['admin___emoji___set-category-bulk']['requestBody']['content']['application/json']; @@ -67,6 +69,7 @@ export type AdminFederationDeleteAllFilesRequest = operations['admin___federatio export type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['admin___federation___refresh-remote-instance-metadata']['requestBody']['content']['application/json']; export type AdminFederationRemoveAllFollowingRequest = operations['admin___federation___remove-all-following']['requestBody']['content']['application/json']; export type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json']; +export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json']; export type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json']; export type AdminGetTableStatsResponse = operations['admin___get-table-stats']['responses']['200']['content']['application/json']; export type AdminGetUserIpsRequest = operations['admin___get-user-ips']['requestBody']['content']['application/json']; @@ -75,6 +78,8 @@ export type AdminInviteCreateRequest = operations['admin___invite___create']['re export type AdminInviteCreateResponse = operations['admin___invite___create']['responses']['200']['content']['application/json']; export type AdminInviteListRequest = operations['admin___invite___list']['requestBody']['content']['application/json']; export type AdminInviteListResponse = operations['admin___invite___list']['responses']['200']['content']['application/json']; +export type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json']; +export type AdminNsfwUserRequest = operations['admin___nsfw-user']['requestBody']['content']['application/json']; export type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json']; export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json']; export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json']; @@ -87,39 +92,28 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json']; export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json']; export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json']; -export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json']; -export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json']; -export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json']; -export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json']; -export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json']; -export type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json']; -export type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json']; -export type AdminShowUserResponse = operations['admin___show-user']['responses']['200']['content']['application/json']; -export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json']; -export type AdminShowUsersResponse = operations['admin___show-users']['responses']['200']['content']['application/json']; -export type AdminNsfwUserRequest = operations['admin___nsfw-user']['requestBody']['content']['application/json']; -export type AdminUnnsfwUserRequest = operations['admin___unnsfw-user']['requestBody']['content']['application/json']; -export type AdminSilenceUserRequest = operations['admin___silence-user']['requestBody']['content']['application/json']; -export type AdminUnsilenceUserRequest = operations['admin___unsilence-user']['requestBody']['content']['application/json']; -export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json']; -export type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json']; -export type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json']; -export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; -export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json']; -export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json']; -export type AdminUpdateUserNoteRequest = operations['admin___update-user-note']['requestBody']['content']['application/json']; +export type AdminRolesAssignRequest = operations['admin___roles___assign']['requestBody']['content']['application/json']; export type AdminRolesCreateRequest = operations['admin___roles___create']['requestBody']['content']['application/json']; export type AdminRolesCreateResponse = operations['admin___roles___create']['responses']['200']['content']['application/json']; export type AdminRolesDeleteRequest = operations['admin___roles___delete']['requestBody']['content']['application/json']; export type AdminRolesListResponse = operations['admin___roles___list']['responses']['200']['content']['application/json']; export type AdminRolesShowRequest = operations['admin___roles___show']['requestBody']['content']['application/json']; export type AdminRolesShowResponse = operations['admin___roles___show']['responses']['200']['content']['application/json']; -export type AdminRolesUpdateRequest = operations['admin___roles___update']['requestBody']['content']['application/json']; -export type AdminRolesAssignRequest = operations['admin___roles___assign']['requestBody']['content']['application/json']; export type AdminRolesUnassignRequest = operations['admin___roles___unassign']['requestBody']['content']['application/json']; +export type AdminRolesUpdateRequest = operations['admin___roles___update']['requestBody']['content']['application/json']; export type AdminRolesUpdateDefaultPoliciesRequest = operations['admin___roles___update-default-policies']['requestBody']['content']['application/json']; export type AdminRolesUsersRequest = operations['admin___roles___users']['requestBody']['content']['application/json']; export type AdminRolesUsersResponse = operations['admin___roles___users']['responses']['200']['content']['application/json']; +export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json']; +export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json']; +export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json']; +export type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json']; +export type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json']; +export type AdminShowUserResponse = operations['admin___show-user']['responses']['200']['content']['application/json']; +export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json']; +export type AdminShowUsersResponse = operations['admin___show-users']['responses']['200']['content']['application/json']; +export type AdminSilenceUserRequest = operations['admin___silence-user']['requestBody']['content']['application/json']; +export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json']; export type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json']; export type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json']; export type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json']; @@ -127,9 +121,17 @@ export type AdminSystemWebhookListRequest = operations['admin___system-webhook__ export type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json']; export type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json']; export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json']; +export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json']; export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json']; export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json']; -export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json']; +export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json']; +export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json']; +export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; +export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json']; +export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json']; +export type AdminUpdateUserNoteRequest = operations['admin___update-user-note']['requestBody']['content']['application/json']; +export type AdminUnnsfwUserRequest = operations['admin___unnsfw-user']['requestBody']['content']['application/json']; +export type AdminUnsilenceUserRequest = operations['admin___unsilence-user']['requestBody']['content']['application/json']; export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json']; export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json']; export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json']; @@ -165,26 +167,29 @@ export type BlockingDeleteRequest = operations['blocking___delete']['requestBody export type BlockingDeleteResponse = operations['blocking___delete']['responses']['200']['content']['application/json']; export type BlockingListRequest = operations['blocking___list']['requestBody']['content']['application/json']; export type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json']; +export type BubbleGameRankingRequest = operations['bubble-game___ranking']['requestBody']['content']['application/json']; +export type BubbleGameRankingResponse = operations['bubble-game___ranking']['responses']['200']['content']['application/json']; +export type BubbleGameRegisterRequest = operations['bubble-game___register']['requestBody']['content']['application/json']; export type ChannelsCreateRequest = operations['channels___create']['requestBody']['content']['application/json']; export type ChannelsCreateResponse = operations['channels___create']['responses']['200']['content']['application/json']; +export type ChannelsFavoriteRequest = operations['channels___favorite']['requestBody']['content']['application/json']; export type ChannelsFeaturedResponse = operations['channels___featured']['responses']['200']['content']['application/json']; export type ChannelsFollowRequest = operations['channels___follow']['requestBody']['content']['application/json']; export type ChannelsFollowedRequest = operations['channels___followed']['requestBody']['content']['application/json']; export type ChannelsFollowedResponse = operations['channels___followed']['responses']['200']['content']['application/json']; +export type ChannelsMyFavoritesResponse = operations['channels___my-favorites']['responses']['200']['content']['application/json']; export type ChannelsOwnedRequest = operations['channels___owned']['requestBody']['content']['application/json']; export type ChannelsOwnedResponse = operations['channels___owned']['responses']['200']['content']['application/json']; +export type ChannelsSearchRequest = operations['channels___search']['requestBody']['content']['application/json']; +export type ChannelsSearchResponse = operations['channels___search']['responses']['200']['content']['application/json']; export type ChannelsShowRequest = operations['channels___show']['requestBody']['content']['application/json']; export type ChannelsShowResponse = operations['channels___show']['responses']['200']['content']['application/json']; export type ChannelsTimelineRequest = operations['channels___timeline']['requestBody']['content']['application/json']; export type ChannelsTimelineResponse = operations['channels___timeline']['responses']['200']['content']['application/json']; +export type ChannelsUnfavoriteRequest = operations['channels___unfavorite']['requestBody']['content']['application/json']; export type ChannelsUnfollowRequest = operations['channels___unfollow']['requestBody']['content']['application/json']; export type ChannelsUpdateRequest = operations['channels___update']['requestBody']['content']['application/json']; export type ChannelsUpdateResponse = operations['channels___update']['responses']['200']['content']['application/json']; -export type ChannelsFavoriteRequest = operations['channels___favorite']['requestBody']['content']['application/json']; -export type ChannelsUnfavoriteRequest = operations['channels___unfavorite']['requestBody']['content']['application/json']; -export type ChannelsMyFavoritesResponse = operations['channels___my-favorites']['responses']['200']['content']['application/json']; -export type ChannelsSearchRequest = operations['channels___search']['requestBody']['content']['application/json']; -export type ChannelsSearchResponse = operations['channels___search']['responses']['200']['content']['application/json']; export type ChartsActiveUsersRequest = operations['charts___active-users']['requestBody']['content']['application/json']; export type ChartsActiveUsersResponse = operations['charts___active-users']['responses']['200']['content']['application/json']; export type ChartsApRequestRequest = operations['charts___ap-request']['requestBody']['content']['application/json']; @@ -210,20 +215,20 @@ export type ChartsUserReactionsResponse = operations['charts___user___reactions' export type ChartsUsersRequest = operations['charts___users']['requestBody']['content']['application/json']; export type ChartsUsersResponse = operations['charts___users']['responses']['200']['content']['application/json']; export type ClipsAddNoteRequest = operations['clips___add-note']['requestBody']['content']['application/json']; -export type ClipsRemoveNoteRequest = operations['clips___remove-note']['requestBody']['content']['application/json']; export type ClipsCreateRequest = operations['clips___create']['requestBody']['content']['application/json']; export type ClipsCreateResponse = operations['clips___create']['responses']['200']['content']['application/json']; export type ClipsDeleteRequest = operations['clips___delete']['requestBody']['content']['application/json']; +export type ClipsFavoriteRequest = operations['clips___favorite']['requestBody']['content']['application/json']; export type ClipsListResponse = operations['clips___list']['responses']['200']['content']['application/json']; +export type ClipsMyFavoritesResponse = operations['clips___my-favorites']['responses']['200']['content']['application/json']; export type ClipsNotesRequest = operations['clips___notes']['requestBody']['content']['application/json']; export type ClipsNotesResponse = operations['clips___notes']['responses']['200']['content']['application/json']; +export type ClipsRemoveNoteRequest = operations['clips___remove-note']['requestBody']['content']['application/json']; export type ClipsShowRequest = operations['clips___show']['requestBody']['content']['application/json']; export type ClipsShowResponse = operations['clips___show']['responses']['200']['content']['application/json']; +export type ClipsUnfavoriteRequest = operations['clips___unfavorite']['requestBody']['content']['application/json']; export type ClipsUpdateRequest = operations['clips___update']['requestBody']['content']['application/json']; export type ClipsUpdateResponse = operations['clips___update']['responses']['200']['content']['application/json']; -export type ClipsFavoriteRequest = operations['clips___favorite']['requestBody']['content']['application/json']; -export type ClipsUnfavoriteRequest = operations['clips___unfavorite']['requestBody']['content']['application/json']; -export type ClipsMyFavoritesResponse = operations['clips___my-favorites']['responses']['200']['content']['application/json']; export type DriveResponse = operations['drive']['responses']['200']['content']['application/json']; export type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json']; export type DriveFilesResponse = operations['drive___files']['responses']['200']['content']['application/json']; @@ -234,10 +239,10 @@ export type DriveFilesCheckExistenceResponse = operations['drive___files___check export type DriveFilesCreateRequest = operations['drive___files___create']['requestBody']['content']['multipart/form-data']; export type DriveFilesCreateResponse = operations['drive___files___create']['responses']['200']['content']['application/json']; export type DriveFilesDeleteRequest = operations['drive___files___delete']['requestBody']['content']['application/json']; -export type DriveFilesFindByHashRequest = operations['drive___files___find-by-hash']['requestBody']['content']['application/json']; -export type DriveFilesFindByHashResponse = operations['drive___files___find-by-hash']['responses']['200']['content']['application/json']; export type DriveFilesFindRequest = operations['drive___files___find']['requestBody']['content']['application/json']; export type DriveFilesFindResponse = operations['drive___files___find']['responses']['200']['content']['application/json']; +export type DriveFilesFindByHashRequest = operations['drive___files___find-by-hash']['requestBody']['content']['application/json']; +export type DriveFilesFindByHashResponse = operations['drive___files___find-by-hash']['responses']['200']['content']['application/json']; export type DriveFilesShowRequest = operations['drive___files___show']['requestBody']['content']['application/json']; export type DriveFilesShowResponse = operations['drive___files___show']['responses']['200']['content']['application/json']; export type DriveFilesUpdateRequest = operations['drive___files___update']['requestBody']['content']['application/json']; @@ -258,6 +263,9 @@ export type DriveStreamRequest = operations['drive___stream']['requestBody']['co export type DriveStreamResponse = operations['drive___stream']['responses']['200']['content']['application/json']; export type EmailAddressAvailableRequest = operations['email-address___available']['requestBody']['content']['application/json']; export type EmailAddressAvailableResponse = operations['email-address___available']['responses']['200']['content']['application/json']; +export type EmojiRequest = operations['emoji']['requestBody']['content']['application/json']; +export type EmojiResponse = operations['emoji']['responses']['200']['content']['application/json']; +export type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json']; export type EndpointRequest = operations['endpoint']['requestBody']['content']['application/json']; export type EndpointResponse = operations['endpoint']['responses']['200']['content']['application/json']; export type EndpointsResponse = operations['endpoints']['responses']['200']['content']['application/json']; @@ -269,18 +277,33 @@ export type FederationInstancesRequest = operations['federation___instances']['r export type FederationInstancesResponse = operations['federation___instances']['responses']['200']['content']['application/json']; export type FederationShowInstanceRequest = operations['federation___show-instance']['requestBody']['content']['application/json']; export type FederationShowInstanceResponse = operations['federation___show-instance']['responses']['200']['content']['application/json']; +export type FederationStatsRequest = operations['federation___stats']['requestBody']['content']['application/json']; +export type FederationStatsResponse = operations['federation___stats']['responses']['200']['content']['application/json']; export type FederationUpdateRemoteUserRequest = operations['federation___update-remote-user']['requestBody']['content']['application/json']; export type FederationUsersRequest = operations['federation___users']['requestBody']['content']['application/json']; export type FederationUsersResponse = operations['federation___users']['responses']['200']['content']['application/json']; -export type FederationStatsRequest = operations['federation___stats']['requestBody']['content']['application/json']; -export type FederationStatsResponse = operations['federation___stats']['responses']['200']['content']['application/json']; +export type FetchExternalResourcesRequest = operations['fetch-external-resources']['requestBody']['content']['application/json']; +export type FetchExternalResourcesResponse = operations['fetch-external-resources']['responses']['200']['content']['application/json']; +export type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json']; +export type FetchRssResponse = operations['fetch-rss']['responses']['200']['content']['application/json']; +export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json']; +export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json']; +export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json']; +export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json']; +export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json']; +export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json']; +export type FlashMyRequest = operations['flash___my']['requestBody']['content']['application/json']; +export type FlashMyResponse = operations['flash___my']['responses']['200']['content']['application/json']; +export type FlashMyLikesRequest = operations['flash___my-likes']['requestBody']['content']['application/json']; +export type FlashMyLikesResponse = operations['flash___my-likes']['responses']['200']['content']['application/json']; +export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json']; +export type FlashShowResponse = operations['flash___show']['responses']['200']['content']['application/json']; +export type FlashUnlikeRequest = operations['flash___unlike']['requestBody']['content']['application/json']; +export type FlashUpdateRequest = operations['flash___update']['requestBody']['content']['application/json']; export type FollowingCreateRequest = operations['following___create']['requestBody']['content']['application/json']; export type FollowingCreateResponse = operations['following___create']['responses']['200']['content']['application/json']; export type FollowingDeleteRequest = operations['following___delete']['requestBody']['content']['application/json']; export type FollowingDeleteResponse = operations['following___delete']['responses']['200']['content']['application/json']; -export type FollowingUpdateRequest = operations['following___update']['requestBody']['content']['application/json']; -export type FollowingUpdateResponse = operations['following___update']['responses']['200']['content']['application/json']; -export type FollowingUpdateAllRequest = operations['following___update-all']['requestBody']['content']['application/json']; export type FollowingInvalidateRequest = operations['following___invalidate']['requestBody']['content']['application/json']; export type FollowingInvalidateResponse = operations['following___invalidate']['responses']['200']['content']['application/json']; export type FollowingRequestsAcceptRequest = operations['following___requests___accept']['requestBody']['content']['application/json']; @@ -288,9 +311,12 @@ export type FollowingRequestsCancelRequest = operations['following___requests___ export type FollowingRequestsCancelResponse = operations['following___requests___cancel']['responses']['200']['content']['application/json']; export type FollowingRequestsListRequest = operations['following___requests___list']['requestBody']['content']['application/json']; export type FollowingRequestsListResponse = operations['following___requests___list']['responses']['200']['content']['application/json']; +export type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json']; export type FollowingRequestsSentRequest = operations['following___requests___sent']['requestBody']['content']['application/json']; export type FollowingRequestsSentResponse = operations['following___requests___sent']['responses']['200']['content']['application/json']; -export type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json']; +export type FollowingUpdateRequest = operations['following___update']['requestBody']['content']['application/json']; +export type FollowingUpdateResponse = operations['following___update']['responses']['200']['content']['application/json']; +export type FollowingUpdateAllRequest = operations['following___update-all']['requestBody']['content']['application/json']; export type GalleryFeaturedRequest = operations['gallery___featured']['requestBody']['content']['application/json']; export type GalleryFeaturedResponse = operations['gallery___featured']['responses']['200']['content']['application/json']; export type GalleryPopularResponse = operations['gallery___popular']['responses']['200']['content']['application/json']; @@ -305,8 +331,8 @@ export type GalleryPostsShowResponse = operations['gallery___posts___show']['res export type GalleryPostsUnlikeRequest = operations['gallery___posts___unlike']['requestBody']['content']['application/json']; export type GalleryPostsUpdateRequest = operations['gallery___posts___update']['requestBody']['content']['application/json']; export type GalleryPostsUpdateResponse = operations['gallery___posts___update']['responses']['200']['content']['application/json']; -export type GetOnlineUsersCountResponse = operations['get-online-users-count']['responses']['200']['content']['application/json']; export type GetAvatarDecorationsResponse = operations['get-avatar-decorations']['responses']['200']['content']['application/json']; +export type GetOnlineUsersCountResponse = operations['get-online-users-count']['responses']['200']['content']['application/json']; export type HashtagsListRequest = operations['hashtags___list']['requestBody']['content']['application/json']; export type HashtagsListResponse = operations['hashtags___list']['responses']['200']['content']['application/json']; export type HashtagsSearchRequest = operations['hashtags___search']['requestBody']['content']['application/json']; @@ -322,19 +348,19 @@ export type I2faDoneResponse = operations['i___2fa___done']['responses']['200'][ export type I2faKeyDoneRequest = operations['i___2fa___key-done']['requestBody']['content']['application/json']; export type I2faKeyDoneResponse = operations['i___2fa___key-done']['responses']['200']['content']['application/json']; export type I2faPasswordLessRequest = operations['i___2fa___password-less']['requestBody']['content']['application/json']; -export type I2faRegisterKeyRequest = operations['i___2fa___register-key']['requestBody']['content']['application/json']; -export type I2faRegisterKeyResponse = operations['i___2fa___register-key']['responses']['200']['content']['application/json']; export type I2faRegisterRequest = operations['i___2fa___register']['requestBody']['content']['application/json']; export type I2faRegisterResponse = operations['i___2fa___register']['responses']['200']['content']['application/json']; -export type I2faUpdateKeyRequest = operations['i___2fa___update-key']['requestBody']['content']['application/json']; +export type I2faRegisterKeyRequest = operations['i___2fa___register-key']['requestBody']['content']['application/json']; +export type I2faRegisterKeyResponse = operations['i___2fa___register-key']['responses']['200']['content']['application/json']; export type I2faRemoveKeyRequest = operations['i___2fa___remove-key']['requestBody']['content']['application/json']; export type I2faUnregisterRequest = operations['i___2fa___unregister']['requestBody']['content']['application/json']; +export type I2faUpdateKeyRequest = operations['i___2fa___update-key']['requestBody']['content']['application/json']; export type IAppsRequest = operations['i___apps']['requestBody']['content']['application/json']; export type IAppsResponse = operations['i___apps']['responses']['200']['content']['application/json']; export type IAuthorizedAppsRequest = operations['i___authorized-apps']['requestBody']['content']['application/json']; export type IAuthorizedAppsResponse = operations['i___authorized-apps']['responses']['200']['content']['application/json']; -export type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json']; export type IChangePasswordRequest = operations['i___change-password']['requestBody']['content']['application/json']; +export type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json']; export type IDeleteAccountRequest = operations['i___delete-account']['requestBody']['content']['application/json']; export type IExportFollowingRequest = operations['i___export-following']['requestBody']['content']['application/json']; export type IFavoritesRequest = operations['i___favorites']['requestBody']['content']['application/json']; @@ -343,12 +369,14 @@ export type IGalleryLikesRequest = operations['i___gallery___likes']['requestBod export type IGalleryLikesResponse = operations['i___gallery___likes']['responses']['200']['content']['application/json']; export type IGalleryPostsRequest = operations['i___gallery___posts']['requestBody']['content']['application/json']; export type IGalleryPostsResponse = operations['i___gallery___posts']['responses']['200']['content']['application/json']; +export type IImportAntennasRequest = operations['i___import-antennas']['requestBody']['content']['application/json']; export type IImportBlockingRequest = operations['i___import-blocking']['requestBody']['content']['application/json']; export type IImportFollowingRequest = operations['i___import-following']['requestBody']['content']['application/json']; export type IImportNotesRequest = operations['i___import-notes']['requestBody']['content']['application/json']; export type IImportMutingRequest = operations['i___import-muting']['requestBody']['content']['application/json']; export type IImportUserListsRequest = operations['i___import-user-lists']['requestBody']['content']['application/json']; -export type IImportAntennasRequest = operations['i___import-antennas']['requestBody']['content']['application/json']; +export type IMoveRequest = operations['i___move']['requestBody']['content']['application/json']; +export type IMoveResponse = operations['i___move']['responses']['200']['content']['application/json']; export type INotificationsRequest = operations['i___notifications']['requestBody']['content']['application/json']; export type INotificationsResponse = operations['i___notifications']['responses']['200']['content']['application/json']; export type INotificationsGroupedRequest = operations['i___notifications-grouped']['requestBody']['content']['application/json']; @@ -361,17 +389,17 @@ export type IPinRequest = operations['i___pin']['requestBody']['content']['appli export type IPinResponse = operations['i___pin']['responses']['200']['content']['application/json']; export type IReadAnnouncementRequest = operations['i___read-announcement']['requestBody']['content']['application/json']; export type IRegenerateTokenRequest = operations['i___regenerate-token']['requestBody']['content']['application/json']; +export type IRegistryGetRequest = operations['i___registry___get']['requestBody']['content']['application/json']; +export type IRegistryGetResponse = operations['i___registry___get']['responses']['200']['content']['application/json']; export type IRegistryGetAllRequest = operations['i___registry___get-all']['requestBody']['content']['application/json']; export type IRegistryGetAllResponse = operations['i___registry___get-all']['responses']['200']['content']['application/json']; export type IRegistryGetUnsecureRequest = operations['i___registry___get-unsecure']['requestBody']['content']['application/json']; export type IRegistryGetDetailRequest = operations['i___registry___get-detail']['requestBody']['content']['application/json']; export type IRegistryGetDetailResponse = operations['i___registry___get-detail']['responses']['200']['content']['application/json']; -export type IRegistryGetRequest = operations['i___registry___get']['requestBody']['content']['application/json']; -export type IRegistryGetResponse = operations['i___registry___get']['responses']['200']['content']['application/json']; -export type IRegistryKeysWithTypeRequest = operations['i___registry___keys-with-type']['requestBody']['content']['application/json']; -export type IRegistryKeysWithTypeResponse = operations['i___registry___keys-with-type']['responses']['200']['content']['application/json']; export type IRegistryKeysRequest = operations['i___registry___keys']['requestBody']['content']['application/json']; export type IRegistryKeysResponse = operations['i___registry___keys']['responses']['200']['content']['application/json']; +export type IRegistryKeysWithTypeRequest = operations['i___registry___keys-with-type']['requestBody']['content']['application/json']; +export type IRegistryKeysWithTypeResponse = operations['i___registry___keys-with-type']['responses']['200']['content']['application/json']; export type IRegistryRemoveRequest = operations['i___registry___remove']['requestBody']['content']['application/json']; export type IRegistryScopesWithDomainResponse = operations['i___registry___scopes-with-domain']['responses']['200']['content']['application/json']; export type IRegistrySetRequest = operations['i___registry___set']['requestBody']['content']['application/json']; @@ -380,40 +408,31 @@ export type ISigninHistoryRequest = operations['i___signin-history']['requestBod export type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json']; export type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; export type IUnpinResponse = operations['i___unpin']['responses']['200']['content']['application/json']; -export type IUpdateEmailRequest = operations['i___update-email']['requestBody']['content']['application/json']; -export type IUpdateEmailResponse = operations['i___update-email']['responses']['200']['content']['application/json']; export type IUpdateRequest = operations['i___update']['requestBody']['content']['application/json']; export type IUpdateResponse = operations['i___update']['responses']['200']['content']['application/json']; -export type IMoveRequest = operations['i___move']['requestBody']['content']['application/json']; -export type IMoveResponse = operations['i___move']['responses']['200']['content']['application/json']; +export type IUpdateEmailRequest = operations['i___update-email']['requestBody']['content']['application/json']; +export type IUpdateEmailResponse = operations['i___update-email']['responses']['200']['content']['application/json']; export type IWebhooksCreateRequest = operations['i___webhooks___create']['requestBody']['content']['application/json']; export type IWebhooksCreateResponse = operations['i___webhooks___create']['responses']['200']['content']['application/json']; +export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json']; export type IWebhooksListResponse = operations['i___webhooks___list']['responses']['200']['content']['application/json']; export type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['content']['application/json']; export type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json']; -export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json']; -export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json']; export type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json']; +export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json']; export type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json']; export type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json']; +export type InviteLimitResponse = operations['invite___limit']['responses']['200']['content']['application/json']; export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json']; export type InviteListResponse = operations['invite___list']['responses']['200']['content']['application/json']; -export type InviteLimitResponse = operations['invite___limit']['responses']['200']['content']['application/json']; export type MetaRequest = operations['meta']['requestBody']['content']['application/json']; export type MetaResponse = operations['meta']['responses']['200']['content']['application/json']; -export type EmojisResponse = operations['emojis']['responses']['200']['content']['application/json']; -export type EmojiRequest = operations['emoji']['requestBody']['content']['application/json']; -export type EmojiResponse = operations['emoji']['responses']['200']['content']['application/json']; export type MiauthGenTokenRequest = operations['miauth___gen-token']['requestBody']['content']['application/json']; export type MiauthGenTokenResponse = operations['miauth___gen-token']['responses']['200']['content']['application/json']; export type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; export type MuteDeleteRequest = operations['mute___delete']['requestBody']['content']['application/json']; export type MuteListRequest = operations['mute___list']['requestBody']['content']['application/json']; export type MuteListResponse = operations['mute___list']['responses']['200']['content']['application/json']; -export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json']; -export type RenoteMuteDeleteRequest = operations['renote-mute___delete']['requestBody']['content']['application/json']; -export type RenoteMuteListRequest = operations['renote-mute___list']['requestBody']['content']['application/json']; -export type RenoteMuteListResponse = operations['renote-mute___list']['responses']['200']['content']['application/json']; export type MyAppsRequest = operations['my___apps']['requestBody']['content']['application/json']; export type MyAppsResponse = operations['my___apps']['responses']['200']['content']['application/json']; export type NotesRequest = operations['notes']['requestBody']['content']['application/json']; @@ -492,49 +511,58 @@ export type PagesShowRequest = operations['pages___show']['requestBody']['conten export type PagesShowResponse = operations['pages___show']['responses']['200']['content']['application/json']; export type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content']['application/json']; export type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json']; -export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json']; -export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json']; -export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json']; -export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json']; -export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json']; -export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json']; -export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json']; -export type FlashShowResponse = operations['flash___show']['responses']['200']['content']['application/json']; -export type FlashUnlikeRequest = operations['flash___unlike']['requestBody']['content']['application/json']; -export type FlashUpdateRequest = operations['flash___update']['requestBody']['content']['application/json']; -export type FlashMyRequest = operations['flash___my']['requestBody']['content']['application/json']; -export type FlashMyResponse = operations['flash___my']['responses']['200']['content']['application/json']; -export type FlashMyLikesRequest = operations['flash___my-likes']['requestBody']['content']['application/json']; -export type FlashMyLikesResponse = operations['flash___my-likes']['responses']['200']['content']['application/json']; export type PingResponse = operations['ping']['responses']['200']['content']['application/json']; export type PinnedUsersResponse = operations['pinned-users']['responses']['200']['content']['application/json']; export type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json']; +export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json']; +export type RenoteMuteDeleteRequest = operations['renote-mute___delete']['requestBody']['content']['application/json']; +export type RenoteMuteListRequest = operations['renote-mute___list']['requestBody']['content']['application/json']; +export type RenoteMuteListResponse = operations['renote-mute___list']['responses']['200']['content']['application/json']; +export type RequestResetPasswordRequest = operations['request-reset-password']['requestBody']['content']['application/json']; +export type ResetPasswordRequest = operations['reset-password']['requestBody']['content']['application/json']; +export type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; +export type ReversiCancelMatchRequest = operations['reversi___cancel-match']['requestBody']['content']['application/json']; +export type ReversiGamesRequest = operations['reversi___games']['requestBody']['content']['application/json']; +export type ReversiGamesResponse = operations['reversi___games']['responses']['200']['content']['application/json']; +export type ReversiInvitationsResponse = operations['reversi___invitations']['responses']['200']['content']['application/json']; +export type ReversiMatchRequest = operations['reversi___match']['requestBody']['content']['application/json']; +export type ReversiMatchResponse = operations['reversi___match']['responses']['200']['content']['application/json']; +export type ReversiShowGameRequest = operations['reversi___show-game']['requestBody']['content']['application/json']; +export type ReversiShowGameResponse = operations['reversi___show-game']['responses']['200']['content']['application/json']; +export type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json']; +export type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json']; +export type ReversiVerifyResponse = operations['reversi___verify']['responses']['200']['content']['application/json']; export type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json']; +export type RolesNotesRequest = operations['roles___notes']['requestBody']['content']['application/json']; +export type RolesNotesResponse = operations['roles___notes']['responses']['200']['content']['application/json']; export type RolesShowRequest = operations['roles___show']['requestBody']['content']['application/json']; export type RolesShowResponse = operations['roles___show']['responses']['200']['content']['application/json']; export type RolesUsersRequest = operations['roles___users']['requestBody']['content']['application/json']; export type RolesUsersResponse = operations['roles___users']['responses']['200']['content']['application/json']; -export type RolesNotesRequest = operations['roles___notes']['requestBody']['content']['application/json']; -export type RolesNotesResponse = operations['roles___notes']['responses']['200']['content']['application/json']; -export type RequestResetPasswordRequest = operations['request-reset-password']['requestBody']['content']['application/json']; -export type ResetPasswordRequest = operations['reset-password']['requestBody']['content']['application/json']; export type ServerInfoResponse = operations['server-info']['responses']['200']['content']['application/json']; +export type SponsorsRequest = operations['sponsors']['requestBody']['content']['application/json']; export type StatsResponse = operations['stats']['responses']['200']['content']['application/json']; +export type SwRegisterRequest = operations['sw___register']['requestBody']['content']['application/json']; +export type SwRegisterResponse = operations['sw___register']['responses']['200']['content']['application/json']; export type SwShowRegistrationRequest = operations['sw___show-registration']['requestBody']['content']['application/json']; export type SwShowRegistrationResponse = operations['sw___show-registration']['responses']['200']['content']['application/json']; +export type SwUnregisterRequest = operations['sw___unregister']['requestBody']['content']['application/json']; export type SwUpdateRegistrationRequest = operations['sw___update-registration']['requestBody']['content']['application/json']; export type SwUpdateRegistrationResponse = operations['sw___update-registration']['responses']['200']['content']['application/json']; -export type SwRegisterRequest = operations['sw___register']['requestBody']['content']['application/json']; -export type SwRegisterResponse = operations['sw___register']['responses']['200']['content']['application/json']; -export type SwUnregisterRequest = operations['sw___unregister']['requestBody']['content']['application/json']; export type TestRequest = operations['test']['requestBody']['content']['application/json']; export type TestResponse = operations['test']['responses']['200']['content']['application/json']; export type UsernameAvailableRequest = operations['username___available']['requestBody']['content']['application/json']; export type UsernameAvailableResponse = operations['username___available']['responses']['200']['content']['application/json']; export type UsersRequest = operations['users']['requestBody']['content']['application/json']; export type UsersResponse = operations['users']['responses']['200']['content']['application/json']; +export type UsersAchievementsRequest = operations['users___achievements']['requestBody']['content']['application/json']; +export type UsersAchievementsResponse = operations['users___achievements']['responses']['200']['content']['application/json']; export type UsersClipsRequest = operations['users___clips']['requestBody']['content']['application/json']; export type UsersClipsResponse = operations['users___clips']['responses']['200']['content']['application/json']; +export type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json']; +export type UsersFeaturedNotesResponse = operations['users___featured-notes']['responses']['200']['content']['application/json']; +export type UsersFlashsRequest = operations['users___flashs']['requestBody']['content']['application/json']; +export type UsersFlashsResponse = operations['users___flashs']['responses']['200']['content']['application/json']; export type UsersFollowersRequest = operations['users___followers']['requestBody']['content']['application/json']; export type UsersFollowersResponse = operations['users___followers']['responses']['200']['content']['application/json']; export type UsersFollowingRequest = operations['users___following']['requestBody']['content']['application/json']; @@ -543,32 +571,28 @@ export type UsersGalleryPostsRequest = operations['users___gallery___posts']['re export type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json']; export type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json']; export type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json']; -export type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json']; -export type UsersFeaturedNotesResponse = operations['users___featured-notes']['responses']['200']['content']['application/json']; export type UsersListsCreateRequest = operations['users___lists___create']['requestBody']['content']['application/json']; export type UsersListsCreateResponse = operations['users___lists___create']['responses']['200']['content']['application/json']; +export type UsersListsCreateFromPublicRequest = operations['users___lists___create-from-public']['requestBody']['content']['application/json']; +export type UsersListsCreateFromPublicResponse = operations['users___lists___create-from-public']['responses']['200']['content']['application/json']; export type UsersListsDeleteRequest = operations['users___lists___delete']['requestBody']['content']['application/json']; +export type UsersListsFavoriteRequest = operations['users___lists___favorite']['requestBody']['content']['application/json']; +export type UsersListsGetMembershipsRequest = operations['users___lists___get-memberships']['requestBody']['content']['application/json']; +export type UsersListsGetMembershipsResponse = operations['users___lists___get-memberships']['responses']['200']['content']['application/json']; export type UsersListsListRequest = operations['users___lists___list']['requestBody']['content']['application/json']; export type UsersListsListResponse = operations['users___lists___list']['responses']['200']['content']['application/json']; export type UsersListsPullRequest = operations['users___lists___pull']['requestBody']['content']['application/json']; export type UsersListsPushRequest = operations['users___lists___push']['requestBody']['content']['application/json']; export type UsersListsShowRequest = operations['users___lists___show']['requestBody']['content']['application/json']; export type UsersListsShowResponse = operations['users___lists___show']['responses']['200']['content']['application/json']; -export type UsersListsFavoriteRequest = operations['users___lists___favorite']['requestBody']['content']['application/json']; export type UsersListsUnfavoriteRequest = operations['users___lists___unfavorite']['requestBody']['content']['application/json']; export type UsersListsUpdateRequest = operations['users___lists___update']['requestBody']['content']['application/json']; export type UsersListsUpdateResponse = operations['users___lists___update']['responses']['200']['content']['application/json']; -export type UsersListsCreateFromPublicRequest = operations['users___lists___create-from-public']['requestBody']['content']['application/json']; -export type UsersListsCreateFromPublicResponse = operations['users___lists___create-from-public']['responses']['200']['content']['application/json']; export type UsersListsUpdateMembershipRequest = operations['users___lists___update-membership']['requestBody']['content']['application/json']; -export type UsersListsGetMembershipsRequest = operations['users___lists___get-memberships']['requestBody']['content']['application/json']; -export type UsersListsGetMembershipsResponse = operations['users___lists___get-memberships']['responses']['200']['content']['application/json']; export type UsersNotesRequest = operations['users___notes']['requestBody']['content']['application/json']; export type UsersNotesResponse = operations['users___notes']['responses']['200']['content']['application/json']; export type UsersPagesRequest = operations['users___pages']['requestBody']['content']['application/json']; export type UsersPagesResponse = operations['users___pages']['responses']['200']['content']['application/json']; -export type UsersFlashsRequest = operations['users___flashs']['requestBody']['content']['application/json']; -export type UsersFlashsResponse = operations['users___flashs']['responses']['200']['content']['application/json']; export type UsersReactionsRequest = operations['users___reactions']['requestBody']['content']['application/json']; export type UsersReactionsResponse = operations['users___reactions']['responses']['200']['content']['application/json']; export type UsersRecommendationRequest = operations['users___recommendation']['requestBody']['content']['application/json']; @@ -576,32 +600,12 @@ export type UsersRecommendationResponse = operations['users___recommendation'][' export type UsersRelationRequest = operations['users___relation']['requestBody']['content']['application/json']; export type UsersRelationResponse = operations['users___relation']['responses']['200']['content']['application/json']; export type UsersReportAbuseRequest = operations['users___report-abuse']['requestBody']['content']['application/json']; -export type UsersSearchByUsernameAndHostRequest = operations['users___search-by-username-and-host']['requestBody']['content']['application/json']; -export type UsersSearchByUsernameAndHostResponse = operations['users___search-by-username-and-host']['responses']['200']['content']['application/json']; export type UsersSearchRequest = operations['users___search']['requestBody']['content']['application/json']; export type UsersSearchResponse = operations['users___search']['responses']['200']['content']['application/json']; +export type UsersSearchByUsernameAndHostRequest = operations['users___search-by-username-and-host']['requestBody']['content']['application/json']; +export type UsersSearchByUsernameAndHostResponse = operations['users___search-by-username-and-host']['responses']['200']['content']['application/json']; export type UsersShowRequest = operations['users___show']['requestBody']['content']['application/json']; export type UsersShowResponse = operations['users___show']['responses']['200']['content']['application/json']; -export type UsersAchievementsRequest = operations['users___achievements']['requestBody']['content']['application/json']; -export type UsersAchievementsResponse = operations['users___achievements']['responses']['200']['content']['application/json']; export type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json']; -export type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json']; -export type FetchRssResponse = operations['fetch-rss']['responses']['200']['content']['application/json']; -export type FetchExternalResourcesRequest = operations['fetch-external-resources']['requestBody']['content']['application/json']; -export type FetchExternalResourcesResponse = operations['fetch-external-resources']['responses']['200']['content']['application/json']; -export type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; -export type SponsorsRequest = operations['sponsors']['requestBody']['content']['application/json']; -export type BubbleGameRegisterRequest = operations['bubble-game___register']['requestBody']['content']['application/json']; -export type BubbleGameRankingRequest = operations['bubble-game___ranking']['requestBody']['content']['application/json']; -export type BubbleGameRankingResponse = operations['bubble-game___ranking']['responses']['200']['content']['application/json']; -export type ReversiCancelMatchRequest = operations['reversi___cancel-match']['requestBody']['content']['application/json']; -export type ReversiGamesRequest = operations['reversi___games']['requestBody']['content']['application/json']; -export type ReversiGamesResponse = operations['reversi___games']['responses']['200']['content']['application/json']; -export type ReversiMatchRequest = operations['reversi___match']['requestBody']['content']['application/json']; -export type ReversiMatchResponse = operations['reversi___match']['responses']['200']['content']['application/json']; -export type ReversiInvitationsResponse = operations['reversi___invitations']['responses']['200']['content']['application/json']; -export type ReversiShowGameRequest = operations['reversi___show-game']['requestBody']['content']['application/json']; -export type ReversiShowGameResponse = operations['reversi___show-game']['responses']['200']['content']['application/json']; -export type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json']; -export type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json']; -export type ReversiVerifyResponse = operations['reversi___verify']['responses']['200']['content']['application/json']; +export type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestBody']['content']['application/json']; +export type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 04574849d4..1a30da4437 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -33,6 +33,7 @@ export type FederationInstance = components['schemas']['FederationInstance']; export type GalleryPost = components['schemas']['GalleryPost']; export type EmojiSimple = components['schemas']['EmojiSimple']; export type EmojiDetailed = components['schemas']['EmojiDetailed']; +export type EmojiDetailedAdmin = components['schemas']['EmojiDetailedAdmin']; export type Flash = components['schemas']['Flash']; export type Signin = components['schemas']['Signin']; export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics']; diff --git a/packages/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts index 6e34ec1508..0ef2d1e7a1 100644 --- a/packages/misskey-js/src/streaming.ts +++ b/packages/misskey-js/src/streaming.ts @@ -1,8 +1,10 @@ import { EventEmitter } from 'eventemitter3'; -import _ReconnectingWebsocket from 'reconnecting-websocket'; +import _ReconnectingWebSocket, { Options } from 'reconnecting-websocket'; import type { BroadcastEvents, Channels } from './streaming.types.js'; -const ReconnectingWebsocket = _ReconnectingWebsocket as unknown as typeof _ReconnectingWebsocket['default']; +// コンストラクタã¨ã‚¯ãƒ©ã‚¹ãã®ã‚‚ã®ã®å®šç¾©ãŒä¸Šæ‰‹ã解決出æ¥ãªã„ãŸã‚å†å®šç¾© +const ReconnectingWebSocketConstructor = _ReconnectingWebSocket as unknown as typeof _ReconnectingWebSocket.default; +type ReconnectingWebSocket = _ReconnectingWebSocket.default; export function urlQuery(obj: Record<string, string | number | boolean | undefined>): string { const params = Object.entries(obj) @@ -43,7 +45,7 @@ export interface IStream extends EventEmitter<StreamEvents> { */ // eslint-disable-next-line import/no-default-export export default class Stream extends EventEmitter<StreamEvents> implements IStream { - private stream: _ReconnectingWebsocket.default; + private stream: ReconnectingWebSocket; public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; private sharedConnectionPools: Pool[] = []; private sharedConnections: SharedConnection[] = []; @@ -51,7 +53,8 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea private idCounter = 0; constructor(origin: string, user: { token: string; } | null, options?: { - WebSocket?: _ReconnectingWebsocket.Options['WebSocket']; + WebSocket?: Options['WebSocket']; + binaryType?: ReconnectingWebSocket['binaryType']; }) { super(); @@ -80,10 +83,13 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea const wsOrigin = origin.replace('http://', 'ws://').replace('https://', 'wss://'); - this.stream = new ReconnectingWebsocket(`${wsOrigin}/streaming?${query}`, '', { + this.stream = new ReconnectingWebSocketConstructor(`${wsOrigin}/streaming?${query}`, '', { minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91 WebSocket: options.WebSocket, }); + if (options.binaryType) { + this.stream.binaryType = options.binaryType; + } this.stream.addEventListener('open', this.onOpen); this.stream.addEventListener('close', this.onClose); this.stream.addEventListener('message', this.onMessage); diff --git a/packages/misskey-reversi/build.js b/packages/misskey-reversi/build.js index a80b71646f..5d534cc6fd 100644 --- a/packages/misskey-reversi/build.js +++ b/packages/misskey-reversi/build.js @@ -23,10 +23,14 @@ const options = { sourcemap: 'linked', }; +const args = process.argv.slice(2).map(arg => arg.toLowerCase()); + // builté…下をã™ã¹ã¦å‰Šé™¤ã™ã‚‹ -fs.rmSync('./built', { recursive: true, force: true }); +if (!args.includes('--no-clean')) { + fs.rmSync('./built', { recursive: true, force: true }); +} -if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { +if (args.includes('--watch')) { await watchSrc(); } else { await buildSrc(); diff --git a/packages/shared/eslint.config.js b/packages/shared/eslint.config.js index 0368d008c0..860eb4a8e8 100644 --- a/packages/shared/eslint.config.js +++ b/packages/shared/eslint.config.js @@ -32,4 +32,11 @@ export default [ '@typescript-eslint/no-var-requires': 'off', }, }, + { + rules: { + 'no-restricted-imports': ['error', { + paths: [{ name: 'punycode' }], + }], + }, + }, ]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5580f1a7e..303ea5a26a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,7 +184,7 @@ importers: version: 7.0.1 argon2: specifier: ^0.40.1 - version: 0.40.1 + version: 0.40.3 async-mutex: specifier: 0.5.0 version: 0.5.0 @@ -373,13 +373,10 @@ importers: version: 2.0.7 psl: specifier: ^1.13.0 - version: 1.13.0 + version: 1.15.0 pug: specifier: 3.0.3 version: 3.0.3 - punycode: - specifier: 2.3.1 - version: 2.3.1 qrcode: specifier: 1.5.4 version: 1.5.4 @@ -631,9 +628,6 @@ importers: '@types/pug': specifier: 2.0.10 version: 2.0.10 - '@types/punycode': - specifier: 2.1.4 - version: 2.1.4 '@types/qrcode': specifier: 1.5.5 version: 1.5.5 @@ -758,8 +752,8 @@ importers: specifier: 3.5.12 version: 3.5.12 aiscript-vscode: - specifier: github:aiscript-dev/aiscript-vscode#v0.1.11 - version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9 + specifier: github:aiscript-dev/aiscript-vscode#v0.1.15 + version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7 astring: specifier: 1.9.0 version: 1.9.0 @@ -841,7 +835,7 @@ importers: photoswipe: specifier: 5.4.4 version: 5.4.4 - punycode: + punycode.js: specifier: 2.3.1 version: 2.3.1 rollup: @@ -978,9 +972,9 @@ importers: '@types/node': specifier: 22.9.0 version: 22.9.0 - '@types/punycode': - specifier: 2.1.4 - version: 2.1.4 + '@types/punycode.js': + specifier: npm:@types/punycode@2.1.4 + version: '@types/punycode@2.1.4' '@types/sanitize-html': specifier: 2.13.0 version: 2.13.0 @@ -1131,7 +1125,7 @@ importers: misskey-js: specifier: workspace:* version: link:../misskey-js - punycode: + punycode.js: specifier: 2.3.1 version: 2.3.1 rollup: @@ -1180,9 +1174,9 @@ importers: '@types/node': specifier: 22.9.0 version: 22.9.0 - '@types/punycode': - specifier: 2.1.4 - version: 2.1.4 + '@types/punycode.js': + specifier: npm:@types/punycode@2.1.4 + version: '@types/punycode@2.1.4' '@types/tinycolor2': specifier: 1.4.6 version: 1.4.6 @@ -1277,6 +1271,9 @@ importers: eslint-plugin-vue: specifier: 9.31.0 version: 9.31.0(eslint@9.14.0) + nodemon: + specifier: 3.1.7 + version: 3.1.7 typescript: specifier: 5.6.3 version: 5.6.3 @@ -1291,13 +1288,13 @@ importers: version: 2.5.8 '@types/form-data': specifier: ^2.5.0 - version: 2.5.0 + version: 2.5.2 '@types/jest': specifier: ^29.5.10 - version: 29.5.12 + version: 29.5.14 '@types/oauth': specifier: ^0.9.4 - version: 0.9.5 + version: 0.9.6 '@types/object-assign-deep': specifier: ^0.4.3 version: 0.4.3 @@ -1309,7 +1306,7 @@ importers: version: 9.0.8 '@types/ws': specifier: ^8.5.10 - version: 8.5.11 + version: 8.5.13 axios: specifier: 1.7.4 version: 1.7.4 @@ -1318,10 +1315,10 @@ importers: version: 1.11.10 form-data: specifier: ^4.0.0 - version: 4.0.0 + version: 4.0.1 https-proxy-agent: specifier: ^7.0.2 - version: 7.0.2 + version: 7.0.5 oauth: specifier: ^0.10.0 version: 0.10.0 @@ -1346,16 +1343,16 @@ importers: devDependencies: '@typescript-eslint/eslint-plugin': specifier: ^6.12.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.1.6))(eslint@8.57.1)(typescript@5.1.6) '@typescript-eslint/parser': specifier: ^6.12.0 - version: 6.21.0(eslint@8.57.0)(typescript@5.1.6) + version: 6.21.0(eslint@8.57.1)(typescript@5.1.6) eslint: specifier: ^8.54.0 - version: 8.57.0 + version: 8.57.1 eslint-config-prettier: specifier: ^9.0.0 - version: 9.1.0(eslint@8.57.0) + version: 9.1.0(eslint@8.57.1) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@22.9.0) @@ -1370,7 +1367,7 @@ importers: version: 3.3.3 ts-jest: specifier: ^29.1.1 - version: 29.1.2(@babel/core@7.23.5)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.5))(esbuild@0.24.0)(jest@29.7.0(@types/node@22.9.0))(typescript@5.1.6) + version: 29.2.5(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.24.0)(jest@29.7.0(@types/node@22.9.0))(typescript@5.1.6) typedoc: specifier: ^0.25.3 version: 0.25.13(typescript@5.1.6) @@ -1788,6 +1785,10 @@ packages: resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} engines: {node: '>=6.9.0'} + '@babel/generator@7.23.5': + resolution: {integrity: sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.24.7': resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} engines: {node: '>=6.9.0'} @@ -1800,14 +1801,26 @@ packages: resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} engines: {node: '>=6.9.0'} + '@babel/helper-environment-visitor@7.22.20': + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + '@babel/helper-environment-visitor@7.24.7': resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} + '@babel/helper-function-name@7.23.0': + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + '@babel/helper-function-name@7.24.7': resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} engines: {node: '>=6.9.0'} + '@babel/helper-hoist-variables@7.22.5': + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + '@babel/helper-hoist-variables@7.24.7': resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} engines: {node: '>=6.9.0'} @@ -1844,6 +1857,10 @@ packages: resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} engines: {node: '>=6.9.0'} + '@babel/helper-split-export-declaration@7.22.6': + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + '@babel/helper-split-export-declaration@7.24.7': resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} engines: {node: '>=6.9.0'} @@ -1852,18 +1869,14 @@ packages: resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.25.7': - resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.24.7': resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.7': - resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.23.5': resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} @@ -1880,6 +1893,10 @@ packages: resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} engines: {node: '>=6.9.0'} + '@babel/highlight@7.23.4': + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + '@babel/highlight@7.24.7': resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} @@ -1889,8 +1906,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.25.7': - resolution: {integrity: sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==} + '@babel/parser@7.25.6': + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} engines: {node: '>=6.0.0'} hasBin: true @@ -1995,8 +2012,8 @@ packages: resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} - '@babel/types@7.25.7': - resolution: {integrity: sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==} + '@babel/types@7.25.6': + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -2076,8 +2093,8 @@ packages: '@discordapp/twemoji@15.1.0': resolution: {integrity: sha512-QdpV4ifTONAXvDjRrMohausZeGrQ1ac/Ox6togUh6Xl3XKJ/KAaMMuAEi0qsb0wDwoVTSZBll5Y6+N3hB2ktBw==} - '@emnapi/runtime@1.3.0': - resolution: {integrity: sha512-XMBySMuNZs3DM96xcJmLW4EfGnf+uGmFNjzpehMjuX5PLB5j87ar2Zc4e3PVeZ3I5g3tYtAqskB28manlF69Zw==} + '@emnapi/runtime@1.2.0': + resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==} '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} @@ -2531,8 +2548,8 @@ packages: resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.0': - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@eslint/js@9.14.0': @@ -2543,8 +2560,8 @@ packages: resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.3': - resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} + '@eslint/plugin-kit@0.2.0': + resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@fastify/accept-negotiator@2.0.0': @@ -2553,8 +2570,8 @@ packages: '@fastify/accepts@5.0.1': resolution: {integrity: sha512-8ji2MGTbceSnAXKYx/U9iWt6Fmf0zJovh0meO5rpwYS/vy0Z3QIR2J/hKmbcTpYfMu5NUliNpsAtMavmzBQhmA==} - '@fastify/ajv-compiler@4.0.1': - resolution: {integrity: sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==} + '@fastify/ajv-compiler@4.0.0': + resolution: {integrity: sha512-dt0jyLAlay14LpIn4Fg1SY7V5NJ9KH0YFDpYVQY5cgIVBvdI8908AMx5zQ0bBYPGT6Wh+bM3f2caMmOXLP3QsQ==} '@fastify/busboy@1.2.1': resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} @@ -2582,8 +2599,8 @@ packages: '@fastify/express@4.0.1': resolution: {integrity: sha512-mEQ6pawaENeZ3swqVtkxdLi8NQC5eKBkclE+7ma1qQMuB+yI6WxDyEp55pdbqPIqBQTN/cGgHv84qxVS7NKC2Q==} - '@fastify/fast-json-stringify-compiler@5.0.1': - resolution: {integrity: sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==} + '@fastify/fast-json-stringify-compiler@5.0.0': + resolution: {integrity: sha512-tywfuZfXsyxLC5kEqrMubbFa9vpAxNtuPE7j9w5si1r+6p5b981pDfZ5Y8HBqmjDQl+PABT7cV5jZgXI2j+I5g==} '@fastify/http-proxy@10.0.1': resolution: {integrity: sha512-wCMwI9RXK5ISe9G1FGPDCCD2KlSAuLtDDU8XBEfiBxYV0nt+aYm4vPhU/0+IhUM6t+r7UWiV+9OYaJxcTem9+g==} @@ -2594,8 +2611,8 @@ packages: '@fastify/multipart@9.0.1': resolution: {integrity: sha512-vt2gOCw/O4EwpN4KlLVJxth4iQlDf7T5ggw2Db2C+UbO2WJBG7y0jEBvu/HT6JIW/lBYaqrrUy9MmTpCKgXEpw==} - '@fastify/reply-from@11.0.1': - resolution: {integrity: sha512-F2Qk88gcqIIiug9V+4I6WeeV1faj1Wu798JyOnwbJcjQhm4LYrHdkpFSVwJE0g1cVjYCFFmH3OVh1HHaninttQ==} + '@fastify/reply-from@11.0.0': + resolution: {integrity: sha512-dv3o8hyy4sxhg1RN9l6ueM+PMMaIPKLjtL2T99H5M7h1Xt8d1RX3r+xC+sL5AqJqLvReX4N+7mTq9QDeB8i6Lg==} '@fastify/send@3.1.1': resolution: {integrity: sha512-LdiV2mle/2tH8vh6GwGl0ubfUAgvY+9yF9oGI1iiwVyNUVOQamvw5n+OFu6iCNNoyuCY80FFURBn4TZCbTe8LA==} @@ -2639,9 +2656,10 @@ packages: resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} engines: {node: '>=18.18.0'} - '@humanwhocodes/config-array@0.11.14': - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -2653,6 +2671,7 @@ packages: '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead '@humanwhocodes/retry@0.3.0': resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} @@ -2881,6 +2900,10 @@ packages: typescript: optional: true + '@jridgewell/gen-mapping@0.3.2': + resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -2889,6 +2912,10 @@ packages: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} + '@jridgewell/set-array@1.1.2': + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + '@jridgewell/set-array@1.2.1': resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} @@ -3192,8 +3219,8 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@1.28.0': - resolution: {integrity: sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==} + '@opentelemetry/core@1.27.0': + resolution: {integrity: sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3364,14 +3391,14 @@ packages: resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} engines: {node: '>=14'} - '@opentelemetry/resources@1.28.0': - resolution: {integrity: sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==} + '@opentelemetry/resources@1.27.0': + resolution: {integrity: sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-base@1.28.0': - resolution: {integrity: sha512-ceUVWuCpIao7Y5xE02Xs3nQi0tOGmMea17ecBdwtCvdo9ekmO+ijc9RFDgfifMl7XCBf41zne/1POM3LqSTZDA==} + '@opentelemetry/sdk-trace-base@1.27.0': + resolution: {integrity: sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3384,10 +3411,6 @@ packages: resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.28.0': - resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} - engines: {node: '>=14'} - '@opentelemetry/sql-common@0.40.1': resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} engines: {node: '>=14'} @@ -4294,8 +4317,8 @@ packages: peerDependencies: '@swc/core': '*' - '@swc/types@0.1.17': - resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} + '@swc/types@0.1.15': + resolution: {integrity: sha512-XKaZ+dzDIQ9Ot9o89oJQ/aluI17+VvUnIpYJTcZtvv1iYX6MzHh3Ik2CSR7MdPKpPwfZXHBeCingb2b4PoDVdw==} '@swc/wasm@1.2.130': resolution: {integrity: sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==} @@ -4446,8 +4469,9 @@ packages: '@types/fluent-ffmpeg@2.1.27': resolution: {integrity: sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A==} - '@types/form-data@2.5.0': - resolution: {integrity: sha512-23/wYiuckYYtFpL+4RPWiWmRQH2BjFuqCUi2+N3amB1a1Drv+i/byTrGvlLwRVLFNAZbwpbQ7JvTK+VCAPMbcg==} + '@types/form-data@2.5.2': + resolution: {integrity: sha512-tfmcyHn1Pp9YHAO5r40+UuZUPAZbUEgqTel3EuEKpmF9hPkXgR4l41853raliXnb4gwyPNoQOfvgGGlHN5WSog==} + deprecated: This is a stub types definition. form-data provides its own type definitions, so you do not need this installed. '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} @@ -4476,9 +4500,6 @@ packages: '@types/istanbul-reports@3.0.1': resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} - '@types/jest@29.5.12': - resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} - '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} @@ -4554,9 +4575,6 @@ packages: '@types/oauth2orize@1.11.5': resolution: {integrity: sha512-C6hrRoh9hCnqis39OpeUZSwgw+TIzcV0CsxwJMGfQjTx4I1r+CLmuEPzoDJr5NRTfc7OMwHNLkQwrGFLKrJjMQ==} - '@types/oauth@0.9.5': - resolution: {integrity: sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog==} - '@types/oauth@0.9.6': resolution: {integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==} @@ -4698,9 +4716,6 @@ packages: '@types/web-push@3.6.4': resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} - '@types/ws@8.5.11': - resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==} - '@types/ws@8.5.13': resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} @@ -4911,8 +4926,8 @@ packages: '@vitest/pretty-format@2.0.5': resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} - '@vitest/pretty-format@2.1.2': - resolution: {integrity: sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==} + '@vitest/pretty-format@2.1.1': + resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==} '@vitest/runner@1.6.0': resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} @@ -4932,8 +4947,8 @@ packages: '@vitest/utils@2.0.5': resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} - '@vitest/utils@2.1.2': - resolution: {integrity: sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==} + '@vitest/utils@2.1.1': + resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==} '@volar/language-core@2.2.0': resolution: {integrity: sha512-a8WG9+4OdeNDW4ywABZIM6S6UN7em8uIlM/BZ2pWQUYrVmX+m8sj/X+QadvO+Li/t/LjAqbWJQtVgxdpEWLALQ==} @@ -5073,9 +5088,9 @@ packages: resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} engines: {node: '>=18'} - aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9: - resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9} - version: 0.1.11 + aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7: + resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7} + version: 0.1.15 engines: {vscode: ^1.83.0} ajv-draft-04@1.0.0: @@ -5106,8 +5121,8 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - alien-signals@0.2.2: - resolution: {integrity: sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==} + alien-signals@0.2.1: + resolution: {integrity: sha512-FlEQrDJe9r2RI4cDlnK2zYqJezvx1uJaWEuwxsnlFqnPwvJbgitNBRumWrLDv8lA+7cCikpMxfJD2TTHiaTklA==} ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -5172,8 +5187,8 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argon2@0.40.1: - resolution: {integrity: sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==} + argon2@0.40.3: + resolution: {integrity: sha512-FrSmz4VeM91jwFvvjsQv9GYp6o/kARWoYKjbjDB2U5io1H3e5X67PYGclFDeQff6UXIhUd4aHR3mxCdBbMMuQw==} engines: {node: '>=16.17.0'} argparse@1.0.10: @@ -5870,8 +5885,8 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + cookie@1.0.1: + resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==} engines: {node: '>=18'} core-util-is@1.0.2: @@ -6431,6 +6446,27 @@ packages: eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + eslint-module-utils@2.11.0: + resolution: {integrity: sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + eslint-module-utils@2.12.0: resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} engines: {node: '>=4'} @@ -6497,9 +6533,10 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true eslint@9.14.0: @@ -6525,10 +6562,6 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.4.2: - resolution: {integrity: sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==} - engines: {node: '>=0.10'} - esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -6697,8 +6730,8 @@ packages: fastify-plugin@4.5.1: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} - fastify-plugin@5.0.1: - resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} + fastify-plugin@5.0.0: + resolution: {integrity: sha512-0725fmH/yYi8ugsjszLci+lLnGBK6cG+WSxM7edY2OXJEU7gr2JiGBoieL2h9mhTych1vFsEfXsAsGGDJ/Rd5w==} fastify-raw-body@5.0.0: resolution: {integrity: sha512-2qfoaQ3BQDhZ1gtbkKZd6n0kKxJISJGM6u/skD9ljdWItAscjXrtZ1lnjr7PavmXX9j4EyCPmBDiIsLn07d5vA==} @@ -6767,8 +6800,8 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} - find-my-way@9.1.0: - resolution: {integrity: sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==} + find-my-way@9.0.1: + resolution: {integrity: sha512-/5NN/R0pFWuff16TMajeKt2JyiW+/OE8nOO8vo1DwZTxLaIURb7lcBYPIgRPh61yCNh9l8voeKwcrkUzmB00vw==} engines: {node: '>=14'} find-package-json@1.2.0: @@ -6841,10 +6874,6 @@ packages: resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==} engines: {node: '>= 18'} - form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} - form-data@4.0.1: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} @@ -6991,10 +7020,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} @@ -7252,6 +7283,10 @@ packages: resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -7289,6 +7324,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -7940,8 +7976,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - light-my-request@6.1.0: - resolution: {integrity: sha512-+NFuhlOGoEwxeQfJ/pobkVFxcnKyDtiX847hLjuB/IzBxIl3q4VJeFI8uRCgb3AlTWL1lgOr+u5+8QdUcr33ng==} + light-my-request@6.0.0: + resolution: {integrity: sha512-kFkFXrmKCL0EEeOmJybMH5amWFd+AFvlvMlvFTRxCUwbhfapZqDmeLMPoWihntnYY6JpoQDE9k+vOzObF1fDqg==} lilconfig@3.1.1: resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} @@ -8013,8 +8049,8 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - loupe@3.1.2: - resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} @@ -8024,6 +8060,10 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lru-cache@10.0.2: + resolution: {integrity: sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==} + engines: {node: 14 || >=16.14} + lru-cache@10.2.2: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} @@ -8391,10 +8431,6 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} - minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} - engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -8539,9 +8575,9 @@ packages: node-addon-api@3.2.1: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} - node-addon-api@7.1.0: - resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} - engines: {node: ^16 || ^18 || >= 20} + node-addon-api@8.3.0: + resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==} + engines: {node: ^18 || ^20 || >= 21} node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} @@ -8568,8 +8604,8 @@ packages: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true - node-gyp-build@4.8.1: - resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true node-gyp@10.2.0: @@ -8637,10 +8673,6 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - npm-run-path@5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9208,10 +9240,6 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-selector-parser@6.0.15: - resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==} - engines: {node: '>=4'} - postcss-selector-parser@6.0.16: resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} engines: {node: '>=4'} @@ -9367,8 +9395,8 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - psl@1.13.0: - resolution: {integrity: sha512-BFwmFXiJoFqlUpZ5Qssolv15DMyc84gTBds1BjsV1BfXEo1UyyD7GsmN67n7J77uRhoSNW1AXtXKPLcBFQn9Aw==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -9415,6 +9443,10 @@ packages: pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -9577,15 +9609,15 @@ packages: regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} - regex@4.4.0: - resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==} + regex@4.3.3: + resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==} regexp.prototype.flags@1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} engines: {node: '>= 0.4'} - regexp.prototype.flags@1.5.3: - resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} remark-gfm@4.0.0: @@ -9675,6 +9707,7 @@ packages: rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rollup@4.26.0: @@ -10375,11 +10408,11 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - tldts-core@6.1.63: - resolution: {integrity: sha512-H1XCt54xY+QPbwhTgmxLkepX0MVHu3USfMmejiCOdkMbRcP22Pn2FVF127r/GWXVDmXTRezyF3Ckvhn4Fs6j7Q==} + tldts-core@6.1.61: + resolution: {integrity: sha512-In7VffkDWUPgwa+c9picLUxvb0RltVwTkSgMNFgvlGSWveCzGBemBqTsgJCL4EDFWZ6WH0fKTsot6yNhzy3ZzQ==} - tldts@6.1.63: - resolution: {integrity: sha512-YWwhsjyn9sB/1rOkSRYxvkN/wl5LFM1QDv6F2pVR+pb/jFne4EOBxHfkKVWvDIBEAw9iGOwwubHtQTm0WRT5sQ==} + tldts@6.1.61: + resolution: {integrity: sha512-rv8LUyez4Ygkopqn+M6OLItAOT9FF3REpPQDkdMx5ix8w4qkuE7Vo2o/vw1nxKQYmJDV8JpAMJQr1b+lTKf0FA==} hasBin: true tmp@0.2.3: @@ -10459,6 +10492,12 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@1.0.1: + resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -10472,12 +10511,13 @@ packages: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} - ts-jest@29.1.2: - resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} - engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} + ts-jest@29.2.5: + resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 '@jest/types': ^29.0.0 babel-jest: ^29.0.0 esbuild: '*' @@ -10486,6 +10526,8 @@ packages: peerDependenciesMeta: '@babel/core': optional: true + '@jest/transform': + optional: true '@jest/types': optional: true babel-jest: @@ -10564,8 +10606,8 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - type-fest@4.27.0: - resolution: {integrity: sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==} + type-fest@4.26.1: + resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} engines: {node: '>=16'} type-is@1.6.18: @@ -10717,8 +10759,8 @@ packages: resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} engines: {node: '>=14.0'} - undici@6.20.0: - resolution: {integrity: sha512-AITZfPuxubm31Sx0vr8bteSalEbs9wQb/BOBi9FPlD9Qpd6HxZ4Q0+hI742jBhkPb4RT2v5MQzaW5VhRVyj+9A==} + undici@6.19.8: + resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} engines: {node: '>=18.17'} unified@11.0.4: @@ -11795,7 +11837,7 @@ snapshots: '@babel/code-frame@7.23.5': dependencies: - '@babel/highlight': 7.24.7 + '@babel/highlight': 7.23.4 chalk: 2.4.2 '@babel/code-frame@7.24.7': @@ -11811,7 +11853,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.2.1 '@babel/code-frame': 7.23.5 - '@babel/generator': 7.24.7 + '@babel/generator': 7.23.5 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) '@babel/helpers': 7.23.5 @@ -11835,10 +11877,10 @@ snapshots: '@babel/helper-compilation-targets': 7.24.7 '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) '@babel/helpers': 7.24.7 - '@babel/parser': 7.25.7 + '@babel/parser': 7.24.7 '@babel/template': 7.24.7 '@babel/traverse': 7.24.7 - '@babel/types': 7.25.7 + '@babel/types': 7.24.7 convert-source-map: 2.0.0 debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -11847,9 +11889,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.24.7': + '@babel/generator@7.23.5': dependencies: '@babel/types': 7.24.7 + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.18 + jsesc: 2.5.2 + + '@babel/generator@7.24.7': + dependencies: + '@babel/types': 7.25.6 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 @@ -11870,37 +11919,48 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-environment-visitor@7.22.20': {} + '@babel/helper-environment-visitor@7.24.7': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 + + '@babel/helper-function-name@7.23.0': + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.25.6 '@babel/helper-function-name@7.24.7': dependencies: '@babel/template': 7.24.7 - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 + + '@babel/helper-hoist-variables@7.22.5': + dependencies: + '@babel/types': 7.25.6 '@babel/helper-hoist-variables@7.24.7': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 '@babel/helper-module-imports@7.22.15': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.24.7 '@babel/helper-module-imports@7.24.7': dependencies: '@babel/traverse': 7.24.7 - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color '@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 - '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-module-imports': 7.22.15 '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.24.7 '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': @@ -11910,7 +11970,7 @@ snapshots: '@babel/helper-module-imports': 7.24.7 '@babel/helper-simple-access': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7 - '@babel/helper-validator-identifier': 7.25.7 + '@babel/helper-validator-identifier': 7.24.7 transitivePeerDependencies: - supports-color @@ -11918,27 +11978,29 @@ snapshots: '@babel/helper-simple-access@7.22.5': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.24.7 '@babel/helper-simple-access@7.24.7': dependencies: '@babel/traverse': 7.24.7 - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color + '@babel/helper-split-export-declaration@7.22.6': + dependencies: + '@babel/types': 7.24.7 + '@babel/helper-split-export-declaration@7.24.7': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 '@babel/helper-string-parser@7.24.7': {} - '@babel/helper-string-parser@7.25.7': {} + '@babel/helper-string-parser@7.24.8': {} '@babel/helper-validator-identifier@7.24.7': {} - '@babel/helper-validator-identifier@7.25.7': {} - '@babel/helper-validator-option@7.23.5': {} '@babel/helper-validator-option@7.24.7': {} @@ -11946,15 +12008,21 @@ snapshots: '@babel/helpers@7.23.5': dependencies: '@babel/template': 7.22.15 - '@babel/traverse': 7.24.7 - '@babel/types': 7.25.7 + '@babel/traverse': 7.23.5 + '@babel/types': 7.24.7 transitivePeerDependencies: - supports-color '@babel/helpers@7.24.7': dependencies: '@babel/template': 7.24.7 - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 + + '@babel/highlight@7.23.4': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 '@babel/highlight@7.24.7': dependencies: @@ -11967,35 +12035,65 @@ snapshots: dependencies: '@babel/types': 7.24.7 - '@babel/parser@7.25.7': + '@babel/parser@7.25.6': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 @@ -12006,36 +12104,78 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true + '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5)': dependencies: '@babel/core': 7.23.5 @@ -12048,31 +12188,31 @@ snapshots: '@babel/template@7.22.15': dependencies: '@babel/code-frame': 7.24.7 - '@babel/parser': 7.25.7 - '@babel/types': 7.25.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.24.7 '@babel/template@7.24.0': dependencies: '@babel/code-frame': 7.24.7 - '@babel/parser': 7.25.7 - '@babel/types': 7.25.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@babel/template@7.24.7': dependencies: '@babel/code-frame': 7.24.7 - '@babel/parser': 7.25.7 - '@babel/types': 7.25.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@babel/traverse@7.23.5': dependencies: '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.25.7 - '@babel/types': 7.25.7 + '@babel/generator': 7.23.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.25.6 + '@babel/types': 7.24.7 debug: 4.3.7(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: @@ -12086,8 +12226,8 @@ snapshots: '@babel/helper-function-name': 7.24.7 '@babel/helper-hoist-variables': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.25.7 - '@babel/types': 7.25.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 debug: 4.3.7(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: @@ -12099,10 +12239,10 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 - '@babel/types@7.25.7': + '@babel/types@7.25.6': dependencies: - '@babel/helper-string-parser': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 '@bcoe/v8-coverage@0.2.3': {} @@ -12253,7 +12393,7 @@ snapshots: jsonfile: 5.0.0 universalify: 0.1.2 - '@emnapi/runtime@1.3.0': + '@emnapi/runtime@1.2.0': dependencies: tslib: 2.7.0 optional: true @@ -12465,9 +12605,9 @@ snapshots: '@esbuild/win32-x64@0.24.0': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': dependencies: - eslint: 8.57.0 + eslint: 8.57.1 eslint-visitor-keys: 3.4.3 '@eslint-community/eslint-utils@4.4.0(eslint@9.14.0)': @@ -12496,7 +12636,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -12521,13 +12661,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.0': {} + '@eslint/js@8.57.1': {} '@eslint/js@9.14.0': {} '@eslint/object-schema@2.1.4': {} - '@eslint/plugin-kit@0.2.3': + '@eslint/plugin-kit@0.2.0': dependencies: levn: 0.4.1 @@ -12536,9 +12676,9 @@ snapshots: '@fastify/accepts@5.0.1': dependencies: accepts: 1.3.8 - fastify-plugin: 5.0.1 + fastify-plugin: 5.0.0 - '@fastify/ajv-compiler@4.0.1': + '@fastify/ajv-compiler@4.0.0': dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) @@ -12554,12 +12694,12 @@ snapshots: '@fastify/cookie@11.0.1': dependencies: - cookie: 1.0.2 - fastify-plugin: 5.0.1 + cookie: 1.0.1 + fastify-plugin: 5.0.0 '@fastify/cors@10.0.1': dependencies: - fastify-plugin: 5.0.1 + fastify-plugin: 5.0.0 mnemonist: 0.39.8 '@fastify/deepmerge@2.0.0': {} @@ -12569,19 +12709,19 @@ snapshots: '@fastify/express@4.0.1': dependencies: express: 4.21.0 - fastify-plugin: 5.0.1 + fastify-plugin: 5.0.0 transitivePeerDependencies: - supports-color - '@fastify/fast-json-stringify-compiler@5.0.1': + '@fastify/fast-json-stringify-compiler@5.0.0': dependencies: fast-json-stringify: 6.0.0 '@fastify/http-proxy@10.0.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)': dependencies: - '@fastify/reply-from': 11.0.1 + '@fastify/reply-from': 11.0.0 fast-querystring: 1.1.2 - fastify-plugin: 5.0.1 + fastify-plugin: 5.0.0 ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil @@ -12596,10 +12736,10 @@ snapshots: '@fastify/busboy': 3.0.0 '@fastify/deepmerge': 2.0.0 '@fastify/error': 4.0.0 - fastify-plugin: 5.0.1 + fastify-plugin: 5.0.0 secure-json-parse: 3.0.0 - '@fastify/reply-from@11.0.1': + '@fastify/reply-from@11.0.0': dependencies: '@fastify/error': 4.0.0 end-of-stream: 1.4.4 @@ -12607,7 +12747,7 @@ snapshots: fast-querystring: 1.1.2 fastify-plugin: 4.5.1 toad-cache: 3.7.0 - undici: 6.20.0 + undici: 6.19.8 '@fastify/send@3.1.1': dependencies: @@ -12622,13 +12762,13 @@ snapshots: '@fastify/accept-negotiator': 2.0.0 '@fastify/send': 3.1.1 content-disposition: 0.5.4 - fastify-plugin: 5.0.1 + fastify-plugin: 5.0.0 fastq: 1.17.1 glob: 11.0.0 '@fastify/view@10.0.1': dependencies: - fastify-plugin: 5.0.1 + fastify-plugin: 5.0.0 toad-cache: 3.7.0 '@github/webauthn-json@2.1.1': {} @@ -12662,10 +12802,10 @@ snapshots: '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.3.0 - '@humanwhocodes/config-array@0.11.14': + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -12746,7 +12886,7 @@ snapshots: '@img/sharp-wasm32@0.33.5': dependencies: - '@emnapi/runtime': 1.3.0 + '@emnapi/runtime': 1.2.0 optional: true '@img/sharp-win32-ia32@0.33.5': @@ -12893,7 +13033,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.25 '@types/node': 22.9.0 chalk: 4.1.2 collect-v8-coverage: 1.0.1 @@ -12943,7 +13083,7 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -12978,6 +13118,12 @@ snapshots: optionalDependencies: typescript: 5.6.3 + '@jridgewell/gen-mapping@0.3.2': + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -12986,6 +13132,8 @@ snapshots: '@jridgewell/resolve-uri@3.1.0': {} + '@jridgewell/set-array@1.1.2': {} + '@jridgewell/set-array@1.2.1': {} '@jridgewell/source-map@0.3.6': @@ -13290,7 +13438,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.27.0 - '@opentelemetry/core@1.28.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.27.0 @@ -13300,7 +13448,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13309,7 +13457,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@types/connect': 3.4.36 transitivePeerDependencies: - supports-color @@ -13326,7 +13474,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13335,7 +13483,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13366,7 +13514,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13385,7 +13533,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13393,7 +13541,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13401,7 +13549,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13410,7 +13558,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13425,7 +13573,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13434,7 +13582,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13442,7 +13590,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -13451,7 +13599,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@types/mysql': 2.15.26 transitivePeerDependencies: - supports-color @@ -13460,7 +13608,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13468,7 +13616,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) '@types/pg': 8.6.1 '@types/pg-pool': 2.0.6 @@ -13480,7 +13628,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -13488,7 +13636,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@types/tedious': 4.0.14 transitivePeerDependencies: - supports-color @@ -13539,25 +13687,23 @@ snapshots: '@opentelemetry/redis-common@0.36.2': {} - '@opentelemetry/resources@1.28.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@1.27.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.28.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.27.0 - '@opentelemetry/sdk-trace-base@1.28.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.28.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.28.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/semantic-conventions@1.25.1': {} '@opentelemetry/semantic-conventions@1.27.0': {} - '@opentelemetry/semantic-conventions@1.28.0': {} - '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -13614,7 +13760,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.28.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -13800,25 +13946,25 @@ snapshots: '@opentelemetry/instrumentation-redis-4': 0.42.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-tedious': 0.15.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-undici': 0.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.28.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.28.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 '@prisma/instrumentation': 5.19.1 '@sentry/core': 8.38.0 - '@sentry/opentelemetry': 8.38.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.28.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0) + '@sentry/opentelemetry': 8.38.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.27.0) '@sentry/types': 8.38.0 '@sentry/utils': 8.38.0 import-in-the-middle: 1.11.2 transitivePeerDependencies: - supports-color - '@sentry/opentelemetry@8.38.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.28.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0)': + '@sentry/opentelemetry@8.38.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.27.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.28.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 '@sentry/core': 8.38.0 '@sentry/types': 8.38.0 '@sentry/utils': 8.38.0 @@ -14468,7 +14614,7 @@ snapshots: '@storybook/instrumenter@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - '@vitest/utils': 2.1.2 + '@vitest/utils': 2.1.1 storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4) '@storybook/manager-api@8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4))': @@ -14570,7 +14716,7 @@ snapshots: '@storybook/manager-api': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)) '@storybook/preview-api': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)) '@storybook/theming': 8.4.4(storybook@8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4)) - '@vue/compiler-core': 3.5.11 + '@vue/compiler-core': 3.5.12 storybook: 8.4.4(bufferutil@4.0.8)(prettier@3.3.3)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 type-fest: 2.19.0 @@ -14665,7 +14811,7 @@ snapshots: '@swc/core@1.9.2': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.17 + '@swc/types': 0.1.15 optionalDependencies: '@swc/core-darwin-arm64': 1.9.2 '@swc/core-darwin-x64': 1.9.2 @@ -14687,7 +14833,7 @@ snapshots: '@swc/counter': 0.1.3 jsonc-parser: 3.2.0 - '@swc/types@0.1.17': + '@swc/types@0.1.15': dependencies: '@swc/counter': 0.1.3 @@ -14785,24 +14931,24 @@ snapshots: '@types/babel__core@7.20.0': dependencies: - '@babel/parser': 7.25.7 - '@babel/types': 7.25.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.20.0 '@types/babel__generator@7.6.4': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 '@types/babel__template@7.4.1': dependencies: - '@babel/parser': 7.25.7 - '@babel/types': 7.25.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@types/babel__traverse@7.20.0': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 '@types/bcryptjs@2.4.6': {} @@ -14874,7 +15020,7 @@ snapshots: dependencies: '@types/node': 22.9.0 - '@types/form-data@2.5.0': + '@types/form-data@2.5.2': dependencies: form-data: 4.0.1 @@ -14909,11 +15055,6 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.0 - '@types/jest@29.5.12': - dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - '@types/jest@29.5.14': dependencies: expect: 29.7.0 @@ -14988,10 +15129,6 @@ snapshots: '@types/express': 4.17.17 '@types/node': 22.9.0 - '@types/oauth@0.9.5': - dependencies: - '@types/node': 22.9.0 - '@types/oauth@0.9.6': dependencies: '@types/node': 22.9.0 @@ -15125,10 +15262,6 @@ snapshots: dependencies: '@types/node': 22.9.0 - '@types/ws@8.5.11': - dependencies: - '@types/node': 22.9.0 - '@types/ws@8.5.13': dependencies: '@types/node': 22.9.0 @@ -15144,20 +15277,20 @@ snapshots: '@types/node': 22.9.0 optional: true - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.1.6))(eslint@8.57.1)(typescript@5.1.6)': dependencies: - '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.1.6) + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.1.6) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.5(supports-color@5.5.0) - eslint: 8.57.0 + debug: 4.3.7(supports-color@8.1.1) + eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 - semver: 7.6.0 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.1.6) optionalDependencies: typescript: 5.1.6 @@ -15175,10 +15308,10 @@ snapshots: debug: 4.3.4 eslint: 9.14.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.0.1(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: @@ -15202,14 +15335,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.1.6)': + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.1.6)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.5(supports-color@5.5.0) - eslint: 8.57.0 + debug: 4.3.7(supports-color@8.1.1) + eslint: 8.57.1 optionalDependencies: typescript: 5.1.6 transitivePeerDependencies: @@ -15234,7 +15367,7 @@ snapshots: '@typescript-eslint/types': 7.17.0 '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 7.17.0 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 eslint: 9.14.0 optionalDependencies: typescript: 5.6.3 @@ -15256,12 +15389,12 @@ snapshots: '@typescript-eslint/types': 7.17.0 '@typescript-eslint/visitor-keys': 7.17.0 - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.1.6)': + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.1.6)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) - debug: 4.3.5(supports-color@5.5.0) - eslint: 8.57.0 + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.1.6) + debug: 4.3.7(supports-color@8.1.1) + eslint: 8.57.1 ts-api-utils: 1.3.0(typescript@5.1.6) optionalDependencies: typescript: 5.1.6 @@ -15272,9 +15405,9 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.6.3) '@typescript-eslint/utils': 7.1.0(eslint@9.14.0)(typescript@5.6.3) - debug: 4.3.4 + debug: 4.3.5 eslint: 9.14.0 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.0.1(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: @@ -15284,7 +15417,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.3) '@typescript-eslint/utils': 7.17.0(eslint@9.14.0)(typescript@5.6.3) - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 eslint: 9.14.0 ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: @@ -15302,11 +15435,11 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.6.0 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.1.6) optionalDependencies: typescript: 5.1.6 @@ -15317,12 +15450,12 @@ snapshots: dependencies: '@typescript-eslint/types': 7.1.0 '@typescript-eslint/visitor-keys': 7.1.0 - debug: 4.3.4 + debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.0.1(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: @@ -15332,7 +15465,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.17.0 '@typescript-eslint/visitor-keys': 7.17.0 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.4 @@ -15343,16 +15476,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.1.6)': + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.1.6)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) - eslint: 8.57.0 - semver: 7.6.0 + eslint: 8.57.1 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -15413,7 +15546,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.4 @@ -15432,7 +15565,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.4 @@ -15464,7 +15597,7 @@ snapshots: dependencies: tinyrainbow: 1.2.0 - '@vitest/pretty-format@2.1.2': + '@vitest/pretty-format@2.1.1': dependencies: tinyrainbow: 1.2.0 @@ -15499,13 +15632,13 @@ snapshots: dependencies: '@vitest/pretty-format': 2.0.5 estree-walker: 3.0.3 - loupe: 3.1.2 + loupe: 3.1.1 tinyrainbow: 1.2.0 - '@vitest/utils@2.1.2': + '@vitest/utils@2.1.1': dependencies: - '@vitest/pretty-format': 2.1.2 - loupe: 3.1.2 + '@vitest/pretty-format': 2.1.1 + loupe: 3.1.1 tinyrainbow: 1.2.0 '@volar/language-core@2.2.0': @@ -15535,7 +15668,7 @@ snapshots: '@vue/compiler-core@3.5.11': dependencies: - '@babel/parser': 7.25.7 + '@babel/parser': 7.25.6 '@vue/shared': 3.5.11 entities: 4.5.0 estree-walker: 2.0.2 @@ -15543,7 +15676,7 @@ snapshots: '@vue/compiler-core@3.5.12': dependencies: - '@babel/parser': 7.25.7 + '@babel/parser': 7.25.6 '@vue/shared': 3.5.12 entities: 4.5.0 estree-walker: 2.0.2 @@ -15561,7 +15694,7 @@ snapshots: '@vue/compiler-sfc@3.5.12': dependencies: - '@babel/parser': 7.25.7 + '@babel/parser': 7.25.6 '@vue/compiler-core': 3.5.12 '@vue/compiler-dom': 3.5.12 '@vue/compiler-ssr': 3.5.12 @@ -15584,8 +15717,8 @@ snapshots: '@vue/language-core@2.0.16(typescript@5.6.3)': dependencies: '@volar/language-core': 2.2.0 - '@vue/compiler-dom': 3.5.11 - '@vue/shared': 3.5.11 + '@vue/compiler-dom': 3.5.12 + '@vue/shared': 3.5.12 computeds: 0.0.1 minimatch: 9.0.4 path-browserify: 1.0.1 @@ -15599,7 +15732,7 @@ snapshots: '@vue/compiler-dom': 3.5.11 '@vue/compiler-vue2': 2.7.16 '@vue/shared': 3.5.11 - alien-signals: 0.2.2 + alien-signals: 0.2.1 minimatch: 9.0.4 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -15671,7 +15804,7 @@ snapshots: agent-base@7.1.0: dependencies: - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 transitivePeerDependencies: - supports-color @@ -15685,7 +15818,7 @@ snapshots: clean-stack: 5.2.0 indent-string: 5.0.0 - aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9: + aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/c3cde89e79a41d93540cf8a48cd619c3f2dcb1b7: dependencies: '@aiscript-dev/aiscript-languageserver': https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz vscode-languageclient: 9.0.1 @@ -15734,7 +15867,7 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alien-signals@0.2.2: {} + alien-signals@0.2.1: {} ansi-colors@4.1.3: {} @@ -15795,11 +15928,11 @@ snapshots: arg@5.0.2: {} - argon2@0.40.1: + argon2@0.40.3: dependencies: '@phc/format': 1.0.0 - node-addon-api: 7.1.0 - node-gyp-build: 4.8.1 + node-addon-api: 8.3.0 + node-gyp-build: 4.8.4 argparse@1.0.10: dependencies: @@ -15817,7 +15950,7 @@ snapshots: array-buffer-byte-length@1.0.0: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 is-array-buffer: 3.0.2 array-buffer-byte-length@1.0.1: @@ -15849,14 +15982,14 @@ snapshots: array.prototype.flat@1.3.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 es-abstract: 1.22.1 es-shim-unscopables: 1.0.0 array.prototype.flatmap@1.3.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 es-abstract: 1.22.1 es-shim-unscopables: 1.0.0 @@ -15864,9 +15997,9 @@ snapshots: arraybuffer.prototype.slice@1.0.1: dependencies: array-buffer-byte-length: 1.0.0 - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 @@ -15994,6 +16127,20 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@29.7.0(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.0 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.24.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + optional: true + babel-plugin-istanbul@6.1.1: dependencies: '@babel/helper-plugin-utils': 7.22.5 @@ -16007,7 +16154,7 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.24.0 - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.20.0 @@ -16027,12 +16174,36 @@ snapshots: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5) + babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) + optional: true + babel-preset-jest@29.6.3(@babel/core@7.23.5): dependencies: '@babel/core': 7.23.5 babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) + babel-preset-jest@29.6.3(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + optional: true + babel-walk@3.0.0-canary-5: dependencies: '@babel/types': 7.24.7 @@ -16181,7 +16352,7 @@ snapshots: bufferutil@4.0.8: dependencies: - node-gyp-build: 4.8.1 + node-gyp-build: 4.6.0 optional: true bullmq@5.26.1: @@ -16324,7 +16495,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.2 + loupe: 3.1.1 pathval: 2.0.0 chalk-template@1.1.0: @@ -16411,7 +16582,7 @@ snapshots: parse5: 7.2.1 parse5-htmlparser2-tree-adapter: 7.0.0 parse5-parser-stream: 7.1.2 - undici: 6.20.0 + undici: 6.19.8 whatwg-mimetype: 4.0.0 cheerio@1.0.0-rc.12: @@ -16619,7 +16790,7 @@ snapshots: cookie@0.7.2: {} - cookie@1.0.2: {} + cookie@1.0.1: {} core-util-is@1.0.2: {} @@ -16704,12 +16875,12 @@ snapshots: css-tree@2.2.1: dependencies: mdn-data: 2.0.28 - source-map-js: 1.2.0 + source-map-js: 1.2.1 css-tree@2.3.1: dependencies: mdn-data: 2.0.30 - source-map-js: 1.2.0 + source-map-js: 1.2.1 css-what@6.1.0: {} @@ -16869,9 +17040,13 @@ snapshots: dependencies: ms: 2.1.2 - debug@4.3.5(supports-color@5.5.0): + debug@4.3.5: dependencies: ms: 2.1.2 + + debug@4.3.7(supports-color@5.5.0): + dependencies: + ms: 2.1.3 optionalDependencies: supports-color: 5.5.0 @@ -16968,7 +17143,7 @@ snapshots: define-properties@1.2.1: dependencies: define-data-property: 1.1.4 - has-property-descriptors: 1.0.0 + has-property-descriptors: 1.0.2 object-keys: 1.1.1 delayed-stream@1.0.0: {} @@ -17141,16 +17316,16 @@ snapshots: array-buffer-byte-length: 1.0.0 arraybuffer.prototype.slice: 1.0.1 available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + call-bind: 1.0.7 es-set-tostringtag: 2.0.1 es-to-primitive: 1.2.1 function.prototype.name: 1.1.5 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 get-symbol-description: 1.0.0 globalthis: 1.0.3 gopd: 1.0.1 has: 1.0.3 - has-property-descriptors: 1.0.0 + has-property-descriptors: 1.0.2 has-proto: 1.0.1 has-symbols: 1.0.3 internal-slot: 1.0.5 @@ -17162,7 +17337,7 @@ snapshots: is-string: 1.0.7 is-typed-array: 1.1.10 is-weakref: 1.0.2 - object-inspect: 1.12.3 + object-inspect: 1.13.2 object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.5.0 @@ -17214,7 +17389,7 @@ snapshots: object-inspect: 1.13.2 object-keys: 1.1.1 object.assign: 4.1.5 - regexp.prototype.flags: 1.5.3 + regexp.prototype.flags: 1.5.2 safe-array-concat: 1.1.2 safe-regex-test: 1.0.3 string.prototype.trim: 1.2.9 @@ -17251,7 +17426,7 @@ snapshots: es-set-tostringtag@2.0.1: dependencies: - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 has: 1.0.3 has-tostringtag: 1.0.0 @@ -17378,9 +17553,9 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@9.1.0(eslint@8.57.0): + eslint-config-prettier@9.1.0(eslint@8.57.1): dependencies: - eslint: 8.57.0 + eslint: 8.57.1 eslint-formatter-pretty@4.1.0: dependencies: @@ -17401,6 +17576,16 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-module-utils@2.11.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0): + dependencies: + debug: 3.2.7(supports-color@8.1.1) + optionalDependencies: + '@typescript-eslint/parser': 7.17.0(eslint@9.14.0)(typescript@5.6.3) + eslint: 9.14.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0): dependencies: debug: 3.2.7(supports-color@8.1.1) @@ -17422,7 +17607,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.14.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -17498,20 +17683,20 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@8.57.0: + eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.11.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -17549,7 +17734,7 @@ snapshots: '@eslint/core': 0.7.0 '@eslint/eslintrc': 3.1.0 '@eslint/js': 9.14.0 - '@eslint/plugin-kit': 0.2.3 + '@eslint/plugin-kit': 0.2.0 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.1 @@ -17595,10 +17780,6 @@ snapshots: esprima@4.0.1: {} - esquery@1.4.2: - dependencies: - estraverse: 5.3.0 - esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -17694,7 +17875,7 @@ snapshots: human-signals: 5.0.0 is-stream: 3.0.0 merge-stream: 2.0.0 - npm-run-path: 5.1.0 + npm-run-path: 5.3.0 onetime: 6.0.0 signal-exit: 4.1.0 strip-final-newline: 3.0.0 @@ -17874,34 +18055,34 @@ snapshots: fastify-plugin@2.3.4: dependencies: - semver: 7.6.0 + semver: 7.6.3 fastify-plugin@4.5.1: {} - fastify-plugin@5.0.1: {} + fastify-plugin@5.0.0: {} fastify-raw-body@5.0.0: dependencies: - fastify-plugin: 5.0.1 + fastify-plugin: 5.0.0 raw-body: 3.0.0 secure-json-parse: 2.7.0 fastify@5.0.0: dependencies: - '@fastify/ajv-compiler': 4.0.1 + '@fastify/ajv-compiler': 4.0.0 '@fastify/error': 4.0.0 - '@fastify/fast-json-stringify-compiler': 5.0.1 + '@fastify/fast-json-stringify-compiler': 5.0.0 abstract-logging: 2.0.1 avvio: 9.0.0 fast-json-stringify: 6.0.0 - find-my-way: 9.1.0 - light-my-request: 6.1.0 + find-my-way: 9.0.1 + light-my-request: 6.0.0 pino: 9.2.0 process-warning: 4.0.0 proxy-addr: 2.0.7 rfdc: 1.4.1 secure-json-parse: 2.7.0 - semver: 7.6.0 + semver: 7.6.3 toad-cache: 3.7.0 fastq@1.17.1: @@ -17984,7 +18165,7 @@ snapshots: transitivePeerDependencies: - supports-color - find-my-way@9.1.0: + find-my-way@9.0.1: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 @@ -18055,12 +18236,6 @@ snapshots: form-data-encoder@4.0.2: {} - form-data@4.0.0: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - form-data@4.0.1: dependencies: asynckit: 0.4.0 @@ -18114,7 +18289,7 @@ snapshots: function.prototype.name@1.1.5: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 es-abstract: 1.22.1 functions-have-names: 1.2.3 @@ -18147,7 +18322,7 @@ snapshots: function-bind: 1.1.2 has-proto: 1.0.1 has-symbols: 1.0.3 - hasown: 2.0.2 + hasown: 2.0.0 get-package-type@0.1.0: {} @@ -18168,8 +18343,8 @@ snapshots: get-symbol-description@1.0.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 get-symbol-description@1.0.2: dependencies: @@ -18207,8 +18382,8 @@ snapshots: dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 + minimatch: 9.0.4 + minipass: 7.1.2 path-scurry: 1.10.1 glob@11.0.0: @@ -18254,14 +18429,14 @@ snapshots: globalthis@1.0.3: dependencies: - define-properties: 1.2.1 + define-properties: 1.2.0 globby@11.1.0: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.3.2 - ignore: 5.3.1 + ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -18309,7 +18484,7 @@ snapshots: lowercase-keys: 3.0.0 p-cancelable: 4.0.1 responselike: 3.0.0 - type-fest: 4.27.0 + type-fest: 4.26.1 graceful-fs@4.2.11: {} @@ -18346,7 +18521,7 @@ snapshots: has-property-descriptors@1.0.0: dependencies: - get-intrinsic: 1.2.4 + get-intrinsic: 1.2.1 has-property-descriptors@1.0.2: dependencies: @@ -18462,7 +18637,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 transitivePeerDependencies: - supports-color @@ -18488,14 +18663,14 @@ snapshots: https-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.4 + debug: 4.3.5 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.0 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18526,6 +18701,8 @@ snapshots: dependencies: minimatch: 9.0.4 + ignore@5.2.4: {} + ignore@5.3.1: {} immutable@4.2.2: {} @@ -18581,7 +18758,7 @@ snapshots: dependencies: es-errors: 1.3.0 hasown: 2.0.2 - side-channel: 1.0.4 + side-channel: 1.0.6 intersection-observer@0.12.2: {} @@ -18648,7 +18825,7 @@ snapshots: is-boolean-object@1.1.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 has-tostringtag: 1.0.0 is-callable@1.2.7: {} @@ -18790,7 +18967,7 @@ snapshots: is-weakset@2.0.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 get-intrinsic: 1.2.1 is-wsl@2.2.0: @@ -18815,7 +18992,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.24.7 - '@babel/parser': 7.25.7 + '@babel/parser': 7.24.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -18825,7 +19002,7 @@ snapshots: istanbul-lib-instrument@6.0.0: dependencies: '@babel/core': 7.24.7 - '@babel/parser': 7.25.7 + '@babel/parser': 7.25.6 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.6.3 @@ -18849,7 +19026,7 @@ snapshots: istanbul-lib-source-maps@5.0.4: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -19125,7 +19302,7 @@ snapshots: jest-snapshot@29.7.0: dependencies: '@babel/core': 7.23.5 - '@babel/generator': 7.24.7 + '@babel/generator': 7.23.5 '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.5) '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.5) '@babel/types': 7.24.7 @@ -19143,7 +19320,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color @@ -19473,9 +19650,9 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - light-my-request@6.1.0: + light-my-request@6.0.0: dependencies: - cookie: 0.7.2 + cookie: 0.6.0 process-warning: 4.0.0 set-cookie-parser: 2.6.0 @@ -19550,12 +19727,18 @@ snapshots: dependencies: get-func-name: 2.0.2 - loupe@3.1.2: {} + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 lowercase-keys@2.0.0: {} lowercase-keys@3.0.0: {} + lru-cache@10.0.2: + dependencies: + semver: 7.6.3 + lru-cache@10.2.2: {} lru-cache@11.0.0: {} @@ -19603,7 +19786,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.0 + semver: 7.6.3 make-error@1.3.6: {} @@ -20085,8 +20268,6 @@ snapshots: minipass@5.0.0: {} - minipass@7.0.4: {} - minipass@7.1.2: {} minizlib@2.1.2: @@ -20166,7 +20347,7 @@ snapshots: outvariant: 1.4.3 path-to-regexp: 6.3.0 strict-event-emitter: 0.5.1 - type-fest: 4.27.0 + type-fest: 4.26.1 yargs: 17.7.2 optionalDependencies: typescript: 5.6.3 @@ -20242,7 +20423,7 @@ snapshots: node-addon-api@3.2.1: optional: true - node-addon-api@7.1.0: {} + node-addon-api@8.3.0: {} node-domexception@1.0.0: {} @@ -20264,7 +20445,7 @@ snapshots: node-gyp-build@4.6.0: optional: true - node-gyp-build@4.8.1: {} + node-gyp-build@4.8.4: {} node-gyp@10.2.0: dependencies: @@ -20290,11 +20471,11 @@ snapshots: nodemon@3.1.7: dependencies: chokidar: 3.5.3 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.7(supports-color@5.5.0) ignore-by-default: 1.0.1 minimatch: 3.1.2 pstree.remy: 1.1.8 - semver: 7.6.0 + semver: 7.6.3 simple-update-notifier: 2.0.0 supports-color: 5.5.0 touch: 3.1.0 @@ -20325,7 +20506,7 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.13.1 - semver: 7.6.0 + semver: 7.6.3 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -20342,10 +20523,6 @@ snapshots: dependencies: path-key: 3.1.1 - npm-run-path@5.1.0: - dependencies: - path-key: 4.0.0 - npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -20442,7 +20619,7 @@ snapshots: oniguruma-to-js@0.4.3: dependencies: - regex: 4.4.0 + regex: 4.3.3 open@8.4.2: dependencies: @@ -20586,8 +20763,8 @@ snapshots: path-scurry@1.10.1: dependencies: - lru-cache: 10.2.2 - minipass: 7.0.4 + lru-cache: 10.0.2 + minipass: 7.1.2 path-scurry@2.0.0: dependencies: @@ -20753,7 +20930,7 @@ snapshots: postcss-calc@9.0.1(postcss@8.4.49): dependencies: postcss: 8.4.49 - postcss-selector-parser: 6.0.15 + postcss-selector-parser: 6.0.16 postcss-value-parser: 4.2.0 postcss-colormin@6.1.0(postcss@8.4.49): @@ -20886,11 +21063,6 @@ snapshots: postcss: 8.4.49 postcss-value-parser: 4.2.0 - postcss-selector-parser@6.0.15: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-selector-parser@6.0.16: dependencies: cssesc: 3.0.0 @@ -21031,7 +21203,7 @@ snapshots: pseudomap@1.0.2: {} - psl@1.13.0: + psl@1.15.0: dependencies: punycode: 2.3.1 @@ -21111,6 +21283,8 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + punycode.js@2.3.1: {} + punycode@2.3.1: {} pure-rand@6.0.0: {} @@ -21187,7 +21361,7 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@babel/traverse': 7.24.7 - '@babel/types': 7.25.7 + '@babel/types': 7.25.6 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.20.0 '@types/doctrine': 0.0.9 @@ -21293,7 +21467,7 @@ snapshots: regenerator-runtime@0.14.0: {} - regex@4.4.0: {} + regex@4.3.3: {} regexp.prototype.flags@1.5.0: dependencies: @@ -21301,7 +21475,7 @@ snapshots: define-properties: 1.2.0 functions-have-names: 1.2.3 - regexp.prototype.flags@1.5.3: + regexp.prototype.flags@1.5.2: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 @@ -21450,8 +21624,8 @@ snapshots: safe-array-concat@1.0.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 has-symbols: 1.0.3 isarray: 2.0.5 @@ -21468,8 +21642,8 @@ snapshots: safe-regex-test@1.0.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 is-regex: 1.1.4 safe-regex-test@1.0.3: @@ -21678,7 +21852,7 @@ snapshots: dependencies: '@hapi/hoek': 11.0.4 '@hapi/wreck': 18.0.1 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.5 joi: 17.11.0 transitivePeerDependencies: - supports-color @@ -21689,7 +21863,7 @@ snapshots: simple-update-notifier@2.0.0: dependencies: - semver: 7.5.4 + semver: 7.6.0 sinon@16.1.3: dependencies: @@ -21976,7 +22150,7 @@ snapshots: string.prototype.trim@1.2.7: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 es-abstract: 1.22.1 @@ -21989,7 +22163,7 @@ snapshots: string.prototype.trimend@1.0.6: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 es-abstract: 1.22.1 @@ -22001,7 +22175,7 @@ snapshots: string.prototype.trimstart@1.0.6: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.0 es-abstract: 1.22.1 @@ -22109,7 +22283,7 @@ snapshots: css-tree: 2.3.1 css-what: 6.1.0 csso: 5.0.5 - picocolors: 1.0.0 + picocolors: 1.0.1 symbol-tree@3.2.4: {} @@ -22188,12 +22362,12 @@ snapshots: tinyspy@3.0.2: {} - tldts-core@6.1.63: + tldts-core@6.1.61: optional: true - tldts@6.1.63: + tldts@6.1.61: dependencies: - tldts-core: 6.1.63 + tldts-core: 6.1.61 optional: true tmp@0.2.3: {} @@ -22230,14 +22404,14 @@ snapshots: tough-cookie@4.1.4: dependencies: - psl: 1.13.0 + psl: 1.15.0 punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 tough-cookie@5.0.0: dependencies: - tldts: 6.1.63 + tldts: 6.1.61 optional: true tr46@0.0.3: {} @@ -22261,6 +22435,10 @@ snapshots: trough@2.2.0: {} + ts-api-utils@1.0.1(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + ts-api-utils@1.3.0(typescript@5.1.6): dependencies: typescript: 5.1.6 @@ -22273,22 +22451,24 @@ snapshots: ts-dedent@2.2.0: {} - ts-jest@29.1.2(@babel/core@7.23.5)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.5))(esbuild@0.24.0)(jest@29.7.0(@types/node@22.9.0))(typescript@5.1.6): + ts-jest@29.2.5(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.24.0)(jest@29.7.0(@types/node@22.9.0))(typescript@5.1.6): dependencies: bs-logger: 0.2.6 + ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 jest: 29.7.0(@types/node@22.9.0) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.6.0 + semver: 7.6.3 typescript: 5.1.6 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.7 + '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.23.5) + babel-jest: 29.7.0(@babel/core@7.24.7) esbuild: 0.24.0 ts-map@1.0.3: {} @@ -22363,7 +22543,7 @@ snapshots: type-fest@2.19.0: {} - type-fest@4.27.0: {} + type-fest@4.26.1: {} type-is@1.6.18: dependencies: @@ -22372,8 +22552,8 @@ snapshots: typed-array-buffer@1.0.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 is-typed-array: 1.1.10 typed-array-buffer@1.0.2: @@ -22384,7 +22564,7 @@ snapshots: typed-array-byte-length@1.0.0: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.10 @@ -22400,7 +22580,7 @@ snapshots: typed-array-byte-offset@1.0.0: dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + call-bind: 1.0.7 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.10 @@ -22416,7 +22596,7 @@ snapshots: typed-array-length@1.0.4: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 for-each: 0.3.3 is-typed-array: 1.1.10 @@ -22495,7 +22675,7 @@ snapshots: dependencies: '@fastify/busboy': 2.1.0 - undici@6.20.0: {} + undici@6.19.8: {} unified@11.0.4: dependencies: @@ -22563,13 +22743,13 @@ snapshots: dependencies: browserslist: 4.22.2 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.0.1 update-browserslist-db@1.0.13(browserslist@4.23.0): dependencies: browserslist: 4.23.0 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.0.1 uri-js@4.4.1: dependencies: @@ -22587,7 +22767,7 @@ snapshots: utf-8-validate@6.0.4: dependencies: - node-gyp-build: 4.8.1 + node-gyp-build: 4.6.0 optional: true util-deprecate@1.0.2: {} @@ -22652,7 +22832,7 @@ snapshots: vite-node@1.6.0(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0): dependencies: cac: 6.7.14 - debug: 4.3.7(supports-color@8.1.1) + debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 vite: 5.4.11(@types/node@22.9.0)(sass@1.79.3)(terser@5.36.0) @@ -22670,7 +22850,7 @@ snapshots: vite-node@1.6.0(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0): dependencies: cac: 6.7.14 - debug: 4.3.7(supports-color@8.1.1) + debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 vite: 5.4.11(@types/node@22.9.0)(sass@1.79.4)(terser@5.36.0) @@ -22840,9 +23020,9 @@ snapshots: vue-docgen-api@4.75.1(vue@3.5.12(typescript@5.6.3)): dependencies: - '@babel/parser': 7.25.7 - '@babel/types': 7.25.7 - '@vue/compiler-dom': 3.5.11 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 + '@vue/compiler-dom': 3.5.12 '@vue/compiler-sfc': 3.5.12 ast-types: 0.16.1 hash-sum: 2.0.0 @@ -22855,12 +23035,12 @@ snapshots: vue-eslint-parser@9.4.3(eslint@9.14.0): dependencies: - debug: 4.3.4 + debug: 4.3.5 eslint: 9.14.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.4.2 + esquery: 1.6.0 lodash: 4.17.21 semver: 7.6.0 transitivePeerDependencies: diff --git a/scripts/dev.mjs b/scripts/dev.mjs index ea3b017ba5..604d6567b9 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -27,7 +27,7 @@ await Promise.all([ stdout: process.stdout, stderr: process.stderr, }), - execa('pnpm', ['--filter', 'misskey-js', 'build'], { + execa('pnpm', ['--filter', 'backend...', 'build'], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, @@ -39,19 +39,6 @@ await Promise.all([ }), ]); -await Promise.all([ - execa('pnpm', ['--filter', 'misskey-reversi', 'build'], { - cwd: _dirname + '/../', - stdout: process.stdout, - stderr: process.stderr, - }), - execa('pnpm', ['--filter', 'misskey-bubble-game', 'build'], { - cwd: _dirname + '/../', - stdout: process.stdout, - stderr: process.stderr, - }), -]); - execa('pnpm', ['build-pre', '--watch'], { cwd: _dirname + '/../', stdout: process.stdout, @@ -70,19 +57,19 @@ execa('pnpm', ['--filter', 'backend', 'dev'], { stderr: process.stderr, }); -execa('pnpm', ['--filter', 'frontend-shared', 'watch'], { +execa('pnpm', ['--filter', 'frontend-shared', 'watch', '--no-clean'], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, }); -execa('pnpm', ['--filter', 'frontend', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], { +execa('pnpm', ['--filter', 'frontend', 'watch'], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, }); -execa('pnpm', ['--filter', 'frontend-embed', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], { +execa('pnpm', ['--filter', 'frontend-embed', 'watch'], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, @@ -94,19 +81,19 @@ execa('pnpm', ['--filter', 'sw', 'watch'], { stderr: process.stderr, }); -execa('pnpm', ['--filter', 'misskey-js', 'watch'], { +execa('pnpm', ['--filter', 'misskey-js', 'watch', '--no-clean'], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, }); -execa('pnpm', ['--filter', 'misskey-reversi', 'watch'], { +execa('pnpm', ['--filter', 'misskey-reversi', 'watch', '--no-clean'], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, }); -execa('pnpm', ['--filter', 'misskey-bubble-game', 'watch'], { +execa('pnpm', ['--filter', 'misskey-bubble-game', 'watch', '--no-clean'], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, |