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 138 lines 4.0 kB view raw
1import { and, eq } from "drizzle-orm"; 2import db from "../../../database"; 3import { externalLinkTable, taskTable } from "../../../database/schema"; 4import { publishEvent } from "../../../events"; 5import { updateExternalLink } from "../../github/services/link-manager"; 6import { updateTaskStatus } from "../../github/services/task-service"; 7import { 8 findAllIntegrationsByGiteaRepo, 9 repoOwnerLogin, 10} from "../services/integration-lookup"; 11import { 12 OUTBOUND_STATE_ECHO_WINDOW_MS, 13 parseIssueUpdatedAtMs, 14} from "../utils/outbound-echo"; 15import { resolveTargetStatus } from "../utils/resolve-column"; 16import { baseUrlFromRepositoryHtmlUrl } from "../utils/webhook-repo"; 17 18type IssueReopenedPayload = { 19 action: string; 20 issue: { 21 number: number; 22 title: string; 23 html_url: string; 24 state: string; 25 updated_at?: string; 26 }; 27 repository: { 28 owner: { login?: string; username?: string }; 29 name: string; 30 html_url: string; 31 }; 32}; 33 34export async function handleGiteaIssueReopened(payload: IssueReopenedPayload) { 35 if (payload.action !== "reopened") { 36 return; 37 } 38 39 const { issue, repository } = payload; 40 41 const baseUrl = baseUrlFromRepositoryHtmlUrl(repository.html_url); 42 if (!baseUrl) return; 43 44 const owner = repoOwnerLogin(repository); 45 const integrations = await findAllIntegrationsByGiteaRepo( 46 baseUrl, 47 owner, 48 repository.name, 49 ); 50 51 for (const integration of integrations) { 52 try { 53 const externalLink = await db.query.externalLinkTable.findFirst({ 54 where: and( 55 eq(externalLinkTable.integrationId, integration.id), 56 eq(externalLinkTable.resourceType, "issue"), 57 eq(externalLinkTable.externalId, issue.number.toString()), 58 ), 59 }); 60 61 if (!externalLink) { 62 continue; 63 } 64 65 const task = await db.query.taskTable.findFirst({ 66 where: eq(taskTable.id, externalLink.taskId), 67 }); 68 69 if (!task) { 70 continue; 71 } 72 73 let existingMetadata: Record<string, unknown> = {}; 74 if (externalLink.metadata) { 75 try { 76 existingMetadata = JSON.parse(externalLink.metadata) as Record< 77 string, 78 unknown 79 >; 80 } catch (error) { 81 console.warn("Failed to parse Gitea issue metadata for reopen sync", { 82 externalLinkId: externalLink.id, 83 metadata: externalLink.metadata, 84 error, 85 }); 86 } 87 } 88 89 const lastOutbound = existingMetadata.lastOutboundStateSyncAt; 90 if (typeof lastOutbound === "number" && Number.isFinite(lastOutbound)) { 91 const eventMs = parseIssueUpdatedAtMs(issue); 92 if ( 93 eventMs !== null && 94 Math.abs(eventMs - lastOutbound) <= OUTBOUND_STATE_ECHO_WINDOW_MS 95 ) { 96 continue; 97 } 98 } 99 100 const targetStatus = await resolveTargetStatus( 101 task.projectId, 102 "issue_reopened", 103 "to-do", 104 ); 105 106 const statusResult = await updateTaskStatus(task.id, targetStatus); 107 if ( 108 statusResult.applied && 109 statusResult.before.status !== statusResult.after.status 110 ) { 111 await publishEvent("task.status_changed", { 112 taskId: statusResult.after.id, 113 projectId: statusResult.after.projectId, 114 userId: null, 115 oldStatus: statusResult.before.status, 116 newStatus: statusResult.after.status, 117 title: statusResult.after.title, 118 assigneeId: statusResult.after.userId, 119 type: "status_changed", 120 }); 121 } 122 123 await updateExternalLink(externalLink.id, { 124 metadata: { 125 ...existingMetadata, 126 state: "open", 127 }, 128 }); 129 } catch (error) { 130 console.error("Gitea issue_reopened handler failed for integration", { 131 integrationId: integration.id, 132 issueNumber: issue.number, 133 repository: `${owner}/${repository.name}`, 134 error, 135 }); 136 } 137 } 138}