Skip to content

Commit ba54b5a

Browse files
Isak-KalliniBluenix2github-advanced-security[bot]
authored
Add webhook sending on news creation (#1060)
* Add in-progress webhook sending on news creation * Fix remaining bugs and limit scope * fix lint * Potential fix for code scanning alert no. 13: Incomplete string escaping or encoding Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Bluenix <bluenixdev@gmail.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 9c015fe commit ba54b5a

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

src/lib/news/server/actions.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,134 @@ const sendNewArticleNotification = async (
6969
});
7070
};
7171

72+
const limitDescription = (text: string): string => {
73+
let description = text.replace(/\n/g, " ").slice(0, 256 - 3);
74+
description += description.length == 256 - 3 ? "..." : "";
75+
76+
const rows = description.split("\n");
77+
if (rows.length > 4) {
78+
description = rows.slice(0, 4).join("\n") + "...";
79+
}
80+
return description;
81+
};
82+
83+
const sendNewArticleWebhook = async (
84+
article: ExtendedPrismaModel<"Article"> & {
85+
tags: Array<Pick<ExtendedPrismaModel<"Tag">, "id">>;
86+
author: ExtendedPrismaModel<"Author">;
87+
},
88+
notificationText: string | null | undefined,
89+
) => {
90+
const member = await authorizedPrismaClient.member.findUnique({
91+
where: {
92+
id: article.author.memberId,
93+
},
94+
});
95+
const tags = await authorizedPrismaClient.tag.findMany({
96+
where: {
97+
id: {
98+
in: article.tags.map((tag) => tag.id),
99+
},
100+
},
101+
});
102+
103+
let title: string | undefined = undefined;
104+
if (article.author.mandateId !== null) {
105+
const mandate = await authorizedPrismaClient.mandate.findUnique({
106+
where: {
107+
id: article.author.mandateId ?? undefined,
108+
},
109+
});
110+
if (mandate) {
111+
const position = await authorizedPrismaClient.position.findUnique({
112+
where: {
113+
id: mandate?.positionId,
114+
},
115+
});
116+
title = position?.nameSv;
117+
}
118+
}
119+
120+
// Create an object mapping key -> value of the following
121+
// keys. Removes potential duplicates (if possible)
122+
const settings = Object.fromEntries(
123+
(
124+
await authorizedPrismaClient.adminSetting.findMany({
125+
where: {
126+
key: {
127+
in: ["discord_webhook_se", "webhook_tags_se"],
128+
},
129+
},
130+
select: {
131+
key: true,
132+
value: true,
133+
},
134+
})
135+
).map((row) => [row.key, row.value]),
136+
);
137+
138+
if (!settings["discord_webhook_se"]) return; // No webhook to call
139+
140+
// If webhook_tags is not set, we allow through all news
141+
if (settings["webhook_tags_se"]) {
142+
const webhookTags = new Set(settings["webhook_tags_se"].split(","));
143+
144+
// Otherwise, webhook_tags acts as a filter. Only allow articles that
145+
// have any of the tags. We assume that the article has very few tags
146+
if (!article.tags.some((tag) => webhookTags.has(tag.id))) return;
147+
}
148+
149+
// In most cases, an event will only have one tag. For the others,
150+
// we just pick an arbitrary tag
151+
let color = undefined;
152+
let footer = undefined;
153+
if (article.tags.length > 0) {
154+
const tag = tags.find((tag) => tag.id === article.tags[0]?.id);
155+
if (tag !== undefined) {
156+
footer = tag.nameSv;
157+
}
158+
159+
if (typeof tag?.color === "string") {
160+
// Convert HEX-string in the form #FFFFFF to an integer for Discord API
161+
color = parseInt(tag.color.slice(1), 16);
162+
}
163+
}
164+
165+
// Limit news description to first few characters as a preview, although
166+
// you could technically fit the entire article within the 4096 character limit
167+
const description = limitDescription(article.bodySv);
168+
169+
const body = {
170+
content: notificationText,
171+
embeds: [
172+
{
173+
title: article.headerSv,
174+
author: {
175+
name: `${member?.firstName} ${member?.lastName} ${title ? "| " + title : ""}`,
176+
icon_url: member?.picturePath,
177+
},
178+
// Default to development URL
179+
url: `${process.env["ORIGIN"] ?? "http://localhost:5173"}/news/${article.slug}`,
180+
description: description,
181+
color: color,
182+
footer: {
183+
text: footer,
184+
},
185+
timestamp: article.createdAt,
186+
},
187+
],
188+
};
189+
190+
const res = await fetch(settings["discord_webhook_se"], {
191+
method: "POST",
192+
headers: { "Content-Type": "application/json" },
193+
body: JSON.stringify(body),
194+
});
195+
if (res.status != 204) {
196+
console.log(`notifications: failed Discord webhook (${res.status})`);
197+
}
198+
};
199+
72200
export const createArticle: Action = async (event) => {
73201
const { request, locals } = event;
74202
const { prisma, user } = locals;
@@ -169,6 +297,13 @@ export const createArticle: Action = async (event) => {
169297
},
170298
notificationText,
171299
);
300+
await sendNewArticleWebhook(
301+
{
302+
...result,
303+
tags,
304+
},
305+
notificationText,
306+
);
172307
}
173308

174309
throw redirect(

0 commit comments

Comments
 (0)