Skip to content

Commit f51de16

Browse files
authored
[SQSERVICES-1646] Servantify Gundeck (#2769)
1 parent 6f092e9 commit f51de16

File tree

19 files changed

+534
-365
lines changed

19 files changed

+534
-365
lines changed

changelog.d/5-internal/pr-2769

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Gundeck push token API and notification API is migrated to Servant
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-- This file is part of the Wire Server implementation.
2+
--
3+
-- Copyright (C) 2022 Wire Swiss GmbH <[email protected]>
4+
--
5+
-- This program is free software: you can redistribute it and/or modify it under
6+
-- the terms of the GNU Affero General Public License as published by the Free
7+
-- Software Foundation, either version 3 of the License, or (at your option) any
8+
-- later version.
9+
--
10+
-- This program is distributed in the hope that it will be useful, but WITHOUT
11+
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12+
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
-- details.
14+
--
15+
-- You should have received a copy of the GNU Affero General Public License along
16+
-- with this program. If not, see <https://www.gnu.org/licenses/>.
17+
18+
module Wire.API.Error.Gundeck where
19+
20+
import Wire.API.Error
21+
22+
data GundeckError
23+
= AddTokenErrorNoBudget
24+
| AddTokenErrorNotFound
25+
| AddTokenErrorInvalid
26+
| AddTokenErrorTooLong
27+
| AddTokenErrorMetadataTooLong
28+
| TokenNotFound
29+
| NotificationNotFound
30+
31+
instance KnownError (MapError e) => IsSwaggerError (e :: GundeckError) where
32+
addToSwagger = addStaticErrorToSwagger @(MapError e)
33+
34+
type instance MapError 'AddTokenErrorNoBudget = 'StaticError 413 "sns-thread-budget-reached" "Too many concurrent calls to SNS; is SNS down?"
35+
36+
type instance MapError 'AddTokenErrorNotFound = 'StaticError 404 "app-not-found" "App does not exist"
37+
38+
type instance MapError 'AddTokenErrorInvalid = 'StaticError 404 "invalid-token" "Invalid push token"
39+
40+
type instance MapError 'AddTokenErrorTooLong = 'StaticError 413 "token-too-long" "Push token length must be < 8192 for GCM or 400 for APNS"
41+
42+
type instance MapError 'AddTokenErrorMetadataTooLong = 'StaticError 413 "metadata-too-long" "Tried to add token to endpoint resulting in metadata length > 2048"
43+
44+
type instance MapError 'TokenNotFound = 'StaticError 404 "not-found" "Push token not found"
45+
46+
type instance MapError 'NotificationNotFound = 'StaticError 404 "not-found" "Some notifications not found"

libs/wire-api/src/Wire/API/Notification.hs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
module Wire.API.Notification
2222
( NotificationId,
23+
RawNotificationId (..),
2324
Event,
2425

2526
-- * QueuedNotification
@@ -32,6 +33,7 @@ module Wire.API.Notification
3233
queuedNotifications,
3334
queuedHasMore,
3435
queuedTime,
36+
GetNotificationsResponse (..),
3537

3638
-- * Swagger
3739
modelEvent,
@@ -46,11 +48,16 @@ import qualified Data.Aeson.Types as Aeson
4648
import Data.Id
4749
import Data.Json.Util
4850
import Data.List.NonEmpty (NonEmpty)
51+
import Data.SOP
4952
import Data.Schema
53+
import Data.String.Conversions (cs)
54+
import Data.Swagger (ToParamSchema (..))
5055
import qualified Data.Swagger as S
5156
import qualified Data.Swagger.Build.Api as Doc
5257
import Data.Time.Clock (UTCTime)
5358
import Imports
59+
import Servant
60+
import Wire.API.Routes.MultiVerb
5461
import Wire.Arbitrary (Arbitrary, GenericUniform (..))
5562

5663
type NotificationId = Id QueuedNotification
@@ -84,8 +91,10 @@ instance ToSchema QueuedNotification where
8491
schema =
8592
object "QueuedNotification" $
8693
QueuedNotification
87-
<$> _queuedNotificationId .= field "id" schema
88-
<*> _queuedNotificationPayload .= field "payload" (nonEmptyArray jsonObject)
94+
<$> _queuedNotificationId
95+
.= field "id" schema
96+
<*> _queuedNotificationPayload
97+
.= field "payload" (nonEmptyArray jsonObject)
8998

9099
makeLenses ''QueuedNotification
91100

@@ -121,8 +130,31 @@ instance ToSchema QueuedNotificationList where
121130
schema =
122131
object "QueuedNotificationList" $
123132
QueuedNotificationList
124-
<$> _queuedNotifications .= field "notifications" (array schema)
125-
<*> _queuedHasMore .= fmap (fromMaybe False) (optField "has_more" schema)
126-
<*> _queuedTime .= maybe_ (optField "time" utcTimeSchema)
133+
<$> _queuedNotifications
134+
.= field "notifications" (array schema)
135+
<*> _queuedHasMore
136+
.= fmap (fromMaybe False) (optField "has_more" schema)
137+
<*> _queuedTime
138+
.= maybe_ (optField "time" utcTimeSchema)
127139

128140
makeLenses ''QueuedNotificationList
141+
142+
newtype RawNotificationId = RawNotificationId {unRawNotificationId :: ByteString}
143+
deriving stock (Eq, Show, Generic)
144+
145+
instance FromHttpApiData RawNotificationId where
146+
parseUrlPiece = pure . RawNotificationId . cs
147+
148+
instance ToParamSchema RawNotificationId where
149+
toParamSchema _ = toParamSchema (Proxy @Text)
150+
151+
data GetNotificationsResponse
152+
= GetNotificationsWithStatusNotFound QueuedNotificationList
153+
| GetNotificationsSuccess QueuedNotificationList
154+
155+
instance AsUnion '[Respond 404 "Notification list" QueuedNotificationList, Respond 200 "Notification list" QueuedNotificationList] GetNotificationsResponse where
156+
toUnion (GetNotificationsSuccess xs) = S (Z (I xs))
157+
toUnion (GetNotificationsWithStatusNotFound xs) = Z (I xs)
158+
fromUnion (S (Z (I xs))) = GetNotificationsSuccess xs
159+
fromUnion (Z (I xs)) = GetNotificationsWithStatusNotFound xs
160+
fromUnion (S (S x)) = case x of {}

libs/wire-api/src/Wire/API/Push/V2/Token.hs

Lines changed: 116 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,29 @@ module Wire.API.Push.V2.Token
3434
Token (..),
3535
AppName (..),
3636

37-
-- * Swagger
38-
modelPushToken,
39-
modelPushTokenList,
40-
typeTransport,
37+
-- * API types
38+
AddTokenError (..),
39+
AddTokenSuccess (..),
40+
AddTokenResponses,
41+
DeleteTokenResponses,
4142
)
4243
where
4344

44-
import Control.Lens (makeLenses)
45-
import Data.Aeson
45+
import Control.Lens (makeLenses, (?~), (^.))
46+
import qualified Data.Aeson as A
4647
import Data.Attoparsec.ByteString (takeByteString)
4748
import Data.ByteString.Conversion
4849
import Data.Id
49-
import Data.Json.Util
50-
import qualified Data.Swagger.Build.Api as Doc
50+
import Data.SOP
51+
import Data.Schema
52+
import Data.Swagger (ToParamSchema)
53+
import qualified Data.Swagger as S
54+
import qualified Generics.SOP as GSOP
5155
import Imports
56+
import Servant
57+
import Wire.API.Error
58+
import qualified Wire.API.Error.Gundeck as E
59+
import Wire.API.Routes.MultiVerb
5260
import Wire.Arbitrary (Arbitrary, GenericUniform (..))
5361

5462
--------------------------------------------------------------------------------
@@ -59,19 +67,14 @@ newtype PushTokenList = PushTokenList
5967
}
6068
deriving stock (Eq, Show)
6169
deriving newtype (Arbitrary)
70+
deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema PushTokenList)
6271

