Skip to content

Commit d063175

Browse files
committed
fix: rework feature flags (meeting, meeting_premium)
1 parent d614efb commit d063175

11 files changed

Lines changed: 241 additions & 57 deletions

File tree

changelog.d/2-features/WPB-21964

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
Add payingTeam feature flag to distinguish paying teams from trial teams. Meetings created by paying team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/payingTeam. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/payingTeam and lock status management.
1+
Add meetingPremium feature flag (renamed from payingTeam) to distinguish premium teams from trial teams. Meetings created by premium team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/meetingPremium. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetingPremium and lock status management.
2+
3+
Add meeting feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meeting. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meeting and lock status management.

libs/galley-types/src/Galley/Types/Teams.hs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,19 @@ newtype instance FeatureDefaults StealthUsersConfig
330330
deriving (FromJSON) via Defaults (LockableFeature StealthUsersConfig)
331331
deriving (ParseFeatureDefaults) via OptionalField StealthUsersConfig
332332

333-
newtype instance FeatureDefaults PayingTeamConfig
334-
= PayingTeamDefaults (LockableFeature PayingTeamConfig)
333+
newtype instance FeatureDefaults MeetingConfig
334+
= MeetingDefaults (LockableFeature MeetingConfig)
335335
deriving stock (Eq, Show)
336336
deriving newtype (Default, GetFeatureDefaults)
337-
deriving (FromJSON) via Defaults (LockableFeature PayingTeamConfig)
338-
deriving (ParseFeatureDefaults) via OptionalField PayingTeamConfig
337+
deriving (FromJSON) via Defaults (LockableFeature MeetingConfig)
338+
deriving (ParseFeatureDefaults) via OptionalField MeetingConfig
339+
340+
newtype instance FeatureDefaults MeetingPremiumConfig
341+
= MeetingPremiumDefaults (LockableFeature MeetingPremiumConfig)
342+
deriving stock (Eq, Show)
343+
deriving newtype (Default, GetFeatureDefaults)
344+
deriving (FromJSON) via Defaults (LockableFeature MeetingPremiumConfig)
345+
deriving (ParseFeatureDefaults) via OptionalField MeetingPremiumConfig
339346

340347
featureKey :: forall cfg. (IsFeatureConfig cfg) => Key.Key
341348
featureKey = Key.fromText $ featureName @cfg

libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ type IFeatureAPI =
9494
:<|> IFeatureStatusLockStatusPut AppsConfig
9595
:<|> IFeatureStatusLockStatusPut SimplifiedUserConnectionRequestQRCodeConfig
9696
:<|> IFeatureStatusLockStatusPut StealthUsersConfig
97-
:<|> IFeatureStatusLockStatusPut PayingTeamConfig
97+
:<|> IFeatureStatusLockStatusPut MeetingConfig
98+
:<|> IFeatureStatusLockStatusPut MeetingPremiumConfig
9899
-- all feature configs
99100
:<|> Named
100101
"feature-configs-internal"

libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ type FeatureAPI =
7575
:<|> FeatureAPIGet SimplifiedUserConnectionRequestQRCodeConfig
7676
:<|> FeatureAPIGet StealthUsersConfig
7777
:<|> FeatureAPIGet CellsInternalConfig
78-
:<|> FeatureAPIGetPut PayingTeamConfig
78+
:<|> FeatureAPIGetPut MeetingConfig
79+
:<|> FeatureAPIGetPut MeetingPremiumConfig
7980

8081
type DeprecationNotice1 = "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022"
8182

libs/wire-api/src/Wire/API/Team/Feature.hs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ module Wire.API.Team.Feature
100100
AppsConfig (..),
101101
SimplifiedUserConnectionRequestQRCodeConfig (..),
102102
StealthUsersConfig (..),
103-
PayingTeamConfig (..),
103+
MeetingConfig (..),
104+
MeetingPremiumConfig (..),
104105
Features,
105106
AllFeatures,
106107
NpProject (..),
@@ -267,7 +268,8 @@ data FeatureSingleton cfg where
267268
FeatureSingletonAssetAuditLogConfig :: FeatureSingleton AssetAuditLogConfig
268269
FeatureSingletonStealthUsersConfig :: FeatureSingleton StealthUsersConfig
269270
FeatureSingletonCellsInternalConfig :: FeatureSingleton CellsInternalConfig
270-
FeatureSingletonPayingTeamConfig :: FeatureSingleton PayingTeamConfig
271+
FeatureSingletonMeetingConfig :: FeatureSingleton MeetingConfig
272+
FeatureSingletonMeetingPremiumConfig :: FeatureSingleton MeetingPremiumConfig
271273

