kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import * as v from "valibot";
2
3export const discordEventKeys = [
4 "taskCreated",
5 "taskStatusChanged",
6 "taskPriorityChanged",
7 "taskTitleChanged",
8 "taskDescriptionChanged",
9 "taskCommentCreated",
10] as const;
11
12export type DiscordEventKey = (typeof discordEventKeys)[number];
13
14const discordWebhookSchema = v.pipe(
15 v.string(),
16 v.url(),
17 v.regex(
18 /^https:\/\/(?:discord\.com|discordapp\.com)\/api\/webhooks\/[^/]+\/[^/\s]+$/i,
19 "Enter a valid Discord webhook URL",
20 ),
21);
22
23export const discordConfigSchema = v.object({
24 webhookUrl: discordWebhookSchema,
25 channelName: v.optional(v.string()),
26 events: v.optional(
27 v.object({
28 taskCreated: v.optional(v.boolean()),
29 taskStatusChanged: v.optional(v.boolean()),
30 taskPriorityChanged: v.optional(v.boolean()),
31 taskTitleChanged: v.optional(v.boolean()),
32 taskDescriptionChanged: v.optional(v.boolean()),
33 taskCommentCreated: v.optional(v.boolean()),
34 }),
35 ),
36});
37
38export type DiscordConfig = v.InferOutput<typeof discordConfigSchema>;
39
40export const defaultDiscordEvents: Record<DiscordEventKey, boolean> = {
41 taskCreated: true,
42 taskStatusChanged: true,
43 taskPriorityChanged: false,
44 taskTitleChanged: false,
45 taskDescriptionChanged: false,
46 taskCommentCreated: true,
47};
48
49export function getDefaultDiscordConfig(webhookUrl: string): DiscordConfig {
50 return {
51 webhookUrl,
52 events: { ...defaultDiscordEvents },
53 };
54}
55
56export function normalizeDiscordConfig(config: DiscordConfig): DiscordConfig {
57 return {
58 ...config,
59 channelName: config.channelName?.trim() || undefined,
60 events: {
61 ...defaultDiscordEvents,
62 ...(config.events ?? {}),
63 },
64 };
65}
66
67export async function validateDiscordConfig(
68 config: unknown,
69): Promise<{ valid: boolean; errors?: string[] }> {
70 try {
71 const parsed = v.parse(discordConfigSchema, config);
72 normalizeDiscordConfig(parsed);
73 return { valid: true };
74 } catch (error) {
75 if (error instanceof v.ValiError) {
76 return {
77 valid: false,
78 errors: error.issues.map((issue) => issue.message),
79 };
80 }
81
82 return {
83 valid: false,
84 errors: [error instanceof Error ? error.message : "Invalid config"],
85 };
86 }
87}