Skip to content

Commit 30dc253

Browse files
authored
Merge pull request #81 from unisoncomputing/cp/hydrate-notification-links
Include links in notification hub entries
2 parents 9fd81fa + ac91fcb commit 30dc253

13 files changed

Lines changed: 112 additions & 80 deletions

File tree

src/Share/BackgroundJobs/Webhooks/Worker.hs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import Share.IDs qualified as IDs
3333
import Share.JWT (JWTParam (..))
3434
import Share.JWT qualified as JWT
3535
import Share.Metrics qualified as Metrics
36+
import Share.Notifications.Ops qualified as NotOps
3637
import Share.Notifications.Queries qualified as NQ
3738
import Share.Notifications.Types
3839
import Share.Notifications.Webhooks.Secrets (WebhookConfig (..), WebhookSecretError)
@@ -138,7 +139,7 @@ data WebhookEventPayload jwt = WebhookEventPayload
138139
-- | The topic of the notification event.
139140
topic :: NotificationTopic,
140141
-- | The data associated with the notification event.
141-
data_ :: HydratedEventPayload,
142+
data_ :: HydratedEvent,
142143
-- | A signed token containing all of the same data.
143144
jwt :: jwt
144145
}
@@ -175,7 +176,7 @@ instance FromJSON (WebhookEventPayload ()) where
175176
<*> pure ()
176177

