Skip to content

Commit 9fd81fa

Browse files
authored
Merge pull request #82 from unisoncomputing/cp/chat-app-msg
Swap to use Slack instead of Zendesk for user support tickets
2 parents 3837d19 + e85afd4 commit 9fd81fa

9 files changed

Lines changed: 230 additions & 184 deletions

File tree

app/Env.hs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import Share.Web.Authentication (cookieSessionTTL)
2525
import Hasql.Pool qualified as Pool
2626
import Hasql.Pool.Config qualified as Pool
2727
import Network.URI (parseURI)
28-
import Servant.API qualified as Servant
2928
import Servant.Client qualified as ServantClient
3029
import System.Environment (lookupEnv)
3130
import System.Exit
@@ -57,9 +56,6 @@ withEnv action = do
5756
githubClientSecret <- fromEnv "SHARE_GITHUB_CLIENT_SECRET" (pure . Right . Text.pack)
5857
hs256Key <- fromEnv "SHARE_HMAC_KEY" (pure . Right . BS.pack)
5958
edDSAKey <- fromEnv "SHARE_EDDSA_KEY" (pure . Right . BS.pack)
60-
zendeskAPIUser <- fromEnv "SHARE_ZENDESK_API_USER" (pure . Right . BS.pack)
61-
zendeskAPIToken <- fromEnv "SHARE_ZENDESK_API_TOKEN" (pure . Right . BS.pack)
62-
let zendeskAuth = Servant.BasicAuthData zendeskAPIUser zendeskAPIToken
6359
commitHash <- fromEnv "SHARE_COMMIT" (pure . Right . Text.pack)
6460
minLogSeverity <-
6561
lookupEnv "SHARE_LOG_LEVEL" >>= \case
@@ -72,6 +68,7 @@ withEnv action = do
7268
shareUiOrigin <- fromEnv "SHARE_SHARE_UI_ORIGIN" (pure . maybeToEither "Invalid SHARE_SHARE_UI_ORIGIN" . parseURI)
7369
websiteOrigin <- fromEnv "SHARE_HOMEPAGE_ORIGIN" (pure . maybeToEither "Invalid SHARE_HOMEPAGE_ORIGIN" . parseURI)
7470
cloudUiOrigin <- fromEnv "SHARE_CLOUD_UI_ORIGIN" (pure . maybeToEither "Invalid SHARE_CLOUD_UI_ORIGIN" . parseURI)
71+
supportTicketWebhookURI <- maybeEnv "SHARE_SUPPORT_TICKET_WEBHOOK_URI" (pure . maybeToEither "Invalid SHARE_SUPPORT_TICKET_WEBHOOK_URI" . parseURI)
7572
maxParallelismPerDownloadRequest <- fromEnv "SHARE_MAX_PARALLELISM_PER_DOWNLOAD_REQUEST" (pure . maybeToEither "Invalid SHARE_MAX_PARALLELISM_PER_DOWNLOAD_REQUEST" . readMaybe)
7673
maxParallelismPerUploadRequest <- fromEnv "SHARE_MAX_PARALLELISM_PER_UPLOAD_REQUEST" (pure . maybeToEither "Invalid SHARE_MAX_PARALLELISM_PER_UPLOAD_REQUEST" . readMaybe)
7774
cloudWebsiteOrigin <- fromEnv "SHARE_CLOUD_HOMEPAGE_ORIGIN" (pure . maybeToEither "Invalid SHARE_CLOUD_HOMEPAGE_ORIGIN" . parseURI)
@@ -162,6 +159,16 @@ withEnv action = do
162159
u <- ServantClient.parseBaseUrl str
163160
pure $ Right u
164161

162+
-- | Parse an environment variable, but only if it exists.
163+
maybeEnv :: String -> (String -> IO (Either String a)) -> IO (Maybe a)
164+
maybeEnv var parser = do
165+
val <- lookupEnv var
166+
case val of
167+
Nothing -> pure Nothing
168+
Just val' -> parser val' >>= \case
169+
Right a -> pure (Just a)
170+
Left err -> putStrLn ("Error with " <> var <> ": " <> err) >> exitWith (ExitFailure 1)
171+
165172
fromEnv :: String -> (String -> IO (Either String a)) -> IO a
166173
fromEnv var from = do
167174
val <- lookupEnv var

docker/docker-compose.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,6 @@ services:
9393
- VAULT_HOST=http://vault:8200/v1
9494
- VAULT_TOKEN=sekrit
9595
- USER_SECRETS_VAULT_MOUNT=secret # A default mount in dev vault
96-
- SHARE_ZENDESK_API_USER=invaliduser@example.com
97-
- SHARE_ZENDESK_API_TOKEN=bad-password
9896
- SHARE_GITHUB_CLIENTID=invalid
9997
- SHARE_GITHUB_CLIENT_SECRET=invalid
10098

package.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ default-extensions:
174174
- QuasiQuotes
175175
- ImportQualifiedPost
176176
- OverloadedRecordDot
177+
- NumericUnderscores
177178

178179
library:
179180
source-dirs: src

share-api.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ library
4141
Share.BackgroundJobs.Webhooks.Worker
4242
Share.BackgroundJobs.Workers
4343
Share.Branch
44+
Share.ChatApps
4445
Share.Codebase
4546
Share.Codebase.Types
4647
Share.Contribution
@@ -173,7 +174,6 @@ library
173174
Share.Web.Support.API
174175
Share.Web.Support.Impl
175176
Share.Web.Support.Types
176-
Share.Web.Support.Zendesk
177177
Share.Web.Types
178178
Share.Web.UCM.Projects.Impl
179179
Share.Web.UCM.Sync.HashJWT
@@ -225,6 +225,7 @@ library
225225
QuasiQuotes
226226
ImportQualifiedPost
227227
OverloadedRecordDot
228+
NumericUnderscores
228229
ghc-options: -Wall -Werror -Wname-shadowing -Wno-type-defaults -Wno-missing-pattern-synonym-signatures -Wincomplete-uni-patterns -Widentities -Wredundant-constraints -Wpartial-fields -fprint-expanded-synonyms -fwrite-ide-info -O2 -funbox-strict-fields
229230
build-depends:
230231
Diff
@@ -375,6 +376,7 @@ executable share-api
375376
QuasiQuotes
376377
ImportQualifiedPost
377378
OverloadedRecordDot
379+
NumericUnderscores
378380
ghc-options: -Wall -Werror -Wname-shadowing -Wno-type-defaults -Wno-missing-pattern-synonym-signatures -Wincomplete-uni-patterns -Widentities -Wredundant-constraints -Wpartial-fields -fprint-expanded-synonyms -fwrite-ide-info -O2 -funbox-strict-fields -threaded -rtsopts "-with-rtsopts=-N -A32m -qn2 -T"
379381
build-depends:
380382
Diff

src/Share/BackgroundJobs/Webhooks/Worker.hs

