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 196 lines 5.5 kB view raw
1import { and, eq } from "drizzle-orm"; 2import db from "../../../database"; 3import { columnTable, projectTable, taskTable } from "../../../database/schema"; 4import { publishEvent } from "../../../events"; 5import getNextTaskNumber from "../../../task/controllers/get-next-task-number"; 6import { 7 createExternalLink, 8 findExternalLink, 9} from "../../github/services/link-manager"; 10import { 11 extractIssuePriority, 12 extractIssueStatus, 13} from "../../github/utils/extract-priority"; 14import { formatTaskDescriptionFromIssue } from "../../github/utils/format"; 15import type { GiteaConfig } from "../config"; 16import { 17 findAllIntegrationsByGiteaRepo, 18 repoOwnerLogin, 19} from "../services/integration-lookup"; 20import { createGiteaClient } from "../utils/gitea-api"; 21import { addLabelsToIssueGitea } from "../utils/labels"; 22import { resolveTargetStatus } from "../utils/resolve-column"; 23import { baseUrlFromRepositoryHtmlUrl } from "../utils/webhook-repo"; 24 25type IssueOpenedPayload = { 26 action: string; 27 issue: { 28 number: number; 29 title: string; 30 body: string | null; 31 html_url: string; 32 labels?: Array<string | { name?: string }>; 33 user: { login?: string; username?: string } | null; 34 }; 35 repository: { 36 owner: { login?: string; username?: string }; 37 name: string; 38 html_url: string; 39 }; 40}; 41 42export async function handleGiteaIssueOpened(payload: IssueOpenedPayload) { 43 const { issue, repository } = payload; 44 45 const baseUrl = baseUrlFromRepositoryHtmlUrl(repository.html_url); 46 if (!baseUrl) { 47 return; 48 } 49 50 const owner = repoOwnerLogin(repository); 51 const integrations = await findAllIntegrationsByGiteaRepo( 52 baseUrl, 53 owner, 54 repository.name, 55 ); 56 57 if (integrations.length === 0) { 58 return; 59 } 60 61 for (const integration of integrations) { 62 let config: GiteaConfig; 63 try { 64 config = JSON.parse(integration.config) as GiteaConfig; 65 } catch (error) { 66 console.error("Invalid Gitea config for integration", { 67 integrationId: integration.id, 68 error, 69 }); 70 continue; 71 } 72 const projectId = integration.projectId; 73 74 const priority = extractIssuePriority(issue.labels); 75 const status = extractIssueStatus(issue.labels); 76 77 const existingLink = await findExternalLink( 78 integration.id, 79 "issue", 80 issue.number.toString(), 81 ); 82 83 if (existingLink) { 84 continue; 85 } 86 87 const nextTaskNumber = await getNextTaskNumber(projectId); 88 89 const resolvedStatus = await resolveTargetStatus( 90 projectId, 91 "issue_opened", 92 status || "to-do", 93 ); 94 95 const targetColumn = await db.query.columnTable.findFirst({ 96 where: and( 97 eq(columnTable.projectId, projectId), 98 eq(columnTable.slug, resolvedStatus), 99 ), 100 }); 101 102 const taskValues: typeof taskTable.$inferInsert = { 103 projectId, 104 userId: null, 105 title: issue.title, 106 description: formatTaskDescriptionFromIssue(issue.body), 107 status: resolvedStatus, 108 columnId: targetColumn?.id ?? null, 109 priority: null, 110 number: nextTaskNumber + 1, 111 }; 112 113 if (priority) taskValues.priority = priority; 114 115 const [createdTask] = await db 116 .insert(taskTable) 117 .values(taskValues) 118 .returning(); 119 120 if (!createdTask) { 121 console.error("Failed to create task from Gitea issue"); 122 continue; 123 } 124 125 await publishEvent("task.created", { 126 ...createdTask, 127 taskId: createdTask.id, 128 userId: createdTask.userId ?? "", 129 type: "task", 130 content: null, 131 source: "gitea", 132 externalId: issue.number.toString(), 133 actor: issue.user?.login ?? issue.user?.username ?? "gitea-webhook", 134 }); 135 136 await createExternalLink({ 137 taskId: createdTask.id, 138 integrationId: integration.id, 139 resourceType: "issue", 140 externalId: issue.number.toString(), 141 url: issue.html_url, 142 title: issue.title, 143 metadata: { 144 state: "open", 145 createdFrom: "gitea", 146 author: issue.user?.login ?? issue.user?.username, 147 }, 148 }); 149 150 const project = await db.query.projectTable.findFirst({ 151 where: eq(projectTable.id, projectId), 152 }); 153 154 if (!project) { 155 continue; 156 } 157 158 const clientUrl = process.env.KANEO_CLIENT_URL || "http://localhost:5173"; 159 const taskUrl = `${clientUrl}/dashboard/workspace/${project.workspaceId}/project/${projectId}/task/${createdTask.id}`; 160 const taskIdentifier = `${project.slug.toUpperCase()}-${createdTask.number}`; 161 162 try { 163 const client = createGiteaClient(config); 164 165 const existingLabels = 166 issue.labels 167 ?.map((label) => (typeof label === "string" ? label : label.name)) 168 .filter(Boolean) || []; 169 170 const labelsToAdd: string[] = []; 171 172 if (priority && !existingLabels.includes(`priority:${priority}`)) { 173 labelsToAdd.push(`priority:${priority}`); 174 } 175 176 if (status && !existingLabels.includes(`status:${status}`)) { 177 labelsToAdd.push(`status:${status}`); 178 } 179 180 if (labelsToAdd.length > 0) { 181 await addLabelsToIssueGitea(config, issue.number, labelsToAdd); 182 } 183 184 if (config.commentTaskLinkOnGiteaIssue !== false) { 185 await client.createIssueComment( 186 config.repositoryOwner, 187 config.repositoryName, 188 issue.number, 189 `[${taskIdentifier}](${taskUrl})`, 190 ); 191 } 192 } catch (error) { 193 console.error("Failed to process Gitea issue:", error); 194 } 195 } 196}