Files
shockbot/utils/body.ts
T
Mateusz Burzyński 071e885d63 Add upload tool and related APIs (#187)
* Add utils for r2 upload

* Add the tool and new routes

* fix auth issue

* sign headers

* add comment

* use our own API key to auth signed uploads

* Restructure things slightly

* tweak

* tweak

* add comments

* tweak

* revert a thing

* twaek

* drop mime type filtering

* new incarnation of mime type filtering

* jsut allow all octet-streams

* simplify further

* tweak

* update lockfile

---------

Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
2026-01-28 05:34:58 +00:00

146 lines
4.1 KiB
TypeScript

import TurndownService from "turndown";
import type { PayloadEvent } from "../external.ts";
import { log } from "./cli.ts";
import type { OctokitWithPlugins } from "./github.ts";
import type { RunContextData } from "./runContextData.ts";
const turndown = new TurndownService();
function hasImages(body: string | null | undefined): boolean {
if (!body) return false;
return body.includes("<img") || body.includes("![");
}
interface ResolveBodyContext {
event: PayloadEvent;
octokit: OctokitWithPlugins;
repo: RunContextData["repo"];
}
/**
* resolves the body of an event by fetching body_html and converting to markdown.
* only fetches body_html if the body contains images (to avoid unnecessary API calls).
* this ensures agents receive markdown with working signed image URLs instead of
* broken user-attachments URLs.
*/
export async function resolveBody(ctx: ResolveBodyContext): Promise<string | null> {
const body = ctx.event.body;
// pass through if no images - no API call needed
if (!hasImages(body)) return body ?? null;
log.debug(`[resolveBody] fetching body_html for ${ctx.event.trigger}`);
const bodyHtml = await fetchBodyHtml(ctx);
log.debug(`[resolveBody] bodyHtml: ${bodyHtml?.substring(0, 300)}`);
if (!bodyHtml) return body ?? null;
const resolved = turndown.turndown(bodyHtml);
log.debug(`[resolveBody] resolved: ${resolved.substring(0, 300)}`);
return resolved;
}
async function fetchBodyHtml(ctx: ResolveBodyContext): Promise<string | undefined> {
const event = ctx.event;
const headers = { accept: "application/vnd.github.full+json" };
const owner = ctx.repo.owner;
const repo = ctx.repo.name;
switch (event.trigger) {
case "issue_comment_created":
if (!event.comment_id) return;
return (
await ctx.octokit.rest.issues.getComment({
owner,
repo,
comment_id: event.comment_id,
headers,
})
).data.body_html;
case "issues_opened":
case "issues_assigned":
case "issues_labeled":
if (!event.issue_number) return;
return (
await ctx.octokit.rest.issues.get({
owner,
repo,
issue_number: event.issue_number,
headers,
})
).data.body_html;
case "pull_request_opened":
case "pull_request_ready_for_review":
case "pull_request_review_requested":
// PRs are also issues - use issues.get which returns body_html
if (!event.issue_number) return;
return (
await ctx.octokit.rest.issues.get({
owner,
repo,
issue_number: event.issue_number,
headers,
})
).data.body_html;
case "pull_request_review_submitted":
if (!event.issue_number || !event.review_id) return;
return (
await ctx.octokit.rest.pulls.getReview({
owner,
repo,
pull_number: event.issue_number,
review_id: event.review_id,
headers,
})
).data.body_html;
case "pull_request_review_comment_created":
if (!event.comment_id) return;
return (
await ctx.octokit.rest.pulls.getReviewComment({
owner,
repo,
comment_id: event.comment_id,
headers,
})
).data.body_html;
case "check_suite_completed":
// body is the PR body
if (!event.issue_number) return;
return (
await ctx.octokit.rest.issues.get({
owner,
repo,
issue_number: event.issue_number,
headers,
})
).data.body_html;
case "implement_plan":
// body is the plan content from an issue comment
if (!event.plan_comment_id) return;
return (
await ctx.octokit.rest.issues.getComment({
owner,
repo,
comment_id: event.plan_comment_id,
headers,
})
).data.body_html;
// triggers without a body field that needs resolution
case "workflow_dispatch":
case "fix_review":
case "unknown":
return undefined;
default:
// exhaustiveness check - TypeScript will error if a trigger is missing
event satisfies never;
return undefined;
}
}