Lines changed: 24 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@ import Control.Monad.Except (ExceptT (..), runExceptT)
1111
import Crypto.JWT (JWTError)
1212
import Data.Aeson (FromJSON (..), ToJSON (..))
1313
import Data.Aeson qualified as Aeson
14-
import Data.Aeson.Types ((.=))
1514
import Data.ByteString.Lazy.Char8 qualified as BL
1615
import Data.List.Extra qualified as List
1716
import Data.Text qualified as Text
1817
import Data.Text.Encoding qualified as Text
1918
import Data.Time (UTCTime)
20-
import Data.Time qualified as Time
21-
import Data.Time.Clock.POSIX qualified as POSIX
2219
import Ki.Unlifted qualified as Ki
2320
import Network.HTTP.Client qualified as HTTPClient
2421
import Network.HTTP.Types qualified as HTTP
@@ -28,6 +25,8 @@ import Share.BackgroundJobs.Errors (reportError)
2825
import Share.BackgroundJobs.Monad (Background)
2926
import Share.BackgroundJobs.Webhooks.Queries qualified as WQ
3027
import Share.BackgroundJobs.Workers (newWorker)
28+
import Share.ChatApps (Author (..))
29+
import Share.ChatApps qualified as ChatApps
3130
import Share.Env qualified as Env
3231
import Share.IDs
3332
import Share.IDs qualified as IDs
@@ -42,7 +41,7 @@ import Share.Postgres qualified as PG
4241
import Share.Postgres.Notifications qualified as Notif
4342
import Share.Prelude
4443
import Share.Utils.Logging qualified as Logging
45-
import Share.Utils.URI (URIParam (..), uriToText)
44+
import Share.Utils.URI (URIParam (..))
4645
import Share.Web.Authorization qualified as AuthZ
4746
import Share.Web.Share.DisplayInfo.Queries qualified as DisplayInfoQ
4847
import Share.Web.Share.DisplayInfo.Types (UnifiedDisplayInfo)
@@ -207,72 +206,12 @@ tryWebhook event webhookId = UnliftIO.handleAny (\someException -> pure $ Just $
207206
| status >= 400 -> throwError $ ReceiverError event.eventId webhookId httpStatus $ HTTPClient.responseBody resp
208207
| otherwise -> pure ()
209208

210-
data ChatProvider
211-
= Slack
212-
| Discord
213-
deriving stock (Show, Eq)
214-
215-
-- A type to unify slack and discord message types
216-
data MessageContent (provider :: ChatProvider) = MessageContent
217-
{ -- Text of the bot message
218-
preText :: Text,
219-
-- Title of the attachment
220-
title :: Text,
221-
-- Text of the attachment
222-
content :: Text,
223-
-- Title link
224-
mainLink :: URI,
225-
authorName :: Text,
226-
authorLink :: URI,
227-
authorAvatarUrl :: Maybe URI,
228-
thumbnailUrl :: Maybe URI,
229-
timestamp :: UTCTime
230-
}
231-
deriving stock (Show, Eq)
232-
233-
instance ToJSON (MessageContent 'Slack) where
234-
toJSON MessageContent {preText, content, title, mainLink, authorName, authorLink, authorAvatarUrl, thumbnailUrl, timestamp} =
235-
Aeson.object
236-
[ "text" .= preText,
237-
"attachments"
238-
.= [ Aeson.object
239-
[ "title" .= cutOffText 250 title,
240-
"title_link" .= uriToText mainLink,
241-
"text" .= content,
242-
"author_name" .= authorName,
243-
"author_link" .= uriToText authorLink,
244-
"author_icon" .= fmap uriToText authorAvatarUrl,
245-
"thumb_url" .= fmap uriToText thumbnailUrl,
246-
"ts" .= (round (POSIX.utcTimeToPOSIXSeconds timestamp) :: Int64),
247-
"color" .= ("#36a64f" :: Text)
248-
]
249-
]
250-
]
251-
252-
instance ToJSON (MessageContent 'Discord) where
253-
toJSON MessageContent {preText, content, title, mainLink, authorName, authorLink, authorAvatarUrl, thumbnailUrl, timestamp} =
254-
Aeson.object
255-
[ "username" .= ("Share Notifications" :: Text),
256-
"avatar_url" .= Links.unisonLogoImage,
257-
"content" .= cutOffText 1950 preText,
258-
"embeds"
259-
.= [ Aeson.object
260-
[ "title" .= cutOffText 250 title,
261-
"url" .= uriToText mainLink,
262-
"description" .= cutOffText 4000 content,
263-
"author" .= Aeson.object ["name" .= cutOffText 250 authorName, "url" .= uriToText authorLink, "icon_url" .= fmap uriToText authorAvatarUrl],
264-
"timestamp" .= (Just $ Text.pack $ Time.formatTime Time.defaultTimeLocale "%FT%T%QZ" timestamp),
265-
"thumbnail" .= fmap (\url -> Aeson.object ["url" .= uriToText url]) thumbnailUrl
266-
]
267-
]
268-
]
269-
270209
buildWebhookRequest :: NotificationWebhookId -> URI -> NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEventPayload -> WebhookEventPayload JWTParam -> Background (Either WebhookSendFailure HTTPClient.Request)
271210
buildWebhookRequest webhookId uri event defaultPayload = do
272211
if
273-
| isSlackWebhook uri -> buildChatAppPayload (Proxy @Slack) uri
274-
| isDiscordWebhook uri -> buildChatAppPayload (Proxy @Discord) uri
275-
| otherwise -> pure $ buildDefaultPayload
212+
| isSlackWebhook uri -> buildChatAppPayload (Proxy @ChatApps.Slack) uri
213+
| isDiscordWebhook uri -> buildChatAppPayload (Proxy @ChatApps.Discord) uri
214+
| otherwise -> pure $ buildDefaultPayload
276215
where
277216
isSlackWebhook :: URI -> Bool
278217
isSlackWebhook uri =
@@ -300,28 +239,31 @@ buildWebhookRequest webhookId uri event defaultPayload = do
300239
HTTPClient.requestBody = HTTPClient.RequestBodyLBS $ Aeson.encode defaultPayload
301240
}
302241

303-
buildChatAppPayload :: forall provider. (ToJSON (MessageContent provider)) => Proxy provider -> URI -> Background (Either WebhookSendFailure HTTPClient.Request)
242+
buildChatAppPayload :: forall provider. (ToJSON (ChatApps.MessageContent provider)) => Proxy provider -> URI -> Background (Either WebhookSendFailure HTTPClient.Request)
304243
buildChatAppPayload _ uri = do
305244
let actorName = event.eventActor ^. DisplayInfo.name_
306245
actorHandle = "(" <> IDs.toText (PrefixedID @"@" $ event.eventActor ^. DisplayInfo.handle_) <> ")"
307246
actorAuthor = maybe "" (<> " ") actorName <> actorHandle
308247
actorAvatarUrl = event.eventActor ^. DisplayInfo.avatarUrl_
309248
actorLink <- Links.userProfilePage (event.eventActor ^. DisplayInfo.handle_)
310-
messageContent :: MessageContent provider <- case event.eventData of
249+
messageContent :: ChatApps.MessageContent provider <- case event.eventData of
311250
HydratedProjectBranchUpdatedPayload payload -> do
312251
let pbShorthand = (projectBranchShortHandFromParts payload.projectInfo.projectShortHand payload.branchInfo.branchShortHand)
313252
title = "Branch " <> IDs.toText pbShorthand <> " was just updated."
314253
preText = title
315254
link <- Links.notificationLink event.eventData
316255
pure $
317-
MessageContent
256+
ChatApps.MessageContent
318257
{ preText = preText,
319258
content = "Branch updated",
320259
title = title,
321-
mainLink = link,
322-
authorName = actorAuthor,
323-
authorLink = actorLink,
324-
authorAvatarUrl = actorAvatarUrl,
260+
mainLink = Just link,
261+
author =
262+
Author
263+
{ authorName = Just actorAuthor,
264+
authorLink = Just actorLink,
265+
authorAvatarUrl = actorAvatarUrl
266+
},
325267
thumbnailUrl = Nothing,
326268
timestamp = event.eventOccurredAt
327269
}
@@ -332,14 +274,17 @@ buildWebhookRequest webhookId uri event defaultPayload = do
332274
preText = "New Contribution in " <> IDs.toText pbShorthand
333275
link <- Links.notificationLink event.eventData
334276
pure $
335-
MessageContent
277+
ChatApps.MessageContent
336278
{ preText = preText,
337279
content = description,
338280
title = title,
339-
mainLink = link,
340-
authorName = actorAuthor,
341-
authorLink = actorLink,
342-
authorAvatarUrl = actorAvatarUrl,
281+
mainLink = Just link,
282+
author =
283+
Author
284+
{ authorName = Just actorAuthor,
285+
authorLink = Just actorLink,
286+
authorAvatarUrl = actorAvatarUrl
287+
},
343288
thumbnailUrl = Nothing,
344289
timestamp = event.eventOccurredAt
345290
}
@@ -355,13 +300,6 @@ buildWebhookRequest webhookId uri event defaultPayload = do
355300
}
356301
)
357302

358-
-- | Nicely cut off text so that it doesn't exceed the max length
359-
cutOffText :: Int -> Text -> Text
360-
cutOffText maxLength text =
361-
if Text.length text > maxLength
362-
then Text.take (maxLength - 3) text <> "..."
363-
else text
364-
365303
attemptWebhookSend ::
366304
AuthZ.AuthZReceipt ->
367305
(NotificationEvent NotificationEventId UnifiedDisplayInfo UTCTime HydratedEventPayload -> NotificationWebhookId -> IO (Maybe WebhookSendFailure)) ->

0 commit comments

Comments
 (0)