kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at cd7cada2f86b4e866a15b4323bb8d6d7ab5bba8b 205 lines 6.5 kB view raw
1import { eq } from "drizzle-orm"; 2import db from "../../database"; 3import { integrationTable } from "../../database/schema"; 4import type { GiteaConfig } from "./config"; 5import { verifyGiteaSignature } from "./utils/verify-signature"; 6import { handleGiteaIssueClosed } from "./webhooks/issue-closed"; 7import { handleGiteaIssueCommentCreated } from "./webhooks/issue-comment-created"; 8import { handleGiteaIssueEdited } from "./webhooks/issue-edited"; 9import { handleGiteaIssueLabeled } from "./webhooks/issue-labeled"; 10import { handleGiteaIssueOpened } from "./webhooks/issue-opened"; 11import { handleGiteaIssueReopened } from "./webhooks/issue-reopened"; 12import { handleGiteaLabelCreated } from "./webhooks/label-created"; 13import { handleGiteaPullRequestClosed } from "./webhooks/pull-request-closed"; 14import { handleGiteaPullRequestOpened } from "./webhooks/pull-request-opened"; 15import { handleGiteaPush } from "./webhooks/push"; 16 17type GiteaPushPayload = Parameters<typeof handleGiteaPush>[0]; 18type GiteaPullRequestPayload = Parameters< 19 typeof handleGiteaPullRequestOpened 20>[0]; 21type GiteaPullRequestClosedPayload = Parameters< 22 typeof handleGiteaPullRequestClosed 23>[0]; 24type GiteaIssuePayload = Parameters<typeof handleGiteaIssueOpened>[0]; 25type GiteaIssueClosedPayload = Parameters<typeof handleGiteaIssueClosed>[0]; 26type GiteaIssueReopenedPayload = Parameters<typeof handleGiteaIssueReopened>[0]; 27type GiteaIssueCommentPayload = Parameters< 28 typeof handleGiteaIssueCommentCreated 29>[0]; 30type GiteaLabelPayload = Parameters<typeof handleGiteaLabelCreated>[0]; 31 32function isRecord(value: unknown): value is Record<string, unknown> { 33 return typeof value === "object" && value !== null; 34} 35 36function hasRepository(value: Record<string, unknown>) { 37 return isRecord(value.repository); 38} 39 40function isPushPayload( 41 payload: Record<string, unknown>, 42): payload is GiteaPushPayload { 43 return typeof payload.ref === "string" && hasRepository(payload); 44} 45 46function isPullRequestPayload( 47 payload: Record<string, unknown>, 48): payload is GiteaPullRequestPayload { 49 return hasRepository(payload) && isRecord(payload.pull_request); 50} 51 52function isIssuePayload( 53 payload: Record<string, unknown>, 54): payload is GiteaIssuePayload { 55 return hasRepository(payload) && isRecord(payload.issue); 56} 57 58function isIssueCommentPayload( 59 payload: Record<string, unknown>, 60): payload is GiteaIssueCommentPayload { 61 return ( 62 hasRepository(payload) && 63 isRecord(payload.issue) && 64 isRecord(payload.comment) 65 ); 66} 67 68function isLabelPayload( 69 payload: Record<string, unknown>, 70): payload is GiteaLabelPayload { 71 return hasRepository(payload); 72} 73 74export async function handleGiteaWebhookRequest( 75 integrationId: string, 76 rawBody: string, 77 signatureHeader: string | undefined, 78 eventHeader: string | undefined, 79): Promise<{ success: boolean; error?: string }> { 80 const integration = await db.query.integrationTable.findFirst({ 81 where: eq(integrationTable.id, integrationId), 82 }); 83 84 if (!integration || integration.type !== "gitea") { 85 return { success: false, error: "Gitea integration not found" }; 86 } 87 88 let config: GiteaConfig; 89 try { 90 config = JSON.parse(integration.config) as GiteaConfig; 91 } catch { 92 return { success: false, error: "Invalid integration config" }; 93 } 94 95 const secret = config.webhookSecret; 96 if (!secret) { 97 return { success: false, error: "Webhook secret not configured" }; 98 } 99 100 if (!verifyGiteaSignature(rawBody, secret, signatureHeader)) { 101 return { success: false, error: "Invalid webhook signature" }; 102 } 103 104 const event = eventHeader || undefined; 105 106 if (!event) { 107 return { success: false, error: "Missing event name" }; 108 } 109 110 let payload: Record<string, unknown>; 111 try { 112 payload = JSON.parse(rawBody) as Record<string, unknown>; 113 } catch { 114 return { success: false, error: "Invalid JSON payload" }; 115 } 116 117 try { 118 await dispatchGiteaEvent(event, payload); 119 return { success: true }; 120 } catch (error) { 121 console.error("[Gitea Webhook] Handler error:", error); 122 return { 123 success: false, 124 error: error instanceof Error ? error.message : "Webhook handler failed", 125 }; 126 } 127} 128 129async function dispatchGiteaEvent( 130 event: string, 131 payload: Record<string, unknown>, 132) { 133 console.log(`[Gitea Webhook] Event: ${event}`); 134 135 switch (event) { 136 case "push": 137 if (isPushPayload(payload)) { 138 await handleGiteaPush(payload); 139 } 140 return; 141 case "pull_request": { 142 const action = payload.action as string | undefined; 143 if ( 144 action === "opened" || 145 action === "reopened" || 146 action === "ready_for_review" 147 ) { 148 if (isPullRequestPayload(payload)) { 149 await handleGiteaPullRequestOpened(payload); 150 } 151 } else if (action === "closed" && isPullRequestPayload(payload)) { 152 await handleGiteaPullRequestClosed( 153 payload as unknown as GiteaPullRequestClosedPayload, 154 ); 155 } 156 return; 157 } 158 case "issues": { 159 const action = payload.action as string | undefined; 160 // Gitea uses "created" for new issues; GitHub-style is "opened" 161 if ( 162 (action === "opened" || action === "created") && 163 isIssuePayload(payload) 164 ) { 165 await handleGiteaIssueOpened(payload); 166 } else if (action === "reopened" && isIssuePayload(payload)) { 167 await handleGiteaIssueReopened( 168 payload as unknown as GiteaIssueReopenedPayload, 169 ); 170 } else if (action === "closed" && isIssuePayload(payload)) { 171 await handleGiteaIssueClosed( 172 payload as unknown as GiteaIssueClosedPayload, 173 ); 174 } else if (action === "edited" && isIssuePayload(payload)) { 175 await handleGiteaIssueEdited(payload); 176 } else if ( 177 isIssuePayload(payload) && 178 (action === "labeled" || 179 action === "unlabeled" || 180 action === "label_updated") 181 ) { 182 await handleGiteaIssueLabeled({ 183 ...payload, 184 action: action ?? "", 185 }); 186 } 187 return; 188 } 189 case "issue_comment": { 190 const action = payload.action as string | undefined; 191 if (action === "created" && isIssueCommentPayload(payload)) { 192 await handleGiteaIssueCommentCreated(payload); 193 } 194 return; 195 } 196 case "issue_label": { 197 if (isLabelPayload(payload)) { 198 await handleGiteaLabelCreated(payload); 199 } 200 return; 201 } 202 default: 203 console.log(`[Gitea Webhook] Ignored event: ${event}`); 204 } 205}