Skip to content

Commit f8c8916

Browse files
authored
Merge pull request #89 from unisoncomputing/cp/more-notification-topics
Implement Ticket Created and New Comment notifications
2 parents 618eed8 + e01711e commit f8c8916

27 files changed

+1320
-233
lines changed

share-api.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ library
6464
Share.Postgres.Causal.Conversions
6565
Share.Postgres.Causal.Queries
6666
Share.Postgres.Causal.Types
67+
Share.Postgres.Comments.Ops
6768
Share.Postgres.Comments.Queries
6869
Share.Postgres.Contributions.Ops
6970
Share.Postgres.Contributions.Queries
@@ -93,6 +94,7 @@ library
9394
Share.Postgres.Sync.Conversions
9495
Share.Postgres.Sync.Queries
9596
Share.Postgres.Sync.Types
97+
Share.Postgres.Tickets.Ops
9698
Share.Postgres.Tickets.Queries
9799
Share.Postgres.Users.Queries
98100
Share.Prelude

sql/2025-06-02_comment-content.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- Create a View with the latest content for each comment.
2+
CREATE VIEW comment_content(comment_id, created_at, updated_at, content, author_id, contribution_id, ticket_id) AS
3+
SELECT DISTINCT ON (c.id)
4+
c.id AS comment_id,
5+
c.created_at,
6+
c.updated_at,
7+
cr.content,
8+
c.author_id,
9+
c.contribution_id,
10+
c.ticket_id
11+
FROM comments c
12+
JOIN comment_revisions cr ON c.id = cr.comment_id
13+
ORDER BY c.id, cr.revision_number DESC
14+
;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
ALTER TYPE notification_topic ADD VALUE 'project:contribution:updated';
2+
ALTER TYPE notification_topic ADD VALUE 'project:ticket:created';
3+
ALTER TYPE notification_topic ADD VALUE 'project:ticket:updated';
4+
ALTER TYPE notification_topic ADD VALUE 'project:contribution:comment';
5+
ALTER TYPE notification_topic ADD VALUE 'project:ticket:comment';
6+
7+
INSERT INTO notification_topic_group_topics (topic_group, topic)
8+
VALUES
9+
('watch_project', 'project:contribution:updated'),
10+
('watch_project', 'project:ticket:created'),
11+
('watch_project', 'project:ticket:updated'),
12+
('watch_project', 'project:contribution:comment'),
13+
('watch_project', 'project:ticket:comment')
14+
;
15+
16+
17+
-- Returns the permission a user must have for an event's resource in order to be notified.
18+
CREATE OR REPLACE FUNCTION topic_permission(topic notification_topic)
19+
RETURNS permission
20+
PARALLEL SAFE
21+
IMMUTABLE
22+
AS $$
23+
BEGIN
24+
CASE topic
25+
WHEN 'project:branch:updated' THEN
26+
RETURN 'project:view'::permission;
27+
WHEN 'project:contribution:created' THEN
28+
RETURN 'project:view'::permission;
29+
WHEN 'project:contribution:updated' THEN
30+
RETURN 'project:view'::permission;
31+
WHEN 'project:contribution:comment' THEN
32+
RETURN 'project:view'::permission;
33+
WHEN 'project:ticket:created' THEN
34+
RETURN 'project:view'::permission;
35+
WHEN 'project:ticket:updated' THEN
36+
RETURN 'project:view'::permission;
37+
WHEN 'project:ticket:comment' THEN
38+
RETURN 'project:view'::permission;
39+
ELSE
40+
RAISE EXCEPTION 'topic_permissions: topic % must declare its necessary permissions', topic;
41+
END CASE;
42+
END;
43+
$$ LANGUAGE plpgsql;
44+

src/Share/BackgroundJobs/Webhooks/Worker.hs

Lines changed: 85 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -240,54 +240,97 @@ buildWebhookRequest webhookId uri event defaultPayload = do
240240
HTTPClient.requestBody = HTTPClient.RequestBodyLBS $ Aeson.encode defaultPayload
241241
}
242242

