Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
87394eb
Minor refactor
eyeinsky Dec 10, 2025
2a220a5
Add integration test
eyeinsky Dec 26, 2025
061804c
Add field to golden tests
eyeinsky Dec 30, 2025
800dd6f
Add field user types: `Contact`, `UserDoc`
eyeinsky Dec 30, 2025
dc9f457
Sync field to Elastic Search
eyeinsky Dec 30, 2025
5e97b52
[wip] Add field to unit tests
eyeinsky Dec 30, 2025
bbeae92
Add changelog
eyeinsky Dec 30, 2025
dd62147
fixup! Add field user types: `Contact`, `UserDoc`
eyeinsky Dec 31, 2025
38e6bb0
Nit-pick in golden tests.
fisx Jan 7, 2026
b66b3f4
typo.
fisx Jan 7, 2026
5f8d00f
Pass userType to all the right places for profile construction.
fisx Jan 8, 2026
dbafad2
Connect brig re-index executable to postgres.
fisx Jan 8, 2026
82fe925
make sanitize-pr
fisx Jan 8, 2026
9077eb0
Cleanup.
fisx Jan 8, 2026
f3aa0fd
Concentrate userDocToContact logic in one place.
fisx Jan 8, 2026
304cfdd
Fixup
fisx Jan 9, 2026
d45c6da
typos.
fisx Jan 9, 2026
e8dc1ef
rm red hearing.
fisx Jan 12, 2026
5a2ae40
hi ci
fisx Jan 12, 2026
663d300
Revert dependency of brig-index from postgres.
fisx Jan 12, 2026
29a61e6
POC (didn't work out) add type field to cassandra user. [WIP]
fisx Jan 13, 2026
7e4586f
Revert "POC (didn't work out) add type field to cassandra user. [WIP]"
fisx Jan 13, 2026
9630d53
Demote TODOs.
fisx Jan 13, 2026
1c097b1
Fix typo.
fisx Jan 13, 2026
7e5ab9a
Cleanup, make source comment clearer (and less wrong).
fisx Jan 13, 2026
2a8370d
Fix source comment
fisx Jan 13, 2026
26e0410
rm stale changelog entry.
fisx Jan 13, 2026
d7189b0
Fix: allow user doc to not contain user type field.
fisx Jan 13, 2026
ce9e8c6
Add important changelog info.
fisx Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/0-release-notes/add-type-field-to-contact
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Since the index mapping has been updated, the elastic search index
needs to be refilled from Cassandra, see
https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
1 change: 0 additions & 1 deletion changelog.d/1-api-changes/add-get-app-endpoint

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/1-api-changes/add-type-field-to-contact
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `type` field to search results received from `GET /search/contacts`
20 changes: 13 additions & 7 deletions integration/test/Test/Apps.hs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ testCreateApp = do
(resp.json %. "category") `shouldMatch` "ai"

-- A teamless user can't get the app
outsideUser <- randomUser OwnDomain def
outsideUser <- randomUser domain def
bindResponse (getApp outsideUser tid appId) $ \resp -> do
resp.status `shouldMatchInt` 403
resp.json %. "label" `shouldMatch` "app-no-permission"
Expand All @@ -88,13 +88,19 @@ testCreateApp = do
void $ bindResponse (createApp owner tid new {category = "notinenum"}) $ \resp -> do
resp.status `shouldMatchInt` 400

let foundUserType exactMatchTerm aType =
searchContacts owner exactMatchTerm OwnDomain `bindResponse` \resp -> do
resp.status `shouldMatchInt` 200
foundDoc <- resp.json %. "documents" >>= asList >>= assertOne
foundDoc %. "type" `shouldMatch` aType

-- App's user is findable from /search/contacts
BrigI.refreshIndex OwnDomain
searchContacts owner new.name OwnDomain `bindResponse` \resp -> do
resp.status `shouldMatchInt` 200
docs <- resp.json %. "documents" >>= asList
foundUids <- for docs objId
foundUids `shouldMatch` [appId]
BrigI.refreshIndex domain
foundUserType new.name "app"

-- Regular members still have the type "regular"
memberName <- regularMember %. "name" & asString
foundUserType memberName "regular"

testRefreshAppCookie :: (HasCallStack) => App ()
testRefreshAppCookie = do
Expand Down
55 changes: 27 additions & 28 deletions libs/wire-api/src/Wire/API/User.hs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ instance (1 <= max) => ToJSON (LimitedQualifiedUserIdList max) where
data UserType = UserTypeRegular | UserTypeApp | UserTypeBot
deriving (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform UserType)
deriving (A.FromJSON, A.ToJSON) via (Schema UserType)

instance Default UserType where
def = UserTypeRegular
Expand Down Expand Up @@ -714,42 +715,40 @@ instance FromJSON (EmailVisibility ()) where
"visible_to_self" -> pure EmailVisibleToSelf
_ -> fail "unexpected value for EmailVisibility settings"

mkUserProfileWithEmail :: Maybe EmailAddress -> User -> UserLegalHoldStatus -> UserProfile
mkUserProfileWithEmail memail u legalHoldStatus =
let ty = case userService u of
Nothing -> UserTypeRegular
Just _ -> UserTypeBot
in -- This profile would be visible to any other user. When a new field is
-- added, please make sure it is OK for other users to have access to it.
UserProfile
{ profileQualifiedId = userQualifiedId u,
profileHandle = userHandle u,
profileName = userDisplayName u,
profileTextStatus = userTextStatus u,
profilePict = userPict u,
profileAssets = userAssets u,
profileAccentId = userAccentId u,
profileService = userService u,
profileDeleted = userDeleted u,
profileExpire = userExpire u,
profileTeam = userTeam u,
profileEmail = memail,
profileLegalholdStatus = legalHoldStatus,
profileSupportedProtocols = userSupportedProtocols u,
profileType = ty,
profileSearchable = userSearchable u
}
-- | Create profile, overwriting the email field. Called `mkUserProfile`.
mkUserProfileWithEmail :: Maybe EmailAddress -> UserType -> User -> UserLegalHoldStatus -> UserProfile
mkUserProfileWithEmail memail userType u legalHoldStatus =
-- This profile would be visible to any other user. When a new field is
-- added, please make sure it is OK for other users to have access to it.
UserProfile
{ profileQualifiedId = userQualifiedId u,
profileHandle = userHandle u,
profileName = userDisplayName u,
profileTextStatus = userTextStatus u,
profilePict = userPict u,
profileAssets = userAssets u,
profileAccentId = userAccentId u,
profileService = userService u,
profileDeleted = userDeleted u,
profileExpire = userExpire u,
profileTeam = userTeam u,
profileEmail = memail,
profileLegalholdStatus = legalHoldStatus,
profileSupportedProtocols = userSupportedProtocols u,
profileType = userType,
profileSearchable = userSearchable u
}

mkUserProfile :: EmailVisibilityConfigWithViewer -> User -> UserLegalHoldStatus -> UserProfile
mkUserProfile emailVisibilityConfigAndViewer u legalHoldStatus =
mkUserProfile :: EmailVisibilityConfigWithViewer -> UserType -> User -> UserLegalHoldStatus -> UserProfile
mkUserProfile emailVisibilityConfigAndViewer userType u legalHoldStatus =
let isEmailVisible = case emailVisibilityConfigAndViewer of
EmailVisibleToSelf -> False
EmailVisibleIfOnTeam -> isJust (userTeam u)
EmailVisibleIfOnSameTeam Nothing -> False
EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership)) ->
Just viewerTeamId == userTeam u
&& TeamMember.hasPermission viewerMembership TeamMember.ViewSameTeamEmails
in mkUserProfileWithEmail (if isEmailVisible then userEmail u else Nothing) u legalHoldStatus
in mkUserProfileWithEmail (if isEmailVisible then userEmail u else Nothing) userType u legalHoldStatus