63-
modelPushTokenList :: Doc.Model
64-
modelPushTokenList = Doc.defineModel "PushTokenList" $ do
65-
Doc.description "List of Native Push Tokens"
66-
Doc.property "tokens" (Doc.array (Doc.ref modelPushToken)) $
67-
Doc.description "Push tokens"
68-
69-
instance ToJSON PushTokenList where
70-
toJSON (PushTokenList t) = object ["tokens" .= t]
71-
72-
instance FromJSON PushTokenList where
73-
parseJSON = withObject "PushTokenList" $ \p ->
74-
PushTokenList <$> p .: "tokens"
72+
instance ToSchema PushTokenList where
73+
schema =
74+
objectWithDocModifier "PushTokenList" (description ?~ "List of Native Push Tokens") $
75+
PushTokenList
76+
<$> pushTokens
77+
.= fieldWithDocModifier "tokens" (description ?~ "Push tokens") (array schema)
7578

7679
data PushToken = PushToken
7780
{ _tokenTransport :: Transport,
@@ -81,39 +84,29 @@ data PushToken = PushToken
8184
}
8285
deriving stock (Eq, Ord, Show, Generic)
8386
deriving (Arbitrary) via (GenericUniform PushToken)
87+
deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema PushToken)
8488

8589
pushToken :: Transport -> AppName -> Token -> ClientId -> PushToken
8690
pushToken = PushToken
8791