272274
type family DeprecatedFeatureName (v :: Version) (cfg :: Type) :: Symbol
273275

@@ -1764,28 +1766,52 @@ instance IsFeatureConfig StealthUsersConfig where
17641766
objectSchema = pure StealthUsersConfig
17651767

17661768
--------------------------------------------------------------------------------
1767-
-- PayingTeam Feature
1769+
-- Meeting Feature
17681770
--
1769-
-- Indicates whether a team is a paying customer. When enabled, meetings created
1770-
-- by team members are not marked as trial. When disabled, meetings are trial.
1771+
-- Controls whether meetings functionality is available. When enabled, users can
1772+
-- create and manage meetings. When disabled, meeting endpoints are not accessible.
17711773

1772-
data PayingTeamConfig = PayingTeamConfig
1774+
data MeetingConfig = MeetingConfig
17731775
deriving (Eq, Show, Generic, GSOP.Generic)
1774-
deriving (Arbitrary) via (GenericUniform PayingTeamConfig)
1775-
deriving (RenderableSymbol) via (RenderableTypeName PayingTeamConfig)
1776-
deriving (ParseDbFeature, Default) via TrivialFeature PayingTeamConfig
1776+
deriving (Arbitrary) via (GenericUniform MeetingConfig)
1777+
deriving (RenderableSymbol) via (RenderableTypeName MeetingConfig)
1778+
deriving (ParseDbFeature, Default) via TrivialFeature MeetingConfig
17771779

1778-
instance ToSchema PayingTeamConfig where
1779-
schema = object "PayingTeamConfig" objectSchema
1780+
instance ToSchema MeetingConfig where
1781+
schema = object "MeetingConfig" objectSchema
17801782

1781-
instance Default (LockableFeature PayingTeamConfig) where
1783+
instance Default (LockableFeature MeetingConfig) where
17821784
def = defUnlockedFeature
17831785

1784-
instance IsFeatureConfig PayingTeamConfig where
1785-
type FeatureSymbol PayingTeamConfig = "payingTeam"
1786-
featureSingleton = FeatureSingletonPayingTeamConfig
1786+
instance IsFeatureConfig MeetingConfig where
1787+
type FeatureSymbol MeetingConfig = "meeting"
1788+
featureSingleton = FeatureSingletonMeetingConfig
17871789

1788-
objectSchema = pure PayingTeamConfig
1790+
objectSchema = pure MeetingConfig
1791+
1792+
--------------------------------------------------------------------------------
1793+
-- MeetingPremium Feature
1794+
--
1795+
-- Indicates whether a team has premium meeting features. When enabled, meetings
1796+
-- created by team members are not marked as trial. When disabled, meetings are trial.
1797+
1798+
data MeetingPremiumConfig = MeetingPremiumConfig
1799+
deriving (Eq, Show, Generic, GSOP.Generic)
1800+
deriving (Arbitrary) via (GenericUniform MeetingPremiumConfig)
1801+
deriving (RenderableSymbol) via (RenderableTypeName MeetingPremiumConfig)
1802+
deriving (ParseDbFeature, Default) via TrivialFeature MeetingPremiumConfig
1803+
1804+
instance ToSchema MeetingPremiumConfig where
1805+
schema = object "MeetingPremiumConfig" objectSchema
1806+
1807+
instance Default (LockableFeature MeetingPremiumConfig) where
1808+
def = defUnlockedFeature
1809+
1810+
instance IsFeatureConfig MeetingPremiumConfig where
1811+
type FeatureSymbol MeetingPremiumConfig = "meetingPremium"
1812+
featureSingleton = FeatureSingletonMeetingPremiumConfig
1813+
1814+
objectSchema = pure MeetingPremiumConfig
17891815