243+
contributionChatMessage :: NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEvent -> Author -> Maybe URI -> ProjectContributionPayload -> (ProjectBranchShortHand -> Text) -> Background (ChatApps.MessageContent provider)
244+
contributionChatMessage event author mainLink payload mkPretext = do
245+
let pbShorthand = (projectBranchShortHandFromParts payload.projectInfo.projectShortHand payload.contributionInfo.contributionSourceBranch.branchShortHand)
246+
title = payload.contributionInfo.contributionTitle
247+
description = fromMaybe "" $ payload.contributionInfo.contributionDescription
248+
pure $
249+
ChatApps.MessageContent
250+
{ preText = mkPretext pbShorthand,
251+
content = description,
252+
title = title,
253+
mainLink,
254+
author,
255+
thumbnailUrl = Nothing,
256+
timestamp = event.eventOccurredAt
257+
}
258+
ticketChatMessage :: NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEvent -> Author -> Maybe URI -> ProjectTicketPayload -> (ProjectShortHand -> Text) -> Background (ChatApps.MessageContent provider)
259+
ticketChatMessage event author mainLink payload mkPretext = do
260+
let title = payload.ticketInfo.ticketTitle
261+
description = fromMaybe "" $ payload.ticketInfo.ticketDescription
262+
pure $
263+
ChatApps.MessageContent
264+
{ preText = mkPretext payload.projectInfo.projectShortHand,
265+
content = description,
266+
title = title,
267+
mainLink,
268+
author,
269+
thumbnailUrl = Nothing,
270+
timestamp = event.eventOccurredAt
271+
}
272+
branchMessage :: NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEvent -> Author -> (Maybe URI) -> ProjectBranchUpdatedPayload -> Background (ChatApps.MessageContent provider)
273+
branchMessage event author mainLink payload = do
274+
let pbShorthand = (projectBranchShortHandFromParts payload.projectInfo.projectShortHand payload.branchInfo.branchShortHand)
275+
title = "Branch " <> IDs.toText pbShorthand <> " was just updated."
276+
preText = title
277+
pure $
278+
ChatApps.MessageContent
279+
{ preText = preText,
280+
content = "Branch updated",
281+
title = title,
282+
mainLink,
283+
author,
284+
thumbnailUrl = Nothing,
285+
timestamp = event.eventOccurredAt
286+
}
287+
mkAuthor :: NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEvent -> Background Author
288+
mkAuthor event = do
289+
actorLink <- Links.userProfilePage (event.eventActor ^. DisplayInfo.handle_)
290+
pure $
291+
Author
292+
{ authorName = Just actorAuthor,
293+
authorLink = Just actorLink,
294+
authorAvatarUrl = actorAvatarUrl
295+
}
296+
where
297+
actorName = event.eventActor ^. DisplayInfo.name_
298+
actorHandle = "(" <> IDs.toText (PrefixedID @"@" $ event.eventActor ^. DisplayInfo.handle_) <> ")"
299+
actorAuthor = maybe "" (<> " ") actorName <> actorHandle
300+
actorAvatarUrl = event.eventActor ^. DisplayInfo.avatarUrl_
243301
buildChatAppPayload :: forall provider. (ToJSON (ChatApps.MessageContent provider)) => Proxy provider -> URI -> Background (Either WebhookSendFailure HTTPClient.Request)
244302
buildChatAppPayload _ uri = do
245-
let actorName = event.eventActor ^. DisplayInfo.name_
246-
actorHandle = "(" <> IDs.toText (PrefixedID @"@" $ event.eventActor ^. DisplayInfo.handle_) <> ")"
247-
actorAuthor = maybe "" (<> " ") actorName <> actorHandle
248-
actorAvatarUrl = event.eventActor ^. DisplayInfo.avatarUrl_
249-
actorLink <- Links.userProfilePage (event.eventActor ^. DisplayInfo.handle_)
250303
let mainLink = Just event.eventData.hydratedEventLink
304+
author <- mkAuthor event
251305
messageContent :: ChatApps.MessageContent provider <- case event.eventData.hydratedEventPayload of
252306
HydratedProjectBranchUpdatedPayload payload -> do
253-
let pbShorthand = (projectBranchShortHandFromParts payload.projectInfo.projectShortHand payload.branchInfo.branchShortHand)
254-
title = "Branch " <> IDs.toText pbShorthand <> " was just updated."
255-
preText = title
256-
pure $
257-
ChatApps.MessageContent
258-
{ preText = preText,
259-
content = "Branch updated",
260-
title = title,
261-
mainLink,
262-
author =
263-
Author
264-
{ authorName = Just actorAuthor,
265-
authorLink = Just actorLink,
266-
authorAvatarUrl = actorAvatarUrl
267-
},
268-
thumbnailUrl = Nothing,
269-
timestamp = event.eventOccurredAt
270-
}
307+
branchMessage event author mainLink payload
271308
HydratedProjectContributionCreatedPayload payload -> do
272-
let pbShorthand = (projectBranchShortHandFromParts payload.projectInfo.projectShortHand payload.contributionInfo.contributionSourceBranch.branchShortHand)
273-
title = payload.contributionInfo.contributionTitle
274-
description = fromMaybe "" $ payload.contributionInfo.contributionDescription
275-
preText = "New Contribution in " <> IDs.toText pbShorthand
276-
pure $
277-
ChatApps.MessageContent
278-
{ preText = preText,
279-
content = description,
280-
title = title,
281-
mainLink,
282-
author =
283-
Author
284-
{ authorName = Just actorAuthor,
285-
authorLink = Just actorLink,
286-
authorAvatarUrl = actorAvatarUrl
287-
},
288-
thumbnailUrl = Nothing,
289-
timestamp = event.eventOccurredAt
290-
}
309+
let mkPretext pbShorthand = "New Contribution in " <> IDs.toText pbShorthand
310+
contributionChatMessage event author mainLink payload mkPretext
311+
HydratedProjectContributionUpdatedPayload payload -> do
312+
let mkPretext pbShorthand = "Updated Contribution in " <> IDs.toText pbShorthand
313+
contributionChatMessage event author mainLink payload mkPretext
314+
HydratedProjectContributionCommentPayload payload comment -> do
315+
let mkPretext pbShorthand = "New Comment on Contribution in " <> IDs.toText pbShorthand
316+
contributionChatMessage event author mainLink payload mkPretext
317+
<&> \msgContent ->
318+
msgContent
319+
{ ChatApps.content = comment.commentContent
320+
}
321+
HydratedProjectTicketCreatedPayload payload -> do
322+
let mkPretext projectShorthand = "New Ticket in " <> IDs.toText projectShorthand
323+
ticketChatMessage event author mainLink payload mkPretext
324+
HydratedProjectTicketUpdatedPayload payload -> do
325+
let mkPretext projectShorthand = "Updated Ticket in " <> IDs.toText projectShorthand
326+
ticketChatMessage event author mainLink payload mkPretext
327+
HydratedProjectTicketCommentPayload payload comment -> do
328+
let mkPretext projectShorthand = "New Comment on Ticket in " <> IDs.toText projectShorthand
329+
ticketChatMessage event author mainLink payload mkPretext
330+
<&> \msgContent ->
331+
msgContent
332+
{ ChatApps.content = comment.commentContent
333+
}
291334
pure $
292335
HTTPClient.requestFromURI uri
293336
& mapLeft (\e -> InvalidRequest event.eventId webhookId e)

