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