diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml index ac2b39cc12c6..b33c528bd2e2 100644 --- a/.github/ISSUE_TEMPLATE/01_bug-report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug-report.yml @@ -1,41 +1,41 @@ -name: ð Bug Report -description: Create a report to help us improve -labels: ["â ïžbug?"] +name: â ïž ãã°å ±å +description: ä¿®æ£/æ¹åã®ããã®å ±å +labels: ["â ïžBug"] body: - type: markdown attributes: value: | - Thanks for reporting! - First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported. - Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first. + ãã°å ±åãæ€èšããŠããã ãããããšãããããŸã! + éè€ãé¿ããããã«ãåæ§ã®åé¡ãæ¢ã«å ±åãããŠããªãã[ã«ããã¿ãããŒéšã®Issue](https://github.com/niri-la/misskey.niri.la/issues)ã確èªããŠã¿ãŠãã ããã + ãŸããã«ããã¿ãããŒéšã§åçŸãããã°ã®ã¿ãå ±åããŠãã ããã - type: textarea attributes: - label: ð¡ Summary - description: Tell us what the bug is + label: ð¡ æŠèŠ + description: ã©ããªãã°ã§ãã? validations: required: true - type: textarea attributes: - label: 𥰠Expected Behavior - description: Tell us what should happen + label: 𥰠æ³å®ãããæå + description: æ¬æ¥ã©ãããã¹ãã§ãã? validations: required: true - type: textarea attributes: - label: ð€¬ Actual Behavior + label: ð€¬ å®éã®æå description: | - Tell us what happens instead of the expected behavior. - Please include errors from the developer console and/or server log files if you have access to them. + å®éã«ã¯äœãèµ·ããŠããŸãã? + å¯èœã§ããã°ãããããããŒããŒã«ã§ã®ãšã©ãŒã¡ãã»ãŒãžãªã©ãèŒããŠãã ããã validations: required: true - type: textarea attributes: - label: ð Steps to Reproduce + label: ð åçŸæé placeholder: | 1. 2. @@ -45,53 +45,17 @@ body: - type: textarea attributes: - label: ð» Frontend Environment + label: ð» ã䜿ãã®ç°å¢ description: | - Tell us where on the platform it happens - DO NOT WRITE "latest". Please provide the specific version. + ã©ããªç°å¢ã§åé¡ãèµ·ããŸããã? + "ææ°"ã®ãããªæžãæ¹ã¯ãããå ·äœçãªããŒãžã§ã³ãæããŠãã ããã - Examples: - * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4 - * Browser: Chrome 113.0.5672.126 - * Server URL: misskey.io - * Misskey: 13.x.x + äŸ: + * æ©çš®ãšOS: MacBook Pro (14inch, 2021), macOS Ventura 13.4 + * ãã©ãŠã¶: Chrome 113.0.5672.126 value: | - * Model and OS of the device(s): - * Browser: - * Server URL: - * Misskey: + * æ©çš®ãšOS: + * ãã©ãŠã¶: render: markdown validations: required: false - - - type: textarea - attributes: - label: ð° Backend Environment (for server admin) - description: | - Tell us where on the platform it happens - DO NOT WRITE "latest". Please provide the specific version. - If you are using a managed service, put that after the version. - - Examples: - * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment - * Misskey: 13.x.x - * Node: 20.x.x - * PostgreSQL: 15.x.x - * Redis: 7.x.x - * OS and Architecture: Ubuntu 22.04.2 LTS aarch64 - value: | - * Installation Method or Hosting Service: - * Misskey: - * Node: - * PostgreSQL: - * Redis: - * OS and Architecture: - render: markdown - validations: - required: false - - - type: checkboxes - attributes: - label: Do you want to address this bug yourself? - options: - - label: Yes, I will patch the bug myself and send a pull request diff --git a/.github/ISSUE_TEMPLATE/02_feature-request.yml b/.github/ISSUE_TEMPLATE/02_feature-request.yml index 8d7b0b2539e9..f6ba3d36ea43 100644 --- a/.github/ISSUE_TEMPLATE/02_feature-request.yml +++ b/.github/ISSUE_TEMPLATE/02_feature-request.yml @@ -1,22 +1,17 @@ -name: âš Feature Request -description: Suggest an idea for this project +name: âš æ°æ©èœã®èŠæ +description: æ°ããæ©èœã®ææ¡ labels: ["âšFeature"] body: - type: textarea attributes: - label: Summary - description: Tell us what the suggestion is + label: æŠèŠ + description: ã©ããªææ¡ã§ãã? validations: required: true - type: textarea attributes: - label: Purpose - description: Describe the specific problem or need you think this feature will solve, and who it will help. + label: ç®ç + description: ãã®æ©èœã®è¿œå ã«ãã£ãŠãã©ããªåé¡ãããŒãºã解決ããããã§ãã? ãŸããããã¯ã©ããªäººã«åœ¹ç«ã¡ããã§ãã? validations: required: true - - type: checkboxes - attributes: - label: Do you want to implement this feature yourself? - options: - - label: Yes, I will implement this by myself and send a pull request diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index e8b65dc3b9fc..000000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,4 +0,0 @@ -contact_links: - - name: ð¬ Misskey official Discord - url: https://discord.gg/Wp8gVStHW3 - about: Chat freely about Misskey diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml index 8fad129115ae..1e03fdc28656 100644 --- a/.github/workflows/check-misskey-js-autogen.yml +++ b/.github/workflows/check-misskey-js-autogen.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v4.1.1 with: submodules: true - ref: ${{ github.event.pull_request.head.sha }} + ref: refs/pull/${{ github.event.number }}/merge - name: setup pnpm uses: pnpm/action-setup@v3 diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 000000000000..b54795f8ce93 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,50 @@ +name: Build and Push Image (Docker Buildx; GitHub Actions) + +on: + push: + tags: + - 'v*' + +env: + DOCKER_REGISTRY_NAME: ghcr.io + DOCKER_IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Docker hub + uses: docker/login-action@v1 + with: + registry: ${{ env.DOCKER_REGISTRY_NAME }} + username: ${{ github.repository_owner }} + password: ${{ github.token }} # github.token + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.DOCKER_REGISTRY_NAME }}/${{ env.DOCKER_IMAGE_NAME }} + + - name: Build & Push + uses: docker/build-push-action@v2 + env: + DOCKER_BUILDKIT: 1 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: ${{ env.DOCKER_REGISTRY_NAME }}/${{ env.DOCKER_IMAGE_NAME }}:latest + build-args: BUILDKIT_INLINE_CACHE=1 diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 1e020b73680b..f28c6ef1d878 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -60,6 +60,7 @@ jobs: e2e: runs-on: ubuntu-latest + if: false strategy: fail-fast: false diff --git a/CHANGELOG.md b/CHANGELOG.md index bafee277d259..20f301d0981c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,26 @@ - ### Client -- +- ### Server - --> -## 2024.3.1 +## 202x.x.x-kinel.x (unreleased) -### General -- +### Client +- Enhance: ç»åã¢ããããŒãæã«çž®å°ããå Žåã®å€§ããã2048x2048以äžãã2560x2560以äžã«å€æŽããŸãã + - æ¢åã®ãã¡ã€ã«ã¯æŽæ°ããããæ°èŠã¢ããããŒãåã«ã®ã¿é©çšãããŸã +- Fix: ãããªãã¯æçš¿ãããŒã æçš¿ã«å€æŽããã¢ãã¬ãŒã·ã§ã³æäœãUIäžã§è¡ããªããªã£ãŠããåé¡ãä¿®æ£ +- Fix: `ãŠãŒã¶ããŒãžã§ã»ã³ã·ãã£ããã£ã³ãã«ã®æçš¿ãéãã`èšå®ãç¡å¹ã«ãªããªãåé¡ãä¿®æ£ + +## 2024.3.1-kinel.1 + +2024.3.1ãšå šãåäžã§ã + +## 2024.3.1 ### Client - Fix: çµµæåé¢ä¿ã®äžå ·åãä¿®æ£ (#13485) @@ -23,8 +32,9 @@ - Unicodeçµµæåãå±¥æŽã«æ®ã£ãŠãã or ãã³çããããŠãããšãªã¢ã¯ã·ã§ã³ãããã衚瀺ã§ããªããªã - Fix: ã«ã¹ã¿ã çµµæåã®ç»åèªã¿èŸŒã¿ã«å€±æããéã¯ããã¹ãã§ã¯ãªããããŒç»åã衚瀺 #13487 -### Server -- +## 2024.3.0-kinel.1 + +2024.3.0ãšå šãåäžã§ã ## 2024.3.0 @@ -64,7 +74,31 @@ - `category`ããã³`licence`ãæå®ãªãã®æåæã«nullã«äžæžããããæåãä¿®æ£ - Fix: éç¥ã®åä¿¡èšå®ã§ãçžäºãã©ããŒããæ£ããåäœããªãåé¡ãä¿®æ£ -## 2024.2.0 +## 2024.2.0-kinel.3 + +### Client +- Fix: ã»ã³ã·ãã£ããã£ã³ãã«ã®æçš¿ãã»ã³ã·ãã£ããªãã¡ã€ã«ãå«ãæçš¿æ±ããããåé¡ãä¿®æ£ + +### Server +- Fix: çŠæ¢ã¯ãŒãèšå®ã«ãã£ãŠãªã¢ãŒãã®æçš¿ããããã¯ããããšãInboxãã¥ãŒã«Delayed JobãšããŠæ®ã£ãŠããŸãåé¡ãä¿®æ£ +- Fix: ããŒã«ã«ãŠãŒã¶ãŒããŸã 誰ããã©ããŒããŠããªããªã¢ãŒããŠãŒã¶ãŒã«ãããéç¥ãåŒãèµ·ããå¯èœæ§ã®ããæçš¿ãæåŠãããšãInboxãã¥ãŒã«Delayed JobãšããŠæ®ã£ãŠããŸãåé¡ãä¿®æ£ + +## 2024.2.0-kinel.2 + +### Server +- ããŒã«ã«ãŠãŒã¶ãŒããŸã 誰ããã©ããŒããŠããªããªã¢ãŒããŠãŒã¶ãŒã«ãããéç¥ãåŒãèµ·ããå¯èœæ§ã®ããæçš¿ãæåŠã§ããããã« + +## 2024.2.0-kinel.1 + +### Client +- ã»ã³ã·ãã£ããã£ã³ãã«ã®Noteã®ç³ãŸãæ¹ããœãããã¥ãŒããšåæ§ã«ãªããŸããã +- ãã®ãã©ãŒã¯ãžã®ãªã³ã¯ãmisskeyæšæºã®ãã®ã«å€æŽã«ãªããŸããã +- NSFWã¬ã€ãã©ã€ã³ãžã®ãªã³ã¯ãã¡ãã¥ãŒã«è¿œå ããŸããã + +### Server +- ãªã¢ãŒããŠãŒã¶ã®åé€ãèšé²ãããªãããã«ãªããŸããã + +## 2024.2.0 (merged to 2024.2.0-kinel.1) ### Note - å€éšãµã€ããããã©ã°ã€ã³ãã€ã³ã¹ããŒã«ããå Žåã®ãã¹ã`/install-extentions`ãã`/install-extensions`ã«å€ãããŸãã以åã®ãã¹ããã¯èªåã§ãªãã€ã¬ã¯ããããããã«ãªã£ãŠããŸãããæ°ãããã¹ã«å€æŽããããšããå§ãããŸãã @@ -152,7 +186,21 @@ - Fix: ã³ã³ãããŒã«ããã«->ã¢ãã¬ãŒã·ã§ã³->ã誰ã§ãæ°èŠç»é²ã§ããããã«ãããã®åæå€ãONããOFFã«å€æŽ #13122 - Fix: ãªã¢ãŒããŠãŒã¶ãŒã埩掻ããŠããã£ãã·ã¥ã«ãã該åœãŠãŒã¶ãŒã®Activityãåãå ¥ããããªãã®ãä¿®æ£ #13273 -## 2023.12.2 +### Service Worker +- Enhance: ãªãã©ã€ã³è¡šç€ºã®ãã¶ã€ã³ãæ¹åã»å€èšèªå¯Ÿå¿ + +## 2023.12.2-kinel.2 + +### Server +- ã¹ãã ãè¡ååŸå³æã«ã¢ã«ãŠã³ããåé€ãããããªå Žåãžã®å¯ŸçãšããŠãã¢ã«ãŠã³ãåé€æã«ã¢ã«ãŠã³ãããŒã¿ãä¿æãããããã«ãªããŸããã + - å人æ å ±ã®ä¿è·ã«é¢ããæ³åŸã«åºã¥ãæ£åœãªèŠæ±ããã£ãå Žåã«ã¯åé€ãããŸãã + - ãŸããããè¯ã察å¿æ¹æ³ãè¿œå ãããéã«ãã®ããŒã¿ã¯åé€ãããŸãã + +## 2023.12.2-kinel.1 + +2023.12.2ãšå šãåäžã§ã + +## 2023.12.2 (merged to 2023.12.2-kinel.1) ### General - v2023.12.1ã§Dockerãå©çšããŠãµãŒããŒãèµ·åã§ããªãåé¡ãä¿®æ£ @@ -160,7 +208,11 @@ ### Client - Enhance: æ€çŽ¢ç»é¢ã«ãããŠEnterããŒæŒäžã§æ€çŽ¢ã§ããããã« -## 2023.12.1 +## 2023.12.1-kinel.1 + +2023.12.1ãšå šãåäžã§ã + +## 2023.12.1 (merged to 2023.12.1-kinel.1) ### Note - ã¢ã¯ã»ã¹ããŒã¯ã³ã®æš©éãåæŽçããããããäžéšã®APIãå€ãAPIããŒã¯ã³ã§ã¯åäœããªããªããŸããã\ @@ -185,7 +237,16 @@ - Fix: ãµãŒãããŒãã£ã¢ããªã±ãŒã·ã§ã³ãWebsocket APIã«ç¡æ¡ä»¶ã«ã¢ã¯ã»ã¹ã§ããåé¡ãä¿®æ£ - Fix: ãµãŒãããŒãã£ã¢ããªã±ãŒã·ã§ã³ããŠãŒã¶ãŒã®èš±å¯ãªãã«éå ¬éã®æ å ±ãèŠãããšãã§ããåé¡ãä¿®æ£ -## 2023.12.0 +## 2023.12.0-kinel.1 + +### General +- Fix: VRChatã®ã¯ãŒã«ãURLã®ãµã ãã€ã«ãfaviconã«ãªãåé¡ãä¿®æ£ +- `mixi:content-rating`ãæå®ãããŠãããŠã§ããµã€ã(DLSiteç)ã®ãµã ãã€ã«ã衚瀺ãããªããªããŸãã + +### Client +- Fix: ãããªãã¯æçš¿ãããŒã æçš¿ã«å€æŽããã¢ãã¬ãŒã·ã§ã³æäœã«ãããã¢ããã°ã®å 容ãšè¡šç€ºãäžé©åã ã£ãåé¡ãä¿®æ£ + +## 2023.12.0 (merged to 2023.12.0-kinel.1) ### Note - äŸåé¢ä¿ã®æŽæ°ã«äŒŽããNode.js 20.10.0ãæå°èŠä»¶ã«ãªããŸãã @@ -290,7 +351,69 @@ - Fix: ããã·ã¥ã¿ã°ã®ãã¬ã³ãé€å€èšå®ãå³æã«å¹æãæã€ããã«ä¿®æ£ - Fix: HTTP Digestãããã®ã¢ã«ãŽãªãºã éšåã«å€§æåã®"SHA-256"ãã䜿ããªã -## 2023.11.1 +## 2023.11.1-kinel.4 + +### Client +- Fix: é³ããªããªãåé¡ + +## 2023.11.1-kinel.3 + +### General +- publicããŒããhomeããŒãã«ããã¢ãã¬ãŒã·ã§ã³ãè¿œå +- featuredçšã¹ã³ã¢ã絶察ã«æŽæ°ããããã« +- èšå®ããã¿ã°ããã¬ã³ãã«è¡šç€ºãããªãããã«ããé ç®ã管çç»é¢ã§èšå®ã§ããããã« +- Fix: å šäœãã€ã©ã€ãã§ãŠãŒã¶ãŒãã¥ãŒããæ£åžžã«æ©èœããªãåé¡ + +### Client +- Enhance: ãã£ã³ãã«ãã¯ãªãããããŒãžãPlayãã®ã£ã©ãªãŒã«URLã®ã³ããŒãã¿ã³ãèšçœ® #11305 +- Enhance: ãµãŠã³ãèšå®ã«ããµãŠã³ããåºåããªãããšãMisskeyãã¢ã¯ãã£ããªæã®ã¿ãµãŠã³ããåºåããããè¿œå +- Fix: å ±ææ©èœããµããŒãããŠããªããã©ãŠã¶ã®å Žåã¯å ±æãã¿ã³ãé衚瀺ã«ãã #11305 +- Fix: éç¥ã®ã°ã«ãŒãã³ã°èšå®ãå€æŽããŠããªããŒãããããŸã§è¡šç€ºãå€ãããªãåé¡ãä¿®æ£ #12470 + +### Server +- Fix: includeSensitiveChannelãfunout timelineã®ç¯å²ã§ã¯æ©èœããªãåé¡ + +## 2023.11.1-kinel.2 + +### General +- ããã«ããã¿ãããŒéšããã€ã©ã€ãããé€å€ãããããã«ãªããŸãã +- ã¢ãã¬ãŒã¿ãŒããŠãŒã¶ãŒã®ã¢ã€ã³ã³ãããã¯ãããŒç»åãæªèšå®ç¶æ ã«ã§ããæ©èœãè¿œå +- Fix: ãã€ã©ã€ãã§ãŠãŒã¶ãã¥ãŒããæ©èœããªãåé¡ + +### Client +- ãªã¢ã¯ã·ã§ã³ã®æšªå¹ ã150pxã«å¶éãããã©ãããŠãŒã¶ãŒãéžã¹ãããã« +- Fix: ããŒããã¥ãŒãã«ããŒãåŒã£ããããšãšã©ãŒãçºçãTLãæŽæ°ãããªãããšãããåé¡ +- Fix: çµµæåå ¥åæã®ãµãžã§ã¹ãããããã +- Fix: ãŠãŒã¶ãŒããŒãžã§ãã€ã©ã€ãããŒãããªãå Žåã¯çŽè¿ã®ããŒãã衚瀺ããããã«ä¿®æ£ + +### Server + +## 2023.11.1-kinel.1 + +### Note +- 2023.9.3-kinel.4以åã§å©çšãããŠãããœãã/ããŒããã¥ãŒãã®èšå®ã¯ããã®ãŸãŸ2023.11.1-kinel.1åãã«ç§»è¡ãããŸãã + - æ¬æ¥ã2023.10.0ã®å€æŽã«ãã以äžã®äºè±¡ãçºçããå¯èœæ§ããããŸããã + - ãœãããã¥ãŒãã®èšå®ããªã»ããããã + - ããŒããã¥ãŒãã®èšå®ã¯ãœãããã¥ãŒãã®ãã®ã«ãªããããŒããã¥ãŒãæ©èœãäžæå»æ¢ + - æ¬æŽæ°ã§ã¯ãããŒããã¥ãŒãæ©èœãåå®è£ ãã2023.9.3-kinel.4以åããã®æŽæ°æã«èšå®ãæ£åžžã«åŒãç¶ããããã«ãªã£ãŠããŸãã + +### General +- Feat: TLäžããããŒããèŠããªããªãã¯ãŒããã¥ãŒãã§ããããŒããã¥ãŒããè¿œå +- Fix: MFM `$[unixtime ]` ã«äžæ£ãªå€ãå ¥åããéã«çºçããåçš®ãšã©ãŒãä¿®æ£ + +### Client +- Enhance: çµµæåã®ãªãŒãã³ã³ããªãŒãæ©èœåŒ·å #12364 +- Enhance: ããŒããã¬ãã¥ãŒã«ãå 容ãé ãããåæ ãããããã« +- fix: ãèšå®ã®ããã¯ã¢ãããã§äžéšã®é ç®ãããã¯ã¢ããã«å«ãŸããŠããªãã£ãåé¡ãä¿®æ£ +- Fix: äžåºŠã«å€§éã®éç¥ãå ¥ã£ãéã«éç¥é³ãé³å²ãããåé¡ãä¿®æ£ +- Fix: ãŠã£ãžã§ããã®ãžã§ããã¥ãŒã«ãŠé³å£°ã®çºé³æ¹æ³å€æŽã«è¿œåŸã§ããŠããªãã£ãã®ãä¿®æ£ #12367 + +### Server +- Fix: äœãããŒãããŠããªããŠãŒã¶ãŒã®ãã£ãŒãã«ã¢ã¯ã»ã¹ãããšãšã©ãŒã«ãªãåé¡ãä¿®æ£ +- Fix: æéçµéã«ããç¡å¹åãããã¢ã³ãããåæå¹åãããšãããµãŒãåèµ·åãŸã§ãã®ç¶æ³ãåæ ãããªãã®ãä¿®æ£ #12303 +- Enhance: MFM `$[ruby ]` ãä»ãœãããŠã§ã¢ãšé£åãããããã« + +## 2023.11.1 (merged to 2023.11.1-kinel.1) ### Note - æªæã®ãã第äžè ããªã¢ãŒããŠãŒã¶ãŒã«ãªãããŸããä»»æã®ã¢ã¯ãã£ããã£ãåãåããŠããŸãåé¡ãä¿®æ£ããŸããã詳ããã¯[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-3f39-6537-3cgc)ãã芧ãã ããã @@ -326,7 +449,7 @@ - Fix: ActivityPubã«é¢ããã»ãã¥ãªãã£ã®åäž - Fix: éå ¬éã®æçš¿ã«å¯ŸããŠè¿ä¿¡ã§ããªãããã« -## 2023.11.0 +## 2023.11.0 (merged to 2023.11.1-kinel.1) ### Note - iOS 16.4æªæºã䜿çšããŠããå Žåã¯iOS 16.4以äžã«ã¢ããããŒãããé¡ãããŸã @@ -402,7 +525,7 @@ - Fix: ãµãŒããŒãµã€ãããã®ãã¹ãéç¥ãæ£ããè¡ããããã«ä¿®æ£ - Fix: GTLã®ããªããŒãã衚瀺ããªãã·ã§ã³ãæ©èœããªãã®ãä¿®æ£ #12233 -## 2023.10.2 +## 2023.10.2 (merged to 2023.11.1-kinel.1) ### General - Feat: ã¢ã³ããã§ããŒã«ã«ã®æçš¿ã®ã¿åéã§ããããã«ãªããŸãã @@ -416,6 +539,7 @@ ### Client - Enhance: TLã®è¿ä¿¡è¡šç€ºãªãã·ã§ã³ãèšæ¶ããããã« - Enhance: æçš¿ãããŠããæéãçµéããŠããããŒãã§ããããšãèŠèŠçã«åããããã +- Feat: çµµæåããã«ãŒã®ã«ããŽãªã«ã/ããå ¥ããããšã§ãã©ã«ãåã衚瀺ã§ããããã« ### Server - Enhance: ã¿ã€ã ã©ã€ã³ååŸæã®ããã©ãŒãã³ã¹ãåäž @@ -426,7 +550,7 @@ - Change: ãŠãŒã¶ãŒã®isCatãtrueã§ãããµãŒããŒã§ã¯nyaizeãè¡ãããªããªããŸãã - isCatãªå Žåãã¯ã©ã€ã¢ã³ãã§nyaizeåŠçãè¡ãããšãæšå¥šããŸã -## 2023.10.1 +## 2023.10.1 (merged to 2023.11.1-kinel.1) ### General - Enhance: ããŒã«ã«ã¿ã€ã ã©ã€ã³ããœãŒã·ã£ã«ã¿ã€ã ã©ã€ã³ã§è¿ä¿¡ãå«ããã©ããèšå®å¯èœã« @@ -437,7 +561,7 @@ - Fix: ãã©ããŒããŠãããŠãŒã¶ãŒããã®èªåã®æçš¿ãžã®è¿ä¿¡ãã¿ã€ã ã©ã€ã³ã«å«ãŸããªãåé¡ãä¿®æ£ - Fix: users/notesã§ã»ã³ã·ãã£ããã£ã³ãã«ã®æçš¿ãå«ãŸããå Žåãããåé¡ãä¿®æ£ -## 2023.10.0 +## 2023.10.0 (merged to 2023.11.1-kinel.1) ### NOTE - 2023.9.2ã§å°å ¥ãããããŒãç·šéæ©èœã¯ã¯ãªãªãã£ã®é«ãå®è£ ãå°é£ã§ããããšãå€æããããæ€åãããŸãã - ã¢ããããŒããè¡ããšãã¿ã€ã ã©ã€ã³ãäžæçã«ãªã»ãããããŸã @@ -486,7 +610,30 @@ - Fix: ããã¡ã€ã«ä»ãã®ã¿ãã®TLã§ãã¡ã€ã«ç¡ãã®æ°çããŒããæµããåé¡ãä¿®æ£ - Fix: ããã»ã¹ãçµäºããªãããããã¯éåžžã«æéããããåé¡ãä¿®æ£ -## 2023.9.3 +## 2023.9.3-kinel.4 + +### Client +- Fix: Renoteã®çç¥è¡šç€ºã§ã»ã³ã·ãã£ããã£ã³ãã«ã®èªåCWãå¹ããªãåé¡ + +### Server +- 2023.10.xåãã®TLãå éšçã«æ§ç¯ããããã«ãªããŸãã + - ããã«ããã2023.9.3-kinel.4æŽæ°åŸã®noteã¯2023.10.x以éã«æŽæ°ããåŸã§ãèŠãããšãåºæ¥ãŸã + - 2023.10.xã«ã¯ãæŽæ°ä»¥åã®noteãTLã§èŠãããšãåºæ¥ãªããšããä»æ§ããããŸãã + - 2023.9.xãå©çšããŠããå ã¯ç¹ã«åœ±é¿ãããŸãã + +## 2023.9.3-kinel.3 (unreleased) + +### General +- ããŒãç·šéæ©èœãåé€ + - roleã«ããçŠæ¢ãããŠããããŠãŒã¶ã«ã¯åœ±é¿ããããŸããã +- æ§ã ãªã«ã©ã ã§ãªãã©ã€ã衚瀺ãããªãã®ã衚瀺ãããããã«æ»ããŸãã + +## 2023.9.3-kinel.2 + +### General +- noteãããã©ã«ãã§ç·šéã§ããªãããã«ãªããŸããã + +## 2023.9.3-kinel.1 ### General - Enhance: ããŒãã®ç¿»èš³æ©èœã®å©çšå¯åŠãããŒã«ã§èšå®å¯èœã« @@ -495,11 +642,15 @@ - Enhance: ã¢ãã¬ãŒã·ã§ã³ãã°æ©èœã®åŒ·å - Enhance: ããŒã«ãªãŒãŒã·ã§ã³ã®æŽæ° +--- + +- Fix: "å³ã¯ãªãã¯ã§ãªã¢ã¯ã·ã§ã³ããã«ãŒãéãããã«ãã"ã2ã€ãã + ### Server - Fix: Redisã«å€ãããŒãžã§ã³ã®ãã£ãã·ã¥ãæ®ã£ãŠããå Žåããã£ãã·ã¥ãæ¶ãããŸã§ã®ééç¥ãå±ããªããªãåé¡ãä¿®æ£ - Fix: åŸæ¹äºææ§ã®ä¿®æ£ -## 2023.9.2 +## 2023.9.2 (merged to 2023.9.3) ### General - Feat: ããŒãã®ç·šéãã§ããããã« @@ -521,11 +672,16 @@ - Enhance: Masterããã»ã¹ã®PIDãæžãåºããããã« - Enhance: admin/ad/createã«ãŠã¬ã¹ãã³ã¹200ãèšå®ããåºåæ å ±ãè¿ãããã« -## 2023.9.1 +## 2023.9.1-kinel.1 ### General - Enhance: ã¢ãã¬ãŒã·ã§ã³ãã°æ©èœã®åŒ·å +--- + +- 4K/8Kç»åã2K以äžã§è¡šç€ºãããŠããŸãåé¡ãä¿®æ£ããŸãã + - æ¢åã®ãã¡ã€ã«ã¯æŽæ°ããããæ°èŠã¢ããããŒãåã«ã®ã¿é©çšãããŸã + ### Client - Fix: ããŒãã®ã¡ãã¥ãŒã«ããã詳现ããã¿ã³ã®è¡šç€ºããã°ã€ã³/ãã°ã¢ãŠãç¶æ ã§çµ±äžãããŠããªãåé¡ãä¿®æ£ @@ -533,7 +689,7 @@ - Fix: ãç¥ããã®ããŒãžããŒã·ã§ã³ãæ©èœããªã - Fix: ããŠãŒã¶ãŒã®æ°èŠæçš¿ãã®éç¥èšå®ãåãæ¿ãããšãµãŒããŒå éšãšã©ãŒãåºã -## 2023.9.0 +## 2023.9.0 (merged to 2023.9.1) ### Note - meilisearchã䜿çšããå Žåãv1.2以äžãå¿ èŠã§ã @@ -629,21 +785,59 @@ - Fix: äžéšã®ãµãŒããŒå éšãšã©ãŒãã¹ã¿ãã¯ãã¬ãŒã¹ãè¿ããªãããã«ä¿®æ£ - Fix: äžéšã®ãªã¢ãŒããŠãŒã¶ãŒããã©ããŒããããšãã§ããªãåé¡ãä¿®æ£ -## 13.14.2 +## 13.14.2-kinel.4 + +### General +- ãç¥ããæ©èœã®åŒ·å + - ãŠãŒã¶ãŒåå¥ã®ãç¥ãããäœæå¯èœã« + - ãç¥ããã®ãããŒè¡šç€ºããã€ã¢ãã°è¡šç€ºãå¯èœã« + - ãç¥ããã®ã¢ã€ã³ã³ãèšå®å¯èœã« +- ãã€ã©ã€ãããããã¹ãã®ã¿ã®ãããã¿noteãé€å€ããããã«ãªããŸãã ### Client -- ãªã¹ãTLã§ããŠãŒã¶ãŒãè¿œå ã»åé€ãããŠãTLãåæåããªãããã« -- URLååŸå€æ°ãé¢æ°ã«å€æŽãCURRENT_URL -> Mk:url() -- Fix: ã¢ãã€ã«è¡šç€ºã®ãšãããŒãžäžéšãããã²ãŒã·ã§ã³ããŒã«é ããåé¡ãä¿®æ£ -- Fix: äžéšã¢ãŒãã«ãã€ã¢ãã°ã§ã¹ã¯ããŒã«ã§ããªãåé¡ãä¿®æ£ -- Fix: Selecting all emojis in Custom emoji is impossible -- Fix: PhotoSwipeã«ããã¡ã¢ãªãªãŒã¯ã®ä¿®æ£ +- ã¡ãã¥ãŒã®ã¹ã€ããã®åäœãæ¹å +- Enhance: ãŠãŒã¶ãŒã¡ãã¥ãŒã§ã¹ã€ããã§ãŠãŒã¶ãŒãªã¹ãã«è¿œå ã»åé€ã§ããããã« +- ç»åã®å§çž®æ¹æ³ãéžæå¯èœã«ããŸãã + - ãµã€ãºå€æŽãè¡ãããéžæå¯èœã«ããŸãã + - 匷å¶çã«éå¯éå§çž®ã§ããããã«ãªããŸãã +- ã»ã³ã·ãã£ããã£ã³ãã«ã®NoteããããŸããæ¹æ³ãCWã«ãªããŸãã +- About Misskeyã«ãã®forkã«ä»ããŠã®æ å ±ãè¿œå ããŸãã +- Fix: æªèªã®ãç¥ããã®ãããã£ãããã¯ãªãã¯ã»ã¿ããããŠããã®å Žã§ãããã£ãããæ¶ããªãåé¡ãä¿®æ£ +- Fix: Misskeyãã©ã°ã€ã³ãã€ã³ã¹ããŒã«ããéã®AiScriptããŒãžã§ã³ã®ãã§ãã¯ã0.14.0以éã«å¯Ÿå¿ããŠããªãåé¡ãä¿®æ£ ### Server -- Fix: APIã®ãªãã»ãããå£ããŠããããã§ããã£ãšèŠããã§ãã£ãšèŠããªãåé¡ãä¿®æ£ -- Fix: å€éšãµãŒããŒã®æçš¿ãã¿ã€ã ã©ã€ã³ã«è¡šç€ºãããªãããšãããåé¡ãä¿®æ£ +- ãã¡ã€ã«ã¢ããããŒãæçã«ãã¡ã€ã«åã®æ¡åŒµåãä¿®æ£ããé¢æ°(correctFilename)ã®æåãæ¹å +- fix: muteãapiããã®user list timelineååŸã§æ©èœããªãåé¡ãä¿®æ£ + +## 13.14.2-kinel.3 + +### General +- æŽæ°åŸååã«è¡šç€ºããã`æŽæ°æ å ±ãèŠã`ãã¿ã³ã®ãªã³ã¯å ã[ã«ããã¿ãããŒéšã®CHANGELOG.md](https://github.com/niri-la/misskey.niri.la/blob/develop/CHANGELOG.md)ã«å€æŽããŸãã -## 13.14.1 +### Client +- é·ãnoteãªã©ããããŸããªãåé¡ãä¿®æ£ããŸãã +- ã»ã³ã·ãã£ããã£ã³ãã«ã®NoteããŠãŒã¶ããŒãžãããããŸããã®ãç¡å¹åã§ããããã«ããŸãã + +### Server + +## 13.14.2-kinel.2 + +### General +- ãã€ã©ã€ãããã®ãã¯noteã®é€å€ã§CWãèŠå¿ããŠããã®ãä¿®æ£ +- ãã£ã³ãã«ãã»ã³ã·ãã£ãæå®ã§ããããã«ãªããŸãã +- ãã€ã©ã€ãããé€å€ãããã£ã³ãã«ãsensitiveãã£ã³ãã«ã®ã¿ã«ããŸãã +- ã»ã³ã·ãã£ããã£ã³ãã«ã®Noteãã¯ããŒã©ãŒã«ããã€ã³ããã¯ã¹ãæåŠããããã«ãªããŸãã + +### Client +- ã»ã³ã·ãã£ããã£ã³ãã«ã®Noteã®ReNoteã¯ããã©ã«ãã§Home TLã«æµããããã«ãªããŸãã +- ã»ã³ã·ãã£ããã£ã³ãã«ã®NoteããŠãŒã¶ããŒãžããé衚瀺ãŸãã¯ãããŸããããã«ãªããŸãã +- Enhance: Renoteèªäœãéå ±ã§ããããã« + +### Server +- éå ±ã®ã¡ãŒã«éç¥ãç¡å¹åãããªãã·ã§ã³ãè¿œå +- éç¥ã®ä¿åäžéãå€æŽå¯èœã« + +## 13.14.2-kinel.1 ### General - æåŸ æ©èœãæ¹åããŸãã @@ -654,22 +848,20 @@ - identiconçæãç¡å¹ã«ããŠããã©ãŒãã³ã¹ãåäžãããããšãã§ããããã«ãªããŸãã - ãµãŒããŒã®ãã·ã³æ å ±ã®å ¬éãç¡å¹ã«ããŠããã©ãŒãã³ã¹ãåäžãããããšãã§ããããã«ãªããŸãã +--- + +- 管çè å°çšã®ä»äººãèŠãwebhookãå¢ããŸãã +- ãã€ã©ã€ããããã¯noteãšãã£ã³ãã«æçš¿ãé€å€ããããã«ãªããŸãã + ### Client -- deck UIã®ã«ã©ã ã®ã¡ãã¥ãŒããã¢ã³ãããšãªã¹ãã®ç·šéç»é¢ãéããããã« - ãã©ã€ããã¡ã€ã«ã®ã¡ãã¥ãŒã§ç»åãã¯ãããã§ããããã« -- ç»åãåç»ãšåæ§ã«ç°¡åã«é ããããã« - Enhance: ããŒãã®åã蟌ã¿ãè€æ°ç»åãšåç»ã衚瀺ãããããã« - ãªãªãžãã«ç»åãä¿æããã«ã¢ããããŒãããå Žåwebpã§ã¢ããããŒããããããã«(Safari以å€) -- èŠãããšã®ããRenoteãçç¥ããŠè¡šç€ºããªã³ã®ãšãã«èªåã®noteã®renoteãçç¥ããããã« - ãã©ã«ããŒããã¡ã€ã«ã«å¯ŸããŠãéçºè ã¢ãŒã䜿çšæãIDãã³ããŒã§ããããã« -- åŒçšå¯Ÿè±¡ãããã£ãšèŠããã§å±éããå Žåããéãããã§ç³ããããã« - ãããã£ãŒã«URLãã³ããŒã§ãããã¿ã³ãè¿œå #11190 - `CURRENT_URL`ã§çŸåšè¡šç€ºäžã®URLãååŸã§ããããã«(AiScript) - ãŠãŒã¶ãŒã®ContextMenuã«ãã¢ã³ããã«è¿œå ããã¿ã³ãè¿œå -- ãã©ããŒããæ°ã«å ¥ãç»é²ãããŠããªããã£ã³ãã«ãéãæã¯æŠèŠããŒãžãéãããã« -- ç»é¢ãã¥ãŒã¯ãã¿ããããå ŽåãããŠã¹ã¯ãªãã¯ãšåæ§ã«ç»åãã¥ãŒã¯ãéããããã« - ãªãã©ã€ã³æã®ç»é¢ã«ãªããŒããã¿ã³ãè¿œå -- Renoteæã«å ¬éç¯å²ã®ããã©ã«ãèšå®ãé©çšãããããã« - Deckã§éã«ãŒãããŒãžã«ã¢ã¯ã»ã¹ããéã«ç°¡æUIã§è¡šç€ºããªãèšå®ãè¿œå - ããŒã«èšå®ç»é¢ã§ããŒã«IDã確èªã§ããããã« - ã³ã³ããã¹ãã¡ãã¥ãŒè¡šç€ºæã®ããã©ãŒãã³ã¹ãæ¹å @@ -677,16 +869,26 @@ - æ¬æã«MFMãå«ãŸããŠããå Žåã«èªåã§ãããŸããæ©èœããè¿ä¿¡å ãåŒçšRNã«ãé©çšãããããã« - position ã¯å¯Ÿè±¡å€ã«ãªããŸãã - AiScriptã0.15.0ã«æŽæ° +- ãªã¹ãTLã§ããŠãŒã¶ãŒãè¿œå ã»åé€ãããŠãTLãåæåããªãããã« +- URLååŸå€æ°ãé¢æ°ã«å€æŽãCURRENT_URL -> Mk:url() - Fix: ãµãŒããŒã¡ããªã¯ã¹ã90床åŸããŠãã - Fix: éãã°ã€ã³æã«ã¯ã¬ãã³ã·ã£ã«ãå¿ èŠãªããŒãžã«è¡ããšãšã©ãŒãåºãåé¡ãä¿®æ£ - Fix: sparkleå ã«ãªã³ã¯ãå ¥ãããšã¯ãªãã¯äžèœã«ãªãåé¡ã®ä¿®æ£ - Fix: ZenUIã§ãããã¢ããã®è¡šç€ºäœçœ®ãããããåé¡ãä¿®æ£ - Fix: ããŒãžé·ç§»ã§ã¹ã¯ããŒã«äœçœ®ãä¿æãããªãåé¡ãä¿®æ£ - Fix: ãã©ã«ããŒã®ããŒãžããŒã·ã§ã³ãæ©èœããªã #11180 -- Fix: é·ãæç« ãæçš¿ããéããã¬ãã¥ãŒãç»é¢ããã¯ã¿åºãåé¡ãä¿®æ£ - Fix: ã·ã¹ãã ãã©ã³ãèšå®ãæ£ããåæ ãããªãåé¡ãä¿®æ£ - Fix: ã¢ã³ã±ãŒãçµäºæã®ããã·ã¥éç¥ãæ£ãã衚瀺ãããªãåé¡ãä¿®æ£ - Fix: MasterVolumeã0ã®æã ãã§ãªãåéç¥é³ã®é³éèšå®ã0ã®ãšãããHTMLAudioElement.playãå®è¡ãããªãããã«å€æŽ +- Fix: ã¢ãã€ã«è¡šç€ºã®ãšãããŒãžäžéšãããã²ãŒã·ã§ã³ããŒã«é ããåé¡ãä¿®æ£ +- Fix: äžéšã¢ãŒãã«ãã€ã¢ãã°ã§ã¹ã¯ããŒã«ã§ããªãåé¡ãä¿®æ£ +- Fix: Selecting all emojis in Custom emoji is impossible +- Fix: PhotoSwipeã«ããã¡ã¢ãªãªãŒã¯ã®ä¿®æ£ + +--- + +- ãã¬ãã¥ãŒã®è¡šç€ºç¶æ ãèšæ¶ããããã« +- çµµæåããã«ãŒã®æ€çŽ¢ã®è¡šç€ºä»¶æ°ã100件ã«å¢å ### Server - JSON.parse ã®åæ°ãåæžããããšã§ãã¹ããªãŒãã³ã°ã®ããã©ãŒãã³ã¹ãåäžããŸãã @@ -706,6 +908,28 @@ - Fix: ã€ã³ã¹ã¿ã³ã¹ã®ã¢ã€ã³ã³ãbase64ã®å Žåã®æåãä¿®æ£ - Fix: ããŒã«ã«ã® `Person` ãæã `acct` URI ã解æãããšãã®ãã°ãä¿®æ£ããŸãã - Fix: ç¡å¹åãããã¢ã³ãããå床æå¹åãããªãããšãããåé¡ãä¿®æ£ +- Fix: APIã®ãªãã»ãããå£ããŠããããã§ããã£ãšèŠããã§ãã£ãšèŠããªãåé¡ãä¿®æ£ +- Fix: å€éšãµãŒããŒã®æçš¿ãã¿ã€ã ã©ã€ã³ã«è¡šç€ºãããªãããšãããåé¡ãä¿®æ£ + +--- + +- éå ±ãDiscordã®Webhookã«éä¿¡ã§ããããã« + +## 13.13.2-kinel-0.0.1 + +### General + +### Client +- deck UIã®ã«ã©ã ã®ã¡ãã¥ãŒããã¢ã³ãããšãªã¹ãã®ç·šéç»é¢ãéããããã« +- ç»åãåç»ãšåæ§ã«ç°¡åã«é ããããã« +- èŠãããšã®ããRenoteãçç¥ããŠè¡šç€ºããªã³ã®ãšãã«èªåã®noteã®renoteãçç¥ããããã« +- ãã©ããŒããæ°ã«å ¥ãç»é²ãããŠããªããã£ã³ãã«ãéãæã¯æŠèŠããŒãžãéãããã« +- åŒçšå¯Ÿè±¡ãããã£ãšèŠããã§å±éããå Žåããéãããã§ç³ããããã« +- Renoteæã«å ¬éç¯å²ã®ããã©ã«ãèšå®ãé©çšãããããã« +- ç»é¢ãã¥ãŒã¯ãã¿ããããå ŽåãããŠã¹ã¯ãªãã¯ãšåæ§ã«ç»åãã¥ãŒã¯ãéããããã« +- Fix: é·ãæç« ãæçš¿ããéããã¬ãã¥ãŒãç»é¢ããã¯ã¿åºãåé¡ãä¿®æ£ + +### Server ## 13.13.2 diff --git a/COPYING b/COPYING index 6a5f3ca1d598..44aedb83aa77 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2024 syuilo and contributors +Copyright © 2014-2024 syuilo and contributors, anatawa12, Sayamame-beans, ni-rila for changes in this fork. 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/locales/en-US.yml b/locales/en-US.yml index d00f23632cc1..6b83e3f32b88 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -118,6 +118,7 @@ pinned: "Pin to profile" you: "You" clickToShow: "Click to show" sensitive: "Sensitive" +sensitiveChannelAutoCW: "Sensitive Channel Auto CW" add: "Add" reaction: "Reactions" reactions: "Reactions" diff --git a/locales/index.d.ts b/locales/index.d.ts index a51d35437a66..a41642e36e79 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -212,6 +212,10 @@ export interface Locale extends ILocale { * ãã®ããŒããåé€ããŠããäžåºŠç·šéããŸããïŒãã®ããŒããžã®ãªã¢ã¯ã·ã§ã³ããªããŒããè¿ä¿¡ãå šãŠåé€ãããŸãã */ "deleteAndEditConfirm": string; + /** + * ããŒã æçš¿ã«ãã + */ + "makeNoteHome": string; /** * ãªã¹ãã«è¿œå */ @@ -488,6 +492,10 @@ export interface Locale extends ILocale { * ã»ã³ã·ãã£ã */ "sensitive": string; + /** + * ã»ã³ã·ãã£ããã£ã³ãã«èªåCW + */ + "sensitiveChannelAutoCW": string; /** * è¿œå */ @@ -932,6 +940,10 @@ export interface Locale extends ILocale { * ãã®ããŒããåé€ããŸããïŒ */ "noteDeleteConfirm": string; + /** + * æ¬åœã«ããŒã æçš¿ã«ããŸããïŒ + */ + "makeNoteHomeConfirm": string; /** * ãã以äžãã³çãã§ããŸãã */ @@ -3200,6 +3212,10 @@ export interface Locale extends ILocale { * å³ã¯ãªãã¯ã§ãªã¢ã¯ã·ã§ã³ããã«ãŒãéã */ "useReactionPickerForContextMenu": string; + /** + * ãŠãŒã¶ããŒãžã§ã»ã³ã·ãã£ããã£ã³ãã«ã®æçš¿ãéãã + */ + "collapseSensitiveChannel": string; /** * {users}ãå ¥åäž */ @@ -4892,6 +4908,10 @@ export interface Locale extends ILocale { * {name}ã®ã»ã³ã·ãã£ããªãã¡ã€ã«ãå«ãæçš¿ */ "userSaysSomethingSensitive": ParameterizedString<"name">; + /** + * {name}ã®ã»ã³ã·ãã£ããã£ã³ãã«ã§ã®æçš¿ + */ + "userSaysSomethingInSensitiveChannel": ParameterizedString<"name">; /** * ã¹ã¯ã€ãããŠã¿ããåãæ¿ãã */ @@ -6939,6 +6959,14 @@ export interface Locale extends ILocale { * ãããžã§ã¯ãã¡ã³ã㌠*/ "projectMembers": string; + /** + * ãã®ãµãŒããŒã§äœ¿çšããŠããforkã®äž»èŠãªéçºè + */ + "forkContributors": string; + /** + * ãã®ãµãŒããŒã§äœ¿çšããŠããforkã®å šãŠã®ã³ã³ããªãã¥ãŒã¿ãŒ + */ + "allForkContributors": string; }; "_displayOfSensitiveMedia": { /** @@ -9198,8 +9226,42 @@ export interface Locale extends ILocale { * ã¡ã³ã·ã§ã³ããããšã */ "mention": string; + /** + * 以äžã®ãŠãŒã¶ãnoteãããšã + */ + "usersLabel": string; + /** + * ãã®ãµãŒããŒã®ãŠãŒã¶ã®@ã«æãŸããéšåãæ¹è¡ã§åºåã£ãŠæå®ããŸã + */ + "usersCaption": string; }; }; + "_imageCompressionMode": { + /** + * ç»åã®å§çž®åœ¢åŒ + */ + "title": string; + /** + * ãªãªãžãã«ç»åãä¿æããªãå Žåã«ãWebå ¬éçšç»åã®å§çž®åœ¢åŒãéžæã§ããŸããçž®å°ããå Žåã¯2560x2560ããå°ãããªãããã«çž®å°ãããŸããéå¯éå§çž®ãæå®ããªãå Žåã¯ãå ç»åã«å¿ããŠéå¯éå§çž®ãå¯éå§çž®ããèªåçã«éžæãããŸãã + */ + "description": string; + /** + * çž®å°ããŠåå§çž®ãã + */ + "resizeCompress": string; + /** + * çž®å°ããåå§çž®ãã + */ + "noResizeCompress": string; + /** + * çž®å°ããŠéå¯éå§çž®ãã + */ + "resizeCompressLossy": string; + /** + * çž®å°ããéå¯éå§çž®ãã + */ + "noResizeCompressLossy": string; + }; "_moderationLogTypes": { /** * ããŒã«ãäœæ @@ -9345,6 +9407,10 @@ export interface Locale extends ILocale { * ãŠãŒã¶ãŒã®ãããŒãè§£é€ */ "unsetUserBanner": string; + /** + * ããŒããããŒã æçš¿ã«å€æŽ + */ + "makeNoteHome": string; }; "_fileViewer": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 61f2db9f831e..e7e17da47c3d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -49,6 +49,7 @@ copyLinkRenote: "ãªããŒãã®ãªã³ã¯ãã³ããŒ" delete: "åé€" deleteAndEdit: "åé€ããŠç·šé" deleteAndEditConfirm: "ãã®ããŒããåé€ããŠããäžåºŠç·šéããŸããïŒãã®ããŒããžã®ãªã¢ã¯ã·ã§ã³ããªããŒããè¿ä¿¡ãå šãŠåé€ãããŸãã" +makeNoteHome: "ããŒã æçš¿ã«ãã" addToList: "ãªã¹ãã«è¿œå " addToAntenna: "ã¢ã³ããã«è¿œå " sendMessage: "ã¡ãã»ãŒãžãéä¿¡" @@ -118,6 +119,7 @@ pinned: "ãã³çã" you: "ããªã" clickToShow: "ã¯ãªãã¯ããŠè¡šç€º" sensitive: "ã»ã³ã·ãã£ã" +sensitiveChannelAutoCW: "ã»ã³ã·ãã£ããã£ã³ãã«èªåCW" add: "è¿œå " reaction: "ãªã¢ã¯ã·ã§ã³" reactions: "ãªã¢ã¯ã·ã§ã³" @@ -229,6 +231,7 @@ blockedUsers: "ãããã¯ãããŠãŒã¶ãŒ" noUsers: "ãŠãŒã¶ãŒã¯ããŸãã" editProfile: "ãããã£ãŒã«ãç·šé" noteDeleteConfirm: "ãã®ããŒããåé€ããŸããïŒ" +makeNoteHomeConfirm: "æ¬åœã«ããŒã æçš¿ã«ããŸããïŒ" pinLimitExceeded: "ãã以äžãã³çãã§ããŸãã" intro: "Misskeyã®ã€ã³ã¹ããŒã«ãå®äºããŸããïŒç®¡çè ã¢ã«ãŠã³ããäœæããŸãããã" done: "å®äº" @@ -796,6 +799,7 @@ emailNotification: "ã¡ãŒã«éç¥" publish: "å ¬é" inChannelSearch: "ãã£ã³ãã«å æ€çŽ¢" useReactionPickerForContextMenu: "å³ã¯ãªãã¯ã§ãªã¢ã¯ã·ã§ã³ããã«ãŒãéã" +collapseSensitiveChannel: "ãŠãŒã¶ããŒãžã§ã»ã³ã·ãã£ããã£ã³ãã«ã®æçš¿ãéãã" typingUsers: "{users}ãå ¥åäž" jumpToSpecifiedDate: "ç¹å®ã®æ¥ä»ã«ãžã£ã³ã" showingPastTimeline: "éå»ã®ã¿ã€ã ã©ã€ã³ã衚瀺ããŠããŸã" @@ -1219,6 +1223,7 @@ backToTitle: "ã¿ã€ãã«ãž" hemisphere: "ãäœãŸãã®å°å" withSensitive: "ã»ã³ã·ãã£ããªãã¡ã€ã«ãå«ãããŒãã衚瀺" userSaysSomethingSensitive: "{name}ã®ã»ã³ã·ãã£ããªãã¡ã€ã«ãå«ãæçš¿" +userSaysSomethingInSensitiveChannel: "{name}ã®ã»ã³ã·ãã£ããã£ã³ãã«ã§ã®æçš¿" enableHorizontalSwipe: "ã¹ã¯ã€ãããŠã¿ããåãæ¿ãã" loading: "èªã¿èŸŒã¿äž" surrender: "ããã" @@ -1810,6 +1815,8 @@ _aboutMisskey: morePatrons: "ä»ã«ãå€ãã®æ¹ãæ¯æŽããŠãããŠããŸããããããšãããããŸãð¥°" patrons: "æ¯æŽè " projectMembers: "ãããžã§ã¯ãã¡ã³ããŒ" + forkContributors: "ãã®ãµãŒããŒã§äœ¿çšããŠããforkã®äž»èŠãªéçºè " + allForkContributors: "ãã®ãµãŒããŒã§äœ¿çšããŠããforkã®å šãŠã®ã³ã³ããªãã¥ãŒã¿ãŒ" _displayOfSensitiveMedia: respect: "ã»ã³ã·ãã£ãèšå®ãããã¡ãã£ã¢ãé ã" @@ -2440,6 +2447,16 @@ _webhookSettings: renote: "Renoteããããšã" reaction: "ãªã¢ã¯ã·ã§ã³ããã£ããšã" mention: "ã¡ã³ã·ã§ã³ããããšã" + usersLabel: "以äžã®ãŠãŒã¶ãnoteãããšã" + usersCaption: "ãã®ãµãŒããŒã®ãŠãŒã¶ã®@ã«æãŸããéšåãæ¹è¡ã§åºåã£ãŠæå®ããŸã" + +_imageCompressionMode: + title: "ç»åã®å§çž®åœ¢åŒ" + description: "ãªãªãžãã«ç»åãä¿æããªãå Žåã«ãWebå ¬éçšç»åã®å§çž®åœ¢åŒãéžæã§ããŸããçž®å°ããå Žåã¯2560x2560ããå°ãããªãããã«çž®å°ãããŸããéå¯éå§çž®ãæå®ããªãå Žåã¯ãå ç»åã«å¿ããŠéå¯éå§çž®ãå¯éå§çž®ããèªåçã«éžæãããŸãã" + resizeCompress: "çž®å°ããŠåå§çž®ãã" + noResizeCompress: "çž®å°ããåå§çž®ãã" + resizeCompressLossy: "çž®å°ããŠéå¯éå§çž®ãã" + noResizeCompressLossy: "çž®å°ããéå¯éå§çž®ãã" _moderationLogTypes: createRole: "ããŒã«ãäœæ" @@ -2478,6 +2495,7 @@ _moderationLogTypes: deleteAvatarDecoration: "ã¢ã€ã³ã³ãã³ã¬ãŒã·ã§ã³ãåé€" unsetUserAvatar: "ãŠãŒã¶ãŒã®ã¢ã€ã³ã³ã解é€" unsetUserBanner: "ãŠãŒã¶ãŒã®ãããŒã解é€" + makeNoteHome: "ããŒããããŒã æçš¿ã«å€æŽ" _fileViewer: title: "ãã¡ã€ã«ã®è©³çŽ°" diff --git a/package.json b/package.json index 41b865456dbb..01032829c550 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "misskey", - "version": "2024.3.1", + "version": "2024.3.1-kinel.1", "codename": "nasubi", "repository": { "type": "git", - "url": "https://github.com/misskey-dev/misskey.git" + "url": "https://github.com/niri-la/misskey.niri.la" }, "packageManager": "pnpm@8.15.4", "workspaces": [ diff --git a/packages/backend/migration/1700906353915-HardMuteAndSoftMuteFromRegistory.js b/packages/backend/migration/1700906353915-HardMuteAndSoftMuteFromRegistory.js new file mode 100644 index 000000000000..27e2d3b93b72 --- /dev/null +++ b/packages/backend/migration/1700906353915-HardMuteAndSoftMuteFromRegistory.js @@ -0,0 +1,66 @@ +export class HardMuteAndSoftMuteFromRegistory1700906353915 { + async up(queryRunner) { + // for servers start using this fork of misskey after 2023.11.1-kinel.1, + // This migration should not be run so opt in this migration by setting + // `MIGRATE_HARD_MUTE_AND_SOFT_MUTE_FROM_REGISTORY` to `true` + if (process.env.MIGRATE_HARD_MUTE_AND_SOFT_MUTE_FROM_REGISTORY !== 'true') return; + + // until 2023.9.3-kinel.4, `mutedWords` means hard muted words + // since 2023.11.1-kinel.1, `mutedWords` means soft muted words and `hardMutedWords` means hard muted words + // so migrate hard muted words to `hardMutedWords` + await queryRunner.query(`UPDATE "user_profile" SET "hardMutedWords" = "mutedWords";`); + + // then, migrate soft muted words from registry + let entries = await queryRunner.query( + `SELECT "userId", "value" FROM "registry_item" + WHERE "registry_item"."domain" IS NULL + AND "registry_item"."key" = $1 + AND "registry_item"."scope" = $2;`, + ['mutedWords', ['client', 'base']]); + + for (let entry of entries) { + await queryRunner.query(`UPDATE "user_profile" SET "mutedWords" = $1 WHERE "user_profile"."userId" = $2;`, + [JSON.stringify(entry.value), entry.userId]); + } + } + + async down(queryRunner) { + // for servers start using this fork of misskey after 2023.11.1-kinel.1, + // This migration should not be run so opt in this migration by setting + // `MIGRATE_HARD_MUTE_AND_SOFT_MUTE_FROM_REGISTORY` to `true` + if (process.env.MIGRATE_HARD_MUTE_AND_SOFT_MUTE_FROM_REGISTORY !== 'true') return; + + const entries = await queryRunner.query(`SELECT "userId", "mutedWords" FROM "user_profile";`); + for (let entry of entries) { + let existingEntry = await queryRunner.query( + `SELECT "id", "userId", "value" FROM "registry_item" + WHERE "registry_item"."domain" IS NULL + AND "registry_item"."key" = $1 + AND "registry_item"."scope" = $2 + AND "registry_item"."userId" = $3;`, + ['mutedWords', ['client', 'base'], entry.userId]); + + if (existingEntry.length > 0) { + await queryRunner.connection.createQueryBuilder() + .update('registry_item') + .set({ value: entry.mutedWords }) + .where('id = :id', { id: existingEntry[0].id }) + .execute(); + } else { + await queryRunner.connection.createQueryBuilder() + .insert() + .into('registry_item') + .values({ + key: 'mutedWords', + scope: ['client', 'base'], + userId: entry.userId, + domain: null, + value: entry.mutedWords + }) + .execute(); + } + } + + await queryRunner.query(`UPDATE "user_profile" SET "mutedWords" = "hardMutedWords";`); + } +} diff --git a/packages/backend/migration/1708005334196-NirilaDeleteUserLogRepository.js b/packages/backend/migration/1708005334196-NirilaDeleteUserLogRepository.js new file mode 100644 index 000000000000..eae6fba01c1c --- /dev/null +++ b/packages/backend/migration/1708005334196-NirilaDeleteUserLogRepository.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: anatawa12 + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class NirilaDeleteUserLogRepository1708005334196 { + constructor() { + this.name = 'NirilaDeleteUserLogRepository1708005334196' + } + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "nirila_delete_user_log" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "username" character varying(128) NOT NULL, "email" character varying(128), "info" jsonb NOT NULL, CONSTRAINT "PK_34be175a3f407ea9f0dd42da572" PRIMARY KEY ("id")); COMMENT ON COLUMN "nirila_delete_user_log"."username" IS 'The username of the deleted User.'; COMMENT ON COLUMN "nirila_delete_user_log"."email" IS 'The email adddress of the deleted User.'`); + await queryRunner.query(`CREATE INDEX "IDX_343c0efea0f002a2bf34960bd1" ON "nirila_delete_user_log" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_940ffb8ea315bef184208780f8" ON "nirila_delete_user_log" ("username") `); + await queryRunner.query(`CREATE INDEX "IDX_da5751593e765f39d09303f768" ON "nirila_delete_user_log" ("email") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_da5751593e765f39d09303f768"`); + await queryRunner.query(`DROP INDEX "public"."IDX_940ffb8ea315bef184208780f8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_343c0efea0f002a2bf34960bd1"`); + await queryRunner.query(`DROP TABLE "nirila_delete_user_log"`); + } +} diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 0ca1fa55c1f8..cb92bd877920 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -93,6 +93,15 @@ type Source = { perUserNotificationsMaxCount?: number; deactivateAntennaThreshold?: number; pidFile: string; + + nirila?: { + abuseDiscordHook?: string; + disableAbuseRepository?: boolean; + maxWebImageSize?: number; + withRepliesInHomeTL?: boolean; + withRepliesInUserList?: boolean; + blockMentionsFromUnfamiliarRemoteUsers?: boolean; + } }; export type Config = { @@ -170,6 +179,15 @@ export type Config = { perUserNotificationsMaxCount: number; deactivateAntennaThreshold: number; pidFile: string; + + nirila: { + abuseDiscordHook?: string; + disableAbuseRepository?: boolean; + maxWebImageSize?: number; + withRepliesInHomeTL?: boolean, + withRepliesInUserList: boolean, + blockMentionsFromUnfamiliarRemoteUsers: boolean; + } }; const _filename = fileURLToPath(import.meta.url); @@ -211,6 +229,11 @@ export function loadConfig(): Config { const redis = convertRedisOptions(config.redis, host); return { + // to avoid merge conflict in the future, this is at top + nirila: Object.assign({ + withRepliesInUserList: true, + blockMentionsFromUnfamiliarRemoteUsers: false, + }, config.nirila ?? {}), version, publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, url: url.origin, diff --git a/packages/backend/src/core/AbuseDiscordHookService.ts b/packages/backend/src/core/AbuseDiscordHookService.ts new file mode 100644 index 000000000000..ca9e53432e6c --- /dev/null +++ b/packages/backend/src/core/AbuseDiscordHookService.ts @@ -0,0 +1,40 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { MiUser } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import type { Config } from '@/config.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; + +@Injectable() +export class AbuseDiscordHookService { + constructor( + @Inject(DI.config) + private config: Config, + + private httpRequestService: HttpRequestService, + ) { + } + + @bindThis + public send(me: MiUser, user: MiUser, comment: string): void { + const webhookUrl = this.config.nirila?.abuseDiscordHook; + if (webhookUrl) { + setImmediate(async () => { + const content = 'New abuse report created!\n' + + `author: \`@${me.username}${me.host ? `@${me.host}` : ''}\`\n` + + `target user: \`@${user.username}${user.host ? `@${user.host}` : ''}\`\n` + + 'Comment:\n' + + comment; + + await this.httpRequestService.send(webhookUrl, { + method: 'POST', + headers: { + 'User-Agent': 'Niri-la-Misskey-Hooks', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content }), + }); + }); + } + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 506bea8c0cb9..e9ca80dd5ddd 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -140,6 +140,7 @@ import { VmimiRelayTimelineService } from './VmimiRelayTimelineService.js'; import { QueueModule } from './QueueModule.js'; import { QueueService } from './QueueService.js'; import { LoggerService } from './LoggerService.js'; +import { AbuseDiscordHookService } from './AbuseDiscordHookService.js'; import type { Provider } from '@nestjs/common'; //#region æååããŒã¹ã§ã®injectionçš(埪ç°åç §å¯Ÿå¿ã®ãã) @@ -419,6 +420,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApPersonService, ApQuestionService, QueueService, + AbuseDiscordHookService, //#region æååããŒã¹ã§ã®injectionçš(埪ç°åç §å¯Ÿå¿ã®ãã) $VmimiRelayTimelineService, @@ -693,6 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApPersonService, ApQuestionService, QueueService, + AbuseDiscordHookService, //#region æååããŒã¹ã§ã®injectionçš(埪ç°åç §å¯Ÿå¿ã®ãã) $VmimiRelayTimelineService, diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 1bc1df1dda4c..c55e098bc400 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -301,19 +301,26 @@ export class DriveService { let img: sharp.Sharp | null = null; let satisfyWebpublic: boolean; let isAnimated: boolean; + let compressedWidth: number; + let compressedHeight: number; try { img = await sharpBmp(path, type); const metadata = await img.metadata(); isAnimated = !!(metadata.pages && metadata.pages > 1); + const maxSize = this.config.nirila?.maxWebImageSize ?? 8192; + // nirila Extension: We want to keep original size as possible + // noinspection PointlessBooleanExpressionJS satisfyWebpublic = !!( type !== 'image/svg+xml' && // security reason type !== 'image/avif' && // not supported by Mastodon and MS Edge !(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) && - metadata.width && metadata.width <= 2048 && - metadata.height && metadata.height <= 2048 + metadata.width && metadata.width <= maxSize && + metadata.height && metadata.height <= maxSize ); + compressedWidth = metadata.width && metadata.width <= maxSize ? metadata.width : maxSize; + compressedHeight = metadata.height && metadata.height <= maxSize ? metadata.height : maxSize; } catch (err) { this.registerLogger.warn(`sharp failed: ${err}`); return { @@ -330,9 +337,9 @@ export class DriveService { try { if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) { - webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048); + webpublic = await this.imageProcessingService.convertSharpToWebp(img, compressedWidth, compressedHeight); } else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) { - webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048); + webpublic = await this.imageProcessingService.convertSharpToPng(img, compressedWidth, compressedHeight); } else { this.registerLogger.debug('web image not created (not an required image)'); } diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index f6dabfadcd6d..461ba9e21e67 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -48,6 +48,11 @@ export class FanoutTimelineService { ) { } + @bindThis + public remove(tl: FanoutTimelineName, id: string, pipeline: Redis.ChainableCommander) { + pipeline.lrem('list:' + tl, 0, id); + } + @bindThis public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { // ãªã¢ãŒãããé ããŠå±ãã(ãããã¯åŸããè¿œå ããã)æçš¿æ¥æãå€ãæçš¿ãè¿œå ããããšããŒãžããŒã·ã§ã³æã«åé¡ãåŒãèµ·ããããã diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts index b3335e38da47..1af2e15db45b 100644 --- a/packages/backend/src/core/FeaturedService.ts +++ b/packages/backend/src/core/FeaturedService.ts @@ -77,6 +77,50 @@ export class FeaturedService { return Array.from(ranking.keys()); } + // TODO: find better place? + @bindThis + public shouldBeIncludedInGlobalOrUserFeatured(note: MiNote): boolean { + if (note.visibility !== 'public') return false; // non-public note + if (note.userHost != null) return false; // remote + if (note.replyId != null) return false; // reply + // Channels are checked outside + + // In nirila misskey, it was very common to notes with `:ohayo_nirila_misskey:` or `:oyasumi_nirila_misskey:` + // Will get many reaction`:ohayo_nirila_misskey:` or `:oyasumi_nirila_misskey:` so exclude them + // if they don't have any images. + if (note.fileIds.length === 0) { + for (const exclusion of ["ãã¯ãã", "ãããã¿", ":ohayo_nirila_misskey:", ":oyasumi_nirila_misskey:", ":kon_nirila_misskey:"]) { + if (note.text?.includes(exclusion)) return false; + if (note.cw?.includes(exclusion)) return false; + } + } + + return true; + } + + @bindThis + private removeNoteFromRankingOf(name: string, windowRange: number, element: string, redisPipeline: Redis.ChainableCommander) { + // removing from current & previous window is enough + const currentWindow = this.getCurrentWindow(windowRange); + const previousWindow = currentWindow - 1; + + redisPipeline.zrem(`${name}:${currentWindow}`, element); + redisPipeline.zrem(`${name}:${previousWindow}`, element); + } + + @bindThis + public async removeNote(note: MiNote): Promise<void> { + const redisPipeline = this.redisClient.pipeline(); + this.removeNoteFromRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, note.id, redisPipeline); + this.removeNoteFromRankingOf(`featuredPerUserNotesRanking:${note.userId}`, PER_USER_NOTES_RANKING_WINDOW, note.id, redisPipeline); + + if (note.channelId) { + this.removeNoteFromRankingOf(`featuredInChannelNotesRanking:${note.channelId}`, GLOBAL_NOTES_RANKING_WINDOW, note.id, redisPipeline); + } + + await redisPipeline.exec(); + } + @bindThis private async removeFromRanking(name: string, windowRange: number, element: string): Promise<void> { const currentWindow = this.getCurrentWindow(windowRange); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 81ae2908d3dc..c01535abea57 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -61,6 +61,8 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import type Logger from '@/logger.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -151,6 +153,7 @@ type Option = { @Injectable() export class NoteCreateService implements OnApplicationShutdown { + private logger: Logger; #shutdownController = new AbortController(); constructor( @@ -219,7 +222,10 @@ export class NoteCreateService implements OnApplicationShutdown { private instanceChart: InstanceChart, private utilityService: UtilityService, private userBlockingService: UserBlockingService, - ) { } + private loggerService: LoggerService, + ) { + this.logger = this.loggerService.getLogger('note:create'); + } @bindThis public async create(user: { @@ -365,6 +371,18 @@ export class NoteCreateService implements OnApplicationShutdown { mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens); } + const willCauseNotification = mentionedUsers.some(u => u.host === null) + || (data.visibility === 'specified' && data.visibleUsers?.some(u => u.host === null)) + || data.reply?.userHost === null || (this.isQuote(data) && data.renote?.userHost === null) || false; + + if (this.config.nirila.blockMentionsFromUnfamiliarRemoteUsers && user.host !== null && willCauseNotification) { + const userEntity = await this.usersRepository.findOneBy({ id: user.id }); + if ((userEntity?.followersCount ?? 0) === 0) { + this.logger.error('Request rejected because user has no local followers', { user: user.id, note: data }); + throw new IdentifiableError('e11b3a16-f543-4885-8eb1-66cad131dbfd', 'Notes including mentions, replies, or renotes from remote users are not allowed until user has at least one local follower.'); + } + } + tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { @@ -603,7 +621,8 @@ 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')); + const userNoteEvent = `note@${user.username}` as const; + webhooks = webhooks.filter(x => (x.userId === user.id && x.on.includes('note')) || x.on.includes(userNoteEvent)); for (const webhook of webhooks) { this.queueService.webhookDeliver(webhook, 'note', { note: noteObj, @@ -739,14 +758,14 @@ export class NoteCreateService implements OnApplicationShutdown { .where('id = :id', { id: renote.id }) .execute(); - // 30%ã®ç¢ºçã3æ¥ä»¥å ã«æçš¿ãããããŒãã®å Žåãã€ã©ã€ãçšã©ã³ãã³ã°æŽæ° - if (Math.random() < 0.3 && (Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) { + // ~~30%ã®ç¢ºçã~~3æ¥ä»¥å ã«æçš¿ãããããŒãã®å Žåãã€ã©ã€ãçšã©ã³ãã³ã°æŽæ° + if ((Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) { if (renote.channelId != null) { if (renote.replyId == null) { this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5); } } else { - if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) { + if (this.featuredService.shouldBeIncludedInGlobalOrUserFeatured(renote)) { this.featuredService.updateGlobalNotesRanking(renote.id, 5); this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5); } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index c258a22927d4..d27b14d9bdb8 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import type { IActivity } from '@/core/activitypub/type.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; +import type { MiWebhook, WebhookEventType } from '@/models/Webhook.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -432,7 +432,7 @@ export class QueueService { } @bindThis - public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) { + public webhookDeliver(webhook: MiWebhook, type: WebhookEventType, content: unknown) { const data = { type, content, diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index cb0b079df0dd..412cf8697fa0 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -196,9 +196,9 @@ export class ReactionService { .where('id = :id', { id: note.id }) .execute(); - // 30%ã®ç¢ºçãã»ã«ãã§ã¯ãªãã3æ¥ä»¥å ã«æçš¿ãããããŒãã®å Žåãã€ã©ã€ãçšã©ã³ãã³ã°æŽæ° + // ~~30%ã®ç¢ºç~~ãã»ã«ãã§ã¯ãªãã3æ¥ä»¥å ã«æçš¿ãããããŒãã®å Žåãã€ã©ã€ãçšã©ã³ãã³ã°æŽæ° if ( - Math.random() < 0.3 && + //Math.random() < 0.3 && note.userId !== user.id && (Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3 ) { @@ -207,7 +207,7 @@ export class ReactionService { this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1); } } else { - if (note.visibility === 'public' && note.userHost == null && note.replyId == null) { + if (this.featuredService.shouldBeIncludedInGlobalOrUserFeatured(note)) { this.featuredService.updateGlobalNotesRanking(note.id, 1); this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1); } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 1621c41bcc54..c29c9e9ed505 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -27,6 +27,7 @@ import { QueueService } from '@/core/QueueService.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import type { MiRemoteUser } from '@/models/User.js'; +import { AbuseDiscordHookService } from '@/core/AbuseDiscordHookService.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; @@ -85,6 +86,7 @@ export class ApInboxService { private apQuestionService: ApQuestionService, private queueService: QueueService, private globalEventService: GlobalEventService, + private abuseDiscordHookService: AbuseDiscordHookService, ) { this.logger = this.apLoggerService.logger; } @@ -526,15 +528,19 @@ export class ApInboxService { }); if (users.length < 1) return 'skip'; + const comment = `${activity.content}\n${JSON.stringify(uris, null, 2)}`; + await this.abuseUserReportsRepository.insert({ id: this.idService.gen(), targetUserId: users[0].id, targetUserHost: users[0].host, reporterId: actor.id, reporterHost: actor.host, - comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`, + comment, }); + this.abuseDiscordHookService.send(actor, users[0], comment); + return 'ok'; } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 14761357a5a8..6d5f225b6f0b 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -303,11 +303,13 @@ export class UserEntityService implements OnModuleInit { schema?: S, includeSecrets?: boolean, userProfile?: MiUserProfile, + asModerator?: boolean, }, ): Promise<Packed<S>> { const opts = Object.assign({ schema: 'UserLite', includeSecrets: false, + asModerator: undefined as boolean | undefined, }, options); const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src }); @@ -315,7 +317,7 @@ export class UserEntityService implements OnModuleInit { const isDetailed = opts.schema !== 'UserLite'; const meId = me ? me.id : null; const isMe = meId === user.id; - const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; + const iAmModerator = opts.asModerator ?? (me ? await this.roleService.isModerator(me as MiUser) : false); const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null; const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin') diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 919f4794a325..bf941aba0038 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -13,6 +13,7 @@ export const DI = { redisForTimelines: Symbol('redisForTimelines'), //#region Repositories + nirilaDeleteUserLogRepository: Symbol('nirilaDeleteUserLogRepository'), usersRepository: Symbol('usersRepository'), notesRepository: Symbol('notesRepository'), announcementsRepository: Symbol('announcementsRepository'), diff --git a/packages/backend/src/models/NirilaDeleteUserLog.ts b/packages/backend/src/models/NirilaDeleteUserLog.ts new file mode 100644 index 000000000000..ab838dcb0e2b --- /dev/null +++ b/packages/backend/src/models/NirilaDeleteUserLog.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: anatawa12 + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { MiUserProfile } from '@/models/UserProfile.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('nirila_delete_user_log') +export class NirilaDeleteUserLog { + // delete user log id + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @Index() + @Column('varchar', { + length: 128, + comment: 'The username of the deleted User.', + }) + public username: MiUser['username']; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: 'The email adddress of the deleted User.', + }) + public email: MiUserProfile['email']; + + // user profile info + @Column('jsonb') + public info: Record<string, any>; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index bd447570ddf0..fa95331ba950 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -5,10 +5,16 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js'; +import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, NirilaDeleteUserLog } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; +const $nirilaDeleteUserLogRepository: Provider = { + provide: DI.nirilaDeleteUserLogRepository, + useFactory: (db: DataSource) => db.getRepository(NirilaDeleteUserLog), + inject: [DI.db], +}; + const $usersRepository: Provider = { provide: DI.usersRepository, useFactory: (db: DataSource) => db.getRepository(MiUser), @@ -415,6 +421,7 @@ const $reversiGamesRepository: Provider = { imports: [ ], providers: [ + $nirilaDeleteUserLogRepository, $usersRepository, $notesRepository, $announcementsRepository, @@ -484,6 +491,7 @@ const $reversiGamesRepository: Provider = { $reversiGamesRepository, ], exports: [ + $nirilaDeleteUserLogRepository, $usersRepository, $notesRepository, $announcementsRepository, diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts index db24c03b3dba..be42342e3fa6 100644 --- a/packages/backend/src/models/Webhook.ts +++ b/packages/backend/src/models/Webhook.ts @@ -8,6 +8,7 @@ import { id } from './util/id.js'; import { MiUser } from './User.js'; export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const; +export type WebhookEventType = (typeof webhookEventTypes)[number] | `note@${string}`; @Entity('webhook') export class MiWebhook { @@ -37,7 +38,7 @@ export class MiWebhook { @Column('varchar', { length: 128, array: true, default: '{}', }) - public on: (typeof webhookEventTypes)[number][]; + public on: WebhookEventType[]; @Column('varchar', { length: 1024, diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 43d42d80dd32..500850d135dd 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { NirilaDeleteUserLog } from '@/models/NirilaDeleteUserLog.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAccessToken } from '@/models/AccessToken.js'; import { MiAd } from '@/models/Ad.js'; @@ -74,6 +75,8 @@ import { MiReversiGame } from '@/models/ReversiGame.js'; import type { Repository } from 'typeorm'; export { + NirilaDeleteUserLog, + MiAbuseUserReport, MiAccessToken, MiAd, @@ -143,6 +146,7 @@ export { MiReversiGame, }; +export type NirilaDeleteUserLogRepository = Repository<NirilaDeleteUserLog>; export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>; export type AccessTokensRepository = Repository<MiAccessToken>; export type AdsRepository = Repository<MiAd>; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 2d14537bbb07..985707c58fca 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -11,6 +11,7 @@ import { DataSource, Logger } from 'typeorm'; import * as highlight from 'cli-highlight'; import { entities as charts } from '@/core/chart/entities.js'; +import { NirilaDeleteUserLog } from '@/models/NirilaDeleteUserLog.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAccessToken } from '@/models/AccessToken.js'; import { MiAd } from '@/models/Ad.js'; @@ -127,6 +128,7 @@ class MyCustomLogger implements Logger { } export const entities = [ + NirilaDeleteUserLog, MiAnnouncement, MiAnnouncementRead, MiMeta, diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 14a53e0c428d..974fce2e21e1 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { DriveFilesRepository, MiUser, NotesRepository, UserProfilesRepository, UsersRepository, NirilaDeleteUserLogRepository, SigninsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; @@ -14,6 +14,10 @@ import type { MiNote } from '@/models/Note.js'; import { EmailService } from '@/core/EmailService.js'; import { bindThis } from '@/decorators.js'; import { SearchService } from '@/core/SearchService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; +import { IdService } from '@/core/IdService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserDeleteJobData } from '../types.js'; @@ -35,6 +39,17 @@ export class DeleteAccountProcessorService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.nirilaDeleteUserLogRepository) + private nirilaDeleteUserLogRepository: NirilaDeleteUserLogRepository, + + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, + + private roleService: RoleService, + private roleEntityService: RoleEntityService, + private idService: IdService, + private userEntityService: UserEntityService, + private driveService: DriveService, private emailService: EmailService, private queueLoggerService: QueueLoggerService, @@ -43,6 +58,77 @@ export class DeleteAccountProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('delete-account'); } + @bindThis + async logDelete(user: MiUser) { + if (user.host != null) { + this.logger.info(`Skipping logging account deletion of ${user.id} ...`); + return; + } + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + // data from src/server/api/endpoints/users/show.ts + const detailedUser = await this.userEntityService.pack<'UserDetailed'>(user, null, { + schema: 'UserDetailed', + asModerator: true, + }); + + // data from src/server/api/endpoints/admin/show-user.ts + + const isModerator = await this.roleService.isModerator(user); + const isSilenced = !(await this.roleService.getUserPolicies(user.id)).canPublicNote; + + const signins = await this.signinsRepository.findBy({ userId: user.id }); + + const roleAssigns = await this.roleService.getUserAssigns(user.id); + const roles = await this.roleService.getUserRoles(user.id); + + const adminInfo = { + email: profile.email, + emailVerified: profile.emailVerified, + autoAcceptFollowed: profile.autoAcceptFollowed, + noCrawle: profile.noCrawle, + preventAiLearning: profile.preventAiLearning, + alwaysMarkNsfw: profile.alwaysMarkNsfw, + autoSensitive: profile.autoSensitive, + carefulBot: profile.carefulBot, + injectFeaturedNote: profile.injectFeaturedNote, + receiveAnnouncementEmail: profile.receiveAnnouncementEmail, + mutedWords: profile.mutedWords, + mutedInstances: profile.mutedInstances, + notificationRecieveConfig: profile.notificationRecieveConfig, + isModerator: isModerator, + isSilenced: isSilenced, + isSuspended: user.isSuspended, + isHibernated: user.isHibernated, + lastActiveDate: user.lastActiveDate, + moderationNote: profile.moderationNote ?? '', + signins, + policies: await this.roleService.getUserPolicies(user.id), + roles: await this.roleEntityService.packMany(roles, { id: user.id }), // note: me is unused param + roleAssigns: roleAssigns.map(a => ({ + createdAt: this.idService.parse(a.id).date.toISOString(), + expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null, + roleId: a.roleId, + })), + }; + + const info: Record<string, any> = { + user: detailedUser, + adminInfo, + }; + + await this.nirilaDeleteUserLogRepository.insert({ + id: this.idService.gen(), + userId: user.id, + username: user.username, + email: profile.email, + info, + }); + + this.logger.info(`Finished logging account deletion of ${user.id} ...`); + } + @bindThis public async process(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> { this.logger.info(`Deleting account of ${job.data.user.id} ...`); @@ -52,6 +138,12 @@ export class DeleteAccountProcessorService { return; } + try { + this.logDelete(user); + } catch (e) { + this.logger.error(`Failed to log delete: ${e}`); + } + { // Delete notes let cursor: MiNote['id'] | null = null; diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 3addead0587b..adbd343b3eb2 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -185,6 +185,7 @@ export class InboxProcessorService { await this.apInboxService.performActivity(authUser.user, activity); } catch (e) { if (e instanceof IdentifiableError) { + if (e.id === 'e11b3a16-f543-4885-8eb1-66cad131dbfd') return 'blocked mentions from unfamiliar user'; if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { return 'blocked notes with prohibited words'; } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 827c9e66b7ce..b70bdc19bac9 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -6,6 +6,7 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; +import * as ep___admin_nirilaDeleteUserLogAccess from './endpoints/admin/nirila-delete-user-log-access.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; @@ -62,6 +63,7 @@ import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; +import * as ep___admin_notePublicToHome from './endpoints/admin/note-public-to-home.js'; import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; @@ -379,6 +381,7 @@ import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; +const $admin_nirilaDeleteUserLogAccess: Provider = { provide: 'ep:admin/nirila-delete-user-log-access', useClass: ep___admin_nirilaDeleteUserLogAccess.default }; 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_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default }; @@ -435,6 +438,7 @@ const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default }; const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default }; const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default }; +const $admin_notePublicToHome: Provider = { provide: 'ep:admin/note-public-to-home', useClass: ep___admin_notePublicToHome.default }; const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default }; const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default }; const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default }; @@ -756,6 +760,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ providers: [ GetterService, ApiLoggerService, + $admin_nirilaDeleteUserLogAccess, $admin_meta, $admin_abuseUserReports, $admin_accounts_create, @@ -812,6 +817,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_relays_list, $admin_relays_remove, $admin_resetPassword, + $admin_notePublicToHome, $admin_resolveAbuseUserReport, $admin_sendEmail, $admin_serverInfo, @@ -1127,6 +1133,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $reversi_verify, ], exports: [ + $admin_nirilaDeleteUserLogAccess, $admin_meta, $admin_abuseUserReports, $admin_accounts_create, @@ -1183,6 +1190,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_relays_list, $admin_relays_remove, $admin_resetPassword, + $admin_notePublicToHome, $admin_resolveAbuseUserReport, $admin_sendEmail, $admin_serverInfo, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 28e7ef58c25f..d09aa5f5445b 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -6,6 +6,7 @@ import { permissions } from 'misskey-js'; import type { KeyOf, Schema } from '@/misc/json-schema.js'; +import * as ep___admin_nirilaDeleteUserLogAccess from './endpoints/admin/nirila-delete-user-log-access.js'; import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; @@ -62,6 +63,7 @@ import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; +import * as ep___admin_notePublicToHome from './endpoints/admin/note-public-to-home.js'; import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; @@ -377,6 +379,7 @@ import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; import * as ep___reversi_verify from './endpoints/reversi/verify.js'; const eps = [ + ['admin/nirila-delete-user-log-access', ep___admin_nirilaDeleteUserLogAccess], ['admin/meta', ep___admin_meta], ['admin/abuse-user-reports', ep___admin_abuseUserReports], ['admin/accounts/create', ep___admin_accounts_create], @@ -433,6 +436,7 @@ const eps = [ ['admin/relays/list', ep___admin_relays_list], ['admin/relays/remove', ep___admin_relays_remove], ['admin/reset-password', ep___admin_resetPassword], + ['admin/note-public-to-home', ep___admin_notePublicToHome], ['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport], ['admin/send-email', ep___admin_sendEmail], ['admin/server-info', ep___admin_serverInfo], diff --git a/packages/backend/src/server/api/endpoints/admin/nirila-delete-user-log-access.ts b/packages/backend/src/server/api/endpoints/admin/nirila-delete-user-log-access.ts new file mode 100644 index 000000000000..03e331749197 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/nirila-delete-user-log-access.ts @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: anatawa12 + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { NirilaDeleteUserLogRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { QueryService } from '@/core/QueryService.js'; +import type { IEndpointMeta } from '@/server/api/endpoints.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'read:admin:nirila-delete-user-log-access', + + errors: { + notFound: { + message: 'DeleteLog satisfies the request not found', + code: 'NOT_FOUND', + id: 'd29927d1-5b8c-4200-aa05-5537722ee7ab', + }, + }, + + res: { + oneOf: [ + { type: "object" }, + { + type: 'array', + items: { type: 'object' }, + }, + ] + }, +} as const satisfies IEndpointMeta; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + + // access single by id + email: { type: 'string' }, + username: { type: 'string' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.nirilaDeleteUserLogRepository) + private nirilaDeleteUserLogRepository: NirilaDeleteUserLogRepository, + + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + // single query + if (ps.email) { + const found = await this.nirilaDeleteUserLogRepository.findOneBy({ email: ps.email }); + if (!found) throw new ApiError(meta.errors.notFound); + return found.info; + } + + if (ps.username) { + const found = await this.nirilaDeleteUserLogRepository.findOneBy({ username: ps.username }); + if (!found) throw new ApiError(meta.errors.notFound); + return found.info; + } + + if (ps.userId) { + const found = await this.nirilaDeleteUserLogRepository.findOneBy({ userId: ps.userId }); + if (!found) throw new ApiError(meta.errors.notFound); + return found.info; + } + + // list query + + const logs = await this.queryService.makePaginationQuery(this.nirilaDeleteUserLogRepository.createQueryBuilder('deleteLog'), ps.sinceId, ps.untilId) + .limit(ps.limit) + .getMany(); + + return logs.map(x => Object.assign(x.info, { id: x.id })); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/note-public-to-home.ts b/packages/backend/src/server/api/endpoints/admin/note-public-to-home.ts new file mode 100644 index 000000000000..4a426ec70cf2 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/note-public-to-home.ts @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; +import type { NotesRepository, UsersRepository } from '@/models/_.js'; +import { MiNote, MiPoll } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { ApiError } from '@/server/api/error.js'; +import type { IEndpointMeta } from '@/server/api/endpoints.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:notes', + + errors: { + noteNotFound: { + message: 'Note not found.', + code: 'NOTE_NOT_FOUND', + id: 'b107f543-27fb-4bac-9549-9bbb64d95e85', + }, + noteNotPublic: { + message: 'Note is not public', + code: 'NOTE_NOT_PUBLIC', + id: '561e3371-6ef1-457b-8fdc-736a6e914782', + }, + }, +} as const satisfies IEndpointMeta; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['noteId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + + @Inject(DI.db) + private db: DataSource, + + private moderationLogService: ModerationLogService, + private fanoutTimelineService: FanoutTimelineService, + private featuredService: FeaturedService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.notesRepository.findOneBy({ id: ps.noteId }); + + if (note == null) { + throw new ApiError(meta.errors.noteNotFound); + } + + if (note.visibility !== 'public') { + throw new ApiError(meta.errors.noteNotPublic); + } + + // Note: by design, visibility of replies and quoted renotes are not changed + // replies and quoted renotes have their own text, so it's another moderation entity + + const user = await this.usersRepository.findOneByOrFail({ id: note.userId }); + await moderationLogService.log(me, 'makeNoteHome', { + noteId: note.id, + noteUserId: note.userId, + noteUserUsername: user.username, + noteUserHost: user.host, + note: note, + }); + + // update basic note info + await this.db.transaction(async transactionalEntityManager => { + // change visibility of the note + await transactionalEntityManager.update(MiNote, { id: note.id }, { visibility: 'home' }); + await transactionalEntityManager.update(MiPoll, { noteId: note.id }, { noteVisibility: 'home' }); + + // change visibility of pure renotes + await transactionalEntityManager.createQueryBuilder() + .from(MiNote, 'note') + .update() + .where('renoteId = :renoteId', { renoteId: note.id }) + .andWhere('text IS NULL') + .andWhere('fileIds = \'{}\'') + .andWhere('hasPoll = false') + .andWhere('visibility = \'public\'') + .set({ visibility: 'home' }) + .execute(); + }); + + // collect renotes after changing visibility of original note + const renotes = await this.notesRepository.createQueryBuilder('note') + .where('note.renoteId = :renoteId', { renoteId: note.id }) + .andWhere('note.text IS NULL') + .andWhere('note.fileIds = \'{}\'') + .andWhere('note.hasPoll = false') + .andWhere('note.visibility = \'home\'') + .getMany(); + + // remove from funout local timeline + const redisPipeline = this.redisForTimelines.pipeline(); + this.fanoutTimelineService.remove('localTimeline', note.id, redisPipeline); + if (note.fileIds.length > 0) { + this.fanoutTimelineService.remove('localTimelineWithFiles', note.id, redisPipeline); + } + for (const renote of renotes) { + this.fanoutTimelineService.remove('localTimeline', renote.id, redisPipeline); + } + await redisPipeline.exec(); + + // remove from highlights + // since renotes are not included in featured, we don't need to remove them + await featuredService.removeNote(note); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 535a3ea30850..4df48d6f3b22 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; import type { WebhooksRepository } from '@/models/_.js'; -import { webhookEventTypes } from '@/models/Webhook.js'; +import { webhookEventTypes, WebhookEventType } from '@/models/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -26,6 +26,11 @@ export const meta = { code: 'TOO_MANY_WEBHOOKS', id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', }, + adminWebhookDenied: { + message: 'You cannot create webhook for other users.', + code: 'ADMIN_WEBHOOK_DENIED', + id: '0d3321b1-6f66-41aa-9fbe-233c60ce19b0', + }, }, res: { @@ -43,8 +48,10 @@ export const meta = { on: { type: 'array', items: { - type: 'string', - enum: webhookEventTypes, + oneOf: [ + { type: 'string', enum: webhookEventTypes }, + { type: 'string', pattern: '^note@[a-zA-Z0-9]{1,20}$' }, + ], }, }, url: { type: 'string' }, @@ -63,7 +70,10 @@ export const paramDef = { url: { type: 'string', minLength: 1, maxLength: 1024 }, secret: { type: 'string', maxLength: 1024, default: '' }, on: { type: 'array', items: { - type: 'string', enum: webhookEventTypes, + oneOf: [ + { type: 'string', enum: webhookEventTypes }, + { type: 'string', pattern: '^note@[a-zA-Z0-9]{1,20}$' }, + ], } }, }, required: ['name', 'url', 'on'], @@ -89,13 +99,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.tooManyWebhooks); } + if (ps.on.some(x => !(webhookEventTypes as readonly string[]).includes(x))) { + if (!await this.roleService.isAdministrator(me)) { + throw new ApiError(meta.errors.adminWebhookDenied); + } + } + const webhook = await this.webhooksRepository.insert({ id: this.idService.gen(), userId: me.id, name: ps.name, url: ps.url, secret: ps.secret, - on: ps.on, + on: ps.on as WebhookEventType[], }).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('webhookCreated', webhook); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index fe07afb2d089..c395fc59e707 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -33,8 +33,10 @@ export const meta = { on: { type: 'array', items: { - type: 'string', - enum: webhookEventTypes, + oneOf: [ + { type: 'string', enum: webhookEventTypes }, + { type: 'string', pattern: '^note@[a-zA-Z0-9]{1,20}$' }, + ], }, }, url: { type: 'string' }, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 5ddb79caf283..012e3ef94638 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -40,8 +40,10 @@ export const meta = { on: { type: 'array', items: { - type: 'string', - enum: webhookEventTypes, + oneOf: [ + { type: 'string', enum: webhookEventTypes }, + { type: 'string', pattern: '^note@[a-zA-Z0-9]{1,20}$' }, + ], }, }, url: { type: 'string' }, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 6e380d76f89e..bf5faedd2262 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -6,9 +6,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { WebhooksRepository } from '@/models/_.js'; -import { webhookEventTypes } from '@/models/Webhook.js'; +import { webhookEventTypes, WebhookEventType } from '@/models/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -24,6 +25,11 @@ export const meta = { code: 'NO_SUCH_WEBHOOK', id: 'fb0fea69-da18-45b1-828d-bd4fd1612518', }, + adminWebhookDenied: { + message: 'You cannot create webhook for other users.', + code: 'UPDATE_ADMIN_WEBHOOK_DENIED', + id: 'eb43c0c4-24a3-487d-b139-f3e4e58f87a4', + }, }, } as const; @@ -36,7 +42,10 @@ export const paramDef = { url: { type: 'string', minLength: 1, maxLength: 1024 }, secret: { type: 'string', maxLength: 1024, default: '' }, on: { type: 'array', items: { - type: 'string', enum: webhookEventTypes, + oneOf: [ + { type: 'string', enum: webhookEventTypes }, + { type: 'string', pattern: '^note@[a-zA-Z0-9]{1,20}$' }, + ], } }, active: { type: 'boolean' }, }, @@ -52,6 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private webhooksRepository: WebhooksRepository, private globalEventService: GlobalEventService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const webhook = await this.webhooksRepository.findOneBy({ @@ -63,11 +73,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchWebhook); } + if (ps.on.some(x => !(webhookEventTypes as readonly string[]).includes(x))) { + if (!await this.roleService.isAdministrator(me)) { + throw new ApiError(meta.errors.adminWebhookDenied); + } + } + await this.webhooksRepository.update(webhook.id, { name: ps.name, url: ps.url, secret: ps.secret, - on: ps.on, + on: ps.on as WebhookEventType[], active: ps.active, }); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index cc76c12f1d89..66338244c027 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -60,6 +60,7 @@ export const paramDef = { untilDate: { type: 'integer' }, allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default withFiles: { type: 'boolean', default: false }, + includeSensitiveChannel: { type: 'boolean' }, }, required: ['userId'], } as const; @@ -81,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const isSelf = me && (me.id === ps.userId); + const includeSensitiveChannel = ps.includeSensitiveChannel ?? isSelf ?? false; const serverSettings = await this.metaService.fetch(); @@ -98,6 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const timeline = await this.getFromDb({ untilId, sinceId, + includeSensitiveChannel, limit: ps.limit, userId: ps.userId, withChannelNotes: ps.withChannelNotes, @@ -128,7 +131,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files excludePureRenotes: !ps.withRenotes, noteFilter: note => { - if (note.channel?.isSensitive && !isSelf) return false; + if (note.channel?.isSensitive && !includeSensitiveChannel) return false; if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; @@ -138,6 +141,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- untilId, sinceId, limit, + includeSensitiveChannel, userId: ps.userId, withChannelNotes: ps.withChannelNotes, withFiles: ps.withFiles, @@ -155,6 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- limit: number, userId: string, withChannelNotes: boolean, + includeSensitiveChannel: boolean, withFiles: boolean, withRenotes: boolean, }, me: MiLocalUser | null) { @@ -170,7 +175,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser'); if (ps.withChannelNotes) { - if (!isSelf) query.andWhere(new Brackets(qb => { + if (!ps.includeSensitiveChannel) query.andWhere(new Brackets(qb => { qb.orWhere('note.channelId IS NULL'); qb.orWhere('channel.isSensitive = false'); })); diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 1750dd6206f1..9c588def21e1 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -14,6 +14,8 @@ import { EmailService } from '@/core/EmailService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; +import { AbuseDiscordHookService } from '@/core/AbuseDiscordHookService.js'; +import type { Config } from '@/config.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -59,6 +61,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.config) + private config: Config, private idService: IdService, private metaService: MetaService, @@ -66,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private getterService: GetterService, private roleService: RoleService, private globalEventService: GlobalEventService, + private abuseDiscordHookService: AbuseDiscordHookService, ) { super(meta, paramDef, async (ps, me) => { // Lookup user @@ -105,12 +110,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } const meta = await this.metaService.fetch(); - if (meta.email) { + if (meta.email && !config.nirila?.disableAbuseRepository) { this.emailService.sendEmail(meta.email, 'New abuse report', sanitizeHtml(ps.comment), sanitizeHtml(ps.comment)); } }); + + this.abuseDiscordHookService.send(me, user, ps.comment); }); } } diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index 9bc652b6a108..f708508e4087 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -28,7 +28,7 @@ block og // FIXME: add embed player for Twitter if images.length meta(property='twitter:card' content='summary_large_image') - each image in images + each image in images meta(property='og:image' content= image.url) else meta(property='twitter:card' content='summary') @@ -36,7 +36,8 @@ block og block meta - if user.host || isRenote || profile.noCrawle + // TODO: make user configurable if sensitive channel note should be indexed or not + if user.host || isRenote || profile.noCrawle || (note.channel && note.channel.isSensitive) meta(name='robots' content='noindex') if profile.preventAiLearning meta(name='robots' content='noimageai') diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 929070d0d290..2fc94392d6e1 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -90,6 +90,7 @@ export const moderationLogTypes = [ 'deleteAvatarDecoration', 'unsetUserAvatar', 'unsetUserBanner', + 'makeNoteHome', ] as const; export type ModerationLogPayloads = { @@ -282,6 +283,13 @@ export type ModerationLogPayloads = { userHost: string | null; fileId: string; }; + makeNoteHome: { + noteId: string; + noteUserId: string; + noteUserUsername: string; + noteUserHost: string | null; + note: any; + }; }; export type Serialized<T> = { diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 0e71d707dd13..e0c9c3af7782 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -1198,6 +1198,32 @@ describe('Timelines', () => { assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); + test.concurrent('[withChannelNotes: true, includeSensitiveChannel: true] ä»äººãååŸããå Žåãã»ã³ã·ãã£ããã£ã³ãã«æçš¿ãå«ãŸãã', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true, includeSensitiveChannel: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + + test.concurrent('[withChannelNotes: true, includeSensitiveChannel: false] èªåãååŸããå Žåãã»ã³ã·ãã£ããã£ã³ãã«æçš¿ãå«ãŸããªã', async () => { + const [bob] = await Promise.all([signup()]); + + const channel = await api('/channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true, includeSensitiveChannel: false }, bob); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + test.concurrent('ãã¥ãŒãããŠãããŠãŒã¶ãŒã«é¢é£ããæçš¿ãå«ãŸããªã', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 03a283cab33b..009c1ce2ad53 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -141,6 +141,13 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> </template> </I18n> + <I18n v-if="muted === 'sensitiveChannel'" :src="i18n.ts.userSaysSomethingInSensitiveChannel" 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.userSaysSomething" tag="small"> <template #name> <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> @@ -211,6 +218,7 @@ const emit = defineEmits<{ }>(); const inTimeline = inject<boolean>('inTimeline', false); +const collapseSensitiveChannel = inject<boolean>('collapseSensitiveChannel', false); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); @@ -272,9 +280,9 @@ const renoteCollapsed = ref( /* 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): boolean | 'sensitiveMute' | 'sensitiveChannel'; */ -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): boolean | 'sensitiveMute' | 'sensitiveChannel' { if (mutedWords == null) return false; if (checkWordMute(noteToCheck, $i, mutedWords)) return true; @@ -283,6 +291,7 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string if (checkOnly) return false; + if (collapseSensitiveChannel && noteToCheck.channel?.isSensitive) return 'sensitiveChannel'; if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; return false; } diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index a1c1ea234118..c7871ba96e82 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -40,7 +40,7 @@ const props = withDefaults(defineProps<{ onlyFiles?: boolean; }>(), { withRenotes: true, - withReplies: false, + withReplies: true, onlyFiles: false, }); diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 188cc37f4140..0f787431b725 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -27,7 +27,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>(); function whatIsNew() { modal.value?.close(); - window.open(`https://misskey-hub.net/docs/releases/#_${version.replace(/\./g, '')}`, '_blank'); + window.open(`https://github.com/niri-la/misskey.niri.la/blob/develop/CHANGELOG.md#${version.replace(/\./g, '')}`, '_blank'); } onMounted(() => { diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index be80baa774f3..63f3be498437 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -120,7 +120,13 @@ function showMenu(ev) { action: () => { window.open(instance.privacyPolicyUrl!, '_blank', 'noopener'); }, - } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { + } : undefined, { + text: 'NSFWã¬ã€ãã©ã€ã³', + icon: 'ti ti-notebook', + action: () => { + window.open('https://kinel.notion.site/NSFW-39a3f0c8708e4e2594594a1c88099fe3', '_blank', 'noopener'); + }, + }, { type: 'divider' }, { text: i18n.ts.help, icon: 'ti ti-help-circle', action: () => { diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 1a49dbf1d512..07c38bbf7094 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -112,6 +112,24 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </FormSection> + <FormSection> + <template #label>{{ i18n.ts._aboutMisskey.forkContributors }}</template> + <div :class="$style.contributors"> + <a href="https://github.com/anatawa12" target="_blank" :class="$style.contributor"> + <img src="https://avatars.githubusercontent.com/u/22656849?v=4" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@anatawa12</span> + </a> + <a href="https://github.com/niwaniwa" target="_blank" :class="$style.contributor"> + <img src="https://avatars.githubusercontent.com/u/10182706?v=4" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@niwaniwa</span> + </a> + <a href="https://github.com/Sayamame-beans" target="_blank" :class="$style.contributor"> + <img src="https://avatars.githubusercontent.com/u/61457993?v=4" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@Sayamame-beans</span> + </a> + </div> + <template #caption><MkLink url="https://github.com/niri-la/misskey.niri.la/graphs/contributors">{{ i18n.ts._aboutMisskey.allForkContributors }}</MkLink></template> + </FormSection> <FormSection> <template #label><Mfm text="$[jelly â€]"/> {{ i18n.ts._aboutMisskey.patrons }}</template> <div :class="$style.patronsWithIcon"> diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index e33c88272187..1e5fc589a685 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <b :class="{ [$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type), - [$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type), + [$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword', 'makeNoteHome'].includes(log.type), [$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type) }" >{{ i18n.ts._moderationLogTypes[log.type] }}</b> @@ -40,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="log.type === 'createAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span> <span v-else-if="log.type === 'updateAvatarDecoration'">: {{ log.info.before.name }}</span> <span v-else-if="log.type === 'deleteAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span> + <span v-else-if="log.type === 'makeNoteHome'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span> </template> <template #icon> <MkAvatar :user="log.user" :class="$style.avatar"/> diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 1919f808640f..b9d65aae9f57 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -44,6 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.keepOriginalUploading }}</template> <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template> </MkSwitch> + <MkSelect v-model="imageCompressionMode"> + <template #label>{{ i18n.ts._imageCompressionMode.title }}</template> + <option value="resizeCompress">{{ i18n.ts._imageCompressionMode.resizeCompress }}</option> + <option value="noResizeCompress">{{ i18n.ts._imageCompressionMode.noResizeCompress }}</option> + <option value="resizeCompressLossy">{{ i18n.ts._imageCompressionMode.resizeCompressLossy }}</option> + <option value="noResizeCompressLossy">{{ i18n.ts._imageCompressionMode.noResizeCompressLossy }}</option> + <template #caption>{{ i18n.ts._imageCompressionMode.description }}</template> + </MkSelect> <MkSwitch v-model="alwaysMarkNsfw" @update:modelValue="saveProfile()"> <template #label>{{ i18n.ts.alwaysMarkSensitive }}</template> </MkSwitch> @@ -73,6 +81,7 @@ import MkChart from '@/components/MkChart.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { signinRequired } from '@/account.js'; +import MkSelect from '@/components/MkSelect.vue'; const $i = signinRequired(); @@ -96,6 +105,7 @@ const meterStyle = computed(() => { }); const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading')); +const imageCompressionMode = computed(defaultStore.makeGetterSetter('imageCompressionMode')); misskeyApi('drive').then(info => { capacity.value = info.capacity; diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index d13b6884bdd9..166695c9cbc1 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -58,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch> <MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch> <MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch> + <MkSwitch v-model="collapseSensitiveChannel">{{ i18n.ts.collapseSensitiveChannel }}</MkSwitch> <MkRadios v-model="reactionsDisplaySize"> <template #label>{{ i18n.ts.reactionsDisplaySize }}</template> <option value="small">{{ i18n.ts.small }}</option> @@ -296,6 +297,7 @@ const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCa const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker')); const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll')); const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu')); +const collapseSensitiveChannel = computed(defaultStore.makeGetterSetter('collapseSensitiveChannel')); const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars')); const showAvatarDecorations = computed(defaultStore.makeGetterSetter('showAvatarDecorations')); const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance')); diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index e9fb1e471e16..463b4febb156 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -29,6 +29,11 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> + + <MkTextarea v-if="$i?.isAdmin" v-model="users"> + <template #label>{{ i18n.ts._webhookSettings._events.usersLabel }}</template> + <template #caption>{{ i18n.ts._webhookSettings._events.usersCaption }}</template> + </MkTextarea> </div> </FormSection> @@ -52,6 +57,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { useRouter } from '@/router/supplier.js'; +import { $i } from '@/account.js'; +import MkTextarea from '@/components/MkTextarea.vue'; const router = useRouter(); @@ -75,9 +82,10 @@ const event_reply = ref(webhook.on.includes('reply')); const event_renote = ref(webhook.on.includes('renote')); const event_reaction = ref(webhook.on.includes('reaction')); const event_mention = ref(webhook.on.includes('mention')); +const users = ref((webhook.on as string[]).filter(x => x.startsWith('note@')).map(x => x.substring('note@'.length)).join('\n')); async function save(): Promise<void> { - const events = []; + const events: string[] = []; if (event_follow.value) events.push('follow'); if (event_followed.value) events.push('followed'); if (event_note.value) events.push('note'); @@ -85,6 +93,7 @@ async function save(): Promise<void> { if (event_renote.value) events.push('renote'); if (event_reaction.value) events.push('reaction'); if (event_mention.value) events.push('mention'); + if (users.value !== '') events.push(...users.value.split('\n').filter(x => x).map(x => `note@${x}`)); os.apiWithDialog('i/webhooks/update', { name: name.value, @@ -119,3 +128,11 @@ definePageMetadata(() => ({ icon: 'ti ti-webhook', })); </script> + +<style lang="scss" module> + +.userItem { + display: flex; +} + +</style> diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue index 5bf85e48f426..88c7aa120659 100644 --- a/packages/frontend/src/pages/settings/webhook.new.vue +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -29,6 +29,11 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> + + <MkTextarea v-if="$i?.isAdmin" v-model="users"> + <template #label>{{ i18n.ts._webhookSettings._events.usersLabel }}</template> + <template #caption>{{ i18n.ts._webhookSettings._events.usersCaption }}</template> + </MkTextarea> </div> </FormSection> @@ -47,6 +52,8 @@ import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { $i } from '@/account.js'; +import MkTextarea from '@/components/MkTextarea.vue'; const name = ref(''); const url = ref(''); @@ -59,9 +66,10 @@ const event_reply = ref(true); const event_renote = ref(true); const event_reaction = ref(true); const event_mention = ref(true); +const users = ref(''); async function create(): Promise<void> { - const events = []; + const events: string[] = []; if (event_follow.value) events.push('follow'); if (event_followed.value) events.push('followed'); if (event_note.value) events.push('note'); @@ -69,6 +77,7 @@ async function create(): Promise<void> { if (event_renote.value) events.push('renote'); if (event_reaction.value) events.push('reaction'); if (event_mention.value) events.push('mention'); + if (users.value !== '') events.push(...users.value.split('\n').filter(x => x).map(x => `note@${x}`)); os.apiWithDialog('i/webhooks/create', { name: name.value, diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index 8dbf90f344ca..53f57dfe5ffe 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -18,17 +18,21 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, provide } from 'vue'; import * as Misskey from 'misskey-js'; import MkNotes from '@/components/MkNotes.vue'; import MkTab from '@/components/MkTab.vue'; import { i18n } from '@/i18n.js'; +import { $i } from '@/account.js'; +import { defaultStore } from '@/store.js'; const props = defineProps<{ user: Misskey.entities.UserDetailed; }>(); const tab = ref<string | null>('all'); +const include = ref<string | null>('all'); +provide<boolean>('collapseSensitiveChannel', defaultStore.state.collapseSensitiveChannel); const pagination = computed(() => tab.value === 'featured' ? { endpoint: 'users/featured-notes' as const, @@ -45,6 +49,7 @@ const pagination = computed(() => tab.value === 'featured' ? { withReplies: tab.value === 'all', withChannelNotes: tab.value === 'all', withFiles: tab.value === 'files', + includeSensitiveChannel: $i != null, }, }); </script> diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index b273bd36f398..4d955726a875 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -143,6 +143,17 @@ export function getNoteMenu(props: { const cleanups = [] as (() => void)[]; + function makeHome(): void { + os.confirm({ + type: 'warning', + text: i18n.ts.makeNoteHomeConfirm, + }).then(({ canceled }) => { + if (canceled) return; + + misskeyApi('admin/note-public-to-home', { noteId: appearNote.id }); + }); + } + function del(): void { os.confirm({ type: 'warning', @@ -405,7 +416,13 @@ export function getNoteMenu(props: { text: i18n.ts.delete, danger: true, action: del, - }] + }, + $i.isModerator || $i.isAdmin ? { + icon: 'ti ti-home', + text: i18n.ts.makeNoteHome, + danger: true, + action: makeHome, + } : undefined] : [] )] .filter(x => x !== undefined); diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts index 6fd9947ac1e7..af6c1b4e376b 100644 --- a/packages/frontend/src/scripts/get-note-summary.ts +++ b/packages/frontend/src/scripts/get-note-summary.ts @@ -10,7 +10,7 @@ import { i18n } from '@/i18n.js'; * æçš¿ãè¡šãæååãååŸããŸãã * @param {*} note (packããã)æçš¿ */ -export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { +export const getNoteSummary = (note?: Misskey.entities.Note | null, sensitiveChannelCW?: boolean): string => { if (note == null) { return ''; } @@ -23,6 +23,10 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { return `(${i18n.ts.invisibleNote})`; } + if (sensitiveChannelCW) { + return i18n.ts.sensitiveChannelAutoCW; + } + let summary = ''; // æ¬æ diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts index 3046b7f518b9..bbd9c2b75630 100644 --- a/packages/frontend/src/scripts/upload/compress-config.ts +++ b/packages/frontend/src/scripts/upload/compress-config.ts @@ -1,36 +1,110 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and misskey-project, anatawa12 * SPDX-License-Identifier: AGPL-3.0-only */ import isAnimated from 'is-file-animated'; import { isWebpSupported } from './isWebpSupported.js'; import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer'; +import { defaultStore } from '@/store'; const compressTypeMap = { - 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' }, - 'image/png': { quality: 1, mimeType: 'image/webp' }, - 'image/webp': { quality: 0.90, mimeType: 'image/webp' }, - 'image/svg+xml': { quality: 1, mimeType: 'image/webp' }, + 'lossy': { quality: 0.90, mimeType: 'image/webp' }, + 'lossless': { quality: 1, mimeType: 'image/webp' }, } as const; const compressTypeMapFallback = { - 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/png': { quality: 1, mimeType: 'image/png' }, - 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, + 'lossy': { quality: 0.85, mimeType: 'image/jpeg' }, + 'lossless': { quality: 1, mimeType: 'image/png' }, } as const; +const inputCompressKindMap = { + 'image/jpeg': 'lossy', + 'image/png': 'lossless', + 'image/webp': 'lossy', + 'image/svg+xml': 'lossless', +} as const; + +const resizeSizeConfig = { maxWidth: 2560, maxHeight: 2560 } as const; +const noResizeSizeConfig = { maxWidth: Number.MAX_SAFE_INTEGER, maxHeight: Number.MAX_SAFE_INTEGER } as const; + +async function isLosslessWebp(file: Blob): Promise<boolean> { + // file header + // 'RIFF': u32 @ 0x00 + // file size: u32 @ 0x04 + // 'WEBP': u32 @ 0x08 + // for simple lossless + // 'VP8L': u32 @ 0x0C + // so read 16 bytes and check those three magic numbers + const buffer = new Uint8Array(await file.slice(0, 16).arrayBuffer()); + + const header = 'RIFF\x00\x00\x00\x00WEBPVP8L'; + for (let i = 0; i < header.length; i++) { + const code = header.charCodeAt(i); + if (code === 0) continue; + if (buffer[i] !== code) return false; + } + return true; +} + +async function inputImageKind(file: File): Promise<'lossy' | 'lossless' | undefined> { + let compressKind: 'lossy' | 'lossless' | undefined = inputCompressKindMap[file.type]; + if (!compressKind) return undefined; // unknown image format + if (await isAnimated(file)) return undefined; // animated image format + // WEBPs can be lossless + if (await isLosslessWebp(file)) compressKind = 'lossless'; + return compressKind; +} + export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfigWithConvertedOutput | undefined> { - const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type]; - if (!imgConfig || await isAnimated(file)) { - return; + const inputCompressKind = await inputImageKind(file); + if (!inputCompressKind) return undefined; + + let compressKind: 'lossy' | 'lossless'; + let resize: boolean; + + switch (defaultStore.state.imageCompressionMode) { + case 'resizeCompress': + case null: + default: + resize = true; + compressKind = inputCompressKind; + break; + case 'noResizeCompress': + resize = false; + compressKind = inputCompressKind; + break; + case 'resizeCompressLossy': + resize = true; + compressKind = 'lossy'; + break; + case 'noResizeCompressLossy': + resize = false; + compressKind = 'lossy'; + break; + } + + const webpSupported = isWebpSupported(); + + const imgFormatConfig = (webpSupported ? compressTypeMap : compressTypeMapFallback)[compressKind]; + const sizeConfig = resize ? resizeSizeConfig : noResizeSizeConfig; + + if (!resize) { + // we don't resize images so we may omit recompression + if (imgFormatConfig.mimeType === file.type && inputCompressKind === compressKind) { + // we don't have to recompress already compressed to preferred image format. + return undefined; + } + + if (!webpSupported && file.type === 'image/webp' && compressKind === 'lossless') { + // lossless webp -> png recompression likely to increase image size so don't recompress + return undefined; + } } return { - maxWidth: 2048, - maxHeight: 2048, debug: true, - ...imgConfig, + ...imgFormatConfig, + ...sizeConfig, }; } diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 516fc296a18a..1bcf2b969941 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -113,6 +113,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: false, }, + imageCompressionMode: { + where: 'account', + default: 'resizeCompress' as 'resizeCompress' | 'noResizeCompress' | 'resizeCompressLossy' | 'noResizeCompressLossy' | null, + }, memo: { where: 'account', default: null, @@ -211,6 +215,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'respect' as 'respect' | 'force' | 'ignore', }, + collapseSensitiveChannel: { + where: 'device', + default: true, + }, highlightSensitiveMedia: { where: 'device', default: false, diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 9b510a629241..d7507b04c481 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -97,7 +97,13 @@ export function openInstanceMenu(ev: MouseEvent) { action: () => { window.open(instance.privacyPolicyUrl, '_blank', 'noopener'); }, - } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { + } : undefined, { + text: 'NSFWã¬ã€ãã©ã€ã³', + icon: 'ti ti-notebook', + action: () => { + window.open('https://kinel.notion.site/NSFW-39a3f0c8708e4e2594594a1c88099fe3', '_blank', 'noopener'); + }, + }, { type: 'divider' }, { text: i18n.ts.help, icon: 'ti ti-help-circle', action: () => { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 8b1e8ddfc94f..30e215603294 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -208,6 +208,15 @@ type AdminInviteListResponse = operations['admin/invite/list']['responses']['200 // @public (undocumented) type AdminMetaResponse = operations['admin/meta']['responses']['200']['content']['application/json']; +// @public (undocumented) +type AdminNirilaDeleteUserLogAccessRequest = operations['admin/nirila-delete-user-log-access']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminNirilaDeleteUserLogAccessResponse = operations['admin/nirila-delete-user-log-access']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminNotePublicToHomeRequest = operations['admin/note-public-to-home']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminPromoCreateRequest = operations['admin/promo/create']['requestBody']['content']['application/json']; @@ -1135,6 +1144,8 @@ declare namespace entities { SigninResponse, EmptyRequest, EmptyResponse, + AdminNirilaDeleteUserLogAccessRequest, + AdminNirilaDeleteUserLogAccessResponse, AdminMetaResponse, AdminAbuseUserReportsRequest, AdminAbuseUserReportsResponse, @@ -1207,6 +1218,7 @@ declare namespace entities { AdminRelaysRemoveRequest, AdminResetPasswordRequest, AdminResetPasswordResponse, + AdminNotePublicToHomeRequest, AdminResolveAbuseUserReportRequest, AdminSendEmailRequest, AdminServerInfoResponse, @@ -2386,10 +2398,13 @@ type ModerationLog = { } | { type: 'unsetUserBanner'; info: ModerationLogPayloads['unsetUserBanner']; +} | { + type: 'makeNoteHome'; + info: ModerationLogPayloads['makeNoteHome']; }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "makeNoteHome"]; // @public (undocumented) type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json']; @@ -2644,7 +2659,7 @@ type PagesUpdateRequest = operations['pages/update']['requestBody']['content'][' function parse(acct: string): Acct; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; +export const permissions: readonly ["read:admin:nirila-delete-user-log-access", "read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 772f001c0734..255b37645b69 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.3.1", + "version": "2024.3.1-kinel.1", "description": "Misskey SDK for JavaScript", "types": "./built/dts/index.d.ts", "exports": { diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 7a58b3eb0401..fb6e1c9231f7 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -3,6 +3,17 @@ import type { Endpoints } from './endpoint.js'; declare module '../api.js' { export interface APIClient { + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:nirila-delete-user-log-access* + */ + request<E extends 'admin/nirila-delete-user-log-access', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * @@ -620,6 +631,17 @@ declare module '../api.js' { credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + request<E extends 'admin/note-public-to-home', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 675511cf47f3..35511287cbbd 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,8 @@ import type { EmptyRequest, EmptyResponse, + AdminNirilaDeleteUserLogAccessRequest, + AdminNirilaDeleteUserLogAccessResponse, AdminMetaResponse, AdminAbuseUserReportsRequest, AdminAbuseUserReportsResponse, @@ -73,6 +75,7 @@ import type { AdminRelaysRemoveRequest, AdminResetPasswordRequest, AdminResetPasswordResponse, + AdminNotePublicToHomeRequest, AdminResolveAbuseUserReportRequest, AdminSendEmailRequest, AdminServerInfoResponse, @@ -556,6 +559,7 @@ import type { } from './entities.js'; export type Endpoints = { + 'admin/nirila-delete-user-log-access': { req: AdminNirilaDeleteUserLogAccessRequest; res: AdminNirilaDeleteUserLogAccessResponse }; 'admin/meta': { req: EmptyRequest; res: AdminMetaResponse }; 'admin/abuse-user-reports': { req: AdminAbuseUserReportsRequest; res: AdminAbuseUserReportsResponse }; 'admin/accounts/create': { req: AdminAccountsCreateRequest; res: AdminAccountsCreateResponse }; @@ -612,6 +616,7 @@ export type Endpoints = { 'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse }; 'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse }; 'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse }; + 'admin/note-public-to-home': { req: AdminNotePublicToHomeRequest; res: EmptyResponse }; 'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse }; 'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse }; 'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 57ad9ce4bc02..7c7af9fd474a 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -3,6 +3,8 @@ import { operations } from './types.js'; export type EmptyRequest = Record<string, unknown> | undefined; export type EmptyResponse = Record<string, unknown> | undefined; +export type AdminNirilaDeleteUserLogAccessRequest = operations['admin/nirila-delete-user-log-access']['requestBody']['content']['application/json']; +export type AdminNirilaDeleteUserLogAccessResponse = operations['admin/nirila-delete-user-log-access']['responses']['200']['content']['application/json']; 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']; @@ -75,6 +77,7 @@ export type AdminRelaysListResponse = operations['admin/relays/list']['responses export type AdminRelaysRemoveRequest = operations['admin/relays/remove']['requestBody']['content']['application/json']; export type AdminResetPasswordRequest = operations['admin/reset-password']['requestBody']['content']['application/json']; export type AdminResetPasswordResponse = operations['admin/reset-password']['responses']['200']['content']['application/json']; +export type AdminNotePublicToHomeRequest = operations['admin/note-public-to-home']['requestBody']['content']['application/json']; export type AdminResolveAbuseUserReportRequest = operations['admin/resolve-abuse-user-report']['requestBody']['content']['application/json']; export type AdminSendEmailRequest = operations['admin/send-email']['requestBody']['content']['application/json']; export type AdminServerInfoResponse = operations['admin/server-info']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index d8c12453eb19..d8479dd44262 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -12,6 +12,15 @@ type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & type OneOf<T extends any[]> = T extends [infer Only] ? Only : T extends [infer A, infer B, ...infer Rest] ? OneOf<[XOR<A, B>, ...Rest]> : never; export type paths = { + '/admin/nirila-delete-user-log-access': { + /** + * admin/nirila-delete-user-log-access + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:nirila-delete-user-log-access* + */ + post: operations['admin/nirila-delete-user-log-access']; + }; '/admin/meta': { /** * admin/meta @@ -517,6 +526,15 @@ export type paths = { */ post: operations['admin/reset-password']; }; + '/admin/note-public-to-home': { + /** + * admin/note-public-to-home + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + post: operations['admin/note-public-to-home']; + }; '/admin/resolve-abuse-user-report': { /** * admin/resolve-abuse-user-report @@ -4859,6 +4877,68 @@ export type external = Record<string, never>; export type operations = { + /** + * admin/nirila-delete-user-log-access + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:nirila-delete-user-log-access* + */ + 'admin/nirila-delete-user-log-access': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + email?: string; + username?: string; + /** Format: misskey:id */ + userId?: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': Record<string, never> | Record<string, never>[]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * admin/meta * @description No description provided. @@ -8173,6 +8253,58 @@ export type operations = { }; }; }; + /** + * admin/note-public-to-home + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + 'admin/note-public-to-home': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * admin/resolve-abuse-user-report * @description No description provided. @@ -19056,7 +19188,7 @@ export type operations = { url: string; /** @default */ secret?: string; - on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + on: (('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction') | string)[]; }; }; }; @@ -19070,7 +19202,7 @@ export type operations = { /** Format: misskey:id */ userId: string; name: string; - on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + on: (('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction') | string)[]; url: string; secret: string; active: boolean; @@ -19129,7 +19261,7 @@ export type operations = { /** Format: misskey:id */ userId: string; name: string; - on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + on: (('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction') | string)[]; url: string; secret: string; active: boolean; @@ -19196,7 +19328,7 @@ export type operations = { /** Format: misskey:id */ userId: string; name: string; - on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + on: (('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction') | string)[]; url: string; secret: string; active: boolean; @@ -19254,7 +19386,7 @@ export type operations = { url: string; /** @default */ secret?: string; - on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + on: (('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction') | string)[]; active: boolean; }; }; @@ -25384,6 +25516,7 @@ export type operations = { allowPartial?: boolean; /** @default false */ withFiles?: boolean; + includeSensitiveChannel?: boolean; }; }; }; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index b690621e98d9..c5489a207ec1 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -9,6 +9,7 @@ export const followingVisibilities = ['public', 'followers', 'private'] as const export const followersVisibilities = ['public', 'followers', 'private'] as const; export const permissions = [ + 'read:admin:nirila-delete-user-log-access', 'read:account', 'write:account', 'read:blocks', @@ -134,6 +135,7 @@ export const moderationLogTypes = [ 'deleteAvatarDecoration', 'unsetUserAvatar', 'unsetUserBanner', + 'makeNoteHome', ] as const; export type ModerationLogPayloads = { @@ -326,4 +328,11 @@ export type ModerationLogPayloads = { userHost: string | null; fileId: string; }; + makeNoteHome: { + noteId: string; + noteUserId: string; + noteUserUsername: string; + noteUserHost: string | null; + note: any; + }; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 35503d6d6fff..4373d53af506 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -134,6 +134,9 @@ export type ModerationLog = { } | { type: 'unsetUserBanner'; info: ModerationLogPayloads['unsetUserBanner']; +} | { + type: 'makeNoteHome'; + info: ModerationLogPayloads['makeNoteHome']; }); export type ServerStats = {