src/Share/Notifications/Queries.hs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import Share.Notifications.Types
3333
import Share.Postgres
3434
import Share.Postgres qualified as PG
3535
import Share.Postgres.Contributions.Queries qualified as ContributionQ
36+
import Share.Postgres.Tickets.Queries qualified as TicketQ
3637
import Share.Postgres.Users.Queries qualified as UsersQ
3738
import Share.Prelude
39+
import Share.Ticket
3840
import Share.Utils.API (Cursor (..), CursorDirection (..))
3941
import Share.Web.Share.DisplayInfo.Queries qualified as DisplayInfoQ
4042
import Share.Web.Share.DisplayInfo.Types (UnifiedDisplayInfo)
@@ -315,17 +317,64 @@ getNotificationSubscription subscriberUserId subscriptionId = do
315317
hydrateEventPayload :: forall m. (QueryA m) => NotificationEventData -> m HydratedEventPayload
316318
hydrateEventPayload = \case
317319
ProjectBranchUpdatedData
318-
(ProjectBranchData {projectId, branchId}) -> do
320+
(ProjectData {projectId})
321+
(BranchData {branchId}) -> do
319322
HydratedProjectBranchUpdatedPayload <$> hydrateProjectBranchPayload projectId branchId
320323
ProjectContributionCreatedData
321-
(ProjectContributionData {projectId, contributionId, fromBranchId, toBranchId, contributorUserId}) -> do
322-
HydratedProjectContributionCreatedPayload <$> hydrateContributionCreatedPayload contributionId projectId fromBranchId toBranchId contributorUserId
324+
(ProjectData {projectId})
325+
(ContributionData {contributionId, fromBranchId, toBranchId, contributorUserId}) -> do
326+
HydratedProjectContributionCreatedPayload <$> hydrateContributionPayload contributionId projectId fromBranchId toBranchId contributorUserId
327+
ProjectContributionUpdatedData
328+
(ProjectData {projectId})
329+
(ContributionData {contributionId, fromBranchId, toBranchId, contributorUserId}) -> do
330+
HydratedProjectContributionUpdatedPayload <$> hydrateContributionPayload contributionId projectId fromBranchId toBranchId contributorUserId
331+
ProjectContributionCommentData
332+
(ProjectData {projectId})
333+
(ContributionData {contributionId, fromBranchId, toBranchId, contributorUserId})
334+
(CommentData {commentId, commentAuthorUserId}) -> do
335+
HydratedProjectContributionCommentPayload
336+
<$> hydrateContributionPayload contributionId projectId fromBranchId toBranchId contributorUserId
337+
<*> hydrateCommentPayload commentId commentAuthorUserId
338+
ProjectTicketCreatedData
339+
(ProjectData {projectId})
340+
(TicketData {ticketId, ticketAuthorUserId}) -> do
341+
HydratedProjectTicketCreatedPayload <$> hydrateTicketPayload projectId ticketId ticketAuthorUserId
342+
ProjectTicketUpdatedData
343+
(ProjectData {projectId})
344+
(TicketData {ticketId, ticketAuthorUserId}) -> do
345+
HydratedProjectTicketUpdatedPayload <$> hydrateTicketPayload projectId ticketId ticketAuthorUserId
346+
ProjectTicketCommentData
347+
(ProjectData {projectId})
348+
(TicketData {ticketId, ticketAuthorUserId})
349+
(CommentData {commentId, commentAuthorUserId}) -> do
350+
HydratedProjectTicketCommentPayload
351+
<$> hydrateTicketPayload projectId ticketId ticketAuthorUserId
352+
<*> hydrateCommentPayload commentId commentAuthorUserId
323353
where
324-
hydrateContributionCreatedPayload :: ContributionId -> ProjectId -> BranchId -> BranchId -> UserId -> m ProjectContributionCreatedPayload
325-
hydrateContributionCreatedPayload contributionId projectId fromBranchId toBranchId authorUserId = do
354+
hydrateTicketPayload :: ProjectId -> TicketId -> UserId -> m ProjectTicketPayload
355+
hydrateTicketPayload projectId ticketId authorUserId = do
356+
projectInfo <- hydrateProjectPayload projectId
357+
ticketInfo <- hydrateTicketInfo ticketId authorUserId
358+
pure $ ProjectTicketPayload {projectInfo, ticketInfo}
359+
hydrateTicketInfo :: TicketId -> UserId -> m TicketPayload
360+
hydrateTicketInfo ticketId authorUserId = do
361+
author <- UsersQ.userDisplayInfoOf id authorUserId
362+
ticket <- TicketQ.ticketById ticketId
363+
pure $
364+
TicketPayload
365+
{ ticketId,
366+
ticketNumber = ticket.number,
367+
ticketTitle = ticket.title,
368+
ticketDescription = ticket.description,
369+
ticketStatus = ticket.status,
370+
ticketAuthor = author,
371+
ticketCreatedAt = ticket.createdAt
372+
}
373+
hydrateContributionPayload :: ContributionId -> ProjectId -> BranchId -> BranchId -> UserId -> m ProjectContributionPayload
374+
hydrateContributionPayload contributionId projectId fromBranchId toBranchId authorUserId = do
326375
projectInfo <- hydrateProjectPayload projectId
327376
contributionInfo <- hydrateContributionInfo contributionId fromBranchId toBranchId authorUserId
328-
pure $ ProjectContributionCreatedPayload {projectInfo, contributionInfo = contributionInfo projectInfo}
377+
pure $ ProjectContributionPayload {projectInfo, contributionInfo = contributionInfo projectInfo}
329378
hydrateContributionInfo :: ContributionId -> BranchId -> BranchId -> UserId -> m (ProjectPayload -> ContributionPayload)
330379
hydrateContributionInfo contributionId fromBranchId toBranchId authorUserId = do
331380
author <- UsersQ.userDisplayInfoOf id authorUserId
@@ -341,7 +390,8 @@ hydrateEventPayload = \case
341390
contributionStatus = contribution.status,
342391
contributionAuthor = author,
343392
contributionSourceBranch = sourceBranch projectInfo,
344-
contributionTargetBranch = targetBranch projectInfo
393+
contributionTargetBranch = targetBranch projectInfo,
394+
contributionCreatedAt = contribution.createdAt
345395
}
346396
hydrateProjectBranchPayload projectId branchId = do
347397
projectInfo <- hydrateProjectPayload projectId
@@ -395,3 +445,22 @@ hydrateEventPayload = \case
395445
projectOwnerHandle,
396446
projectOwnerUserId
397447
}
448+
hydrateCommentPayload :: CommentId -> UserId -> m CommentPayload
449+
hydrateCommentPayload commentId commentAuthorUserId = do
450+
let construct (commentId, commentContent, commentCreatedAt) commentAuthor =
451+
CommentPayload
452+
{ commentId,
453+
commentContent,
454+
commentCreatedAt,
455+
commentAuthor
456+
}
457+
construct
458+
<$> ( queryExpect1Row
459+
[sql|
460+
SELECT cc.comment_id, cc.content, cc.created_at, cc.updated_at
461+
FROM comment_content cc
462+
JOIN users author ON cc.author_id = author.id
463+
WHERE cc.comment_id = #{commentId}
464+
|]
465+
)
466+
<*> (UsersQ.userDisplayInfoOf id commentAuthorUserId)

0 commit comments

Comments
 (0)