Compare commits

..

No commits in common. "c3000e954bf404631973543b0c62c720f739f460" and "bafb6fe93bde281b8bf3c5b6a96343834e525930" have entirely different histories.

5 changed files with 29 additions and 102 deletions

View File

@ -60,7 +60,7 @@ class PRHandler {
await this.handleStateChange(pullRequest, repository)
}
log({ type: "pr", action, pr: payload.number, repo: repository.full_name, user: pullRequest.user.login, title: pullRequest.title })
log({ type: "pr", action, pr: payload.number, repo: repository.full_name, user: pullRequest.user.login })
}
static async handleOpened(pullRequest: Gitea.PullRequest, repository: Gitea.Repository) {
@ -116,8 +116,8 @@ class PRHandler {
let message = `
> ### [${pullRequest.title}](<${pullRequest.html_url}>)
> **${repositoryFullName}**
>
${body
>
${body
.split("\n")
.map((line) => `> ${line}`)
.join("\n")}
@ -153,7 +153,7 @@ class CommentHandler {
await this.handleDeleted(comment)
}
log({ type: "comment", action, pr: issue.number, repo: repository.full_name, user: comment.user.login, body: comment.body.slice(0, 200) })
log({ type: "comment", action, pr: issue.number, repo: repository.full_name, user: comment.user.login })
}
static async handleCreated(issue: Gitea.Issue, comment: Gitea.Comment, repo: string) {

View File

@ -1,12 +1,5 @@
import { mkdirSync } from "node:fs"
import { getConfig } from "../config"
import { log } from "../log"
const crashLogDir = getConfig("dataDir")
mkdirSync(crashLogDir, { recursive: true })
const crashLogPath = `${crashLogDir}/crash.log`
export const logCrash = async (error: unknown) => {
try {
const stack = error instanceof Error ? error.stack : ""
@ -15,7 +8,7 @@ export const logCrash = async (error: unknown) => {
const crashLog = `Spike crashed at ${new Date().toISOString()}:\n${message}\n${stack ?? ""}\n`
// overwrite the crash log file
const file = Bun.file(crashLogPath)
const file = Bun.file(`${process.env.DATA_DIR}/crash.log`)
file.write(crashLog)
} catch (writeError) {
log({ type: "error", error: writeError, context: "writing crash log" })
@ -40,7 +33,7 @@ export const alertAboutCrashLog = async (client: any) => {
const clearCrashLog = async () => {
try {
const file = Bun.file(crashLogPath)
const file = Bun.file(`${process.env.DATA_DIR}/crash.log`)
if (!(await file.exists())) return
const contents = await file.text()
await file.write("")

View File

@ -3,10 +3,10 @@ import { basename } from "node:path"
import { getConfig } from "./config"
export type LogEvent =
| { type: "webhook"; eventType: string; repo: string; sender?: string; ref?: string }
| { type: "webhook"; eventType: string; repo: string }
| { type: "webhook-ignored"; eventType: string; repo: string }
| { type: "pr"; action: string; pr: number; repo: string; user: string; title?: string }
| { type: "comment"; action: string; pr: number; repo: string; user: string; body?: string }
| { type: "pr"; action: string; pr: number; repo: string; user: string }
| { type: "comment"; action: string; pr: number; repo: string; user: string }
| { type: "comment-skipped"; pr: number; repo: string; commentId: number }
| { type: "review"; action: string; pr: number; repo: string; user: string }
| { type: "thread-auto-created"; pr: number; repo: string }
@ -59,22 +59,17 @@ export function listLogFiles(): string[] {
}
export function readLogFile(filename: string): StoredLogEvent[] {
const raw = readLogFileRaw(filename)
if (!raw) return []
return raw
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line) as StoredLogEvent)
}
export function readLogFileRaw(filename: string): string | undefined {
if (filename !== basename(filename)) throw new Error("Invalid filename")
// Reject path traversal attempts (e.g. "../../../etc/passwd")
if (filename !== basename(filename)) return []
const path = `${logsDir}/${filename}`
try {
return readFileSync(path, "utf-8")
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line) as StoredLogEvent)
} catch {
return
return []
}
}
@ -101,9 +96,6 @@ onLog((event) => {
if ("repo" in event) parts.push(dim(event.repo))
if ("pr" in event) parts.push(cyan(`#${event.pr}`))
if ("user" in event) parts.push(dim(event.user))
if ("sender" in event && event.sender) parts.push(dim(event.sender))
if ("ref" in event && event.ref) parts.push(dim(event.ref))
if ("title" in event && event.title) parts.push(event.title)
if ("detail" in event) parts.push(event.detail)
if ("command" in event) parts.push(event.command)
if ("eventType" in event) parts.push(event.eventType)

View File

@ -1,7 +1,7 @@
import { serve } from "bun"
import { handleGiteaWebhook } from "../bridge"
import { startDiscord } from "../discord"
import { listLogFiles, log, readLogFileRaw } from "../log"
import { log } from "../log"
import { LogsPage } from "./logs"
await startDiscord()
@ -18,9 +18,7 @@ const server = serve({
const payload = await req.json()
const eventType = req.headers.get("X-Gitea-Event") || "unknown"
const repo = (payload as any)?.repository?.full_name || "unknown"
const sender = (payload as any)?.sender?.login as string | undefined
const ref = (payload as any)?.ref as string | undefined
log({ type: "webhook", eventType, repo, sender, ref })
log({ type: "webhook", eventType, repo })
try {
await handleGiteaWebhook(payload, eventType as any)
@ -32,24 +30,7 @@ const server = serve({
},
},
"/logs": {
GET: (req) => {
const accept = req.headers.get("Accept") || ""
const wantsRaw = !accept.includes("text/html")
if (!wantsRaw) return LogsPage(req)
const file = new URL(req.url).searchParams.get("file")
if (!file) {
const files = listLogFiles()
return Response.json(files)
}
try {
const raw = readLogFileRaw(file)
if (!raw) return new Response("Not found", { status: 404 })
return new Response(raw, { headers: { "Content-Type": "application/jsonl" } })
} catch {
return new Response("Internal server error", { status: 500 })
}
},
GET: (req) => LogsPage(req),
},
"/discord/auth": async () => {
const permissions = 536870912 // from https://discord.com/developers/applications

View File

@ -17,19 +17,7 @@ const typeColors: Record<string, string> = {
error: "#f87171",
}
const summaryFields = ["type", "ts"] as const
function getEventMeta(event: StoredLogEvent): Record<string, string> {
const meta: Record<string, string> = {}
for (const [key, value] of Object.entries(event)) {
if (summaryFields.includes(key as any)) continue
if (value === undefined) continue
meta[key] = typeof value === "string" ? value : String(value)
}
return meta
}
function EventRow({ event, index }: { event: StoredLogEvent; index: number }) {
function EventRow({ event }: { event: StoredLogEvent }) {
const color = typeColors[event.type] || "#9ca3af"
const time = event.ts.slice(11, 19)
@ -47,41 +35,14 @@ function EventRow({ event, index }: { event: StoredLogEvent; index: number }) {
details.push(msg)
}
const meta = getEventMeta(event)
const hasExtra = Object.keys(meta).length > 0
return (
<>
<tr
style={`border-bottom: 1px solid #2a2a2a${hasExtra ? "; cursor: pointer" : ""}`}
data-row={index}
onclick={hasExtra ? `(function(e){var d=document.getElementById('detail-${index}');if(d){d.style.display=d.style.display==='none'?'table-row':'none';e.currentTarget.querySelector('.expand-icon').textContent=d.style.display==='none'?'+':'-'}})(event)` : undefined}
>
<td style="padding: 6px 4px 6px 12px; color: #4a4a4a; font-size: 11px; width: 16px; user-select: none">
{hasExtra && <span class="expand-icon">+</span>}
</td>
<td data-ts={event.ts} style="padding: 6px 12px; color: #6b7280; white-space: nowrap; font-size: 13px">{time}</td>
<td style={`padding: 6px 12px; color: ${color}; white-space: nowrap; font-weight: 600; font-size: 13px`}>
{event.type}
</td>
<td style="padding: 6px 12px; color: #d1d5db; font-size: 13px">{details.join(" ")}</td>
</tr>
{hasExtra && (
<tr id={`detail-${index}`} style="display: none; background: #141414">
<td></td>
<td colspan="3" style="padding: 8px 12px 12px">
<div style="display: flex; flex-wrap: wrap; gap: 6px 20px; font-size: 12px">
{Object.entries(meta).map(([key, value]) => (
<div style="display: flex; gap: 6px">
<span style="color: #6b7280">{key}</span>
<span style="color: #d1d5db; max-width: 500px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">{value}</span>
</div>
))}
</div>
</td>
</tr>
)}
</>
<tr style="border-bottom: 1px solid #2a2a2a">
<td data-ts={event.ts} style="padding: 6px 12px; color: #6b7280; white-space: nowrap; font-size: 13px">{time}</td>
<td style={`padding: 6px 12px; color: ${color}; white-space: nowrap; font-weight: 600; font-size: 13px`}>
{event.type}
</td>
<td style="padding: 6px 12px; color: #d1d5db; font-size: 13px">{details.join(" ")}</td>
</tr>
)
}
@ -196,8 +157,8 @@ export function LogsPage(req: Request) {
{/* Events table */}
<table style="width: 100%; border-collapse: collapse">
<tbody>
{events.map((event, i) => (
<EventRow event={event} index={i} />
{events.map((event) => (
<EventRow event={event} />
))}
</tbody>
</table>