--------------------------------------------------------------------------------
-- NewUser
Expand Down
11 changes: 6 additions & 5 deletions libs/wire-api/src/Wire/API/User/Search.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ import Data.Aeson hiding (object, (.=))
import Data.Aeson qualified as Aeson
import Data.Attoparsec.ByteString.Char8 (string)
import Data.ByteString.Char8 qualified as C8
import Data.ByteString.Conversion
import Data.ByteString.Conversion qualified as BS
import Data.ByteString.Conversion as BS
import Data.Id (TeamId, UserGroupId, UserId)
import Data.Json.Util (UTCTimeMillis)
import Data.OpenApi (ToParamSchema (..))
Expand All @@ -59,7 +58,7 @@ import Imports
import Servant.API (FromHttpApiData, ToHttpApiData (..))
import Web.Internal.HttpApiData (parseQueryParam)
import Wire.API.Team.Role (Role)
import Wire.API.User (ManagedBy)
import Wire.API.User (ManagedBy, UserType)
import Wire.API.User.Identity (EmailAddress)
import Wire.Arbitrary (Arbitrary, GenericUniform (..))

Expand Down Expand Up @@ -138,14 +137,15 @@ deriving via (Schema (SearchResult TeamContact)) instance S.ToSchema (SearchResu
--------------------------------------------------------------------------------
-- Contact

-- | Returned by 'searchIndex' under @/contacts/search@.
-- | Returned by 'searchIndex' under @/search/contacts@.
-- This is a subset of 'User' and json instances should reflect that.
data Contact = Contact
{ contactQualifiedId :: Qualified UserId,
contactName :: Text,
contactColorId :: Maybe Int,
contactHandle :: Maybe Text,
contactTeam :: Maybe TeamId
contactTeam :: Maybe TeamId,
contactType :: UserType
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform Contact)
Expand All @@ -161,6 +161,7 @@ instance ToSchema Contact where
<*> contactColorId .= optField "accent_id" (maybeWithDefault Aeson.Null schema)
<*> contactHandle .= optField "handle" (maybeWithDefault Aeson.Null schema)
<*> contactTeam .= optField "team" (maybeWithDefault Aeson.Null schema)
<*> contactType .= field "type" schema

--------------------------------------------------------------------------------
-- TeamContact
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Data.Id (Id (Id))
import Data.Qualified (Qualified (Qualified, qDomain, qUnqualified))
import Data.UUID qualified as UUID (fromString)
import Imports (Maybe (Just, Nothing), fromJust)
import Wire.API.User
import Wire.API.User.Search (Contact (..))

testObject_Contact_user_1 :: Contact
Expand All @@ -35,7 +36,8 @@ testObject_Contact_user_1 =
contactName = "",
contactColorId = Just 6,
contactHandle = Just "\1089530\NUL|\SO",
contactTeam = Nothing
contactTeam = Nothing,
contactType = UserTypeRegular
}