88-
modelPushToken :: Doc.Model
89-
modelPushToken = Doc.defineModel "PushToken" $ do
90-
Doc.description "Native Push Token"
91-
Doc.property "transport" typeTransport $
92-
Doc.description "Transport"
93-
Doc.property "app" Doc.string' $
94-
Doc.description "Application"
95-
Doc.property "token" Doc.bytes' $
96-
Doc.description "Access Token"
97-
Doc.property "client" Doc.bytes' $ do
98-
Doc.description "Client ID"
99-
Doc.optional
100-
101-
instance ToJSON PushToken where
102-
toJSON p =
103-
object $
104-
"transport" .= _tokenTransport p
105-
# "app" .= _tokenApp p
106-
# "token" .= _token p
107-
# "client" .= _tokenClient p
108-
# []
109-
110-
instance FromJSON PushToken where
111-
parseJSON = withObject "PushToken" $ \p ->
112-
PushToken
113-
<$> p .: "transport"
114-
<*> p .: "app"
115-
<*> p .: "token"
116-
<*> p .: "client"
92+
instance ToSchema PushToken where
93+
schema =
94+
objectWithDocModifier "PushToken" desc $
95+
PushToken
96+
<$> _tokenTransport
97+
.= fieldWithDocModifier "transport" transDesc schema
98+
<*> _tokenApp
99+
.= fieldWithDocModifier "app" appDesc schema
100+
<*> _token
101+
.= fieldWithDocModifier "token" tokenDesc schema
102+
<*> _tokenClient
103+
.= fieldWithDocModifier "client" clientIdDesc schema
104+
where
105+
desc = description ?~ "Native Push Token"
106+
transDesc = description ?~ "Transport"
107+
appDesc = description ?~ "Application"
108+
tokenDesc = description ?~ "Access Token"
109+
clientIdDesc = description ?~ "Client ID"
117110

118111
--------------------------------------------------------------------------------
119112
-- Transport
@@ -126,33 +119,18 @@ data Transport
126119
| APNSVoIPSandbox
127120
deriving stock (Eq, Ord, Show, Bounded, Enum, Generic)
128121
deriving (Arbitrary) via (GenericUniform Transport)
129-
130-
typeTransport :: Doc.DataType
131-
typeTransport =
132-
Doc.string $
133-
Doc.enum
134-
[ "GCM",
135-
"APNS",
136-
"APNS_SANDBOX",
137-
"APNS_VOIP",
138-
"APNS_VOIP_SANDBOX"
139-
]
140-
141-
instance ToJSON Transport where
142-
toJSON GCM = "GCM"
143-
toJSON APNS = "APNS"
144-
toJSON APNSSandbox = "APNS_SANDBOX"
145-
toJSON APNSVoIP = "APNS_VOIP"
146-
toJSON APNSVoIPSandbox = "APNS_VOIP_SANDBOX"
147-
148-
instance FromJSON Transport where
149-
parseJSON = withText "transport" $ \case
150-
"GCM" -> pure GCM
151-
"APNS" -> pure APNS
152-
"APNS_SANDBOX" -> pure APNSSandbox
153-
"APNS_VOIP" -> pure APNSVoIP
154-
"APNS_VOIP_SANDBOX" -> pure APNSVoIPSandbox
155-
x -> fail $ "Invalid push transport: " ++ show x
122+
deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Transport)
123+
124+
instance ToSchema Transport where
125+
schema =
126+
enum @Text "Access" $
127+
mconcat
128+
[ element "GCM" GCM,
129+
element "APNS" APNS,
130+
element "APNS_SANDBOX" APNSSandbox,
131+
element "APNS_VOIP" APNSVoIP,
132+
element "APNS_VOIP_SANDBOX" APNSVoIPSandbox
133+
]
156134

