@@ -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+
72200export 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