158 lines · 6.8 KB
import { z } from "zod";
import { all, first, run } from "../db";
import { forbidden, notFoundError } from "../errors";
import { applyLabels, indexIssue, issueUrl, loadIssue } from "../issues";
import type { IssueRow } from "../issues";
import { addIssueEvent } from "../pulls";
import { limitWrite } from "../limits";
import { assertNotArchived, hasPermission } from "../repos";
import { changeIssueState, insertComment, insertIssue } from "../mcp/issue-writes";
import { markdownBody, title } from "~/lib/validation";
import type { RepoRow } from "~/lib/types";
import { readJson, requireScopedRepo, unprocessable } from "./http";
import type { ApiContext, ScopedRepo } from "./http";

const LIST_LIMIT = 100;
const MAX_COMMENTS = 100;

/** Opening issues and commenting need only read access (as on the web), but a fine-grained token must opt in to issue writes. */
function requireIssueWriteGrant(ctx: ApiContext): void {
  const grant = ctx.credential.grant;
  if (grant?.kind === "fine" && grant.permissions.issues !== "write") {
    throw forbidden("This token cannot write issues");
  }
  if (grant?.kind === "deploy") throw forbidden("Deploy keys cannot write issues");
}

function canMaintain(scoped: ScopedRepo): boolean {
  return hasPermission(scoped.permission, "write");
}

async function loadPlainIssue(repo: RepoRow, number: number): Promise<IssueRow> {
  const issue = await loadIssue(repo.id, number);
  if (!issue || issue.is_pull === 1) throw notFoundError("Issue not found");
  return issue;
}

async function issueJson(repo: RepoRow, issue: IssueRow) {
  const [author, labels] = await Promise.all([
    first<{ name: string }>("SELECT name FROM accounts WHERE id = ?", issue.author_id),
    all<{ name: string }>(