import { repoDb, stmt } from "./scoped-db"; import { captureServerEvent } from "./analytics"; import { registerLocator } from "./scoped-db/locators"; import { scopedId } from "./scoped-db/ids"; import { repoActivityStatement } from "./issue-scope"; import { emitNotification, emitWebhook } from "./events/emit"; import { forbidden } from "./errors"; import { issueUrl, setIssueState } from "./issues"; import type { IssueRow } from "./issues"; import type { RepoAccess } from "./repos"; import type { RepoRow, Viewer } from "~/lib/types"; export function canMaintainRepo(access: RepoAccess): boolean { return access.permission === "write" || access.permission === "admin"; } /** Notifies the thread's participants, watchers and @mentions about activity on an issue or pull request. */ export async function notifyIssueThread( repo: RepoRow, issue: IssueRow, actor: Viewer, reason: "comment" | "state_change", text: string | null, ): Promise { await emitNotification({ repoId: repo.id, issueId: issue.id, actorId: actor.id, reason, title: `${repo.owner_name}/${repo.name}#${issue.number}: ${issue.title}`, url: issueUrl(repo, issue), mentionText: text, directRecipients: [], }); } /** * Adds a comment to an issue or pull request conversation, from the web or an emailed reply. The caller * has already checked read access and that the repository is not archived; locked threads need write. */ export async function createIssueComment( access: RepoAccess, issue: IssueRow, viewer: Viewer, body: string, options: { closeAfter?: boolean } = {}, ): Promise { if (issue.locked === 1 && !canMaintainRepo(access)) throw forbidden("This conversation is locked"); const now = Date.now(); const id = scopedId(issue.repo_id); await repoDb(issue.repo_id).batch([ stmt( "INSERT INTO comments (id, issue_id, author_id, body, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", id, issue.id, viewer.id, body, now, now, ), stmt("UPDATE issues SET comments_count = comments_count + 1, updated_at = ? WHERE id = ?", now, issue.id), repoActivityStatement(viewer.id, issue.repo_id, "comment", { number: issue.number, title: issue.title, isPull: issue.is_pull === 1, }), ]); await registerLocator("comment", id, issue.repo_id); captureServerEvent( "comment_created", viewer.id, { source: "web", on_pull: issue.is_pull === 1, private_repo: access.repo.visibility !== "public" }, { ownerAccountId: access.repo.owner_id }, ); if (options.closeAfter && (issue.author_id === viewer.id || canMaintainRepo(access))) await setIssueState(issue, "closed", viewer.id); await emitWebhook(access.repo.id, "issue_comment", { action: "created", issue: { number: issue.number, title: issue.title, html_url: issueUrl(access.repo, issue), }, comment: { body, user: { login: viewer.name } }, sender: { login: viewer.name, id: viewer.id }, }); await notifyIssueThread(access.repo, issue, viewer, "comment", body); }