157135
instance FromByteString Transport where
158136
parser =
@@ -168,12 +146,72 @@ newtype Token = Token
168146
{ tokenText :: Text
169147
}
170148
deriving stock (Eq, Ord, Show)
171-
deriving newtype (FromJSON, ToJSON, FromByteString, ToByteString, Arbitrary)
149+
deriving newtype (FromHttpApiData, ToHttpApiData, FromByteString, ToByteString, Arbitrary)
150+
deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Token)
151+
152+
instance ToParamSchema Token where
153+
toParamSchema _ = S.toParamSchema (Proxy @Text)
154+
155+
instance ToSchema Token where
156+
schema = Token <$> tokenText .= schema
172157

173158
newtype AppName = AppName
174159
{ appNameText :: Text
175160
}
176161
deriving stock (Eq, Ord, Show)
177-
deriving newtype (FromJSON, ToJSON, IsString, Arbitrary)
162+
deriving newtype (IsString, Arbitrary)
163+
deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema AppName)
164+
165+
instance ToSchema AppName where
166+
schema = AppName <$> appNameText .= schema
178167

179168
makeLenses ''PushToken
169+
170+
--------------------------------------------------------------------------------
171+
-- Add token types
172+
173+
type AddTokenErrorResponses =
174+
'[ ErrorResponse 'E.AddTokenErrorNoBudget,
175+
ErrorResponse 'E.AddTokenErrorNotFound,
176+
ErrorResponse 'E.AddTokenErrorInvalid,
177+
ErrorResponse 'E.AddTokenErrorTooLong,
178+
ErrorResponse 'E.AddTokenErrorMetadataTooLong
179+
]
180+
181+
type AddTokenSuccessResponses =
182+
WithHeaders
183+
'[ Header "Location" Token
184+
]
185+
AddTokenSuccess
186+
(Respond 201 "Push token registered" PushToken)
187+
188+
type AddTokenResponses = AddTokenErrorResponses .++ '[AddTokenSuccessResponses]
189+
190+
data AddTokenError
191+
= AddTokenErrorNoBudget
192+
| AddTokenErrorNotFound
193+
| AddTokenErrorInvalid
194+
| AddTokenErrorTooLong
195+
| AddTokenErrorMetadataTooLong
196+
deriving (Show, Generic)
197+
deriving (AsUnion AddTokenErrorResponses) via GenericAsUnion AddTokenErrorResponses AddTokenError
198+
199+
instance GSOP.Generic AddTokenError
200+
201+
data AddTokenSuccess = AddTokenSuccess PushToken
202+
203+
instance AsHeaders '[Token] PushToken AddTokenSuccess where
204+
fromHeaders (I _ :* Nil, t) = AddTokenSuccess t
205+
toHeaders (AddTokenSuccess t) = (I (t ^. token) :* Nil, t)
206+
207+
instance (res ~ AddTokenResponses) => AsUnion res (Either AddTokenError AddTokenSuccess) where
208+
toUnion = eitherToUnion (toUnion @AddTokenErrorResponses) (Z . I)
209+
fromUnion = eitherFromUnion (fromUnion @AddTokenErrorResponses) (unI . unZ)
210+
211+
--------------------------------------------------------------------------------
212+
-- Delete token types
213+
214+
type DeleteTokenResponses =
215+
'[ ErrorResponse 'E.TokenNotFound,
216+
RespondEmpty 204 "Push token unregistered"
217+
]

0 commit comments

Comments
 (0)