参加承認・公開文書に関する歴史的な設計・準備コードの原文移管。 設計・コード断片・机上チェックをBot実装PASS、DB migration、Cloudflare Pages公開、Telegram実E2Eと扱わない。旧原本:‣。

原文本文

対象:seigo-gace/GateApp-Bot の artifacts/bot/src/bot/gateHandlers.ts、artifacts/bot/src/services/inviteService.ts。既存コード全体を置換せず、関数とハンドラの差分で反映する。

追加:審査コールバックの段階ガード

新規 artifacts/bot/src/services/gateStepGuard.ts

export type GateStep =
  | "challenge" | "q1" | "q2" | "q3" | "rules";

export function matchesExpectedStep(
  session: { step:string; target_id:number } | undefined,
  expected:GateStep,
  targetId:number,
):boolean {
  return !!session && session.step===expected && session.target_id===targetId;
}

export function isConsentStep(step: string): step is "q1"|"q2"|"rules" {
  return step==="q1" || step==="q2" || step==="rules";
}

gateHandlers.tsの各アクション内でgetGateSessionとgetTargetByIdの直後に必ずmatchesExpectedStep(session, expectedStep, target.target_id)を挿入。失敗したらctx.answerCbQuery("操作期限切れです")等で拒否し、stepNextを呼ばない。

特に challenge はstep==="challenge"、質問は callbackに含まれるq1/q2/q3、ルール承諾はstep==="rules"を要求。現行実装のgate:rule:acceptは状態の照合不足。

さらにcallbackの同時二重押下をDBで直列化する。表示だけのチェックでは競合するため、セッション更新にWHERE tg_user_id=? AND target_id=? AND step=?条件を付け、更新対象0件なら処理せず終了する方式を使う。単にgetGateSessionを2回呼ぶ実装は不十分。

追加:参加申請本人照合(設計コード)

既存createInviteLink()はmember_limit:1/creates_join_request:false。これを「使用人数制限」だけで本人限定とみなさない。

新規 artifacts/bot/src/services/joinRequestGate.ts

import type { Telegram } from "telegraf";

export async function decideJoinRequest(args:{
 telegram:Telegram;
 chatId:number;
 requesterUserId:number;
 approval:{
   userId:number;
   status:"approved"|"pending"|"rejected";
   consentComplete:boolean;
   banned:boolean;
 };
}):Promise<"approved"|"declined"> {
  const {telegram,chatId,requesterUserId,approval}=args;
  const allowed =
    requesterUserId===approval.userId &&
    approval.status==="approved" &&
    approval.consentComplete &&
    !approval.banned;

  if (!allowed) {
    await telegram.declineChatJoinRequest(chatId,requesterUserId);
    return "declined";
  }
  await telegram.approveChatJoinRequest(chatId,requesterUserId);
  return "approved";
}

接続時の必須条件: chat_join_request イベントから実際のrequest.from.idとrequest.chat.idを受け取り、登録済みtargets.chat_id・approvals・gate_consents・ブラックリストをDB照合して上記関数へ渡す。ここで使うapprovalは未認証の入力ではなくDBの実値から組み立てる。Botに招待リンク・参加申請承認権限が必要。

招待リンク発行パラメータの差分:

const link = await bot.telegram.createChatInviteLink(chatId, {
  name: linkName,
  expire_date: Math.floor(expireAt.getTime()/1000),
  creates_join_request: true,
  // member_limitを同時設定しない
});

注意:この変更だけでは完成ではない。本人のjoin request承認までを一つの実装単位として検証し、入室前に別人を拒否する。既存のchat_member後追いキックは防御の補助として維持・整合し、二重処分や誤BANを防ぐ。審査済み/再審査/招待期限切れ・リンク盗用・同時申請の試験が必須。

既存資産を残す

challengeService.tsの足し算・引き算・同数字選択、最大失敗3回、questions最大3問の器、rules承諾ボタン、approvals・invite_linksDB、管理画面は削除・再実装しない。

GateApp-Botのコールバック処理は現在ユーザーIDごとにセッションを持つため、複数group同時申請を安全に行うにはセッション分離か直列化が必要。新しい別DBを作らない。