kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import {
2 findExternalLinksByTask,
3 updateExternalLink,
4} from "../../github/services/link-manager";
5import { formatIssueBody } from "../../github/utils/format";
6import type { PluginContext, TaskDescriptionChangedEvent } from "../../types";
7import type { GiteaConfig } from "../config";
8import { createGiteaClient } from "../utils/gitea-api";
9
10type LinkSyncState = {
11 timestamp: string;
12 source: string;
13 value: string;
14};
15
16type LinkMetadata = {
17 lastSync?: {
18 description?: LinkSyncState;
19 };
20 [key: string]: unknown;
21};
22
23export async function handleTaskDescriptionChanged(
24 event: TaskDescriptionChangedEvent,
25 context: PluginContext,
26): Promise<void> {
27 const config = context.config as GiteaConfig;
28 if (!config.baseUrl || !config.accessToken) {
29 return;
30 }
31
32 const { repositoryOwner, repositoryName } = config;
33
34 try {
35 const links = await findExternalLinksByTask(event.taskId);
36 const issueLink = links.find(
37 (link) =>
38 link.integrationId === context.integrationId &&
39 link.resourceType === "issue",
40 );
41
42 if (!issueLink) {
43 return;
44 }
45
46 let metadata: LinkMetadata = {};
47 if (issueLink.metadata) {
48 try {
49 metadata = JSON.parse(issueLink.metadata) as LinkMetadata;
50 } catch (error) {
51 console.warn(
52 "Failed to parse Gitea issue link metadata for description sync",
53 {
54 issueLinkId: issueLink.id,
55 taskId: issueLink.taskId,
56 metadata: issueLink.metadata,
57 error,
58 },
59 );
60 }
61 }
62
63 const lastDescSync = metadata.lastSync?.description;
64 const newDescNormalized = event.newDescription || "";
65
66 if (lastDescSync) {
67 if (
68 lastDescSync.value === newDescNormalized &&
69 lastDescSync.source === "gitea"
70 ) {
71 console.log("Skipping description sync - already synced from Gitea");
72 return;
73 }
74
75 const timeSinceLastSync =
76 Date.now() - new Date(lastDescSync.timestamp).getTime();
77 if (
78 timeSinceLastSync < 2000 &&
79 lastDescSync.source === "gitea" &&
80 newDescNormalized === lastDescSync.value
81 ) {
82 console.log(
83 `Skipping description sync - recent sync detected (${timeSinceLastSync}ms ago)`,
84 );
85 return;
86 }
87 }
88
89 const client = createGiteaClient(config);
90 const issueNumber = Number.parseInt(issueLink.externalId, 10);
91 if (Number.isNaN(issueNumber)) {
92 console.warn("Skipping Gitea description sync for invalid issue number", {
93 issueLinkId: issueLink.id,
94 externalId: issueLink.externalId,
95 taskId: issueLink.taskId,
96 });
97 return;
98 }
99
100 const formattedBody = formatIssueBody(event.newDescription, event.taskId);
101
102 await client.updateIssue(repositoryOwner, repositoryName, issueNumber, {
103 body: formattedBody,
104 });
105
106 await updateExternalLink(issueLink.id, {
107 metadata: {
108 ...metadata,
109 lastSync: {
110 ...(metadata.lastSync ?? {}),
111 description: {
112 timestamp: new Date().toISOString(),
113 source: "kaneo",
114 value: newDescNormalized,
115 },
116 },
117 },
118 });
119
120 console.log(`Synced task description to Gitea issue #${issueNumber}`);
121 } catch (error) {
122 console.error("Failed to update Gitea issue description:", error);
123 }
124}