17901816
---------------------------------------------------------------------------------
17911817
-- FeatureStatus
@@ -1881,7 +1907,8 @@ type Features =
18811907
AssetAuditLogConfig,
18821908
StealthUsersConfig,
18831909
CellsInternalConfig,
1884-
PayingTeamConfig
1910+
MeetingConfig,
1911+
MeetingPremiumConfig
18851912
]
18861913

18871914
-- | list of available features as a record

services/galley/src/Galley/API/Internal.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ allFeaturesAPI =
293293
<@> featureAPI1Full
294294
<@> featureAPI1Full
295295
<@> featureAPI1Full
296+
<@> featureAPI1Full
296297

297298
featureAPI :: API IFeatureAPI GalleyEffects
298299
featureAPI =
@@ -316,7 +317,8 @@ featureAPI =
316317
<@> mkNamedAPI @'("ilock", AppsConfig) (updateLockStatus @AppsConfig)
317318
<@> mkNamedAPI @'("ilock", SimplifiedUserConnectionRequestQRCodeConfig) (updateLockStatus @SimplifiedUserConnectionRequestQRCodeConfig)
318319
<@> mkNamedAPI @'("ilock", StealthUsersConfig) (updateLockStatus @StealthUsersConfig)
319-
<@> mkNamedAPI @'("ilock", PayingTeamConfig) (updateLockStatus @PayingTeamConfig)
320+
<@> mkNamedAPI @'("ilock", MeetingConfig) (updateLockStatus @MeetingConfig)
321+
<@> mkNamedAPI @'("ilock", MeetingPremiumConfig) (updateLockStatus @MeetingPremiumConfig)
320322
-- all features
321323
<@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser)
322324

services/galley/src/Galley/API/Meetings.hs

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,31 @@ import Wire.API.Error.Galley
4545
import Wire.API.Federation.Client (FederatorClient)
4646
import Wire.API.Federation.Error
4747
import Wire.API.Meeting
48-
import Wire.API.Team.Feature (FeatureStatus (..), LockableFeature (..), PayingTeamConfig)
48+
import Wire.API.Team.Feature (FeatureStatus (..), LockableFeature (..), MeetingConfig, MeetingPremiumConfig)
4949
import Wire.FederationAPIAccess ()
5050
import Wire.MeetingsSubsystem qualified as Meetings
5151
import Wire.NotificationSubsystem
5252
import Wire.Sem.Now (Now)
5353
import Wire.TeamStore qualified as TeamStore
5454

55+
-- | Check if meetings feature is enabled for the user (if they're in a team)
56+
checkMeetingsEnabled ::
57+
( Member TeamStore r,
58+
Member TeamFeatureStore r,
59+
Member (ErrorS 'InvalidOperation) r,
60+
Member (Input Opts) r
61+
) =>
62+
UserId ->
63+
Sem r ()
64+
checkMeetingsEnabled userId = do
65+
maybeTeamId <- TeamStore.getOneUserTeam userId
66+
case maybeTeamId of
67+
Nothing -> pure () -- Personal users can use meetings
68+
Just teamId -> do
69+
meetingFeature <- getFeatureForTeam @MeetingConfig teamId
70+
unless (meetingFeature.status == FeatureStatusEnabled) $
71+
throwS @'InvalidOperation
72+
5573
createMeeting ::
5674
( Member Meetings.MeetingsSubsystem r,
5775
Member (ErrorS 'InvalidOperation) r,
@@ -73,63 +91,80 @@ createMeeting ::
7391
NewMeeting ->
7492
Sem r Meeting
7593
createMeeting lUser newMeeting = do
94+
-- Check if meetings feature is enabled
95+
checkMeetingsEnabled (tUnqualified lUser)
96+
7697
-- Validate that endDate > startDate
7798
when (newMeeting.endDate <= newMeeting.startDate) $
7899
throwS @'InvalidOperation
79100

80-
-- Determine trial status based on team membership and paying team feature
81-
trial <- do
82-
maybeTeamId <- TeamStore.getOneUserTeam (tUnqualified lUser)
83-
case maybeTeamId of
84-
Nothing -> pure True -- Personal users are trial
85-
Just teamId -> do
86-
-- Verify user is a team member (not just a collaborator)
87-
maybeMember <- TeamStore.getTeamMember teamId (tUnqualified lUser)
88-
case maybeMember of
89-
Nothing -> throwS @'NotATeamMember -- User not a member
90-
Just _member -> do
91-
-- Check paying team feature status
92-
payingFeature <- getFeatureForTeam @PayingTeamConfig teamId
93-
pure $ case payingFeature of
94-
LockableFeature {status = FeatureStatusEnabled} -> False -- paying team, not trial
95-
_ -> True -- non-paying team or disabled, is trial
101+
-- Determine trial status based on team membership and premium feature
102+
maybeTeamId <- TeamStore.getOneUserTeam (tUnqualified lUser)
103+
trial <- case maybeTeamId of
104+
Nothing -> pure True -- Personal users create trial meetings
105+
Just teamId -> do
106+
-- Verify user is a team member (not just a collaborator)
107+
maybeMember <- TeamStore.getTeamMember teamId (tUnqualified lUser)
108+
case maybeMember of
109+
Nothing -> throwS @'NotATeamMember -- User not a member
110+
Just _member -> do
111+
-- Check meeting premium feature status to determine trial
112+
premiumFeature <- getFeatureForTeam @MeetingPremiumConfig teamId
113+
pure $ case premiumFeature of
114+
LockableFeature {status = FeatureStatusEnabled} -> False -- premium team, not trial
115+
_ -> True -- non-premium team or disabled, is trial
96116
(meeting, conversation) <- Meetings.createMeeting lUser newMeeting trial
97117
notifyCreatedConversation lUser Nothing conversation InternalAdd
98118
pure meeting
99119

100120
getMeeting ::
101121
( Member Meetings.MeetingsSubsystem r,
102-
Member (ErrorS 'MeetingNotFound) r
122+
Member (ErrorS 'MeetingNotFound) r,
123+
Member TeamStore r,
124+
Member TeamFeatureStore r,
125+
Member (ErrorS 'InvalidOperation) r,
126+
Member (Input Opts) r
103127
) =>
104128
Local UserId ->
105129
Domain ->
106130
MeetingId ->
107131
Sem r Meeting
108132
getMeeting zUser domain meetingId = do
133+
checkMeetingsEnabled (tUnqualified zUser)
109134
let qMeetingId = Qualified meetingId domain
110135
maybeMeeting <- Meetings.getMeeting zUser qMeetingId
111136
case maybeMeeting of
112137
Nothing -> throwS @'MeetingNotFound
113138
Just meeting -> pure meeting
114139

115140
listMeetings ::
116-
( Member Meetings.MeetingsSubsystem r
141+
( Member Meetings.MeetingsSubsystem r,
142+
Member TeamStore r,
143+
Member TeamFeatureStore r,
144+
Member (ErrorS 'InvalidOperation) r,
145+
Member (Input Opts) r
117146
) =>
118147
Local UserId ->
119148
Sem r [Meeting]
120-
listMeetings = Meetings.listMeetings
149+
listMeetings lUser = do
150+
checkMeetingsEnabled (tUnqualified lUser)
151+
Meetings.listMeetings lUser
121152

122153
updateMeeting ::
123154
( Member Meetings.MeetingsSubsystem r,
124155
Member (ErrorS 'MeetingNotFound) r,
125-
Member (ErrorS 'InvalidOperation) r
156+
Member (ErrorS 'InvalidOperation) r,
157+
Member TeamStore r,
158+
Member TeamFeatureStore r,
159+
Member (Input Opts) r
126160
) =>
127161
Local UserId ->
128162
Domain ->
129163
MeetingId ->
130164
UpdateMeeting ->
131165
Sem r Meeting
132166
updateMeeting zUser domain meetingId update = do
167+
checkMeetingsEnabled (tUnqualified zUser)
133168
-- Validate that at least one field is being updated
134169
when (isNothing update.title && isNothing update.startDate && isNothing update.endDate && isNothing update.recurrence) $
135170
throwS @'InvalidOperation
@@ -145,41 +180,56 @@ updateMeeting zUser domain meetingId update = do
145180

146181
deleteMeeting ::
147182
( Member Meetings.MeetingsSubsystem r,
148-
Member (ErrorS 'MeetingNotFound) r
183+
Member (ErrorS 'MeetingNotFound) r,
184+
Member (ErrorS 'InvalidOperation) r,
185+
Member TeamStore r,
186+
Member TeamFeatureStore r,
187+
Member (Input Opts) r
149188
) =>
150189
Local UserId ->
151190
Domain ->
152191
MeetingId ->
153192
Sem r ()
154193
deleteMeeting zUser domain meetingId = do
194+
checkMeetingsEnabled (tUnqualified zUser)
155195
let qMeetingId = Qualified meetingId domain
156196
success <- Meetings.deleteMeeting zUser qMeetingId
157197
unless success $ throwS @'MeetingNotFound
158198

159199
addMeetingInvitation ::
160200
( Member Meetings.MeetingsSubsystem r,
161-
Member (ErrorS 'MeetingNotFound) r
201+
Member (ErrorS 'MeetingNotFound) r,
202+
Member (ErrorS 'InvalidOperation) r,
203+
Member TeamStore r,
204+
Member TeamFeatureStore r,
205+
Member (Input Opts) r
162206
) =>
163207
Local UserId ->
164208
Domain ->
165209
MeetingId ->
166210
MeetingEmailsInvitation ->
167211
Sem r ()
168212
addMeetingInvitation zUser domain meetingId (MeetingEmailsInvitation emails) = do
213+
checkMeetingsEnabled (tUnqualified zUser)
169214
let qMeetingId = Qualified meetingId domain
170215
success <- Meetings.addInvitedEmails zUser qMeetingId emails
171216
unless success $ throwS @'MeetingNotFound
172217

173218
removeMeetingInvitation ::
174219
( Member Meetings.MeetingsSubsystem r,
175-
Member (ErrorS 'MeetingNotFound) r
220+
Member (ErrorS 'MeetingNotFound) r,
221+
Member (ErrorS 'InvalidOperation) r,
222+
Member TeamStore r,
223+
Member TeamFeatureStore r,
224+
Member (Input Opts) r
176225
) =>
177226
Local UserId ->
178227
Domain ->
179228
MeetingId ->
180229
MeetingEmailsInvitation ->
181230
Sem r ()
182231
removeMeetingInvitation zUser domain meetingId (MeetingEmailsInvitation emails) = do
232+
checkMeetingsEnabled (tUnqualified zUser)
183233
let qMeetingId = Qualified meetingId domain
184234
success <- Meetings.removeInvitedEmails zUser qMeetingId emails
185235
unless success $ throwS @'MeetingNotFound

services/galley/src/Galley/API/Public/Feature.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ featureAPI =
7676
<@> mkNamedAPI @'("get", SimplifiedUserConnectionRequestQRCodeConfig) getFeature
7777
<@> mkNamedAPI @'("get", StealthUsersConfig) getFeature
7878
<@> mkNamedAPI @'("get", CellsInternalConfig) getFeature
79-
<@> featureAPIGetPut
79+
<@> featureAPIGetPut @MeetingConfig
80+
<@> featureAPIGetPut @MeetingPremiumConfig
8081

8182
deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects
8283
deprecatedFeatureConfigAPI =

services/galley/src/Galley/API/Teams/Features.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,4 +486,6 @@ instance SetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig
486486

487487
instance SetFeatureConfig StealthUsersConfig
488488

489-
instance SetFeatureConfig PayingTeamConfig
489+
instance SetFeatureConfig MeetingConfig
490+
491+
instance SetFeatureConfig MeetingPremiumConfig

services/galley/src/Galley/API/Teams/Features/Get.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,9 @@ instance GetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig
428428

429429
instance GetFeatureConfig StealthUsersConfig
430430

431-
instance GetFeatureConfig PayingTeamConfig
431+
instance GetFeatureConfig MeetingConfig
432+
433+
instance GetFeatureConfig MeetingPremiumConfig
432434

433435
-- | If second factor auth is enabled, make sure that end-points that don't support it, but
434436
-- should, are blocked completely. (This is a workaround until we have 2FA for those

0 commit comments

Comments
 (0)