Files
simplex-chat/src/Simplex/Chat/Operators.hs
T

415 lines
18 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Chat.Operators where
import Control.Monad (foldM)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
import qualified Data.Aeson.TH as JQ
import Data.FileEmbed
import Data.Foldable1 (fold1)
import Data.Int (Int64)
import Data.Kind (Type)
import Data.List (find, foldl')
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isNothing)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Text (Text)
import Data.Time (addUTCTime)
import Data.Time.Clock (UTCTime, nominalDay)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import Language.Haskell.TH.Syntax (lift)
import Simplex.Chat.Operators.Conditions
import Simplex.Chat.Types.Util (textParseJSON)
import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..))
import Simplex.Messaging.Util (safeDecodeUtf8)
usageConditionsCommit :: Text
usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c"
previousConditionsCommit :: Text
previousConditionsCommit = "edf99fcd1d7d38d2501d19608b94c084cf00f2ac"
usageConditionsText :: Text
usageConditionsText =
$( let s = $(embedFile =<< makeRelativeToProject "PRIVACY.md")
in [|stripFrontMatter (safeDecodeUtf8 $(lift s))|]
)
data EntityStored = ESStored | ESNew
data SEntityStored (s :: EntityStored) where
SESStored :: SEntityStored 'ESStored
SESNew :: SEntityStored 'ESNew
data DBEntityId' (s :: EntityStored) where
DBEntityId :: Int64 -> DBEntityId' 'ESStored
NewDBEntity :: DBEntityId' 'ESNew
deriving instance Show (DBEntityId' s)
type DBEntityId = DBEntityId' 'ESStored
type NewDBEntity = DBEntityId' 'ESNew
data ADBEntityId = forall s. AEI (SEntityStored s) (DBEntityId' s)
pattern ADBEntityId :: Int64 -> ADBEntityId
pattern ADBEntityId i = AEI SESStored (DBEntityId i)
pattern ANewDBEntity :: ADBEntityId
pattern ANewDBEntity = AEI SESNew NewDBEntity
data OperatorTag = OTSimplex | OTXyz
deriving (Eq, Ord, Show)
instance FromField OperatorTag where fromField = fromTextField_ textDecode
instance ToField OperatorTag where toField = toField . textEncode
instance FromJSON OperatorTag where
parseJSON = textParseJSON "OperatorTag"
instance ToJSON OperatorTag where
toJSON = J.String . textEncode
toEncoding = JE.text . textEncode
instance TextEncoding OperatorTag where
textDecode = \case
"simplex" -> Just OTSimplex
"xyz" -> Just OTXyz
_ -> Nothing
textEncode = \case
OTSimplex -> "simplex"
OTXyz -> "xyz"
-- this and other types only define instances of serialization for known DB IDs only,
-- entities without IDs cannot be serialized to JSON
instance FromField DBEntityId where fromField f = DBEntityId <$> fromField f
instance ToField DBEntityId where toField (DBEntityId i) = toField i
data UsageConditions = UsageConditions
{ conditionsId :: Int64,
conditionsCommit :: Text,
notifiedAt :: Maybe UTCTime,
createdAt :: UTCTime
}
deriving (Show)
data UsageConditionsAction
= UCAReview {operators :: [ServerOperator], deadline :: Maybe UTCTime, showNotice :: Bool}
| UCAAccepted {operators :: [ServerOperator]}
deriving (Show)
usageConditionsAction :: [ServerOperator] -> UsageConditions -> UTCTime -> Maybe UsageConditionsAction
usageConditionsAction operators UsageConditions {createdAt, notifiedAt} now = do
let enabledOperators = filter (\ServerOperator {enabled} -> enabled) operators
if null enabledOperators
then Nothing
else
if all conditionsAccepted enabledOperators
then
let acceptedForOperators = filter conditionsAccepted operators
in Just $ UCAAccepted acceptedForOperators
else
let acceptForOperators = filter (not . conditionsAccepted) enabledOperators
deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt)
showNotice = isNothing notifiedAt
in Just $ UCAReview acceptForOperators deadline showNotice
conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> Maybe UTCTime
conditionsRequiredOrDeadline createdAt notifiedAtOrNow =
if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt
then Just $ conditionsDeadline notifiedAtOrNow
else Nothing -- required
where
conditionsDeadline :: UTCTime -> UTCTime
conditionsDeadline = addUTCTime (31 * nominalDay)
data ConditionsAcceptance
= CAAccepted {acceptedAt :: Maybe UTCTime}
| CARequired {deadline :: Maybe UTCTime}
deriving (Show)
type ServerOperator = ServerOperator' DBEntityId
type NewServerOperator = ServerOperator' NewDBEntity
type AServerOperator = ServerOperator' ADBEntityId
data ServerOperator' s = ServerOperator
{ operatorId :: s,
operatorTag :: Maybe OperatorTag,
appVendor :: Bool,
tradeName :: Text,
legalName :: Maybe Text,
serverDomains :: [Text],
conditionsAcceptance :: ConditionsAcceptance,
enabled :: Bool,
roles :: ServerRoles
}
deriving (Show)
aServerOperator :: ServerOperator -> AServerOperator
aServerOperator op@ServerOperator {operatorId = DBEntityId opId} = op {operatorId = ADBEntityId opId}
conditionsAccepted :: ServerOperator -> Bool
conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of
CAAccepted {} -> True
_ -> False
data OperatorEnabled = OperatorEnabled
{ operatorId' :: OperatorId,
enabled' :: Bool,
roles' :: ServerRoles
}
deriving (Show)
type UserServers = UserServers' DBEntityId
type AUserServers = UserServers' ADBEntityId
data UserServers' s = UserServers
{ operator :: Maybe (ServerOperator' s),
smpServers :: [UserServer' s 'PSMP],
xftpServers :: [UserServer' s 'PXFTP]
}
deriving (Show)
type UserServer p = UserServer' DBEntityId p
type NewUserServer p = UserServer' NewDBEntity p
type AUserServer p = UserServer' ADBEntityId p
data UserServer' s p = UserServer
{ serverId :: s,
serverOperatorId :: Maybe OperatorId,
server :: ProtoServerWithAuth p,
tested :: Maybe Bool,
enabled :: Bool
}
deriving (Show)
data PresetOperatorServers = PresetOperatorServers
{ operator :: NewServerOperator,
presetSMPServers :: NonEmpty (PresetServer 'PSMP),
presetXFTPServers :: NonEmpty (PresetServer 'PXFTP),
useSMP :: Int,
useXFTP :: Int
}
data PresetServer p = PresetServer
{ useServer :: Bool,
server :: ProtoServerWithAuth p
}
-- This function should be used inside DB transaction to update conditions in the database
-- it returns (conditions to mark as accepted to SimpleX operator, conditions to add)
usageConditionsToAdd :: Bool -> Text -> Text -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, [UsageConditions])
usageConditionsToAdd newUser prevCommit sourceCommit createdAt = \case
[]
| newUser -> (Just sourceCond, [sourceCond])
| otherwise -> (Just prevCond, [prevCond, sourceCond])
where
prevCond = conditions 1 prevCommit
sourceCond = conditions 2 sourceCommit
conds -> (Nothing, if hasSourceCond then [] else [sourceCond])
where
hasSourceCond = any ((sourceCommit ==) . conditionsCommit) conds
sourceCond = conditions cId sourceCommit
cId = maximum (map conditionsId conds) + 1
where
conditions cId commit = UsageConditions {conditionsId = cId, conditionsCommit = commit, notifiedAt = Nothing, createdAt}
-- This function should be used inside DB transaction to update operators.
-- It allows to add/remove/update preset operators in the database preserving enabled and roles settings,
-- and preserves custom operators without tags for forward compatibility.
updatedServerOperators :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [AServerOperator]
updatedServerOperators presetSrvs storedOps =
foldr addPreset [] presetSrvs
<> map aServerOperator (filter (isNothing . operatorTag) storedOps) -- TODO remove domains of preset operators from custom
where
addPreset PresetOperatorServers {operator = presetOp} = (storedOp' :)
where
storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of
Just ServerOperator {operatorId = DBEntityId opId, conditionsAcceptance, enabled, roles} ->
presetOp {operatorId = ADBEntityId opId, conditionsAcceptance, enabled, roles}
Nothing -> presetOp {operatorId = ANewDBEntity}
-- This function should be used inside DB transaction to update servers.
-- It assumes that the list of operators was amended using updatedServerOperators,
-- that [ServerOperator] has the same operators as [PresetOperatorServers],
-- and that they all have serverOperatorId set.
--
-- presets -> stored or user-supplied servers, possibly with incorrect operators
updatedUserServers' :: NonEmpty PresetOperatorServers -> [UserServers] -> ([AUserServers], NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))
updatedUserServers' presetSrvs storedSrvs = (userServers, agentSMPServers, agentXFTPServers)
where
userServers = undefined
agentSMPServers = undefined
agentXFTPServers = undefined
-- make set of known tags of preset operators
knownPresetOps :: Set (Maybe OperatorTag)
knownPresetOps = foldl' (\s PresetOperatorServers {operator} -> S.insert (operatorTag operator) s) S.empty presetSrvs
-- make map domain -> operator
-- storedSrvs:
-- - remove preset operators with tags not present in presets)
-- - flatten
-- - set correct operators based on domains
-- - split servers to with/without preset operators
-- - make Map (protoserver, stored server record) from servers with preset operators
-- presetSrvs: flatten, update using map above, prepare agent servers, reassemble to userServers
-- add other operators and servers without operator
--
-- (storedPresets, storedOthers) = partition (isJust . operatorTag . operator) storedSrvs
-- (storedOthersKeep, storeOthersPresets)
-- userServers = foldr addOther (foldr addPreset [] presetSrvs) storedOthers
-- updatedUserServers :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [UserServer 'PSMP] -> [UserServer 'PXFTP] -> Either String ([UserServer 'PSMP], [UserServer 'PXFTP])
-- updatedUserServers presetSrvs storedOps smpSrvs xftpSrvs = do
-- smpSrvs' <- updatedSrvs useSMP smpSrvs =<< presetSrvsToStore presetSMPServers
-- xftpSrvs' <- updatedSrvs useXFTP xftpSrvs =<< presetSrvsToStore presetXFTPServers
-- pure (smpSrvs', xftpSrvs')
-- where
-- presetSrvsToStore :: forall p. (PresetOperatorServers -> NonEmpty (PresetServer p)) -> Either String (NonEmpty (Bool, UserServer p))
-- presetSrvsToStore presetSel = fold1 <$> mapM operatorSrvs presetSrvs
-- where
-- operatorSrvs :: PresetOperatorServers -> Either String (NonEmpty (Bool, UserServer p))
-- operatorSrvs op@PresetOperatorServers {operator} = case find ((operatorTag operator ==) . operatorTag) storedOps of
-- Nothing -> Left "preset operator not stored"
-- Just op' -> Right $ L.map (userSrv op') (presetSel op)
-- userSrv op PresetServer {server, useServer} =
-- let srv = UserServer {serverId = Nothing, serverOperatorId = operatorId op, server, tested = Nothing, enabled = False}
-- in (useServer, srv)
-- updatedSrvs :: forall p. (PresetOperatorServers -> Int) -> [UserServer p] -> NonEmpty (Bool, UserServer p) -> Either String [UserServer p]
-- updatedSrvs useSel storedSrvs presetSrvs =
-- fmap enabledSrvs . addOtherServers =<< foldM updatedSrv (storedSrvs', []) presetSrvs
-- where
-- storedSrvs' :: Map (ProtoServerWithAuth p) (UserServer p)
-- storedSrvs' = foldl' (\m us@UserServer {server} -> M.insert server us m) M.empty storedSrvs
-- updatedSrv :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> (Bool, UserServer p) -> Either String (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)])
-- updatedSrv srvs srv = undefined
-- addOtherServers :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> Either String [(Bool, UserServer p)]
-- addOtherServers = undefined
-- enabledSrvs :: [(Bool, UserServer p)] -> [UserServer p]
-- enabledSrvs = undefined
-- addSrv srv@ServerCfg {server = ProtocolServerWithAuth ProtocolServer {host}} uss =
-- case find (\us -> any [\h -> any (\d -> d `T.isSuffixOf` ) serverDomains (operator us)] host) uss of
-- Just opId
-- where
-- hasOperatorDomain ServerCfg {server = ProtocolServerWithAuth ProtocolServer {host}} us
-- addSrv srv uss = ... а тут просто найти оператора в списке и вставить ему сервер через add и как то ругнуться если его нет (но такого не должно быть). Либо вообще есть вариант сразу читать в этом формате - сначала прочитать операторов и в цикле читать серверы каждого - это вот может быть еще проще
-- groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] -> [UserServers]
-- groupByOperator srvOperators smpSrvs xftpSrvs =
-- map createOperatorServers (M.toList combinedMap)
-- where
-- srvOperatorId ServerCfg {operator} = DBEntityId <$> operator
-- operatorMap :: Map (Maybe DBEntityId) (Maybe ServerOperator)
-- operatorMap = M.fromList [(Just (operatorId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing
-- initialMap :: Map (Maybe DBEntityId) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP])
-- initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap]
-- smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs
-- combinedMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (smps, server : xftps)) (srvOperatorId server) acc) smpsMap xftpSrvs
-- createOperatorServers (key, (groupedSmps, groupedXftps)) =
-- UserServers
-- { operator = fromMaybe Nothing (M.lookup key operatorMap),
-- smpServers = groupedSmps,
-- xftpServers = groupedXftps
-- }
data UserServersError
= USEStorageMissing
| USEProxyMissing
| USEDuplicateSMP {server :: AProtoServerWithAuth}
| USEDuplicateXFTP {server :: AProtoServerWithAuth}
deriving (Show)
validateUserServers :: NonEmpty UserServers -> [UserServersError]
validateUserServers userServers =
let storageMissing_ = if any (canUseForRole storage) userServers then [] else [USEStorageMissing]
proxyMissing_ = if any (canUseForRole proxy) userServers then [] else [USEProxyMissing]
allSMPServers = map (\UserServer {server} -> server) $ concatMap (\UserServers {smpServers} -> smpServers) userServers
duplicateSMPServers = findDuplicatesByHost allSMPServers
duplicateSMPErrors = map (USEDuplicateSMP . AProtoServerWithAuth SPSMP) duplicateSMPServers
allXFTPServers = map (\UserServer {server} -> server) $ concatMap (\UserServers {xftpServers} -> xftpServers) userServers
duplicateXFTPServers = findDuplicatesByHost allXFTPServers
duplicateXFTPErrors = map (USEDuplicateXFTP . AProtoServerWithAuth SPXFTP) duplicateXFTPServers
in storageMissing_ <> proxyMissing_ <> duplicateSMPErrors <> duplicateXFTPErrors
where
canUseForRole :: (ServerRoles -> Bool) -> UserServers -> Bool
canUseForRole roleSel UserServers {operator, smpServers, xftpServers} = case operator of
Just ServerOperator {roles} -> roleSel roles
Nothing -> not (null smpServers) && not (null xftpServers)
findDuplicatesByHost :: [ProtoServerWithAuth p] -> [ProtoServerWithAuth p]
findDuplicatesByHost servers =
let allHosts = concatMap (L.toList . host . protoServer) servers
hostCounts = M.fromListWith (+) [(host, 1 :: Int) | host <- allHosts]
duplicateHosts = M.keys $ M.filter (> 1) hostCounts
in filter (\srv -> any (`elem` duplicateHosts) (L.toList $ host . protoServer $ srv)) servers
instance ToJSON DBEntityId where
toEncoding (DBEntityId i) = toEncoding i
toJSON (DBEntityId i) = toJSON i
instance FromJSON DBEntityId where
parseJSON v = DBEntityId <$> parseJSON v
$(JQ.deriveJSON defaultJSON ''UsageConditions)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance)
instance ToJSON ServerOperator where
toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerOperator')
toJSON = $(JQ.mkToJSON defaultJSON ''ServerOperator')
instance FromJSON ServerOperator where
parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerOperator')
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction)
instance ProtocolTypeI p => ToJSON (UserServer p) where
toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer')
toJSON = $(JQ.mkToJSON defaultJSON ''UserServer')
instance ProtocolTypeI p => FromJSON (UserServer p) where
parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer')
instance ToJSON UserServers where
toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServers')
toJSON = $(JQ.mkToJSON defaultJSON ''UserServers')
instance FromJSON UserServers where
parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServers')
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError)