Skip to content

チャンネルの非掲載・フォロー承認制・フォロワー管理に対応 - #22

Merged
mattyatea merged 11 commits into
developfrom
codex/channel-follow-approval
Aug 21, 2026
Merged

チャンネルの非掲載・フォロー承認制・フォロワー管理に対応#22
mattyatea merged 11 commits into
developfrom
codex/channel-follow-approval

Conversation

@mattyatea

@mattyatea mattyatea commented Aug 21, 2026

Copy link
Copy Markdown

What

チャンネルを身内・個人向けに運用しやすくするため、次の機能を追加します。

  • チャンネル一覧・トレンドから除外しつつ、明示的な検索では見つけられる「非掲載」設定
  • チャンネル単位のフォロー承認制と、申請中表示・申請取消
  • オーナーと共同管理者によるフォロー申請の承認・拒否
  • オーナーと共同管理者によるフォロワー一覧表示・削除
  • チャンネルページ上部で分離した「フォロー申請」「フォロワー管理」タブ
  • 未処理の申請がある場合のヘッダータブ点滅表示
  • フォローボタン内のフォロワー数表示
  • フォロー申請専用テーブル、フォロワー数カラム、管理API、Misskey.jsの生成型
  • 承認制を解除した場合、保留中の申請を一括でフォローへ移行
flowchart LR
    U[ユーザー] -->|フォロー| C{承認制?}
    C -->|いいえ| F[フォロー中]
    C -->|はい| R[申請中]
    R -->|承認| F
    R -->|拒否・本人が取消| X[未フォロー]
    F -->|管理者が削除・本人が解除| X
Loading
erDiagram
    channel ||--o{ channel_follow_request : has
    user ||--o{ channel_follow_request : submits
    channel ||--o{ channel_following : has
    user ||--o{ channel_following : follows
Loading

Why

身内チャンネルや個人チャンネルを通常の発見面に載せずに運用し、参加者をチャンネル管理者側で管理できるようにするためです。共同管理者にも申請・フォロワー管理を許可することで、オーナーだけに管理負荷が集中しないようにします。

Additional info (optional)

  • 「非掲載」はアクセス制御ではありません。検索またはURLから直接閲覧できます。
  • フォロー申請・フォロワー管理はオーナーと共同管理者だけが利用でき、モデレーター権限だけでは利用できません。
  • チャンネルに管理権限を持つフォロワーは管理画面から削除できません。
  • フォロワー数は専用カラムで管理し、フォロー・解除・承認・アカウント削除と同期します。
  • 承認制を無効化すると、保留中の申請は新しいフォローIDを採番してフォローへ移行します。
  • フォロー申請の承認・取消・承認制変更はチャンネル行をロックして直列化し、競合時の状態不整合を防ぎます。
  • origin/develop を通常マージし、PRの競合を解消済みです。
  • Issueは依頼により作成していません。

確認済み:

  • Backend typecheck
  • Backend unit test: ChannelFollowingService 16件
  • Backend E2E: 二重フォロー時の ALREADY_FOLLOWING
  • Backend E2E: 非掲載、申請、共同管理者承認・削除、承認制解除、フォロワー数同期
  • Migration clean check
  • 変更対象のBackend / Frontend ESLint(エラー0)
  • Misskey.js生成・API Extractor
  • 全体の本番ビルド
  • LANプレビューのHTTP 200
  • git diff --check
  • 最終 codex review --base origin/develop(指摘なし)

Frontend全体のtypecheckは、今回の変更外にある MkLightbox.item.audio-visualizer.vueMkLightbox.item.vueadmin/job-queue.vue の上流エラー3件により完走しません。

Checklist

  • Read the contribution guide
  • Test working in a local environment
  • (If needed) Add story of storybook
  • (If needed) Update CHANGELOG.md
  • (If possible) Add tests

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@mattyatea, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3361c2a7-70ce-4195-b346-782099afcd7c

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2574f and 2b6895c.

⛔ Files ignored due to path filters (1)
  • CHANGELOG.md is excluded by !CHANGELOG.md
📒 Files selected for processing (41)
  • locales/ja-JP.yml
  • packages/backend/migration/1787278753407-ChannelFollowApproval.js
  • packages/backend/src/core/ChannelFollowingService.ts
  • packages/backend/src/core/ChannelService.ts
  • packages/backend/src/core/entities/ChannelEntityService.ts
  • packages/backend/src/di-symbols.ts
  • packages/backend/src/models/Channel.ts
  • packages/backend/src/models/ChannelFollowRequest.ts
  • packages/backend/src/models/RepositoryModule.ts
  • packages/backend/src/models/_.ts
  • packages/backend/src/models/json-schema/channel.ts
  • packages/backend/src/postgres.ts
  • packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
  • packages/backend/src/server/api/endpoint-list.ts
  • packages/backend/src/server/api/endpoints/channels/create.ts
  • packages/backend/src/server/api/endpoints/channels/featured.ts
  • packages/backend/src/server/api/endpoints/channels/follow-requests/approve.ts
  • packages/backend/src/server/api/endpoints/channels/follow-requests/list.ts
  • packages/backend/src/server/api/endpoints/channels/follow-requests/reject.ts
  • packages/backend/src/server/api/endpoints/channels/follow.ts
  • packages/backend/src/server/api/endpoints/channels/followers.ts
  • packages/backend/src/server/api/endpoints/channels/followers/remove.ts
  • packages/backend/src/server/api/endpoints/channels/search.ts
  • packages/backend/src/server/api/endpoints/channels/update.ts
  • packages/backend/test/e2e/channel-follow-approval.ts
  • packages/backend/test/e2e/channel.ts
  • packages/backend/test/unit/ChannelFollowingService.ts
  • packages/frontend/.storybook/fakes.ts
  • packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
  • packages/frontend/src/components/MkChannelFollowButton.vue
  • packages/frontend/src/components/global/MkPageHeader.tabs.vue
  • packages/frontend/src/pages/channel-editor.vue
  • packages/frontend/src/pages/channel.follow-requests.vue
  • packages/frontend/src/pages/channel.followers.vue
  • packages/frontend/src/pages/channel.vue
  • packages/i18n/src/autogen/locale.ts
  • packages/misskey-js/etc/misskey-js.api.md
  • packages/misskey-js/src/autogen/apiClientJSDoc.ts
  • packages/misskey-js/src/autogen/endpoint.ts
  • packages/misskey-js/src/autogen/entities.ts
  • packages/misskey-js/src/autogen/types.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

このPRによるapi.jsonの差分

差分はこちら
--- base
+++ head
@@ -27354,6 +27354,14 @@
                   "isLocalOnly": {
                     "type": "boolean",
                     "default": false
+                  },
+                  "isUnlisted": {
+                    "type": "boolean",
+                    "default": false
+                  },
+                  "isFollowApprovalRequired": {
+                    "type": "boolean",
+                    "default": false
                   }
                 },
                 "required": [
@@ -27840,8 +27848,27 @@
           }
         },
         "responses": {
-          "204": {
-            "description": "OK (without any results)"
+          "200": {
+            "description": "OK (with results)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "state": {
+                      "type": "string",
+                      "enum": [
+                        "following",
+                        "pending"
+                      ]
+                    }
+                  },
+                  "required": [
+                    "state"
+                  ]
+                }
+              }
+            }
           },
           "400": {
             "description": "Client error",
@@ -27969,6 +27996,574 @@
         }
       }
     },