testObject_Contact_user_2 :: Contact
Expand All @@ -49,7 +51,8 @@ testObject_Contact_user_2 =
contactName = "\SYND",
contactColorId = Just (-5),
contactHandle = Just "",
contactTeam = Just (Id (fromJust (UUID.fromString "00000002-0000-0008-0000-000400000002")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000002-0000-0008-0000-000400000002"))),
contactType = UserTypeApp
}

testObject_Contact_user_3 :: Contact
Expand All @@ -63,7 +66,8 @@ testObject_Contact_user_3 =
contactName = "S\1037187D\GS",
contactColorId = Just (-4),
contactHandle = Just "\175177~\35955c",
contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0005-0000-000700000008")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0005-0000-000700000008"))),
contactType = UserTypeBot
}

testObject_Contact_user_4 :: Contact
Expand All @@ -77,7 +81,8 @@ testObject_Contact_user_4 =
contactName = "@=\ETX",
contactColorId = Nothing,
contactHandle = Just "6",
contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000500000004")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000500000004"))),
contactType = UserTypeRegular
}

testObject_Contact_user_5 :: Contact
Expand All @@ -91,7 +96,8 @@ testObject_Contact_user_5 =
contactName = "5m~\DC4`",
contactColorId = Nothing,
contactHandle = Nothing,
contactTeam = Nothing
contactTeam = Nothing,
contactType = UserTypeRegular
}

testObject_Contact_user_6 :: Contact
Expand All @@ -105,7 +111,8 @@ testObject_Contact_user_6 =
contactName = "Cst\995547U",
contactColorId = Nothing,
contactHandle = Just "qI",
contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0004-0000-000600000000")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0004-0000-000600000000"))),
contactType = UserTypeRegular
}

testObject_Contact_user_7 :: Contact
Expand All @@ -119,7 +126,8 @@ testObject_Contact_user_7 =
contactName = "\b74\ENQ",
contactColorId = Just 5,
contactHandle = Just "",
contactTeam = Just (Id (fromJust (UUID.fromString "00000008-0000-0001-0000-000400000008")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000008-0000-0001-0000-000400000008"))),
contactType = UserTypeRegular
}

testObject_Contact_user_8 :: Contact
Expand All @@ -133,7 +141,8 @@ testObject_Contact_user_8 =
contactName = "w\1050194\993461#\\",
contactColorId = Just (-2),
contactHandle = Nothing,
contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0007-0000-000500000002")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0007-0000-000500000002"))),
contactType = UserTypeRegular
}

testObject_Contact_user_9 :: Contact
Expand All @@ -147,7 +156,8 @@ testObject_Contact_user_9 =
contactName = ",\1041199 \v\1077257",
contactColorId = Just 5,
contactHandle = Nothing,
contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0002-0000-000500000000")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0002-0000-000500000000"))),
contactType = UserTypeRegular
}

testObject_Contact_user_10 :: Contact
Expand All @@ -161,7 +171,8 @@ testObject_Contact_user_10 =
contactName = "(\1103086\1105553H/",
contactColorId = Just 0,
contactHandle = Nothing,
contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0006-0000-000700000000")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0006-0000-000700000000"))),
contactType = UserTypeRegular
}

testObject_Contact_user_11 :: Contact
Expand All @@ -175,7 +186,8 @@ testObject_Contact_user_11 =
contactName = "+\DC4\1063683<",
contactColorId = Just 6,
contactHandle = Nothing,
contactTeam = Just (Id (fromJust (UUID.fromString "00000007-0000-0008-0000-000600000004")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000007-0000-0008-0000-000600000004"))),
contactType = UserTypeRegular
}

testObject_Contact_user_12 :: Contact
Expand All @@ -189,7 +201,8 @@ testObject_Contact_user_12 =
contactName = "l\DC1\ETB`\ETX",
contactColorId = Just (-4),
contactHandle = Just "",
contactTeam = Nothing
contactTeam = Nothing,
contactType = UserTypeRegular
}

testObject_Contact_user_13 :: Contact
Expand All @@ -203,7 +216,8 @@ testObject_Contact_user_13 =
contactName = "\SYN\1030541\v8z",
contactColorId = Just (-3),
contactHandle = Just "E\EM\US[58",
contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0003-0000-000000000005")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0003-0000-000000000005"))),
contactType = UserTypeRegular
}

testObject_Contact_user_14 :: Contact
Expand All @@ -217,7 +231,8 @@ testObject_Contact_user_14 =
contactName = "7",
contactColorId = Just (-2),
contactHandle = Just "h\CAN",
contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0008-0000-000700000008")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0008-0000-000700000008"))),
contactType = UserTypeRegular
}

testObject_Contact_user_15 :: Contact
Expand All @@ -231,7 +246,8 @@ testObject_Contact_user_15 =
contactName = "U6\ESC*\SO",
contactColorId = Nothing,
contactHandle = Nothing,
contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0006-0000-000800000006")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0006-0000-000800000006"))),
contactType = UserTypeRegular
}

testObject_Contact_user_16 :: Contact
Expand All @@ -245,7 +261,8 @@ testObject_Contact_user_16 =
contactName = "l",
contactColorId = Nothing,
contactHandle = Nothing,
contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0006-0000-000200000007")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0006-0000-000200000007"))),
contactType = UserTypeRegular
}

testObject_Contact_user_17 :: Contact
Expand All @@ -259,7 +276,8 @@ testObject_Contact_user_17 =
contactName = "fI\8868\&3z",
contactColorId = Nothing,
contactHandle = Just "3",
contactTeam = Just (Id (fromJust (UUID.fromString "00000004-0000-0007-0000-000000000001")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000004-0000-0007-0000-000000000001"))),
contactType = UserTypeRegular
}

testObject_Contact_user_18 :: Contact
Expand All @@ -273,7 +291,8 @@ testObject_Contact_user_18 =
contactName = "\"jC\74801\144577\DC2",
contactColorId = Nothing,
contactHandle = Nothing,
contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000007")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000007"))),
contactType = UserTypeRegular
}

testObject_Contact_user_19 :: Contact
Expand All @@ -287,7 +306,8 @@ testObject_Contact_user_19 =
contactName = "I",
contactColorId = Just (-1),
contactHandle = Just "\"7\ACK!",
contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0004-0000-000000000003")))
contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0004-0000-000000000003"))),
contactType = UserTypeRegular
}

testObject_Contact_user_20 :: Contact
Expand All @@ -301,5 +321,6 @@ testObject_Contact_user_20 =
contactName = "|K\n\n\t",
contactColorId = Nothing,
contactHandle = Nothing,
contactTeam = Nothing
contactTeam = Nothing,
contactType = UserTypeRegular
}
Loading