177178
tryWebhook ::
178-
NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEventPayload ->
179+
NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEvent ->
179180
NotificationWebhookId ->
180181
Background (Maybe WebhookSendFailure)
181182
tryWebhook event webhookId = UnliftIO.handleAny (\someException -> pure $ Just $ InvalidRequest event.eventId webhookId someException) do
@@ -206,7 +207,7 @@ tryWebhook event webhookId = UnliftIO.handleAny (\someException -> pure $ Just $
206207
| status >= 400 -> throwError $ ReceiverError event.eventId webhookId httpStatus $ HTTPClient.responseBody resp
207208
| otherwise -> pure ()
208209

209-
buildWebhookRequest :: NotificationWebhookId -> URI -> NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEventPayload -> WebhookEventPayload JWTParam -> Background (Either WebhookSendFailure HTTPClient.Request)
210+
buildWebhookRequest :: NotificationWebhookId -> URI -> NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEvent -> WebhookEventPayload JWTParam -> Background (Either WebhookSendFailure HTTPClient.Request)
210211
buildWebhookRequest webhookId uri event defaultPayload = do
211212
if
212213
| isSlackWebhook uri -> buildChatAppPayload (Proxy @ChatApps.Slack) uri
@@ -246,18 +247,18 @@ buildWebhookRequest webhookId uri event defaultPayload = do
246247
actorAuthor = maybe "" (<> " ") actorName <> actorHandle
247248
actorAvatarUrl = event.eventActor ^. DisplayInfo.avatarUrl_
248249
actorLink <- Links.userProfilePage (event.eventActor ^. DisplayInfo.handle_)
249-
messageContent :: ChatApps.MessageContent provider <- case event.eventData of
250+
let mainLink = Just event.eventData.hydratedEventLink
251+
messageContent :: ChatApps.MessageContent provider <- case event.eventData.hydratedEventPayload of
250252
HydratedProjectBranchUpdatedPayload payload -> do
251253
let pbShorthand = (projectBranchShortHandFromParts payload.projectInfo.projectShortHand payload.branchInfo.branchShortHand)
252254
title = "Branch " <> IDs.toText pbShorthand <> " was just updated."
253255
preText = title
254-
link <- Links.notificationLink event.eventData
255256
pure $
256257
ChatApps.MessageContent
257258
{ preText = preText,
258259
content = "Branch updated",
259260
title = title,
260-
mainLink = Just link,
261+
mainLink,
261262
author =
262263
Author
263264
{ authorName = Just actorAuthor,
@@ -272,13 +273,12 @@ buildWebhookRequest webhookId uri event defaultPayload = do
272273
title = payload.contributionInfo.contributionTitle
273274
description = fromMaybe "" $ payload.contributionInfo.contributionDescription
274275
preText = "New Contribution in " <> IDs.toText pbShorthand
275-
link <- Links.notificationLink event.eventData
276276
pure $
277277
ChatApps.MessageContent
278278
{ preText = preText,
279279
content = description,
280280
title = title,
281-
mainLink = Just link,
281+
mainLink,
282282
author =
283283
Author
284284
{ authorName = Just actorAuthor,
@@ -302,13 +302,14 @@ buildWebhookRequest webhookId uri event defaultPayload = do
302302

303303
attemptWebhookSend ::
304304
AuthZ.AuthZReceipt ->
305-
(NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEventPayload -> NotificationWebhookId -> IO (Maybe WebhookSendFailure)) ->
305+
(NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEvent -> NotificationWebhookId -> IO (Maybe WebhookSendFailure)) ->
306306
NotificationEventId ->
307307
NotificationWebhookId ->
308308
PG.Transaction e (Maybe WebhookSendFailure)
309309
attemptWebhookSend _authZReceipt tryWebhookIO eventId webhookId = do
310310
event <- NQ.expectEvent eventId
311-
hydratedEvent <- forOf eventData_ event NQ.hydrateEventData
311+
hydratedEventPayload <- forOf eventData_ event NQ.hydrateEventPayload
312+
hydratedEvent <- for hydratedEventPayload NotOps.hydrateEvent
312313
populatedEvent <- hydratedEvent & DisplayInfoQ.unifiedDisplayInfoForUserOf eventUserInfo_
313314
PG.transactionUnsafeIO (tryWebhookIO populatedEvent webhookId) >>= \case
314315
Just err -> do

src/Share/Env.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@ data Env ctx = Env
5959
maxParallelismPerDownloadRequest :: Int,
6060
maxParallelismPerUploadRequest :: Int
6161
}
62+
deriving (Functor)

src/Share/Metrics.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ serveMetricsMiddleware env = do
7878
refreshGauges getMetrics
7979
Prom.prometheus prometheusSettings app req handleResponse
8080
where
81-
runPG = PG.runSessionWithPool (Env.pgConnectionPool env) . PG.transaction PG.ReadCommitted PG.Read
81+
runPG = PG.runSessionWithEnv env . PG.transaction PG.ReadCommitted PG.Read
8282
prometheusSettings =
8383
Prom.def
8484
{ Prom.prometheusEndPoint = ["metrics"],

src/Share/Notifications/API.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import Data.Text qualified as Text
3939
import Data.Time (UTCTime)
4040
import Servant
4141
import Share.IDs
42-
import Share.Notifications.Types (DeliveryMethodId, HydratedEventPayload, NotificationDeliveryMethod, NotificationHubEntry, NotificationStatus, NotificationSubscription, NotificationTopic, SubscriptionFilter)
42+
import Share.Notifications.Types (DeliveryMethodId, HydratedEvent, NotificationDeliveryMethod, NotificationHubEntry, NotificationStatus, NotificationSubscription, NotificationTopic, SubscriptionFilter)
4343
import Share.OAuth.Session (AuthenticatedUserId)
4444
import Share.Prelude
4545
import Share.Utils.URI (URIParam)
@@ -213,7 +213,7 @@ type GetHubEntriesEndpoint =
213213
:> Get '[JSON] GetHubEntriesResponse
214214

215215
data GetHubEntriesResponse = GetHubEntriesResponse
216-
{ notifications :: [NotificationHubEntry UnifiedDisplayInfo HydratedEventPayload]
216+
{ notifications :: [NotificationHubEntry UnifiedDisplayInfo HydratedEvent]
217217
}
218218

219219
instance ToJSON GetHubEntriesResponse where

src/Share/Notifications/Impl.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module Share.Notifications.Impl (server) where
22

3+
import Control.Lens (forOf, traversed)
34
import Data.Time
45
import Servant
56
import Servant.Server.Generic (AsServerT)
@@ -80,7 +81,9 @@ getHubEntriesEndpoint :: UserHandle -> UserId -> Maybe Int -> Maybe UTCTime -> M
8081
getHubEntriesEndpoint userHandle callerUserId limit afterTime mayStatusFilter = do
8182
User {user_id = notificationUserId} <- UserQ.expectUserByHandle userHandle
8283
_authZReceipt <- AuthZ.permissionGuard $ AuthZ.checkNotificationsGet callerUserId notificationUserId
83-
notifications <- PG.runTransaction $ NotificationQ.listNotificationHubEntries notificationUserId limit afterTime (API.getStatusFilter <$> mayStatusFilter)
84+
notifications <- PG.runTransaction do
85+
notifs <- NotificationQ.listNotificationHubEntryPayloads notificationUserId limit afterTime (API.getStatusFilter <$> mayStatusFilter)
86+
forOf (traversed . traversed) notifs NotifOps.hydrateEvent
8487
pure $ API.GetHubEntriesResponse {notifications}
8588

8689
updateHubEntriesEndpoint :: UserHandle -> UserId -> API.UpdateHubEntriesRequest -> WebApp ()

src/Share/Notifications/Ops.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Share.Notifications.Ops
33
createWebhookDeliveryMethod,
44
updateWebhookDeliveryMethod,
55
deleteWebhookDeliveryMethod,
6+
hydrateEvent,
67
)
78
where
89

@@ -16,6 +17,7 @@ import Share.Prelude
1617
import Share.Utils.URI (URIParam (..))
1718
import Share.Web.App (WebApp)
1819
import Share.Web.Errors (respondError)
20+
import Share.Web.UI.Links qualified as Links
1921

2022
listNotificationDeliveryMethods :: UserId -> Maybe NotificationSubscriptionId -> WebApp [NotificationDeliveryMethod]
2123
listNotificationDeliveryMethods userId maySubscriptionId = do
@@ -75,3 +77,8 @@ deleteWebhookDeliveryMethod notificationUser webhookDeliveryMethodId = do
7577
Right _ -> do
7678
PG.runTransaction $ do
7779
NotifQ.deleteWebhookDeliveryMethod notificationUser webhookDeliveryMethodId
80+
81+
hydrateEvent :: HydratedEventPayload -> PG.Transaction e HydratedEvent
82+
hydrateEvent hydratedEventPayload = do
83+
hydratedEventLink <- Links.notificationLink hydratedEventPayload
84+
pure $ HydratedEvent {hydratedEventPayload, hydratedEventLink}

src/Share/Notifications/Queries.hs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Share.Notifications.Queries
22
( recordEvent,
33
expectEvent,
4-
listNotificationHubEntries,
4+
listNotificationHubEntryPayloads,
55
updateNotificationHubEntries,
66
addSubscriptionDeliveryMethods,
77
removeSubscriptionDeliveryMethods,
@@ -17,7 +17,7 @@ module Share.Notifications.Queries
1717
deleteNotificationSubscription,
1818
updateNotificationSubscription,
1919
getNotificationSubscription,
20-
hydrateEventData,
20+
hydrateEventPayload,
2121
)
2222
where
2323

@@ -54,8 +54,8 @@ expectEvent eventId = do
5454
WHERE id = #{eventId}
5555
|]
5656

57-
listNotificationHubEntries :: UserId -> Maybe Int -> Maybe UTCTime -> Maybe (NESet NotificationStatus) -> Transaction e [NotificationHubEntry UnifiedDisplayInfo HydratedEventPayload]
58-
listNotificationHubEntries notificationUserId mayLimit afterTime statusFilter = do
57+
listNotificationHubEntryPayloads :: UserId -> Maybe Int -> Maybe UTCTime -> Maybe (NESet NotificationStatus) -> Transaction e [NotificationHubEntry UnifiedDisplayInfo HydratedEventPayload]
58+
listNotificationHubEntryPayloads notificationUserId mayLimit afterTime statusFilter = do
5959
let limit = clamp (0, 1000) . fromIntegral @Int @Int32 . fromMaybe 50 $ mayLimit
6060
let statusFilterList = Foldable.toList <$> statusFilter
6161
dbNotifications <-
@@ -70,8 +70,8 @@ listNotificationHubEntries notificationUserId mayLimit afterTime statusFilter =
7070
ORDER BY hub.created_at DESC
7171
LIMIT #{limit}
7272
|]
73-
hydrated <- PG.pipelined $ forOf (traversed . traversed) dbNotifications hydrateEventData
74-
hydrated & DisplayInfoQ.unifiedDisplayInfoForUserOf (traversed . hubEntryUserInfo_)
73+
hydratedPayloads <- PG.pipelined $ forOf (traversed . traversed) dbNotifications hydrateEventPayload
74+
hydratedPayloads & DisplayInfoQ.unifiedDisplayInfoForUserOf (traversed . hubEntryUserInfo_)
7575

7676
updateNotificationHubEntries :: (QueryA m) => NESet NotificationHubEntryId -> NotificationStatus -> m ()
7777
updateNotificationHubEntries hubEntryIds status = do
@@ -293,8 +293,8 @@ getNotificationSubscription subscriberUserId subscriptionId = do
293293
-- (preferably pipelined).
294294
--
295295
-- If need be we can write a batch job in plpgsql to hydrate them all at once.
296-
hydrateEventData :: forall m. (QueryA m) => NotificationEventData -> m HydratedEventPayload
297-
hydrateEventData = \case
296+
hydrateEventPayload :: forall m. (QueryA m) => NotificationEventData -> m HydratedEventPayload
297+
hydrateEventPayload = \case
298298
ProjectBranchUpdatedData
299299
(ProjectBranchData {projectId, branchId}) -> do
300300
HydratedProjectBranchUpdatedPayload <$> hydrateProjectBranchPayload projectId branchId

src/Share/Notifications/Types.hs

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Share.Notifications.Types
1818
NotificationEmailDeliveryConfig (..),
1919
NotificationWebhookConfig (..),
2020
HydratedEventPayload (..),
21+
HydratedEvent (..),
2122
BranchPayload (..),
2223
ProjectPayload (..),
2324
ContributionPayload (..),
@@ -527,27 +528,42 @@ instance FromJSON ProjectContributionCreatedPayload where
527528
contributionInfo <- o .: "contribution"
528529
pure ProjectContributionCreatedPayload {projectInfo, contributionInfo}
529530

531+
data HydratedEvent = HydratedEvent
532+
{ hydratedEventPayload :: HydratedEventPayload,
533+
hydratedEventLink :: URI
534+
}
535+
deriving stock (Show, Eq)
536+
537+
instance ToJSON HydratedEvent where
538+
toJSON he@(HydratedEvent {hydratedEventPayload, hydratedEventLink}) =
539+
let kind :: Text = case hydratedEventTopic he of
540+
ProjectBranchUpdated -> "projectBranchUpdated"
541+
ProjectContributionCreated -> "projectContributionCreated"
542+
payload = case hydratedEventPayload of
543+
HydratedProjectBranchUpdatedPayload p -> Aeson.toJSON p
544+
HydratedProjectContributionCreatedPayload p -> Aeson.toJSON p
545+
in Aeson.object
546+
[ "payload" .= payload,
547+
"link" .= URIParam hydratedEventLink,
548+
"kind" .= kind
549+
]
550+
551+
instance FromJSON HydratedEvent where
552+
parseJSON = Aeson.withObject "HydratedEvent" \o -> do
553+
kind <- o .: "kind"
554+
hydratedEventLink <- o .: "link"
555+
hydratedEventPayload <- case kind of
556+
"projectBranchUpdated" -> HydratedProjectBranchUpdatedPayload <$> o .: "payload"
557+
"projectContributionCreated" -> HydratedProjectContributionCreatedPayload <$> o .: "payload"
558+
_ -> fail $ "Unknown event kind: " <> Text.unpack kind
559+
pure HydratedEvent {hydratedEventPayload, hydratedEventLink}
560+
530561
data HydratedEventPayload
531562
= HydratedProjectBranchUpdatedPayload ProjectBranchUpdatedPayload
532563
| HydratedProjectContributionCreatedPayload ProjectContributionCreatedPayload
533564
deriving stock (Show, Eq)
534565

535-
hydratedEventTopic :: HydratedEventPayload -> NotificationTopic
536-
hydratedEventTopic = \case
566+
hydratedEventTopic :: HydratedEvent -> NotificationTopic
567+
hydratedEventTopic (HydratedEvent {hydratedEventPayload}) = case hydratedEventPayload of
537568
HydratedProjectBranchUpdatedPayload _ -> ProjectBranchUpdated
538569
HydratedProjectContributionCreatedPayload _ -> ProjectContributionCreated
539-
540-
instance ToJSON HydratedEventPayload where
541-
toJSON = \case
542-
(HydratedProjectBranchUpdatedPayload payload) ->
543-
Aeson.object ["kind" .= ("projectBranchUpdated" :: Text), "payload" .= payload]
544-
(HydratedProjectContributionCreatedPayload payload) ->
545-
Aeson.object ["kind" .= ("projectContributionCreated" :: Text), "payload" .= payload]
546-
547-
instance FromJSON HydratedEventPayload where
548-
parseJSON = Aeson.withObject "HydratedEventPayload" \o -> do
549-
kind <- o .: "kind"
550-
case kind of
551-
"projectBranchUpdated" -> HydratedProjectBranchUpdatedPayload <$> o .: "payload"
552-
"projectContributionCreated" -> HydratedProjectContributionCreatedPayload <$> o .: "payload"
553-
_ -> fail $ "Unknown event kind: " <> Text.unpack kind

0 commit comments

Comments
 (0)