+    "/channels/follow-requests/approve": {
+      "post": {
+        "operationId": "post___channels___follow-requests___approve",
+        "summary": "channels/follow-requests/approve",
+        "description": "No description provided.\n\n**Credential required**: *Yes* / **Permission**: *write:channels*",
+        "externalDocs": {
+          "description": "Source code",
+          "url": "https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/channels/follow-requests/approve.ts"
+        },
+        "tags": [
+          "channels"
+        ],
+        "security": [
+          {
+            "bearerAuth": []
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "channelId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "userId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  }
+                },
+                "required": [
+                  "channelId",
+                  "userId"
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "204": {
+            "description": "OK (without any results)"
+          },
+          "400": {
+            "description": "Client error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "NO_SUCH_CHANNEL": {
+                    "value": {
+                      "error": {
+                        "message": "No such channel.",
+                        "code": "NO_SUCH_CHANNEL",
+                        "id": "99490eaf-ef2c-4431-98db-7b632ec6002f"
+                      }
+                    }
+                  },
+                  "ACCESS_DENIED": {
+                    "value": {
+                      "error": {
+                        "message": "You do not have permission to manage this channel.",
+                        "code": "ACCESS_DENIED",
+                        "id": "f357e948-98bc-40c6-b25e-f76ebfdd75b5"
+                      }
+                    }
+                  },
+                  "NO_SUCH_CHANNEL_FOLLOW_REQUEST": {
+                    "value": {
+                      "error": {
+                        "message": "No such channel follow request.",
+                        "code": "NO_SUCH_CHANNEL_FOLLOW_REQUEST",
+                        "id": "bcac94b8-f83b-48e5-84da-3281b94a10b0"
+                      }
+                    }
+                  },
+                  "INVALID_PARAM": {
+                    "value": {
+                      "error": {
+                        "message": "Invalid param.",
+                        "code": "INVALID_PARAM",
+                        "id": "3d81ceae-475f-4600-b2a8-2bc116157532"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "CREDENTIAL_REQUIRED": {
+                    "value": {
+                      "error": {
+                        "message": "Credential required.",
+                        "code": "CREDENTIAL_REQUIRED",
+                        "id": "1384574d-a912-4b81-8601-c7b1c4085df1"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Forbidden error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "AUTHENTICATION_FAILED": {
+                    "value": {
+                      "error": {
+                        "message": "Authentication failed. Please ensure your token is correct.",
+                        "code": "AUTHENTICATION_FAILED",
+                        "id": "b0a7f5f8-dc2f-4171-b91f-de88ad238e14"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "418": {
+            "description": "I'm Ai",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "I_AM_AI": {
+                    "value": {
+                      "error": {
+                        "message": "You sent a request to Ai-chan, Misskey's showgirl, instead of the server.",
+                        "code": "I_AM_AI",
+                        "id": "60c46cd1-f23a-46b1-bebe-5d2b73951a84"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "INTERNAL_ERROR": {
+                    "value": {
+                      "error": {
+                        "message": "Internal error occurred. Please contact us if the error persists.",
+                        "code": "INTERNAL_ERROR",
+                        "id": "5d37dbcb-891e-41ca-a3d6-e690c97775ac"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/channels/follow-requests/list": {
+      "post": {
+        "operationId": "post___channels___follow-requests___list",
+        "summary": "channels/follow-requests/list",
+        "description": "No description provided.\n\n**Credential required**: *Yes* / **Permission**: *read:channels*",
+        "externalDocs": {
+          "description": "Source code",
+          "url": "https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/channels/follow-requests/list.ts"
+        },
+        "tags": [
+          "channels"
+        ],
+        "security": [
+          {
+            "bearerAuth": []
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "channelId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "sinceId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "untilId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "limit": {
+                    "type": "integer",
+                    "minimum": 1,
+                    "maximum": 100,
+                    "default": 10
+                  }
+                },
+                "required": [
+                  "channelId"
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "OK (with results)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "type": "object",
+                    "properties": {
+                      "id": {
+                        "type": "string",
+                        "format": "id"
+                      },
+                      "createdAt": {
+                        "type": "string",
+                        "format": "date-time"
+                      },
+                      "user": {
+                        "$ref": "#/components/schemas/UserLite"
+                      }
+                    },
+                    "required": [
+                      "id",
+                      "createdAt",
+                      "user"
+                    ]
+                  }
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Client error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "NO_SUCH_CHANNEL": {
+                    "value": {
+                      "error": {
+                        "message": "No such channel.",
+                        "code": "NO_SUCH_CHANNEL",
+                        "id": "448f1a4e-9cd0-4d6b-a9b8-302c486ad552"
+                      }
+                    }
+                  },
+                  "ACCESS_DENIED": {
+                    "value": {
+                      "error": {
+                        "message": "You do not have permission to manage this channel.",
+                        "code": "ACCESS_DENIED",
+                        "id": "34fc619d-9f19-4e52-8a29-04642575f7cf"
+                      }
+                    }
+                  },
+                  "INVALID_PARAM": {
+                    "value": {
+                      "error": {
+                        "message": "Invalid param.",
+                        "code": "INVALID_PARAM",
+                        "id": "3d81ceae-475f-4600-b2a8-2bc116157532"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "CREDENTIAL_REQUIRED": {
+                    "value": {
+                      "error": {
+                        "message": "Credential required.",
+                        "code": "CREDENTIAL_REQUIRED",
+                        "id": "1384574d-a912-4b81-8601-c7b1c4085df1"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Forbidden error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "AUTHENTICATION_FAILED": {
+                    "value": {
+                      "error": {
+                        "message": "Authentication failed. Please ensure your token is correct.",
+                        "code": "AUTHENTICATION_FAILED",
+                        "id": "b0a7f5f8-dc2f-4171-b91f-de88ad238e14"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "418": {
+            "description": "I'm Ai",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "I_AM_AI": {
+                    "value": {
+                      "error": {
+                        "message": "You sent a request to Ai-chan, Misskey's showgirl, instead of the server.",
+                        "code": "I_AM_AI",
+                        "id": "60c46cd1-f23a-46b1-bebe-5d2b73951a84"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "INTERNAL_ERROR": {
+                    "value": {
+                      "error": {
+                        "message": "Internal error occurred. Please contact us if the error persists.",
+                        "code": "INTERNAL_ERROR",
+                        "id": "5d37dbcb-891e-41ca-a3d6-e690c97775ac"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/channels/follow-requests/reject": {
+      "post": {
+        "operationId": "post___channels___follow-requests___reject",
+        "summary": "channels/follow-requests/reject",
+        "description": "No description provided.\n\n**Credential required**: *Yes* / **Permission**: *write:channels*",
+        "externalDocs": {
+          "description": "Source code",
+          "url": "https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/channels/follow-requests/reject.ts"
+        },
+        "tags": [
+          "channels"
+        ],
+        "security": [
+          {
+            "bearerAuth": []
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "channelId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "userId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  }
+                },
+                "required": [
+                  "channelId",
+                  "userId"
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "204": {
+            "description": "OK (without any results)"
+          },
+          "400": {
+            "description": "Client error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "NO_SUCH_CHANNEL": {
+                    "value": {
+                      "error": {
+                        "message": "No such channel.",
+                        "code": "NO_SUCH_CHANNEL",
+                        "id": "f9bad040-6ccb-438d-a2a5-14aea3efc318"
+                      }
+                    }
+                  },
+                  "ACCESS_DENIED": {
+                    "value": {
+                      "error": {
+                        "message": "You do not have permission to manage this channel.",
+                        "code": "ACCESS_DENIED",
+                        "id": "93f735ad-e05d-470d-8612-2aa23d2a4a45"
+                      }
+                    }
+                  },
+                  "NO_SUCH_CHANNEL_FOLLOW_REQUEST": {
+                    "value": {
+                      "error": {
+                        "message": "No such channel follow request.",
+                        "code": "NO_SUCH_CHANNEL_FOLLOW_REQUEST",
+                        "id": "b06622f9-6a22-479e-914c-13b37d944eb2"
+                      }
+                    }
+                  },
+                  "INVALID_PARAM": {
+                    "value": {
+                      "error": {
+                        "message": "Invalid param.",
+                        "code": "INVALID_PARAM",
+                        "id": "3d81ceae-475f-4600-b2a8-2bc116157532"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "CREDENTIAL_REQUIRED": {
+                    "value": {
+                      "error": {
+                        "message": "Credential required.",
+                        "code": "CREDENTIAL_REQUIRED",
+                        "id": "1384574d-a912-4b81-8601-c7b1c4085df1"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Forbidden error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "AUTHENTICATION_FAILED": {
+                    "value": {
+                      "error": {
+                        "message": "Authentication failed. Please ensure your token is correct.",
+                        "code": "AUTHENTICATION_FAILED",
+                        "id": "b0a7f5f8-dc2f-4171-b91f-de88ad238e14"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "418": {
+            "description": "I'm Ai",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "I_AM_AI": {
+                    "value": {
+                      "error": {
+                        "message": "You sent a request to Ai-chan, Misskey's showgirl, instead of the server.",
+                        "code": "I_AM_AI",
+                        "id": "60c46cd1-f23a-46b1-bebe-5d2b73951a84"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "INTERNAL_ERROR": {
+                    "value": {
+                      "error": {
+                        "message": "Internal error occurred. Please contact us if the error persists.",
+                        "code": "INTERNAL_ERROR",
+                        "id": "5d37dbcb-891e-41ca-a3d6-e690c97775ac"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/channels/followed": {
       "post": {
         "operationId": "post___channels___followed",
@@ -28140,6 +28735,385 @@
         }
       }
     },
+    "/channels/followers": {
+      "post": {
+        "operationId": "post___channels___followers",
+        "summary": "channels/followers",
+        "description": "No description provided.\n\n**Credential required**: *Yes* / **Permission**: *read:channels*",
+        "externalDocs": {
+          "description": "Source code",
+          "url": "https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/channels/followers.ts"
+        },
+        "tags": [
+          "channels"
+        ],
+        "security": [
+          {
+            "bearerAuth": []
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "channelId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "sinceId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "untilId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "limit": {
+                    "type": "integer",
+                    "minimum": 1,
+                    "maximum": 100,
+                    "default": 10
+                  }
+                },
+                "required": [
+                  "channelId"
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "OK (with results)",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "type": "object",
+                    "properties": {
+                      "id": {
+                        "type": "string",
+                        "format": "id"
+                      },
+                      "createdAt": {
+                        "type": "string",
+                        "format": "date-time"
+                      },
+                      "user": {
+                        "$ref": "#/components/schemas/UserLite"
+                      }
+                    },
+                    "required": [
+                      "id",
+                      "createdAt",
+                      "user"
+                    ]
+                  }
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Client error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "NO_SUCH_CHANNEL": {
+                    "value": {
+                      "error": {
+                        "message": "No such channel.",
+                        "code": "NO_SUCH_CHANNEL",
+                        "id": "d662d052-7760-46d1-83cc-60f857c88c4f"
+                      }
+                    }
+                  },
+                  "ACCESS_DENIED": {
+                    "value": {
+                      "error": {
+                        "message": "You do not have permission to manage this channel.",
+                        "code": "ACCESS_DENIED",
+                        "id": "d9c62aa5-3331-41fe-8f73-666b549b895d"
+                      }
+                    }
+                  },
+                  "INVALID_PARAM": {
+                    "value": {
+                      "error": {
+                        "message": "Invalid param.",
+                        "code": "INVALID_PARAM",
+                        "id": "3d81ceae-475f-4600-b2a8-2bc116157532"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "CREDENTIAL_REQUIRED": {
+                    "value": {
+                      "error": {
+                        "message": "Credential required.",
+                        "code": "CREDENTIAL_REQUIRED",
+                        "id": "1384574d-a912-4b81-8601-c7b1c4085df1"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Forbidden error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "AUTHENTICATION_FAILED": {
+                    "value": {
+                      "error": {
+                        "message": "Authentication failed. Please ensure your token is correct.",
+                        "code": "AUTHENTICATION_FAILED",
+                        "id": "b0a7f5f8-dc2f-4171-b91f-de88ad238e14"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "418": {
+            "description": "I'm Ai",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "I_AM_AI": {
+                    "value": {
+                      "error": {
+                        "message": "You sent a request to Ai-chan, Misskey's showgirl, instead of the server.",
+                        "code": "I_AM_AI",
+                        "id": "60c46cd1-f23a-46b1-bebe-5d2b73951a84"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "INTERNAL_ERROR": {
+                    "value": {
+                      "error": {
+                        "message": "Internal error occurred. Please contact us if the error persists.",
+                        "code": "INTERNAL_ERROR",
+                        "id": "5d37dbcb-891e-41ca-a3d6-e690c97775ac"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/channels/followers/remove": {
+      "post": {
+        "operationId": "post___channels___followers___remove",
+        "summary": "channels/followers/remove",
+        "description": "No description provided.\n\n**Credential required**: *Yes* / **Permission**: *write:channels*",
+        "externalDocs": {
+          "description": "Source code",
+          "url": "https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/channels/followers/remove.ts"
+        },
+        "tags": [
+          "channels"
+        ],
+        "security": [
+          {
+            "bearerAuth": []
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "channelId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  },
+                  "userId": {
+                    "type": "string",
+                    "format": "misskey:id"
+                  }
+                },
+                "required": [
+                  "channelId",
+                  "userId"
+                ]
+              }
+            }
+          }
+        },
+        "responses": {
+          "204": {
+            "description": "OK (without any results)"
+          },
+          "400": {
+            "description": "Client error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "NO_SUCH_CHANNEL": {
+                    "value": {
+                      "error": {
+                        "message": "No such channel.",
+                        "code": "NO_SUCH_CHANNEL",
+                        "id": "0426c4c2-11e0-4c62-8004-97e5b42b9bee"
+                      }
+                    }
+                  },
+                  "ACCESS_DENIED": {
+                    "value": {
+                      "error": {
+                        "message": "You do not have permission to manage this channel.",
+                        "code": "ACCESS_DENIED",
+                        "id": "450ed3ae-138c-4ab3-8538-92fb50c9dec7"
+                      }
+                    }
+                  },
+                  "INVALID_PARAM": {
+                    "value": {
+                      "error": {
+                        "message": "Invalid param.",
+                        "code": "INVALID_PARAM",
+                        "id": "3d81ceae-475f-4600-b2a8-2bc116157532"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Authentication error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "CREDENTIAL_REQUIRED": {
+                    "value": {
+                      "error": {
+                        "message": "Credential required.",
+                        "code": "CREDENTIAL_REQUIRED",
+                        "id": "1384574d-a912-4b81-8601-c7b1c4085df1"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Forbidden error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "AUTHENTICATION_FAILED": {
+                    "value": {
+                      "error": {
+                        "message": "Authentication failed. Please ensure your token is correct.",
+                        "code": "AUTHENTICATION_FAILED",
+                        "id": "b0a7f5f8-dc2f-4171-b91f-de88ad238e14"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "418": {
+            "description": "I'm Ai",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "I_AM_AI": {
+                    "value": {
+                      "error": {
+                        "message": "You sent a request to Ai-chan, Misskey's showgirl, instead of the server.",
+                        "code": "I_AM_AI",
+                        "id": "60c46cd1-f23a-46b1-bebe-5d2b73951a84"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal server error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                },
+                "examples": {
+                  "INTERNAL_ERROR": {
+                    "value": {
+                      "error": {
+                        "message": "Internal error occurred. Please contact us if the error persists.",
+                        "code": "INTERNAL_ERROR",
+                        "id": "5d37dbcb-891e-41ca-a3d6-e690c97775ac"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/channels/mute/create": {
       "post": {
         "operationId": "post___channels___mute___create",
@@ -29856,6 +30830,12 @@
                   "isLocalOnly": {
                     "type": "boolean"
                   },
+                  "isUnlisted": {
+                    "type": "boolean"
+                  },
+                  "isFollowApprovalRequired": {
+                    "type": "boolean"
+                  },
                   "transferAdminUserId": {
                     "type": "string",
                     "format": "misskey:id"
@@ -100066,6 +101046,9 @@
           "usersCount": {
             "type": "number"
           },
+          "followersCount": {
+            "type": "number"
+          },
           "notesCount": {
             "type": "number"
           },
@@ -100078,6 +101061,9 @@
           "isFollowing": {
             "type": "boolean"
           },
+          "hasPendingFollowRequest": {
+            "type": "boolean"
+          },
           "isFavorited": {
             "type": "boolean"
           },
@@ -100099,6 +101085,12 @@
           },
           "isLocalOnly": {
             "type": "boolean"
+          },
+          "isUnlisted": {
+            "type": "boolean"
+          },
+          "isFollowApprovalRequired": {
+            "type": "boolean"
           }
         },
         "required": [
@@ -100114,10 +101106,13 @@
           "color",
           "isArchived",
           "usersCount",
+          "followersCount",
           "notesCount",
           "isSensitive",
           "allowRenoteToExternal",
-          "isLocalOnly"
+          "isLocalOnly",
+          "isUnlisted",
+          "isFollowApprovalRequired"
         ]
       },
       "QueueCount": {

Get diff files from Workflow Page

@github-actions

Copy link
Copy Markdown

Thank you for sending us a great Pull Request! 👍
Please regenerate misskey-js type definitions! 🙏

example:

pnpm run build-misskey-js-with-types

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

⚙️ Backend Diagnostics Report

Memory: After GC

Metric @ Base @ Head Δ MAD
HeapUsed 75.7 MB
± 0 MB
75.9 MB
± 0 MB
$\color{orange}{\text{+0.2~MB}}$
$\color{orange}{\text{+0.3\%}}$
0 MB

Only metrics showing significant changes are displayed.

V8 Heap Snapshot Statistics

Metric @ Base @ Head Δ MAD
$\color{gray}{\rule{8pt}{8pt}}$ Total 89 MB
± 15 KB
89 MB
± 248 B
$\color{orange}{\text{+213 KB}}$
$\color{orange}{\text{+0.2\%}}$
15 KB
$\color{orange}{\rule{8pt}{8pt}}$ Code 25 MB 25 MB $\text{+24 KB}$ 11 KB
$\color{red}{\rule{8pt}{8pt}}$ Strings 30 MB 31 MB $\text{+52 KB}$ 16 B
$\color{cyan}{\rule{8pt}{8pt}}$ JS arrays 4.6 MB 4.7 MB $\text{+33 KB}$ 0 B
$\color{green}{\rule{8pt}{8pt}}$ Typed arrays 566 KB 566 KB 0 B 0 B
$\color{yellow}{\rule{8pt}{8pt}}$ System objects 2.3 MB 2.3 MB $\text{+7.2 KB}$ 521 B
$\color{violet}{\rule{8pt}{8pt}}$ Other JS objs 26 MB 26 MB $\text{+96 KB}$ 56 B
$\color{pink}{\rule{8pt}{8pt}}$ Other non-JS objs 39 KB 39 KB 0 B 0 B

Download representative heap snapshot: base / head

@mattyatea
mattyatea marked this pull request as ready for review August 21, 2026 08:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4aea9537a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}, ...(canManageChannelEvents.value ? [{
}, ...(canManageChannelFollowers.value ? [{
key: 'followers',
title: i18n.ts._channel.followRequests,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Label the tab as follower management

Managers who want to remove an existing follower only see a tab labeled “Follow requests,” even though the tab also contains the complete follower list and removal controls. This makes the newly added follower-management feature difficult to discover; the same commit adds the more accurate _channel.followerManagement locale key, but it is otherwise unused.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b6895cac5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +266 to +268
for (const request of requests) {
if (await this.insertFollowing(manager, request.followerId, targetChannel.id)) {
approvedFollowerIds.push(request.followerId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Batch pending approvals instead of issuing per-request writes

When approval is disabled on a channel with many pending requests, this loop performs a following insert and a separate counter update for every request, sequentially, while holding the channel's write lock. A popular channel with thousands of requests can therefore generate thousands of round trips, time out the channels/update call, and block follow/unfollow operations for the duration; convert the requests to followings and update the count with bounded bulk queries.

Useful? React with 👍 / 👎.

Comment on lines +615 to +621
}, ...(canManageChannelFollowers.value ? [{
key: 'followRequests',
title: i18n.ts._channel.followRequests,
icon: 'ti ti-user-check',
indicate: hasPendingChannelFollowRequests.value,
}, {
key: 'followerManagement',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset manager-only tabs when switching channels

When this page instance is reused for another channel, tab retains followRequests or followerManagement; the channel watcher only changes it when the destination is followed or favorited. If a manager opens one of these tabs and then navigates to an unfollowed channel they do not manage, the tab is removed from headerTabs and both guarded content branches are false, leaving a blank page with no selected tab until the user manually chooses another tab.

Useful? React with 👍 / 👎.

@mattyatea
mattyatea merged commit 0167ffd into develop Aug 21, 2026
39 of 47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant