erebos 0.1.9 → 0.2.0
raw patch · 49 files changed
+4604/−2740 lines, 49 filesdep +ansi-terminaldep +uuid-typesdep +websocketsdep −haskelinedep −uuiddep ~stmPVP ok
version bump matches the API change (PVP)
Dependencies added: ansi-terminal, uuid-types, websockets
Dependencies removed: haskeline, uuid
Dependency ranges changed: stm
API changes (from Hackage documentation)
- Erebos.Attach: instance Erebos.Storage.Storable Erebos.Attach.AttachIdentity
- Erebos.Channel: ChannelAccept :: Stored ChannelRequest -> Stored PublicKexKey -> ChannelAcceptData
- Erebos.Channel: ChannelRequest :: [Stored (Signed IdentityData)] -> Stored PublicKexKey -> ChannelRequestData
- Erebos.Channel: [caKey] :: ChannelAcceptData -> Stored PublicKexKey
- Erebos.Channel: [caRequest] :: ChannelAcceptData -> Stored ChannelRequest
- Erebos.Channel: [crKey] :: ChannelRequestData -> Stored PublicKexKey
- Erebos.Channel: [crPeers] :: ChannelRequestData -> [Stored (Signed IdentityData)]
- Erebos.Channel: acceptChannelRequest :: (MonadStorage m, MonadIO m, MonadError String m) => UnifiedIdentity -> UnifiedIdentity -> Stored ChannelRequest -> m (Stored ChannelAccept, Channel)
- Erebos.Channel: acceptedChannel :: (MonadIO m, MonadError String m) => UnifiedIdentity -> UnifiedIdentity -> Stored ChannelAccept -> m Channel
- Erebos.Channel: channelDecrypt :: (ByteArray ba, MonadIO m, MonadError String m) => Channel -> ba -> m (ba, Word64)
- Erebos.Channel: channelEncrypt :: (ByteArray ba, MonadIO m, MonadError String m) => Channel -> ba -> m (ba, Word64)
- Erebos.Channel: createChannelRequest :: (MonadStorage m, MonadIO m, MonadError String m) => UnifiedIdentity -> UnifiedIdentity -> m (Stored ChannelRequest)
- Erebos.Channel: data Channel
- Erebos.Channel: data ChannelAcceptData
- Erebos.Channel: data ChannelRequestData
- Erebos.Channel: instance Erebos.Storage.Storable Erebos.Channel.ChannelAcceptData
- Erebos.Channel: instance Erebos.Storage.Storable Erebos.Channel.ChannelRequestData
- Erebos.Channel: instance GHC.Show.Show Erebos.Channel.ChannelRequestData
- Erebos.Channel: type ChannelAccept = Signed ChannelAcceptData
- Erebos.Channel: type ChannelRequest = Signed ChannelRequestData
- Erebos.Chatroom: instance Erebos.Storage.Storable Erebos.Chatroom.ChatMessageData
- Erebos.Chatroom: instance Erebos.Storage.Storable Erebos.Chatroom.ChatroomData
- Erebos.Chatroom: instance Erebos.Storage.Storable Erebos.Chatroom.ChatroomService
- Erebos.Chatroom: instance Erebos.Storage.Storable Erebos.Chatroom.ChatroomStateData
- Erebos.Contact: instance Erebos.Storage.Storable Erebos.Contact.ContactAccepted
- Erebos.Contact: instance Erebos.Storage.Storable Erebos.Contact.ContactData
- Erebos.Discovery: instance Erebos.Storage.Storable Erebos.Discovery.DiscoveryService
- Erebos.Identity: instance Erebos.Storage.Storable Erebos.Identity.ExtendedIdentityData
- Erebos.Identity: instance Erebos.Storage.Storable Erebos.Identity.IdentityData
- Erebos.Identity: instance Erebos.Storage.Storable Erebos.Identity.IdentityExtension
- Erebos.Identity: instance GHC.Classes.Eq (m (Erebos.Storage.Stored (Erebos.PubKey.Signed Erebos.Identity.ExtendedIdentityData))) => GHC.Classes.Eq (Erebos.Identity.Identity m)
- Erebos.Identity: instance GHC.Show.Show (m (Erebos.Storage.Stored (Erebos.PubKey.Signed Erebos.Identity.ExtendedIdentityData))) => GHC.Show.Show (Erebos.Identity.Identity m)
- Erebos.Message: DirectMessage :: ComposedIdentity -> [Stored DirectMessage] -> ZonedTime -> Text -> DirectMessage
- Erebos.Message: DirectMessageAttributes :: ServiceHandler DirectMessage () -> DirectMessageAttributes
- Erebos.Message: DirectMessageThread :: ComposedIdentity -> [Stored DirectMessage] -> [Stored DirectMessage] -> [Stored DirectMessage] -> [Stored DirectMessage] -> DirectMessageThread
- Erebos.Message: [dmOwnerMismatch] :: DirectMessageAttributes -> ServiceHandler DirectMessage ()
- Erebos.Message: [msgFrom] :: DirectMessage -> ComposedIdentity
- Erebos.Message: [msgHead] :: DirectMessageThread -> [Stored DirectMessage]
- Erebos.Message: [msgPeer] :: DirectMessageThread -> ComposedIdentity
- Erebos.Message: [msgPrev] :: DirectMessage -> [Stored DirectMessage]
- Erebos.Message: [msgReceived] :: DirectMessageThread -> [Stored DirectMessage]
- Erebos.Message: [msgSeen] :: DirectMessageThread -> [Stored DirectMessage]
- Erebos.Message: [msgSent] :: DirectMessageThread -> [Stored DirectMessage]
- Erebos.Message: [msgText] :: DirectMessage -> Text
- Erebos.Message: [msgTime] :: DirectMessage -> ZonedTime
- Erebos.Message: data DirectMessage
- Erebos.Message: data DirectMessageAttributes
- Erebos.Message: data DirectMessageThread
- Erebos.Message: data DirectMessageThreads
- Erebos.Message: defaultDirectMessageAttributes :: DirectMessageAttributes
- Erebos.Message: formatDirectMessage :: TimeZone -> DirectMessage -> String
- Erebos.Message: formatMessage :: TimeZone -> DirectMessage -> String
- Erebos.Message: instance Erebos.Service.Service Erebos.Message.DirectMessage
- Erebos.Message: instance Erebos.State.SharedType Erebos.Message.DirectMessageThreads
- Erebos.Message: instance Erebos.Storage.Merge.Mergeable Erebos.Message.DirectMessageThreads
- Erebos.Message: instance Erebos.Storage.Storable Erebos.Message.DirectMessage
- Erebos.Message: instance Erebos.Storage.Storable Erebos.Message.MessageState
- Erebos.Message: instance GHC.Classes.Eq Erebos.Message.DirectMessageThreads
- Erebos.Message: messageThreadView :: [Stored MessageState] -> [DirectMessageThread]
- Erebos.Message: sendDirectMessage :: forall (f :: Type -> Type) m. (Foldable f, Applicative f, MonadHead LocalState m, MonadError String m) => Identity f -> Text -> m (Stored DirectMessage)
- Erebos.Message: threadToList :: DirectMessageThread -> [DirectMessage]
- Erebos.Message: toThreadList :: DirectMessageThreads -> [DirectMessageThread]
- Erebos.Message: watchReceivedMessages :: Head LocalState -> (Stored DirectMessage -> IO ()) -> IO WatchedHead
- Erebos.Network: instance Foreign.Storable.Storable Erebos.Network.InetAddress
- Erebos.Network: peerAddress :: Peer -> PeerAddress
- Erebos.Network: peerIdentity :: MonadIO m => Peer -> m PeerIdentity
- Erebos.Network.Protocol: Acknowledged :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: AcknowledgedSingle :: Integer -> TransportHeaderItem
- Erebos.Network.Protocol: AnnounceSelf :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: AnnounceUpdate :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: ChannelClosed :: ChannelState
- Erebos.Network.Protocol: ChannelCookieConfirmed :: ChannelState
- Erebos.Network.Protocol: ChannelCookieReceived :: ChannelState
- Erebos.Network.Protocol: ChannelCookieWait :: ChannelState
- Erebos.Network.Protocol: ChannelEstablished :: Channel -> ChannelState
- Erebos.Network.Protocol: ChannelNone :: ChannelState
- Erebos.Network.Protocol: ChannelOurAccept :: Stored ChannelAccept -> Channel -> ChannelState
- Erebos.Network.Protocol: ChannelOurRequest :: Stored ChannelRequest -> ChannelState
- Erebos.Network.Protocol: ChannelPeerRequest :: WaitingRef -> ChannelState
- Erebos.Network.Protocol: CookieEcho :: Cookie -> TransportHeaderItem
- Erebos.Network.Protocol: CookieSet :: Cookie -> TransportHeaderItem
- Erebos.Network.Protocol: DataRequest :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: DataResponse :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: EncryptedOnly :: SecurityRequirement
- Erebos.Network.Protocol: Initiation :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: NewConnection :: Connection addr -> Maybe RefDigest -> ControlMessage addr
- Erebos.Network.Protocol: PlaintextAllowed :: SecurityRequirement
- Erebos.Network.Protocol: PlaintextOnly :: SecurityRequirement
- Erebos.Network.Protocol: ProtocolVersion :: Text -> TransportHeaderItem
- Erebos.Network.Protocol: ReceivedAnnounce :: addr -> RefDigest -> ControlMessage addr
- Erebos.Network.Protocol: Rejected :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: RequestConnection :: addr -> ControlRequest addr
- Erebos.Network.Protocol: SendAnnounce :: addr -> ControlRequest addr
- Erebos.Network.Protocol: ServiceRef :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: ServiceType :: ServiceID -> TransportHeaderItem
- Erebos.Network.Protocol: StreamOpen :: Word8 -> TransportHeaderItem
- Erebos.Network.Protocol: TrChannelAccept :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: TrChannelRequest :: RefDigest -> TransportHeaderItem
- Erebos.Network.Protocol: TransportHeader :: [TransportHeaderItem] -> TransportHeader
- Erebos.Network.Protocol: TransportPacket :: TransportHeader -> [a] -> TransportPacket a
- Erebos.Network.Protocol: UpdateSelfIdentity :: UnifiedIdentity -> ControlRequest addr
- Erebos.Network.Protocol: WaitingRef :: Storage -> PartialRef -> (Ref -> WaitingRefCallback) -> TVar (Either [RefDigest] Ref) -> WaitingRef
- Erebos.Network.Protocol: [wrefAction] :: WaitingRef -> Ref -> WaitingRefCallback
- Erebos.Network.Protocol: [wrefPartial] :: WaitingRef -> PartialRef
- Erebos.Network.Protocol: [wrefStatus] :: WaitingRef -> TVar (Either [RefDigest] Ref)
- Erebos.Network.Protocol: [wrefStorage] :: WaitingRef -> Storage
- Erebos.Network.Protocol: canReadFlow :: Flow r w -> STM Bool
- Erebos.Network.Protocol: canWriteFlow :: Flow r w -> STM Bool
- Erebos.Network.Protocol: connAddReadStream :: Connection addr -> Word8 -> STM RawStreamReader
- Erebos.Network.Protocol: connAddWriteStream :: Connection addr -> STM (Either String (TransportHeaderItem, RawStreamWriter, IO ()))
- Erebos.Network.Protocol: connAddress :: Connection addr -> addr
- Erebos.Network.Protocol: connClose :: Connection addr -> STM ()
- Erebos.Network.Protocol: connData :: Connection addr -> Flow (Maybe (Bool, TransportPacket PartialObject)) (SecurityRequirement, TransportPacket Ref, [TransportHeaderItem])
- Erebos.Network.Protocol: connGetChannel :: Connection addr -> STM ChannelState
- Erebos.Network.Protocol: connSetChannel :: Connection addr -> ChannelState -> STM ()
- Erebos.Network.Protocol: data ChannelState
- Erebos.Network.Protocol: data Connection addr
- Erebos.Network.Protocol: data ControlMessage addr
- Erebos.Network.Protocol: data ControlRequest addr
- Erebos.Network.Protocol: data Flow r w
- Erebos.Network.Protocol: data SecurityRequirement
- Erebos.Network.Protocol: data TransportHeader
- Erebos.Network.Protocol: data TransportHeaderItem
- Erebos.Network.Protocol: data TransportPacket a
- Erebos.Network.Protocol: data WaitingRef
- Erebos.Network.Protocol: erebosNetworkProtocol :: (Eq addr, Ord addr, Show addr) => UnifiedIdentity -> (String -> STM ()) -> SymFlow (addr, ByteString) -> Flow (ControlRequest addr) (ControlMessage addr) -> IO ()
- Erebos.Network.Protocol: instance Data.Binary.Class.Binary Erebos.Network.Protocol.ParsedCookie
- Erebos.Network.Protocol: instance GHC.Classes.Eq (Erebos.Network.Protocol.Connection addr)
- Erebos.Network.Protocol: instance GHC.Classes.Eq Erebos.Network.Protocol.Cookie
- Erebos.Network.Protocol: instance GHC.Classes.Eq Erebos.Network.Protocol.ParsedCookie
- Erebos.Network.Protocol: instance GHC.Classes.Eq Erebos.Network.Protocol.SecurityRequirement
- Erebos.Network.Protocol: instance GHC.Classes.Eq Erebos.Network.Protocol.TransportHeaderItem
- Erebos.Network.Protocol: instance GHC.Classes.Ord Erebos.Network.Protocol.SecurityRequirement
- Erebos.Network.Protocol: instance GHC.Show.Show Erebos.Network.Protocol.Cookie
- Erebos.Network.Protocol: instance GHC.Show.Show Erebos.Network.Protocol.ParsedCookie
- Erebos.Network.Protocol: instance GHC.Show.Show Erebos.Network.Protocol.TransportHeader
- Erebos.Network.Protocol: instance GHC.Show.Show Erebos.Network.Protocol.TransportHeaderItem
- Erebos.Network.Protocol: mapFlow :: (r -> r') -> (w' -> w) -> Flow r w -> Flow r' w'
- Erebos.Network.Protocol: newFlow :: STM (Flow a b, Flow b a)
- Erebos.Network.Protocol: newFlowIO :: IO (Flow a b, Flow b a)
- Erebos.Network.Protocol: readFlow :: Flow r w -> STM r
- Erebos.Network.Protocol: readFlowIO :: Flow r w -> IO r
- Erebos.Network.Protocol: readObjectsFromStream :: PartialStorage -> RawStreamReader -> IO (Except String [PartialObject])
- Erebos.Network.Protocol: readStreamToList :: RawStreamReader -> IO (Word64, [(Word64, ByteString)])
- Erebos.Network.Protocol: transportToObject :: PartialStorage -> TransportHeader -> PartialObject
- Erebos.Network.Protocol: tryReadFlow :: Flow r w -> STM (Maybe r)
- Erebos.Network.Protocol: tryWriteFlow :: Flow r w -> w -> STM Bool
- Erebos.Network.Protocol: type RawStreamReader = Flow StreamPacket Void
- Erebos.Network.Protocol: type RawStreamWriter = Flow Void StreamPacket
- Erebos.Network.Protocol: type SymFlow a = Flow a a
- Erebos.Network.Protocol: type WaitingRefCallback = ExceptT String IO ()
- Erebos.Network.Protocol: wrDigest :: WaitingRef -> RefDigest
- Erebos.Network.Protocol: writeByteStringToStream :: RawStreamWriter -> ByteString -> IO ()
- Erebos.Network.Protocol: writeFlow :: Flow r w -> w -> STM ()
- Erebos.Network.Protocol: writeFlowBulk :: Flow r w -> [w] -> STM ()
- Erebos.Network.Protocol: writeFlowIO :: Flow r w -> w -> IO ()
- Erebos.Pairing: instance Erebos.Storage.Storable a => Erebos.Storage.Storable (Erebos.Pairing.PairingService a)
- Erebos.PubKey: instance Erebos.Storage.Storable Erebos.PubKey.PublicKexKey
- Erebos.PubKey: instance Erebos.Storage.Storable Erebos.PubKey.PublicKey
- Erebos.PubKey: instance Erebos.Storage.Storable Erebos.PubKey.Signature
- Erebos.PubKey: instance Erebos.Storage.Storable a => Erebos.Storage.Storable (Erebos.PubKey.Signed a)
- Erebos.Service: instance Control.Monad.Error.Class.MonadError GHC.Base.String (Erebos.Service.ServiceHandler s)
- Erebos.Service: instance Erebos.Storage.MonadStorage (Erebos.Service.ServiceHandler s)
- Erebos.Service: instance Erebos.Storage.StorableUUID Erebos.Service.ServiceID
- Erebos.Service: instance GHC.Classes.Eq Erebos.Service.ServiceID
- Erebos.Service: instance GHC.Classes.Ord Erebos.Service.ServiceID
- Erebos.Service: instance GHC.Show.Show Erebos.Service.ServiceID
- Erebos.Set: instance Erebos.Storage.Storable a => Erebos.Storage.Storable (Erebos.Set.SetItem a)
- Erebos.State: instance (Erebos.Storage.HeadType a, Control.Monad.IO.Class.MonadIO m) => Erebos.State.MonadHead a (Control.Monad.Trans.Reader.ReaderT (Erebos.Storage.Head a) m)
- Erebos.State: instance Erebos.Storage.HeadType Erebos.State.LocalState
- Erebos.State: instance Erebos.Storage.Storable Erebos.State.LocalState
- Erebos.State: instance Erebos.Storage.Storable Erebos.State.SharedState
- Erebos.State: instance Erebos.Storage.StorableUUID Erebos.State.SharedTypeID
- Erebos.State: interactiveIdentityUpdate :: forall (f :: Type -> Type) m. (Foldable f, MonadStorage m, MonadIO m, MonadError String m) => Identity f -> m UnifiedIdentity
- Erebos.State: loadLocalStateHead :: MonadIO m => Storage -> m (Head LocalState)
- Erebos.State: updateSharedIdentity :: (MonadHead LocalState m, MonadError String m) => m ()
- Erebos.Storage: Blob :: ByteString -> Object' (c :: Type -> Type)
- Erebos.Storage: Rec :: [(ByteString, RecItem' c)] -> Object' (c :: Type -> Type)
- Erebos.Storage: RecBinary :: ByteString -> RecItem' (c :: Type -> Type)
- Erebos.Storage: RecDate :: ZonedTime -> RecItem' (c :: Type -> Type)
- Erebos.Storage: RecEmpty :: RecItem' (c :: Type -> Type)
- Erebos.Storage: RecInt :: Integer -> RecItem' (c :: Type -> Type)
- Erebos.Storage: RecNum :: Rational -> RecItem' (c :: Type -> Type)
- Erebos.Storage: RecRef :: Ref' c -> RecItem' (c :: Type -> Type)
- Erebos.Storage: RecText :: Text -> RecItem' (c :: Type -> Type)
- Erebos.Storage: RecUUID :: UUID -> RecItem' (c :: Type -> Type)
- Erebos.Storage: RecUnknown :: ByteString -> ByteString -> RecItem' (c :: Type -> Type)
- Erebos.Storage: StoreInfo :: ZonedTime -> Maybe Text -> StoreInfo
- Erebos.Storage: UnknownObject :: ByteString -> ByteString -> Object' (c :: Type -> Type)
- Erebos.Storage: ZeroObject :: Object' (c :: Type -> Type)
- Erebos.Storage: [infoDate] :: StoreInfo -> ZonedTime
- Erebos.Storage: [infoNote] :: StoreInfo -> Maybe Text
- Erebos.Storage: beginHistory :: Storable a => Storage -> StoreInfo -> a -> IO (StoredHistory a)
- Erebos.Storage: class Storable a
- Erebos.Storage: class StorableDate a
- Erebos.Storage: class StorableText a
- Erebos.Storage: class StorableUUID a
- Erebos.Storage: class (Traversable compl, Monad compl) => StorageCompleteness (compl :: Type -> Type)
- Erebos.Storage: class Storable a => ZeroStorable a
- Erebos.Storage: collectObjects :: Object -> [Object]
- Erebos.Storage: collectStoredObjects :: Stored Object -> [Stored Object]
- Erebos.Storage: copyRef :: forall (c :: Type -> Type) (c' :: Type -> Type) m. (StorageCompleteness c, StorageCompleteness c', MonadIO m) => Storage' c' -> Ref' c -> m (LoadResult c (Ref' c'))
- Erebos.Storage: copyStored :: forall (c :: Type -> Type) (c' :: Type -> Type) m a. (StorageCompleteness c, StorageCompleteness c', MonadIO m) => Storage' c' -> Stored' c a -> m (LoadResult c (Stored' c' a))
- Erebos.Storage: data Load a
- Erebos.Storage: data LoadRec a
- Erebos.Storage: data Object' (c :: Type -> Type)
- Erebos.Storage: data RecItem' (c :: Type -> Type)
- Erebos.Storage: data RefDigest
- Erebos.Storage: data Store
- Erebos.Storage: data StoreInfo
- Erebos.Storage: deserializeObject :: PartialStorage -> ByteString -> Except String (PartialObject, ByteString)
- Erebos.Storage: deserializeObjects :: PartialStorage -> ByteString -> Except String [PartialObject]
- Erebos.Storage: evalLoad :: Load a -> Ref -> a
- Erebos.Storage: evalStore :: forall (c :: Type -> Type). StorageCompleteness c => Storage' c -> Store -> IO (Ref' c)
- Erebos.Storage: evalStoreObject :: forall (c :: Type -> Type). StorageCompleteness c => Storage' c -> Store -> IO (Object' c)
- Erebos.Storage: fromDate :: StorableDate a => ZonedTime -> a
- Erebos.Storage: fromHistory :: StoredHistory a -> a
- Erebos.Storage: fromHistoryAt :: ZonedTime -> StoredHistory a -> Maybe a
- Erebos.Storage: fromStored :: Stored a -> a
- Erebos.Storage: fromText :: (StorableText a, MonadError String m) => Text -> m a
- Erebos.Storage: fromUUID :: StorableUUID a => UUID -> a
- Erebos.Storage: fromZero :: ZeroStorable a => Storage -> a
- Erebos.Storage: hashToRefDigest :: ByteString -> RefDigest
- Erebos.Storage: headTypeID :: HeadType a => proxy a -> HeadTypeID
- Erebos.Storage: instance Control.Monad.Error.Class.MonadError GHC.Base.String Erebos.Storage.Load
- Erebos.Storage: instance Control.Monad.Error.Class.MonadError GHC.Base.String Erebos.Storage.LoadRec
- Erebos.Storage: instance Control.Monad.IO.Class.MonadIO m => Erebos.Storage.MonadStorage (Control.Monad.Trans.Reader.ReaderT (Erebos.Storage.Head a) m)
- Erebos.Storage: instance Control.Monad.IO.Class.MonadIO m => Erebos.Storage.MonadStorage (Control.Monad.Trans.Reader.ReaderT Erebos.Storage.Storage m)
- Erebos.Storage: instance Erebos.Storage.Storable Data.ByteString.Internal.Type.ByteString
- Erebos.Storage: instance Erebos.Storage.Storable Erebos.Storage.Object
- Erebos.Storage: instance Erebos.Storage.Storable a => Erebos.Storage.Storable (Erebos.Storage.History a)
- Erebos.Storage: instance Erebos.Storage.Storable a => Erebos.Storage.Storable (Erebos.Storage.Stored a)
- Erebos.Storage: instance Erebos.Storage.Storable a => Erebos.Storage.Storable [a]
- Erebos.Storage: instance Erebos.Storage.Storable a => Erebos.Storage.ZeroStorable [a]
- Erebos.Storage: instance Erebos.Storage.StorableDate Data.Time.Calendar.Days.Day
- Erebos.Storage: instance Erebos.Storage.StorableDate Data.Time.Clock.Internal.UTCTime.UTCTime
- Erebos.Storage: instance Erebos.Storage.StorableDate Data.Time.LocalTime.Internal.ZonedTime.ZonedTime
- Erebos.Storage: instance Erebos.Storage.StorableText Data.Text.Internal.Text
- Erebos.Storage: instance Erebos.Storage.StorableText [GHC.Types.Char]
- Erebos.Storage: instance Erebos.Storage.StorableUUID Data.UUID.Types.Internal.UUID
- Erebos.Storage: instance Erebos.Storage.StorableUUID Erebos.Storage.Internal.HeadID
- Erebos.Storage: instance Erebos.Storage.StorableUUID Erebos.Storage.Internal.HeadTypeID
- Erebos.Storage: instance Erebos.Storage.ZeroStorable a => Erebos.Storage.ZeroStorable (Erebos.Storage.Stored a)
- Erebos.Storage: instance GHC.Base.Alternative Erebos.Storage.Load
- Erebos.Storage: instance GHC.Base.Alternative Erebos.Storage.LoadRec
- Erebos.Storage: instance GHC.Base.Applicative (Erebos.Storage.StoreRecM c)
- Erebos.Storage: instance GHC.Base.Applicative Erebos.Storage.Load
- Erebos.Storage: instance GHC.Base.Applicative Erebos.Storage.LoadRec
- Erebos.Storage: instance GHC.Base.Functor (Erebos.Storage.StoreRecM c)
- Erebos.Storage: instance GHC.Base.Functor Erebos.Storage.Load
- Erebos.Storage: instance GHC.Base.Functor Erebos.Storage.LoadRec
- Erebos.Storage: instance GHC.Base.Monad (Erebos.Storage.StoreRecM c)
- Erebos.Storage: instance GHC.Base.Monad Erebos.Storage.Load
- Erebos.Storage: instance GHC.Base.Monad Erebos.Storage.LoadRec
- Erebos.Storage: instance GHC.Base.MonadPlus Erebos.Storage.Load
- Erebos.Storage: instance GHC.Base.MonadPlus Erebos.Storage.LoadRec
- Erebos.Storage: instance GHC.Show.Show (Erebos.Storage.Object' c)
- Erebos.Storage: instance GHC.Show.Show (Erebos.Storage.RecItem' c)
- Erebos.Storage: instance GHC.Show.Show Erebos.Storage.StoreInfo
- Erebos.Storage: instance GHC.Show.Show a => GHC.Show.Show (Erebos.Storage.History a)
- Erebos.Storage: ioLoadBytes :: StorageCompleteness compl => Ref' compl -> IO (compl ByteString)
- Erebos.Storage: ioLoadObject :: StorageCompleteness c => Ref' c -> IO (c (Object' c))
- Erebos.Storage: lazyLoadBytes :: forall (c :: Type -> Type). StorageCompleteness c => Ref' c -> LoadResult c ByteString
- Erebos.Storage: load :: Storable a => Ref -> a
- Erebos.Storage: load' :: Storable a => Load a
- Erebos.Storage: loadBinaries :: ByteArray a => String -> LoadRec [a]
- Erebos.Storage: loadBinary :: ByteArray a => String -> LoadRec a
- Erebos.Storage: loadBlob :: (ByteString -> a) -> Load a
- Erebos.Storage: loadCurrentObject :: Load Object
- Erebos.Storage: loadCurrentRef :: Load Ref
- Erebos.Storage: loadDate :: StorableDate a => String -> LoadRec a
- Erebos.Storage: loadEmpty :: String -> LoadRec ()
- Erebos.Storage: loadHeadRaw :: MonadIO m => Storage -> HeadTypeID -> HeadID -> m (Maybe Ref)
- Erebos.Storage: loadInt :: Num a => String -> LoadRec a
- Erebos.Storage: loadMbBinary :: ByteArray a => String -> LoadRec (Maybe a)
- Erebos.Storage: loadMbDate :: StorableDate a => String -> LoadRec (Maybe a)
- Erebos.Storage: loadMbEmpty :: String -> LoadRec (Maybe ())
- Erebos.Storage: loadMbInt :: Num a => String -> LoadRec (Maybe a)
- Erebos.Storage: loadMbNum :: (Real a, Fractional a) => String -> LoadRec (Maybe a)
- Erebos.Storage: loadMbRawRef :: String -> LoadRec (Maybe Ref)
- Erebos.Storage: loadMbRef :: Storable a => String -> LoadRec (Maybe a)
- Erebos.Storage: loadMbText :: StorableText a => String -> LoadRec (Maybe a)
- Erebos.Storage: loadMbUUID :: StorableUUID a => String -> LoadRec (Maybe a)
- Erebos.Storage: loadNum :: (Real a, Fractional a) => String -> LoadRec a
- Erebos.Storage: loadRawRef :: String -> LoadRec Ref
- Erebos.Storage: loadRawRefs :: String -> LoadRec [Ref]
- Erebos.Storage: loadRec :: LoadRec a -> Load a
- Erebos.Storage: loadRecCurrentRef :: LoadRec Ref
- Erebos.Storage: loadRecItems :: LoadRec [(ByteString, RecItem)]
- Erebos.Storage: loadRef :: Storable a => String -> LoadRec a
- Erebos.Storage: loadRefs :: Storable a => String -> LoadRec [a]
- Erebos.Storage: loadText :: StorableText a => String -> LoadRec a
- Erebos.Storage: loadTexts :: StorableText a => String -> LoadRec [a]
- Erebos.Storage: loadUUID :: StorableUUID a => String -> LoadRec a
- Erebos.Storage: loadZRef :: ZeroStorable a => String -> LoadRec a
- Erebos.Storage: loadZero :: a -> Load a
- Erebos.Storage: makeStoreInfo :: IO StoreInfo
- Erebos.Storage: mkHeadTypeID :: String -> HeadTypeID
- Erebos.Storage: modifyHistory :: Storable a => StoreInfo -> (a -> a) -> StoredHistory a -> IO (StoredHistory a)
- Erebos.Storage: partialRef :: PartialStorage -> Ref -> PartialRef
- Erebos.Storage: partialRefFromDigest :: PartialStorage -> RefDigest -> PartialRef
- Erebos.Storage: readRef :: Storage -> ByteString -> IO (Maybe Ref)
- Erebos.Storage: refDigest :: forall (c :: Type -> Type). Ref' c -> RefDigest
- Erebos.Storage: refDigestFromByteString :: ByteArrayAccess ba => ba -> Maybe RefDigest
- Erebos.Storage: refFromDigest :: forall (c :: Type -> Type). Storage' c -> RefDigest -> IO (Maybe (Ref' c))
- Erebos.Storage: replaceHeadRaw :: MonadIO m => Storage -> HeadTypeID -> HeadID -> Ref -> Ref -> m (Either (Maybe Ref) Ref)
- Erebos.Storage: serializeObject :: forall (c :: Type -> Type). Object' c -> ByteString
- Erebos.Storage: showRef :: forall (c :: Type -> Type). Ref' c -> ByteString
- Erebos.Storage: showRefDigest :: RefDigest -> ByteString
- Erebos.Storage: store :: forall (c :: Type -> Type). (Storable a, StorageCompleteness c) => Storage' c -> a -> IO (Ref' c)
- Erebos.Storage: store' :: Storable a => a -> Store
- Erebos.Storage: storeBinary :: forall a (c :: Type -> Type). ByteArrayAccess a => String -> a -> StoreRec c
- Erebos.Storage: storeBlob :: ByteString -> Store
- Erebos.Storage: storeDate :: forall a (c :: Type -> Type). StorableDate a => String -> a -> StoreRec c
- Erebos.Storage: storeEmpty :: forall (c :: Type -> Type). String -> StoreRec c
- Erebos.Storage: storeHeadRaw :: MonadIO m => Storage -> HeadTypeID -> Ref -> m HeadID
- Erebos.Storage: storeInt :: forall a (c :: Type -> Type). Integral a => String -> a -> StoreRec c
- Erebos.Storage: storeMbBinary :: forall a (c :: Type -> Type). ByteArrayAccess a => String -> Maybe a -> StoreRec c
- Erebos.Storage: storeMbDate :: forall a (c :: Type -> Type). StorableDate a => String -> Maybe a -> StoreRec c
- Erebos.Storage: storeMbEmpty :: forall (c :: Type -> Type). String -> Maybe () -> StoreRec c
- Erebos.Storage: storeMbInt :: forall a (c :: Type -> Type). Integral a => String -> Maybe a -> StoreRec c
- Erebos.Storage: storeMbNum :: forall a (c :: Type -> Type). (Real a, Fractional a) => String -> Maybe a -> StoreRec c
- Erebos.Storage: storeMbRawRef :: forall (c :: Type -> Type). StorageCompleteness c => String -> Maybe Ref -> StoreRec c
- Erebos.Storage: storeMbRef :: forall a (c :: Type -> Type). (Storable a, StorageCompleteness c) => String -> Maybe a -> StoreRec c
- Erebos.Storage: storeMbText :: forall a (c :: Type -> Type). StorableText a => String -> Maybe a -> StoreRec c
- Erebos.Storage: storeMbUUID :: forall a (c :: Type -> Type). StorableUUID a => String -> Maybe a -> StoreRec c
- Erebos.Storage: storeNum :: forall a (c :: Type -> Type). (Real a, Fractional a) => String -> a -> StoreRec c
- Erebos.Storage: storeObject :: PartialStorage -> PartialObject -> IO PartialRef
- Erebos.Storage: storeRawBytes :: PartialStorage -> ByteString -> IO PartialRef
- Erebos.Storage: storeRawRef :: forall (c :: Type -> Type). StorageCompleteness c => String -> Ref -> StoreRec c
- Erebos.Storage: storeRec :: (forall (c :: Type -> Type). StorageCompleteness c => StoreRec c) -> Store
- Erebos.Storage: storeRecItems :: forall (c :: Type -> Type). StorageCompleteness c => [(ByteString, RecItem)] -> StoreRec c
- Erebos.Storage: storeRef :: forall a (c :: Type -> Type). (Storable a, StorageCompleteness c) => String -> a -> StoreRec c
- Erebos.Storage: storeText :: forall a (c :: Type -> Type). StorableText a => String -> a -> StoreRec c
- Erebos.Storage: storeUUID :: forall a (c :: Type -> Type). StorableUUID a => String -> a -> StoreRec c
- Erebos.Storage: storeZRef :: forall a (c :: Type -> Type). (ZeroStorable a, StorageCompleteness c) => String -> a -> StoreRec c
- Erebos.Storage: storeZero :: Store
- Erebos.Storage: storedFromHistory :: StoredHistory a -> Stored a
- Erebos.Storage: storedHistoryList :: StoredHistory a -> [Stored a]
- Erebos.Storage: storedRef :: Stored a -> Ref
- Erebos.Storage: toDate :: StorableDate a => a -> ZonedTime
- Erebos.Storage: toText :: StorableText a => a -> Text
- Erebos.Storage: toUUID :: StorableUUID a => a -> UUID
- Erebos.Storage: type Head = Head' Complete
- Erebos.Storage: type Object = Object' Complete
- Erebos.Storage: type PartialObject = Object' Partial
- Erebos.Storage: type PartialRef = Ref' Partial
- Erebos.Storage: type RecItem = RecItem' Complete
- Erebos.Storage: type Ref = Ref' Complete
- Erebos.Storage: type StoreRec (c :: Type -> Type) = StoreRecM c ()
- Erebos.Storage: type Stored a = Stored' Complete a
- Erebos.Storage: type StoredHistory a = Stored History a
- Erebos.Storage: unsafeMapStored :: (a -> b) -> Stored a -> Stored b
- Erebos.Storage: wrappedLoad :: Storable a => Ref -> Stored a
- Erebos.Storage: wrappedStore :: (MonadIO m, Storable a) => Storage -> a -> m (Stored a)
- Erebos.Storage.Internal: Generation :: Int -> Generation
- Erebos.Storage.Internal: Head :: HeadID -> Stored' c a -> Head' (c :: Type -> Type) a
- Erebos.Storage.Internal: HeadID :: UUID -> HeadID
- Erebos.Storage.Internal: HeadTypeID :: UUID -> HeadTypeID
- Erebos.Storage.Internal: Ref :: Storage' c -> RefDigest -> Ref' (c :: Type -> Type)
- Erebos.Storage.Internal: RefDigest :: Digest Blake2b_256 -> RefDigest
- Erebos.Storage.Internal: Storage :: StorageBacking c -> Maybe (Storage' Identity) -> MVar (BasicHashTable RefDigest Generation) -> MVar (BasicHashTable RefDigest [RefDigest]) -> Storage' (c :: Type -> Type)
- Erebos.Storage.Internal: StorageDir :: FilePath -> MVar (Maybe WatchManager, [HeadTypeID], WatchList c) -> StorageBacking (c :: Type -> Type)
- Erebos.Storage.Internal: StorageMemory :: MVar [((HeadTypeID, HeadID), Ref' c)] -> MVar (Map RefDigest ByteString) -> MVar (Map RefDigest ScrubbedBytes) -> MVar (WatchList c) -> StorageBacking (c :: Type -> Type)
- Erebos.Storage.Internal: Stored :: Ref' c -> a -> Stored' (c :: Type -> Type) a
- Erebos.Storage.Internal: WatchID :: Int -> WatchID
- Erebos.Storage.Internal: WatchList :: WatchID -> [WatchListItem c] -> WatchList (c :: Type -> Type)
- Erebos.Storage.Internal: WatchListItem :: WatchID -> (HeadTypeID, HeadID) -> (Ref' c -> IO ()) -> WatchListItem (c :: Type -> Type)
- Erebos.Storage.Internal: [dirPath] :: StorageBacking (c :: Type -> Type) -> FilePath
- Erebos.Storage.Internal: [dirWatchers] :: StorageBacking (c :: Type -> Type) -> MVar (Maybe WatchManager, [HeadTypeID], WatchList c)
- Erebos.Storage.Internal: [memHeads] :: StorageBacking (c :: Type -> Type) -> MVar [((HeadTypeID, HeadID), Ref' c)]
- Erebos.Storage.Internal: [memKeys] :: StorageBacking (c :: Type -> Type) -> MVar (Map RefDigest ScrubbedBytes)
- Erebos.Storage.Internal: [memObjs] :: StorageBacking (c :: Type -> Type) -> MVar (Map RefDigest ByteString)
- Erebos.Storage.Internal: [memWatchers] :: StorageBacking (c :: Type -> Type) -> MVar (WatchList c)
- Erebos.Storage.Internal: [stBacking] :: Storage' (c :: Type -> Type) -> StorageBacking c
- Erebos.Storage.Internal: [stParent] :: Storage' (c :: Type -> Type) -> Maybe (Storage' Identity)
- Erebos.Storage.Internal: [stRefGeneration] :: Storage' (c :: Type -> Type) -> MVar (BasicHashTable RefDigest Generation)
- Erebos.Storage.Internal: [stRefRoots] :: Storage' (c :: Type -> Type) -> MVar (BasicHashTable RefDigest [RefDigest])
- Erebos.Storage.Internal: [wlFun] :: WatchListItem (c :: Type -> Type) -> Ref' c -> IO ()
- Erebos.Storage.Internal: [wlHead] :: WatchListItem (c :: Type -> Type) -> (HeadTypeID, HeadID)
- Erebos.Storage.Internal: [wlID] :: WatchListItem (c :: Type -> Type) -> WatchID
- Erebos.Storage.Internal: [wlList] :: WatchList (c :: Type -> Type) -> [WatchListItem c]
- Erebos.Storage.Internal: [wlNext] :: WatchList (c :: Type -> Type) -> WatchID
- Erebos.Storage.Internal: class (Traversable compl, Monad compl) => StorageCompleteness (compl :: Type -> Type) where {
- Erebos.Storage.Internal: data Head' (c :: Type -> Type) a
- Erebos.Storage.Internal: data Ref' (c :: Type -> Type)
- Erebos.Storage.Internal: data Storage' (c :: Type -> Type)
- Erebos.Storage.Internal: data StorageBacking (c :: Type -> Type)
- Erebos.Storage.Internal: data Stored' (c :: Type -> Type) a
- Erebos.Storage.Internal: data WatchList (c :: Type -> Type)
- Erebos.Storage.Internal: data WatchListItem (c :: Type -> Type)
- Erebos.Storage.Internal: hashToRefDigest :: ByteString -> RefDigest
- Erebos.Storage.Internal: instance Control.DeepSeq.NFData Erebos.Storage.Internal.RefDigest
- Erebos.Storage.Internal: instance Data.ByteArray.Types.ByteArrayAccess (Erebos.Storage.Internal.Ref' c)
- Erebos.Storage.Internal: instance Data.ByteArray.Types.ByteArrayAccess Erebos.Storage.Internal.RefDigest
- Erebos.Storage.Internal: instance Data.Hashable.Class.Hashable (Erebos.Storage.Internal.Ref' c)
- Erebos.Storage.Internal: instance Data.Hashable.Class.Hashable Erebos.Storage.Internal.RefDigest
- Erebos.Storage.Internal: instance Erebos.Storage.Internal.StorageCompleteness Erebos.Storage.Internal.Complete
- Erebos.Storage.Internal: instance Erebos.Storage.Internal.StorageCompleteness Erebos.Storage.Internal.Partial
- Erebos.Storage.Internal: instance GHC.Classes.Eq (Erebos.Storage.Internal.Head' c a)
- Erebos.Storage.Internal: instance GHC.Classes.Eq (Erebos.Storage.Internal.Ref' c)
- Erebos.Storage.Internal: instance GHC.Classes.Eq (Erebos.Storage.Internal.Storage' c)
- Erebos.Storage.Internal: instance GHC.Classes.Eq (Erebos.Storage.Internal.StorageBacking c)
- Erebos.Storage.Internal: instance GHC.Classes.Eq (Erebos.Storage.Internal.Stored' c a)
- Erebos.Storage.Internal: instance GHC.Classes.Eq Erebos.Storage.Internal.Generation
- Erebos.Storage.Internal: instance GHC.Classes.Eq Erebos.Storage.Internal.HeadID
- Erebos.Storage.Internal: instance GHC.Classes.Eq Erebos.Storage.Internal.HeadTypeID
- Erebos.Storage.Internal: instance GHC.Classes.Eq Erebos.Storage.Internal.RefDigest
- Erebos.Storage.Internal: instance GHC.Classes.Eq Erebos.Storage.Internal.WatchID
- Erebos.Storage.Internal: instance GHC.Classes.Ord (Erebos.Storage.Internal.Stored' c a)
- Erebos.Storage.Internal: instance GHC.Classes.Ord Erebos.Storage.Internal.HeadID
- Erebos.Storage.Internal: instance GHC.Classes.Ord Erebos.Storage.Internal.HeadTypeID
- Erebos.Storage.Internal: instance GHC.Classes.Ord Erebos.Storage.Internal.RefDigest
- Erebos.Storage.Internal: instance GHC.Classes.Ord Erebos.Storage.Internal.WatchID
- Erebos.Storage.Internal: instance GHC.Num.Num Erebos.Storage.Internal.WatchID
- Erebos.Storage.Internal: instance GHC.Show.Show (Erebos.Storage.Internal.Ref' c)
- Erebos.Storage.Internal: instance GHC.Show.Show (Erebos.Storage.Internal.Storage' c)
- Erebos.Storage.Internal: instance GHC.Show.Show Erebos.Storage.Internal.Generation
- Erebos.Storage.Internal: instance GHC.Show.Show Erebos.Storage.Internal.HeadID
- Erebos.Storage.Internal: instance GHC.Show.Show Erebos.Storage.Internal.RefDigest
- Erebos.Storage.Internal: instance GHC.Show.Show a => GHC.Show.Show (Erebos.Storage.Internal.Head' c a)
- Erebos.Storage.Internal: instance GHC.Show.Show a => GHC.Show.Show (Erebos.Storage.Internal.Stored' c a)
- Erebos.Storage.Internal: ioLoadBytes :: StorageCompleteness compl => Ref' compl -> IO (compl ByteString)
- Erebos.Storage.Internal: ioLoadBytesFromStorage :: forall (c :: Type -> Type). Storage' c -> RefDigest -> IO (Maybe ByteString)
- Erebos.Storage.Internal: newtype Generation
- Erebos.Storage.Internal: newtype HeadID
- Erebos.Storage.Internal: newtype HeadTypeID
- Erebos.Storage.Internal: newtype RefDigest
- Erebos.Storage.Internal: newtype WatchID
- Erebos.Storage.Internal: openLockFile :: FilePath -> IO Handle
- Erebos.Storage.Internal: readHex :: ByteArray ba => ByteString -> Maybe ba
- Erebos.Storage.Internal: readRefDigest :: ByteString -> Maybe RefDigest
- Erebos.Storage.Internal: refDigest :: forall (c :: Type -> Type). Ref' c -> RefDigest
- Erebos.Storage.Internal: refDigestFromByteString :: ByteArrayAccess ba => ba -> Maybe RefDigest
- Erebos.Storage.Internal: refPath :: FilePath -> RefDigest -> FilePath
- Erebos.Storage.Internal: refStorage :: forall (c :: Type -> Type). Ref' c -> Storage' c
- Erebos.Storage.Internal: returnLoadResult :: StorageCompleteness compl => compl a -> LoadResult compl a
- Erebos.Storage.Internal: showHex :: ByteArrayAccess ba => ba -> ByteString
- Erebos.Storage.Internal: showParentStorage :: forall (c :: Type -> Type). Storage' c -> String
- Erebos.Storage.Internal: showRef :: forall (c :: Type -> Type). Ref' c -> ByteString
- Erebos.Storage.Internal: showRefDigest :: RefDigest -> ByteString
- Erebos.Storage.Internal: showRefDigestParts :: RefDigest -> (ByteString, ByteString)
- Erebos.Storage.Internal: storedStorage :: forall (c :: Type -> Type) a. Stored' c a -> Storage' c
- Erebos.Storage.Internal: type Complete = Identity
- Erebos.Storage.Internal: type LoadResult (compl :: Type -> Type) a;
- Erebos.Storage.Internal: type Partial = Either RefDigest
- Erebos.Storage.Internal: unsafeStoreRawBytes :: forall (c :: Type -> Type). Storage' c -> ByteString -> IO (Ref' c)
- Erebos.Storage.Internal: writeFileChecked :: FilePath -> Maybe ByteString -> ByteString -> IO (Either (Maybe ByteString) ())
- Erebos.Storage.Internal: writeFileOnce :: FilePath -> ByteString -> IO ()
- Erebos.Storage.Internal: }
- Erebos.Storage.Merge: instance Erebos.Storage.Merge.Mergeable [Erebos.Storage.Stored Erebos.Storage.Object]
- Erebos.Sync: instance Erebos.Storage.Storable Erebos.Sync.SyncService
+ Erebos.Attach: instance Erebos.Object.Internal.Storable Erebos.Attach.AttachIdentity
+ Erebos.Chatroom: instance Erebos.Object.Internal.Storable Erebos.Chatroom.ChatMessageData
+ Erebos.Chatroom: instance Erebos.Object.Internal.Storable Erebos.Chatroom.ChatroomData
+ Erebos.Chatroom: instance Erebos.Object.Internal.Storable Erebos.Chatroom.ChatroomService
+ Erebos.Chatroom: instance Erebos.Object.Internal.Storable Erebos.Chatroom.ChatroomStateData
+ Erebos.Chatroom: isSameChatroom :: ChatroomState -> ChatroomState -> Bool
+ Erebos.Contact: instance Erebos.Object.Internal.Storable Erebos.Contact.ContactAccepted
+ Erebos.Contact: instance Erebos.Object.Internal.Storable Erebos.Contact.ContactData
+ Erebos.Conversation: isChatroomStateConversation :: ChatroomState -> Conversation -> Bool
+ Erebos.Conversation: isSameConversation :: Conversation -> Conversation -> Bool
+ Erebos.DirectMessage: DirectMessage :: ComposedIdentity -> [Stored DirectMessage] -> ZonedTime -> Text -> DirectMessage
+ Erebos.DirectMessage: DirectMessageAttributes :: ServiceHandler DirectMessage () -> DirectMessageAttributes
+ Erebos.DirectMessage: DirectMessageThread :: ComposedIdentity -> [Stored DirectMessage] -> [Stored DirectMessage] -> [Stored DirectMessage] -> [Stored DirectMessage] -> DirectMessageThread
+ Erebos.DirectMessage: [dmOwnerMismatch] :: DirectMessageAttributes -> ServiceHandler DirectMessage ()
+ Erebos.DirectMessage: [msgFrom] :: DirectMessage -> ComposedIdentity
+ Erebos.DirectMessage: [msgHead] :: DirectMessageThread -> [Stored DirectMessage]
+ Erebos.DirectMessage: [msgPeer] :: DirectMessageThread -> ComposedIdentity
+ Erebos.DirectMessage: [msgPrev] :: DirectMessage -> [Stored DirectMessage]
+ Erebos.DirectMessage: [msgReceived] :: DirectMessageThread -> [Stored DirectMessage]
+ Erebos.DirectMessage: [msgSeen] :: DirectMessageThread -> [Stored DirectMessage]
+ Erebos.DirectMessage: [msgSent] :: DirectMessageThread -> [Stored DirectMessage]
+ Erebos.DirectMessage: [msgText] :: DirectMessage -> Text
+ Erebos.DirectMessage: [msgTime] :: DirectMessage -> ZonedTime
+ Erebos.DirectMessage: createOrUpdateDirectMessagePeer :: forall (f :: Type -> Type) m. (Foldable f, Applicative f, MonadHead LocalState m) => Identity f -> m ()
+ Erebos.DirectMessage: data DirectMessage
+ Erebos.DirectMessage: data DirectMessageAttributes
+ Erebos.DirectMessage: data DirectMessageThread
+ Erebos.DirectMessage: data DirectMessageThreads
+ Erebos.DirectMessage: defaultDirectMessageAttributes :: DirectMessageAttributes
+ Erebos.DirectMessage: dmThreadList :: DirectMessageThreads -> [DirectMessageThread]
+ Erebos.DirectMessage: dmThreadToList :: DirectMessageThread -> [DirectMessage]
+ Erebos.DirectMessage: dmThreadToListSince :: DirectMessageThread -> DirectMessageThread -> [DirectMessage]
+ Erebos.DirectMessage: dmThreadView :: [Stored MessageState] -> [DirectMessageThread]
+ Erebos.DirectMessage: formatDirectMessage :: TimeZone -> DirectMessage -> String
+ Erebos.DirectMessage: instance Erebos.Object.Internal.Storable Erebos.DirectMessage.DirectMessage
+ Erebos.DirectMessage: instance Erebos.Object.Internal.Storable Erebos.DirectMessage.MessageState
+ Erebos.DirectMessage: instance Erebos.Service.Service Erebos.DirectMessage.DirectMessage
+ Erebos.DirectMessage: instance Erebos.State.SharedType Erebos.DirectMessage.DirectMessageThreads
+ Erebos.DirectMessage: instance Erebos.Storage.Merge.Mergeable Erebos.DirectMessage.DirectMessageThreads
+ Erebos.DirectMessage: instance GHC.Classes.Eq Erebos.DirectMessage.DirectMessageThreads
+ Erebos.DirectMessage: sendDirectMessage :: forall (f :: Type -> Type) m. (Foldable f, Applicative f, MonadHead LocalState m) => Identity f -> Text -> m ()
+ Erebos.DirectMessage: updateDirectMessagePeer :: forall (f :: Type -> Type) m. (Foldable f, Applicative f, MonadHead LocalState m) => Identity f -> m ()
+ Erebos.DirectMessage: watchDirectMessageThreads :: Head LocalState -> (DirectMessageThread -> DirectMessageThread -> IO ()) -> IO WatchedHead
+ Erebos.Discovery: [dconnTunnel] :: DiscoveryConnection -> Bool
+ Erebos.Discovery: [discoveryProvideTunnel] :: DiscoveryAttributes -> Peer -> PeerAddress -> Bool
+ Erebos.Discovery: discoverySetupTunnel :: Peer -> RefDigest -> IO ()
+ Erebos.Discovery: instance Erebos.Network.PeerAddressType Erebos.Discovery.TunnelAddress
+ Erebos.Discovery: instance Erebos.Object.Internal.Storable Erebos.Discovery.DiscoveryService
+ Erebos.Discovery: instance Erebos.Object.Internal.StorableText Erebos.Discovery.DiscoveryAddress
+ Erebos.Discovery: instance GHC.Classes.Eq Erebos.Discovery.TunnelAddress
+ Erebos.Discovery: instance GHC.Classes.Ord Erebos.Discovery.TunnelAddress
+ Erebos.Discovery: instance GHC.Show.Show Erebos.Discovery.TunnelAddress
+ Erebos.Error: ManyErrors :: [ErebosError] -> ErebosError
+ Erebos.Error: OtherError :: String -> ErebosError
+ Erebos.Error: class FromErebosError e
+ Erebos.Error: data ErebosError
+ Erebos.Error: fromErebosError :: FromErebosError e => ErebosError -> e
+ Erebos.Error: instance Erebos.Error.FromErebosError Erebos.Error.ErebosError
+ Erebos.Error: instance GHC.Base.Monoid Erebos.Error.ErebosError
+ Erebos.Error: instance GHC.Base.Semigroup Erebos.Error.ErebosError
+ Erebos.Error: showErebosError :: ErebosError -> String
+ Erebos.Error: throwOtherError :: (MonadError e m, FromErebosError e) => String -> m a
+ Erebos.Identity: instance Erebos.Object.Internal.Storable Erebos.Identity.ExtendedIdentityData
+ Erebos.Identity: instance Erebos.Object.Internal.Storable Erebos.Identity.IdentityData
+ Erebos.Identity: instance Erebos.Object.Internal.Storable Erebos.Identity.IdentityExtension
+ Erebos.Identity: instance GHC.Classes.Eq (m (Erebos.Storage.Internal.Stored (Erebos.PubKey.Signed Erebos.Identity.ExtendedIdentityData))) => GHC.Classes.Eq (Erebos.Identity.Identity m)
+ Erebos.Identity: instance GHC.Show.Show (m (Erebos.Storage.Internal.Stored (Erebos.PubKey.Signed Erebos.Identity.ExtendedIdentityData))) => GHC.Show.Show (Erebos.Identity.Identity m)
+ Erebos.Network: CustomPeerAddress :: addr -> PeerAddress
+ Erebos.Network: [serverErrorPrefix] :: ServerOptions -> String
+ Erebos.Network: [serverTestLog] :: ServerOptions -> Bool
+ Erebos.Network: class (Eq addr, Ord addr, Show addr, Typeable addr) => PeerAddressType addr
+ Erebos.Network: connectionToAddressClosed :: PeerAddressType addr => addr -> IO ()
+ Erebos.Network: findPeer :: Server -> (Peer -> IO Bool) -> IO (Maybe Peer)
+ Erebos.Network: getPeerAddress :: MonadIO m => Peer -> m PeerAddress
+ Erebos.Network: getPeerAddresses :: MonadIO m => Peer -> m [PeerAddress]
+ Erebos.Network: getPeerIdentity :: MonadIO m => Peer -> m PeerIdentity
+ Erebos.Network: receivedFromCustomAddress :: PeerAddressType addr => Server -> addr -> ByteString -> IO ()
+ Erebos.Network: sendBytesToAddress :: PeerAddressType addr => addr -> ByteString -> IO ()
+ Erebos.Network: serverPeerCustom :: PeerAddressType addr => Server -> addr -> IO Peer
+ Erebos.Network: serviceUpdatedPeer :: Service s => ServiceHandler s ()
+ Erebos.Object: Blob :: ByteString -> Object' (c :: Type -> Type)
+ Erebos.Object: Rec :: [(ByteString, RecItem' c)] -> Object' (c :: Type -> Type)
+ Erebos.Object: RecBinary :: ByteString -> RecItem' (c :: Type -> Type)
+ Erebos.Object: RecDate :: ZonedTime -> RecItem' (c :: Type -> Type)
+ Erebos.Object: RecEmpty :: RecItem' (c :: Type -> Type)
+ Erebos.Object: RecInt :: Integer -> RecItem' (c :: Type -> Type)
+ Erebos.Object: RecNum :: Rational -> RecItem' (c :: Type -> Type)
+ Erebos.Object: RecRef :: Ref' c -> RecItem' (c :: Type -> Type)
+ Erebos.Object: RecText :: Text -> RecItem' (c :: Type -> Type)
+ Erebos.Object: RecUUID :: UUID -> RecItem' (c :: Type -> Type)
+ Erebos.Object: RecUnknown :: ByteString -> ByteString -> RecItem' (c :: Type -> Type)
+ Erebos.Object: RecWeak :: RefDigest -> RecItem' (c :: Type -> Type)
+ Erebos.Object: UnknownObject :: ByteString -> ByteString -> Object' (c :: Type -> Type)
+ Erebos.Object: ZeroObject :: Object' (c :: Type -> Type)
+ Erebos.Object: copyRef :: forall (c :: Type -> Type) (c' :: Type -> Type) m. (StorageCompleteness c, StorageCompleteness c', MonadIO m) => Storage' c' -> Ref' c -> m (LoadResult c (Ref' c'))
+ Erebos.Object: data Object' (c :: Type -> Type)
+ Erebos.Object: data RecItem' (c :: Type -> Type)
+ Erebos.Object: data RefDigest
+ Erebos.Object: deserializeObject :: PartialStorage -> ByteString -> Except ErebosError (PartialObject, ByteString)
+ Erebos.Object: deserializeObjects :: PartialStorage -> ByteString -> Except ErebosError [PartialObject]
+ Erebos.Object: hashToRefDigest :: ByteString -> RefDigest
+ Erebos.Object: ioLoadBytes :: StorageCompleteness compl => Ref' compl -> IO (compl ByteString)
+ Erebos.Object: ioLoadObject :: StorageCompleteness c => Ref' c -> IO (c (Object' c))
+ Erebos.Object: lazyLoadBytes :: forall (c :: Type -> Type). StorageCompleteness c => Ref' c -> LoadResult c ByteString
+ Erebos.Object: partialRef :: PartialStorage -> Ref -> PartialRef
+ Erebos.Object: partialRefFromDigest :: PartialStorage -> RefDigest -> PartialRef
+ Erebos.Object: readRef :: Storage -> ByteString -> IO (Maybe Ref)
+ Erebos.Object: readRefDigest :: ByteString -> Maybe RefDigest
+ Erebos.Object: refDigest :: forall (c :: Type -> Type). Ref' c -> RefDigest
+ Erebos.Object: refDigestFromByteString :: ByteString -> Maybe RefDigest
+ Erebos.Object: refFromDigest :: forall (c :: Type -> Type). Storage' c -> RefDigest -> IO (Maybe (Ref' c))
+ Erebos.Object: serializeObject :: forall (c :: Type -> Type). Object' c -> ByteString
+ Erebos.Object: showRef :: forall (c :: Type -> Type). Ref' c -> ByteString
+ Erebos.Object: showRefDigest :: RefDigest -> ByteString
+ Erebos.Object: storeRawBytes :: PartialStorage -> ByteString -> IO PartialRef
+ Erebos.Object: type Object = Object' Complete
+ Erebos.Object: type PartialObject = Object' Partial
+ Erebos.Object: type PartialRef = Ref' Partial
+ Erebos.Object: type RecItem = RecItem' Complete
+ Erebos.Object: type Ref = Ref' Complete
+ Erebos.Pairing: instance Erebos.Object.Internal.Storable a => Erebos.Object.Internal.Storable (Erebos.Pairing.PairingService a)
+ Erebos.PubKey: instance Erebos.Object.Internal.Storable Erebos.PubKey.PublicKexKey
+ Erebos.PubKey: instance Erebos.Object.Internal.Storable Erebos.PubKey.PublicKey
+ Erebos.PubKey: instance Erebos.Object.Internal.Storable Erebos.PubKey.Signature
+ Erebos.PubKey: instance Erebos.Object.Internal.Storable a => Erebos.Object.Internal.Storable (Erebos.PubKey.Signed a)
+ Erebos.Service: ServiceOpenStream :: (RawStreamWriter -> IO ()) -> ServiceReply s
+ Erebos.Service: [svcNewStreams] :: ServiceInput s -> [RawStreamReader]
+ Erebos.Service: [svcPeerAddress] :: ServiceInput s -> PeerAddress
+ Erebos.Service: instance Control.Monad.Error.Class.MonadError Erebos.Error.ErebosError (Erebos.Service.ServiceHandler s)
+ Erebos.Service: instance Erebos.Object.Internal.MonadStorage (Erebos.Service.ServiceHandler s)
+ Erebos.Service: serviceUpdatedPeer :: Service s => ServiceHandler s ()
+ Erebos.Service.Stream: StreamClosed :: Word64 -> StreamPacket
+ Erebos.Service.Stream: StreamData :: Word64 -> ByteString -> StreamPacket
+ Erebos.Service.Stream: [stpData] :: StreamPacket -> ByteString
+ Erebos.Service.Stream: [stpSequence] :: StreamPacket -> Word64
+ Erebos.Service.Stream: closeStream :: StreamWriter -> IO ()
+ Erebos.Service.Stream: data StreamPacket
+ Erebos.Service.Stream: data StreamReader
+ Erebos.Service.Stream: data StreamWriter
+ Erebos.Service.Stream: getStreamReaderNumber :: StreamReader -> IO Int
+ Erebos.Service.Stream: getStreamWriterNumber :: StreamWriter -> IO Int
+ Erebos.Service.Stream: openStream :: Service s => ServiceHandler s StreamWriter
+ Erebos.Service.Stream: readStreamPacket :: StreamReader -> IO StreamPacket
+ Erebos.Service.Stream: receivedStreams :: Service s => ServiceHandler s [StreamReader]
+ Erebos.Service.Stream: writeStream :: StreamWriter -> ByteString -> IO ()
+ Erebos.Service.Stream: writeStreamPacket :: StreamWriter -> StreamPacket -> IO ()
+ Erebos.Set: instance Erebos.Object.Internal.Storable a => Erebos.Object.Internal.Storable (Erebos.Set.SetItem a)
+ Erebos.State: ($dmmstore) :: (MonadStorage m, MonadIO m, Storable a) => a -> m (Stored a)
+ Erebos.State: LocalHeadT :: (Storage -> Stored h -> m (a, Stored h)) -> LocalHeadT h (m :: Type -> Type) a
+ Erebos.State: SharedState :: [Stored SharedState] -> Maybe SharedTypeID -> [Ref] -> SharedState
+ Erebos.State: [lsPrev] :: LocalState -> Maybe RefDigest
+ Erebos.State: [runLocalHeadT] :: LocalHeadT h (m :: Type -> Type) a -> Storage -> Stored h -> m (a, Stored h)
+ Erebos.State: [ssPrev] :: SharedState -> [Stored SharedState]
+ Erebos.State: [ssType] :: SharedState -> Maybe SharedTypeID
+ Erebos.State: [ssValue] :: SharedState -> [Ref]
+ Erebos.State: class Monad m => MonadStorage (m :: Type -> Type)
+ Erebos.State: getStorage :: MonadStorage m => m Storage
+ Erebos.State: instance (Erebos.Storage.Head.HeadType a, Control.Monad.IO.Class.MonadIO m) => Erebos.State.MonadHead a (Control.Monad.Trans.Reader.ReaderT (Erebos.Storage.Head.Head a) m)
+ Erebos.State: instance (Erebos.Storage.Head.HeadType h, Control.Monad.IO.Class.MonadIO m) => Erebos.State.MonadHead h (Erebos.State.LocalHeadT h m)
+ Erebos.State: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Erebos.State.LocalHeadT h m)
+ Erebos.State: instance Control.Monad.IO.Class.MonadIO m => Erebos.Object.Internal.MonadStorage (Erebos.State.LocalHeadT h m)
+ Erebos.State: instance Erebos.Object.Internal.Storable Erebos.State.LocalState
+ Erebos.State: instance Erebos.Object.Internal.Storable Erebos.State.SharedState
+ Erebos.State: instance Erebos.Object.Internal.StorableUUID Erebos.State.SharedTypeID
+ Erebos.State: instance Erebos.Storage.Head.HeadType Erebos.State.LocalState
+ Erebos.State: instance GHC.Base.Functor m => GHC.Base.Functor (Erebos.State.LocalHeadT h m)
+ Erebos.State: instance GHC.Base.Monad m => GHC.Base.Applicative (Erebos.State.LocalHeadT h m)
+ Erebos.State: instance GHC.Base.Monad m => GHC.Base.Monad (Erebos.State.LocalHeadT h m)
+ Erebos.State: mstore :: (MonadStorage m, Storable a) => a -> m (Stored a)
+ Erebos.State: newtype LocalHeadT h (m :: Type -> Type) a
+ Erebos.State: updateLocalState :: MonadHead LocalState m => (Stored LocalState -> m (Stored LocalState, b)) -> m b
+ Erebos.State: updateLocalState_ :: MonadHead LocalState m => (Stored LocalState -> m (Stored LocalState)) -> m ()
+ Erebos.Storable: ($dmmstore) :: (MonadStorage m, MonadIO m, Storable a) => a -> m (Stored a)
+ Erebos.Storable: class Monad m => MonadStorage (m :: Type -> Type)
+ Erebos.Storable: class Storable a
+ Erebos.Storable: class StorableDate a
+ Erebos.Storable: class StorableText a
+ Erebos.Storable: class StorableUUID a
+ Erebos.Storable: class Storable a => ZeroStorable a
+ Erebos.Storable: copyStored :: MonadIO m => Storage -> Stored a -> m (Stored a)
+ Erebos.Storable: data Load a
+ Erebos.Storable: data LoadRec a
+ Erebos.Storable: data Store
+ Erebos.Storable: data Stored a
+ Erebos.Storable: fromDate :: StorableDate a => ZonedTime -> a
+ Erebos.Storable: fromStored :: Stored a -> a
+ Erebos.Storable: fromText :: (StorableText a, MonadError ErebosError m) => Text -> m a
+ Erebos.Storable: fromUUID :: StorableUUID a => UUID -> a
+ Erebos.Storable: fromZero :: ZeroStorable a => Storage -> a
+ Erebos.Storable: getStorage :: MonadStorage m => m Storage
+ Erebos.Storable: load :: Storable a => Ref -> a
+ Erebos.Storable: load' :: Storable a => Load a
+ Erebos.Storable: loadBinaries :: ByteArray a => String -> LoadRec [a]
+ Erebos.Storable: loadBinary :: ByteArray a => String -> LoadRec a
+ Erebos.Storable: loadBlob :: (ByteString -> a) -> Load a
+ Erebos.Storable: loadCurrentObject :: Load Object
+ Erebos.Storable: loadCurrentRef :: Load Ref
+ Erebos.Storable: loadDate :: StorableDate a => String -> LoadRec a
+ Erebos.Storable: loadEmpty :: String -> LoadRec ()
+ Erebos.Storable: loadInt :: Num a => String -> LoadRec a
+ Erebos.Storable: loadMbBinary :: ByteArray a => String -> LoadRec (Maybe a)
+ Erebos.Storable: loadMbDate :: StorableDate a => String -> LoadRec (Maybe a)
+ Erebos.Storable: loadMbEmpty :: String -> LoadRec (Maybe ())
+ Erebos.Storable: loadMbInt :: Num a => String -> LoadRec (Maybe a)
+ Erebos.Storable: loadMbNum :: (Real a, Fractional a) => String -> LoadRec (Maybe a)
+ Erebos.Storable: loadMbRawRef :: String -> LoadRec (Maybe Ref)
+ Erebos.Storable: loadMbRawWeak :: String -> LoadRec (Maybe RefDigest)
+ Erebos.Storable: loadMbRef :: Storable a => String -> LoadRec (Maybe a)
+ Erebos.Storable: loadMbText :: StorableText a => String -> LoadRec (Maybe a)
+ Erebos.Storable: loadMbUUID :: StorableUUID a => String -> LoadRec (Maybe a)
+ Erebos.Storable: loadNum :: (Real a, Fractional a) => String -> LoadRec a
+ Erebos.Storable: loadRawRef :: String -> LoadRec Ref
+ Erebos.Storable: loadRawRefs :: String -> LoadRec [Ref]
+ Erebos.Storable: loadRawWeak :: String -> LoadRec RefDigest
+ Erebos.Storable: loadRawWeaks :: String -> LoadRec [RefDigest]
+ Erebos.Storable: loadRec :: LoadRec a -> Load a
+ Erebos.Storable: loadRecCurrentRef :: LoadRec Ref
+ Erebos.Storable: loadRecItems :: LoadRec [(ByteString, RecItem)]
+ Erebos.Storable: loadRef :: Storable a => String -> LoadRec a
+ Erebos.Storable: loadRefs :: Storable a => String -> LoadRec [a]
+ Erebos.Storable: loadText :: StorableText a => String -> LoadRec a
+ Erebos.Storable: loadTexts :: StorableText a => String -> LoadRec [a]
+ Erebos.Storable: loadUUID :: StorableUUID a => String -> LoadRec a
+ Erebos.Storable: loadZRef :: ZeroStorable a => String -> LoadRec a
+ Erebos.Storable: loadZero :: a -> Load a
+ Erebos.Storable: mstore :: (MonadStorage m, Storable a) => a -> m (Stored a)
+ Erebos.Storable: store :: forall (c :: Type -> Type). (Storable a, StorageCompleteness c) => Storage' c -> a -> IO (Ref' c)
+ Erebos.Storable: store' :: Storable a => a -> Store
+ Erebos.Storable: storeBinary :: forall a (c :: Type -> Type). ByteArrayAccess a => String -> a -> StoreRec c
+ Erebos.Storable: storeBlob :: ByteString -> Store
+ Erebos.Storable: storeDate :: forall a (c :: Type -> Type). StorableDate a => String -> a -> StoreRec c
+ Erebos.Storable: storeEmpty :: forall (c :: Type -> Type). String -> StoreRec c
+ Erebos.Storable: storeInt :: forall a (c :: Type -> Type). Integral a => String -> a -> StoreRec c
+ Erebos.Storable: storeMbBinary :: forall a (c :: Type -> Type). ByteArrayAccess a => String -> Maybe a -> StoreRec c
+ Erebos.Storable: storeMbDate :: forall a (c :: Type -> Type). StorableDate a => String -> Maybe a -> StoreRec c
+ Erebos.Storable: storeMbEmpty :: forall (c :: Type -> Type). String -> Maybe () -> StoreRec c
+ Erebos.Storable: storeMbInt :: forall a (c :: Type -> Type). Integral a => String -> Maybe a -> StoreRec c
+ Erebos.Storable: storeMbNum :: forall a (c :: Type -> Type). (Real a, Fractional a) => String -> Maybe a -> StoreRec c
+ Erebos.Storable: storeMbRawRef :: forall (c :: Type -> Type). StorageCompleteness c => String -> Maybe Ref -> StoreRec c
+ Erebos.Storable: storeMbRawWeak :: forall (c :: Type -> Type). StorageCompleteness c => String -> Maybe RefDigest -> StoreRec c
+ Erebos.Storable: storeMbRef :: forall a (c :: Type -> Type). (Storable a, StorageCompleteness c) => String -> Maybe a -> StoreRec c
+ Erebos.Storable: storeMbText :: forall a (c :: Type -> Type). StorableText a => String -> Maybe a -> StoreRec c
+ Erebos.Storable: storeMbUUID :: forall a (c :: Type -> Type). StorableUUID a => String -> Maybe a -> StoreRec c
+ Erebos.Storable: storeMbWeak :: forall a (c :: Type -> Type). (Storable a, StorageCompleteness c) => String -> Maybe a -> StoreRec c
+ Erebos.Storable: storeNum :: forall a (c :: Type -> Type). (Real a, Fractional a) => String -> a -> StoreRec c
+ Erebos.Storable: storeRawRef :: forall (c :: Type -> Type). StorageCompleteness c => String -> Ref -> StoreRec c
+ Erebos.Storable: storeRawWeak :: forall (c :: Type -> Type). StorageCompleteness c => String -> RefDigest -> StoreRec c
+ Erebos.Storable: storeRec :: (forall (c :: Type -> Type). StorageCompleteness c => StoreRec c) -> Store
+ Erebos.Storable: storeRecItems :: forall (c :: Type -> Type). StorageCompleteness c => [(ByteString, RecItem)] -> StoreRec c
+ Erebos.Storable: storeRef :: forall a (c :: Type -> Type). (Storable a, StorageCompleteness c) => String -> a -> StoreRec c
+ Erebos.Storable: storeText :: forall a (c :: Type -> Type). StorableText a => String -> a -> StoreRec c
+ Erebos.Storable: storeUUID :: forall a (c :: Type -> Type). StorableUUID a => String -> a -> StoreRec c
+ Erebos.Storable: storeWeak :: forall a (c :: Type -> Type). (Storable a, StorageCompleteness c) => String -> a -> StoreRec c
+ Erebos.Storable: storeZRef :: forall a (c :: Type -> Type). (ZeroStorable a, StorageCompleteness c) => String -> a -> StoreRec c
+ Erebos.Storable: storeZWeak :: forall a (c :: Type -> Type). (ZeroStorable a, StorageCompleteness c) => String -> a -> StoreRec c
+ Erebos.Storable: storeZero :: Store
+ Erebos.Storable: storedRef :: Stored a -> Ref
+ Erebos.Storable: toDate :: StorableDate a => a -> ZonedTime
+ Erebos.Storable: toText :: StorableText a => a -> Text
+ Erebos.Storable: toUUID :: StorableUUID a => a -> UUID
+ Erebos.Storable: type Storage = Storage' Complete
+ Erebos.Storable: type StoreRec (c :: Type -> Type) = StoreRecM c ()
+ Erebos.Storable: unsafeMapStored :: (a -> b) -> Stored a -> Stored b
+ Erebos.Storable: wrappedLoad :: Storable a => Ref -> Stored a
+ Erebos.Storable: wrappedStore :: (MonadIO m, Storable a) => Storage -> a -> m (Stored a)
+ Erebos.Storage: data Head a
+ Erebos.Storage: data HeadID
+ Erebos.Storage.Backend: ($dmbackendListKeys) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> IO [RefDigest]
+ Erebos.Storage.Backend: ($dmbackendLoadBytes) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> RefDigest -> IO (Maybe ByteString)
+ Erebos.Storage.Backend: ($dmbackendLoadHead) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> HeadTypeID -> HeadID -> IO (Maybe RefDigest)
+ Erebos.Storage.Backend: ($dmbackendLoadHeads) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> HeadTypeID -> IO [(HeadID, RefDigest)]
+ Erebos.Storage.Backend: ($dmbackendLoadKey) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> RefDigest -> IO (Maybe ScrubbedBytes)
+ Erebos.Storage.Backend: ($dmbackendParent) :: (StorageBackend bck, BackendParent bck ~ ()) => bck -> BackendParent bck
+ Erebos.Storage.Backend: ($dmbackendRemoveKey) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> RefDigest -> IO ()
+ Erebos.Storage.Backend: ($dmbackendReplaceHead) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> HeadTypeID -> HeadID -> RefDigest -> RefDigest -> IO (Either (Maybe RefDigest) RefDigest)
+ Erebos.Storage.Backend: ($dmbackendStoreBytes) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> RefDigest -> ByteString -> IO ()
+ Erebos.Storage.Backend: ($dmbackendStoreHead) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> HeadTypeID -> HeadID -> RefDigest -> IO ()
+ Erebos.Storage.Backend: ($dmbackendStoreKey) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> RefDigest -> ScrubbedBytes -> IO ()
+ Erebos.Storage.Backend: ($dmbackendUnwatchHead) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> WatchID -> IO ()
+ Erebos.Storage.Backend: ($dmbackendWatchHead) :: (StorageBackend bck, BackendParent bck ~ Storage) => bck -> HeadTypeID -> HeadID -> (RefDigest -> IO ()) -> IO WatchID
+ Erebos.Storage.Backend: backendListKeys :: StorageBackend bck => bck -> IO [RefDigest]
+ Erebos.Storage.Backend: backendLoadBytes :: StorageBackend bck => bck -> RefDigest -> IO (Maybe ByteString)
+ Erebos.Storage.Backend: backendLoadHead :: StorageBackend bck => bck -> HeadTypeID -> HeadID -> IO (Maybe RefDigest)
+ Erebos.Storage.Backend: backendLoadHeads :: StorageBackend bck => bck -> HeadTypeID -> IO [(HeadID, RefDigest)]
+ Erebos.Storage.Backend: backendLoadKey :: StorageBackend bck => bck -> RefDigest -> IO (Maybe ScrubbedBytes)
+ Erebos.Storage.Backend: backendParent :: StorageBackend bck => bck -> BackendParent bck
+ Erebos.Storage.Backend: backendRemoveKey :: StorageBackend bck => bck -> RefDigest -> IO ()
+ Erebos.Storage.Backend: backendReplaceHead :: StorageBackend bck => bck -> HeadTypeID -> HeadID -> RefDigest -> RefDigest -> IO (Either (Maybe RefDigest) RefDigest)
+ Erebos.Storage.Backend: backendStoreBytes :: StorageBackend bck => bck -> RefDigest -> ByteString -> IO ()
+ Erebos.Storage.Backend: backendStoreHead :: StorageBackend bck => bck -> HeadTypeID -> HeadID -> RefDigest -> IO ()
+ Erebos.Storage.Backend: backendStoreKey :: StorageBackend bck => bck -> RefDigest -> ScrubbedBytes -> IO ()
+ Erebos.Storage.Backend: backendUnwatchHead :: StorageBackend bck => bck -> WatchID -> IO ()
+ Erebos.Storage.Backend: backendWatchHead :: StorageBackend bck => bck -> HeadTypeID -> HeadID -> (RefDigest -> IO ()) -> IO WatchID
+ Erebos.Storage.Backend: class (Eq bck, Show bck, Typeable bck, Typeable BackendParent bck) => StorageBackend bck where {
+ Erebos.Storage.Backend: data WatchID
+ Erebos.Storage.Backend: newStorage :: StorageBackend bck => bck -> IO (Storage' (BackendCompleteness bck))
+ Erebos.Storage.Backend: nextWatchID :: WatchID -> WatchID
+ Erebos.Storage.Backend: startWatchID :: WatchID
+ Erebos.Storage.Backend: type BackendCompleteness bck = Complete;
+ Erebos.Storage.Backend: type BackendParent bck = ();
+ Erebos.Storage.Backend: type Complete = Identity
+ Erebos.Storage.Backend: type Partial = Either RefDigest
+ Erebos.Storage.Backend: type PartialStorage = Storage' Partial
+ Erebos.Storage.Backend: type Storage = Storage' Complete
+ Erebos.Storage.Backend: }
+ Erebos.Storage.Head: class Storable a => HeadType a
+ Erebos.Storage.Head: data Head a
+ Erebos.Storage.Head: data HeadID
+ Erebos.Storage.Head: data HeadTypeID
+ Erebos.Storage.Head: data WatchedHead
+ Erebos.Storage.Head: headId :: Head a -> HeadID
+ Erebos.Storage.Head: headObject :: Head a -> a
+ Erebos.Storage.Head: headRef :: Head a -> Ref
+ Erebos.Storage.Head: headStorage :: Head a -> Storage
+ Erebos.Storage.Head: headStoredObject :: Head a -> Stored a
+ Erebos.Storage.Head: headTypeID :: HeadType a => proxy a -> HeadTypeID
+ Erebos.Storage.Head: instance Control.Monad.IO.Class.MonadIO m => Erebos.Object.Internal.MonadStorage (Control.Monad.Trans.Reader.ReaderT (Erebos.Storage.Head.Head a) m)
+ Erebos.Storage.Head: instance GHC.Classes.Eq (Erebos.Storage.Head.Head a)
+ Erebos.Storage.Head: instance GHC.Show.Show a => GHC.Show.Show (Erebos.Storage.Head.Head a)
+ Erebos.Storage.Head: loadHead :: (HeadType a, MonadIO m) => Storage -> HeadID -> m (Maybe (Head a))
+ Erebos.Storage.Head: loadHeadRaw :: MonadIO m => Storage -> HeadTypeID -> HeadID -> m (Maybe Ref)
+ Erebos.Storage.Head: loadHeads :: forall a m. (MonadIO m, HeadType a) => Storage -> m [Head a]
+ Erebos.Storage.Head: mkHeadTypeID :: String -> HeadTypeID
+ Erebos.Storage.Head: reloadHead :: (HeadType a, MonadIO m) => Head a -> m (Maybe (Head a))
+ Erebos.Storage.Head: replaceHead :: (HeadType a, MonadIO m) => Head a -> Stored a -> m (Either (Maybe (Head a)) (Head a))
+ Erebos.Storage.Head: replaceHeadRaw :: MonadIO m => Storage -> HeadTypeID -> HeadID -> Ref -> Ref -> m (Either (Maybe Ref) Ref)
+ Erebos.Storage.Head: storeHead :: forall a m. (MonadIO m, HeadType a) => Storage -> a -> m (Head a)
+ Erebos.Storage.Head: storeHeadRaw :: MonadIO m => Storage -> HeadTypeID -> Ref -> m HeadID
+ Erebos.Storage.Head: unwatchHead :: WatchedHead -> IO ()
+ Erebos.Storage.Head: updateHead :: (HeadType a, MonadIO m) => Head a -> (Stored a -> m (Stored a, b)) -> m (Maybe (Head a), b)
+ Erebos.Storage.Head: updateHead_ :: (HeadType a, MonadIO m) => Head a -> (Stored a -> m (Stored a)) -> m (Maybe (Head a))
+ Erebos.Storage.Head: watchHead :: HeadType a => Head a -> (Head a -> IO ()) -> IO WatchedHead
+ Erebos.Storage.Head: watchHeadRaw :: Eq b => Storage -> HeadTypeID -> HeadID -> (Ref -> b) -> (b -> IO ()) -> IO WatchedHead
+ Erebos.Storage.Head: watchHeadWith :: (HeadType a, Eq b) => Head a -> (Head a -> b) -> (b -> IO ()) -> IO WatchedHead
+ Erebos.Storage.Merge: generationsBy :: Ord a => (a -> [a]) -> [a] -> NonEmpty (Set a)
+ Erebos.Storage.Merge: instance Erebos.Storage.Merge.Mergeable [Erebos.Storage.Internal.Stored Erebos.Object.Internal.Object]
+ Erebos.Storage.Merge: storedDifference :: Storable a => [Stored a] -> [Stored a] -> [Stored a]
+ Erebos.Sync: instance Erebos.Object.Internal.Storable Erebos.Sync.SyncService
- Erebos.Attach: attachAccept :: (MonadIO m, MonadError String m) => Peer -> m ()
+ Erebos.Attach: attachAccept :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m ()
- Erebos.Attach: attachReject :: (MonadIO m, MonadError String m) => Peer -> m ()
+ Erebos.Attach: attachReject :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m ()
- Erebos.Attach: attachToOwner :: (MonadIO m, MonadError String m) => Peer -> m ()
+ Erebos.Attach: attachToOwner :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m ()
- Erebos.Chatroom: chatroomSetSubscribe :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => Stored ChatroomStateData -> Bool -> m ()
+ Erebos.Chatroom: chatroomSetSubscribe :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> Bool -> m ()
- Erebos.Chatroom: createChatroom :: (MonadStorage m, MonadHead LocalState m, MonadIO m, MonadError String m) => Maybe Text -> Maybe Text -> m ChatroomState
+ Erebos.Chatroom: createChatroom :: (MonadStorage m, MonadHead LocalState m, MonadIO m, MonadError e m, FromErebosError e) => Maybe Text -> Maybe Text -> m ChatroomState
- Erebos.Chatroom: deleteChatroomByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => Stored ChatroomStateData -> m ()
+ Erebos.Chatroom: deleteChatroomByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> m ()
- Erebos.Chatroom: joinChatroom :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => ChatroomState -> m ()
+ Erebos.Chatroom: joinChatroom :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => ChatroomState -> m ()
- Erebos.Chatroom: joinChatroomAs :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => UnifiedIdentity -> ChatroomState -> m ()
+ Erebos.Chatroom: joinChatroomAs :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => UnifiedIdentity -> ChatroomState -> m ()
- Erebos.Chatroom: joinChatroomAsByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => UnifiedIdentity -> Stored ChatroomStateData -> m ()
+ Erebos.Chatroom: joinChatroomAsByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => UnifiedIdentity -> Stored ChatroomStateData -> m ()
- Erebos.Chatroom: joinChatroomByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => Stored ChatroomStateData -> m ()
+ Erebos.Chatroom: joinChatroomByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> m ()
- Erebos.Chatroom: leaveChatroom :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => ChatroomState -> m ()
+ Erebos.Chatroom: leaveChatroom :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => ChatroomState -> m ()
- Erebos.Chatroom: leaveChatroomByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => Stored ChatroomStateData -> m ()
+ Erebos.Chatroom: leaveChatroomByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> m ()
- Erebos.Chatroom: sendChatroomMessage :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => ChatroomState -> Text -> m ()
+ Erebos.Chatroom: sendChatroomMessage :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => ChatroomState -> Text -> m ()
- Erebos.Chatroom: sendChatroomMessageByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => Stored ChatroomStateData -> Text -> m ()
+ Erebos.Chatroom: sendChatroomMessageByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> Text -> m ()
- Erebos.Chatroom: updateChatroomByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError String m) => Stored ChatroomStateData -> Maybe Text -> Maybe Text -> m (Maybe ChatroomState)
+ Erebos.Chatroom: updateChatroomByStateData :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> Maybe Text -> Maybe Text -> m (Maybe ChatroomState)
- Erebos.Contact: contactAccept :: (MonadIO m, MonadError String m) => Peer -> m ()
+ Erebos.Contact: contactAccept :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m ()
- Erebos.Contact: contactReject :: (MonadIO m, MonadError String m) => Peer -> m ()
+ Erebos.Contact: contactReject :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m ()
- Erebos.Contact: contactRequest :: (MonadIO m, MonadError String m) => Peer -> m ()
+ Erebos.Contact: contactRequest :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m ()
- Erebos.Conversation: deleteConversation :: (MonadHead LocalState m, MonadError String m) => Conversation -> m ()
+ Erebos.Conversation: deleteConversation :: (MonadHead LocalState m, MonadError e m, FromErebosError e) => Conversation -> m ()
- Erebos.Conversation: sendMessage :: (MonadHead LocalState m, MonadError String m) => Conversation -> Text -> m (Maybe Message)
+ Erebos.Conversation: sendMessage :: (MonadHead LocalState m, MonadError e m, FromErebosError e) => Conversation -> Text -> m ()
- Erebos.Discovery: DiscoveryAcknowledged :: [Text] -> Maybe Text -> Maybe Word16 -> Maybe Text -> Maybe Word16 -> DiscoveryService
+ Erebos.Discovery: DiscoveryAcknowledged :: [DiscoveryAddress] -> Maybe Text -> Maybe Word16 -> Maybe Text -> Maybe Word16 -> DiscoveryService
- Erebos.Discovery: DiscoveryAttributes :: Maybe Word16 -> Maybe Text -> Maybe Word16 -> Maybe Text -> DiscoveryAttributes
+ Erebos.Discovery: DiscoveryAttributes :: Maybe Word16 -> Maybe Text -> Maybe Word16 -> Maybe Text -> (Peer -> PeerAddress -> Bool) -> DiscoveryAttributes
- Erebos.Discovery: DiscoveryConnection :: Ref -> Ref -> Maybe Text -> Maybe IceRemoteInfo -> DiscoveryConnection
+ Erebos.Discovery: DiscoveryConnection :: Either Ref RefDigest -> Either Ref RefDigest -> Maybe Text -> Bool -> Maybe IceRemoteInfo -> DiscoveryConnection
- Erebos.Discovery: DiscoveryResult :: Ref -> [Text] -> DiscoveryService
+ Erebos.Discovery: DiscoveryResult :: Either Ref RefDigest -> [DiscoveryAddress] -> DiscoveryService
- Erebos.Discovery: DiscoverySearch :: Ref -> DiscoveryService
+ Erebos.Discovery: DiscoverySearch :: Either Ref RefDigest -> DiscoveryService
- Erebos.Discovery: DiscoverySelf :: [Text] -> Maybe Int -> DiscoveryService
+ Erebos.Discovery: DiscoverySelf :: [DiscoveryAddress] -> Maybe Int -> DiscoveryService
- Erebos.Discovery: [dconnSource] :: DiscoveryConnection -> Ref
+ Erebos.Discovery: [dconnSource] :: DiscoveryConnection -> Either Ref RefDigest
- Erebos.Discovery: [dconnTarget] :: DiscoveryConnection -> Ref
+ Erebos.Discovery: [dconnTarget] :: DiscoveryConnection -> Either Ref RefDigest
- Erebos.Discovery: discoverySearch :: (MonadIO m, MonadError String m) => Server -> Ref -> m ()
+ Erebos.Discovery: discoverySearch :: (MonadIO m, MonadError e m, FromErebosError e) => Server -> RefDigest -> m ()
- Erebos.Identity: createIdentity :: Storage -> Maybe Text -> Maybe UnifiedIdentity -> IO UnifiedIdentity
+ Erebos.Identity: createIdentity :: (MonadStorage m, MonadError e m, FromErebosError e, MonadIO m) => Maybe Text -> Maybe UnifiedIdentity -> m UnifiedIdentity
- Erebos.Identity: mergeIdentity :: forall m (f :: Type -> Type). (MonadStorage m, MonadError String m, MonadIO m) => Identity f -> m UnifiedIdentity
+ Erebos.Identity: mergeIdentity :: forall m e (f :: Type -> Type). (MonadStorage m, MonadError e m, FromErebosError e, MonadIO m) => Identity f -> m UnifiedIdentity
- Erebos.Network: PeerIdentityRef :: WaitingRef -> TVar [UnifiedIdentity -> ExceptT String IO ()] -> PeerIdentity
+ Erebos.Network: PeerIdentityRef :: WaitingRef -> TVar [UnifiedIdentity -> ExceptT ErebosError IO ()] -> PeerIdentity
- Erebos.Network: PeerIdentityUnknown :: TVar [UnifiedIdentity -> ExceptT String IO ()] -> PeerIdentity
+ Erebos.Network: PeerIdentityUnknown :: TVar [UnifiedIdentity -> ExceptT ErebosError IO ()] -> PeerIdentity
- Erebos.Network: ServerOptions :: PortNumber -> Bool -> ServerOptions
+ Erebos.Network: ServerOptions :: PortNumber -> Bool -> String -> Bool -> ServerOptions
- Erebos.Network: modifyServiceGlobalState :: forall s a m proxy. (Service s, MonadIO m, MonadError String m) => Server -> proxy s -> (ServiceGlobalState s -> (ServiceGlobalState s, a)) -> m a
+ Erebos.Network: modifyServiceGlobalState :: forall s a m e proxy. (Service s, MonadIO m, MonadError e m, FromErebosError e) => Server -> proxy s -> (ServiceGlobalState s -> (ServiceGlobalState s, a)) -> m a
- Erebos.Network: sendToPeerWith :: (Service s, MonadIO m, MonadError String m) => Peer -> (ServiceState s -> ExceptT String IO (Maybe s, ServiceState s)) -> m ()
+ Erebos.Network: sendToPeerWith :: (Service s, MonadIO m, MonadError e m, FromErebosError e) => Peer -> (ServiceState s -> ExceptT ErebosError IO (Maybe s, ServiceState s)) -> m ()
- Erebos.Pairing: OurRequest :: UnifiedIdentity -> UnifiedIdentity -> Bytes -> PairingState a
+ Erebos.Pairing: OurRequest :: UnifiedIdentity -> UnifiedIdentity -> ByteString -> PairingState a
- Erebos.Pairing: PairingFailedOther :: String -> PairingFailureReason a
+ Erebos.Pairing: PairingFailedOther :: ErebosError -> PairingFailureReason a
- Erebos.Pairing: PairingRequestNonce :: Bytes -> PairingService a
+ Erebos.Pairing: PairingRequestNonce :: ByteString -> PairingService a
- Erebos.Pairing: PairingResponse :: Bytes -> PairingService a
+ Erebos.Pairing: PairingResponse :: ByteString -> PairingService a
- Erebos.Pairing: PeerRequest :: UnifiedIdentity -> UnifiedIdentity -> Bytes -> RefDigest -> PairingState a
+ Erebos.Pairing: PeerRequest :: UnifiedIdentity -> UnifiedIdentity -> ByteString -> RefDigest -> PairingState a
- Erebos.Pairing: pairingAccept :: (PairingResult a, MonadIO m, MonadError String m) => proxy a -> Peer -> m ()
+ Erebos.Pairing: pairingAccept :: (PairingResult a, MonadIO m, MonadError e m, FromErebosError e) => proxy a -> Peer -> m ()
- Erebos.Pairing: pairingReject :: (PairingResult a, MonadIO m, MonadError String m) => proxy a -> Peer -> m ()
+ Erebos.Pairing: pairingReject :: (PairingResult a, MonadIO m, MonadError e m, FromErebosError e) => proxy a -> Peer -> m ()
- Erebos.Pairing: pairingRequest :: (PairingResult a, MonadIO m, MonadError String m) => proxy a -> Peer -> m ()
+ Erebos.Pairing: pairingRequest :: (PairingResult a, MonadIO m, MonadError e m, FromErebosError e) => proxy a -> Peer -> m ()
- Erebos.PubKey: loadKey :: (KeyPair sec pub, MonadIO m, MonadError String m) => Stored pub -> m sec
+ Erebos.PubKey: loadKey :: (KeyPair sec pub, MonadIO m, MonadError e m, FromErebosError e) => Stored pub -> m sec
- Erebos.Service: GlobalStorageWatcher :: (Stored LocalState -> a) -> (Server -> a -> ExceptT String IO ()) -> SomeStorageWatcher s
+ Erebos.Service: GlobalStorageWatcher :: (Stored LocalState -> a) -> (Server -> a -> ExceptT ErebosError IO ()) -> SomeStorageWatcher s
- Erebos.Service: ServiceInput :: ServiceAttributes s -> Peer -> UnifiedIdentity -> Server -> (String -> IO ()) -> ServiceInput s
+ Erebos.Service: ServiceInput :: ServiceAttributes s -> Peer -> PeerAddress -> UnifiedIdentity -> Server -> (String -> IO ()) -> [RawStreamReader] -> ServiceInput s
- Erebos.Set: storeSetAdd :: (Mergeable a, MonadIO m) => Storage -> a -> Set a -> m (Set a)
+ Erebos.Set: storeSetAdd :: (Mergeable a, MonadStorage m) => a -> Set a -> m (Set a)
- Erebos.Set: storeSetAddComponent :: (Mergeable a, MonadStorage m, MonadIO m) => Stored (Component a) -> Set a -> m (Set a)
+ Erebos.Set: storeSetAddComponent :: (Mergeable a, MonadStorage m) => Stored (Component a) -> Set a -> m (Set a)
- Erebos.State: LocalState :: Stored (Signed ExtendedIdentityData) -> [Stored SharedState] -> [(ByteString, RecItem)] -> LocalState
+ Erebos.State: LocalState :: Maybe RefDigest -> Stored (Signed ExtendedIdentityData) -> [Stored SharedState] -> [(ByteString, RecItem)] -> LocalState
- Erebos.State: makeSharedStateUpdate :: forall a m. (MonadIO m, SharedType a) => Storage -> a -> [Stored SharedState] -> m (Stored SharedState)
+ Erebos.State: makeSharedStateUpdate :: (SharedType a, MonadStorage m) => a -> [Stored SharedState] -> m (Stored SharedState)
- Erebos.State: mergeSharedIdentity :: (MonadHead LocalState m, MonadError String m) => m UnifiedIdentity
+ Erebos.State: mergeSharedIdentity :: (MonadHead LocalState m, MonadError e m, FromErebosError e) => m UnifiedIdentity
- Erebos.Storage.Key: loadKey :: (KeyPair sec pub, MonadIO m, MonadError String m) => Stored pub -> m sec
+ Erebos.Storage.Key: loadKey :: (KeyPair sec pub, MonadIO m, MonadError e m, FromErebosError e) => Stored pub -> m sec
- Erebos.Storage.Merge: generations :: Storable a => [Stored a] -> [Set (Stored a)]
+ Erebos.Storage.Merge: generations :: Storable a => [Stored a] -> NonEmpty (Set (Stored a))
Files
- CHANGELOG.md +28/−0
- README.md +8/−2
- erebos.cabal +25/−10
- main/Main.hs +364/−313
- main/State.hs +115/−0
- main/Terminal.hs +421/−0
- main/Test.hs +248/−124
- main/Test/Service.hs +22/−2
- main/WebSocket.hs +48/−0
- src/Erebos/Attach.hs +6/−6
- src/Erebos/Channel.hs +0/−175
- src/Erebos/Chatroom.hs +27/−22
- src/Erebos/Contact.hs +9/−11
- src/Erebos/Conversation.hs +29/−15
- src/Erebos/DirectMessage.hs +345/−0
- src/Erebos/Discovery.hs +330/−113
- src/Erebos/Error.hs +39/−0
- src/Erebos/ICE.chs +32/−21
- src/Erebos/ICE/pjproject.c +10/−10
- src/Erebos/ICE/pjproject.h +8/−8
- src/Erebos/Identity.hs +24/−21
- src/Erebos/Message.hs +0/−283
- src/Erebos/Network.hs +174/−125
- src/Erebos/Network.hs-boot +2/−1
- src/Erebos/Network/Address.hs +65/−0
- src/Erebos/Network/Channel.hs +175/−0
- src/Erebos/Network/Protocol.hs +67/−30
- src/Erebos/Network/ifaddrs.c +5/−5
- src/Erebos/Network/ifaddrs.h +3/−3
- src/Erebos/Object.hs +23/−0
- src/Erebos/Object/Internal.hs +772/−0
- src/Erebos/Pairing.hs +36/−34
- src/Erebos/PubKey.hs +5/−6
- src/Erebos/Service.hs +18/−13
- src/Erebos/Service/Stream.hs +74/−0
- src/Erebos/Set.hs +6/−6
- src/Erebos/State.hs +58/−77
- src/Erebos/Storable.hs +44/−0
- src/Erebos/Storage.hs +29/−1087
- src/Erebos/Storage/Backend.hs +28/−0
- src/Erebos/Storage/Disk.hs +230/−0
- src/Erebos/Storage/Head.hs +258/−0
- src/Erebos/Storage/Internal.hs +163/−143
- src/Erebos/Storage/Key.hs +22/−56
- src/Erebos/Storage/Memory.hs +107/−0
- src/Erebos/Storage/Merge.hs +38/−10
- src/Erebos/Sync.hs +3/−2
- src/Erebos/UUID.hs +24/−0
- src/Erebos/Util.hs +37/−6
CHANGELOG.md view
@@ -1,5 +1,33 @@ # Revision history for erebos +## 0.2.0 -- 2025-08-06++* Weak references in records+* Use XDG data directory for default storage path+* Added `/identity` command to show details of current identity+* Support tunnel for peers in discovery service+* New CLI prompt implementation providing cleaner interface+ * Avoids displaying sent messages twice – both in previous prompt and in message history+ * Print received messages only for selected conversation+ * Clear tab completion options after use++* API+ * Split `Erebos.Storage` into multiple modules+ * Removed deprecated `Message.formatMessage` alias+ * Renamed `Erebos.Message` module to `Erebos.DirectMessage`+ * Added `StorageBackend` type class to allow custom storage implementation+ * `MonadError` constraints use generic error type+ * Replaced `Erebos.Network.peerAddress` with `getPeerAddress` and added `getPeerAddresses`+ * Renamed `Erebos.Network.peerIdentity` to `getPeerIdentity`+ * Renamed some functions in `Erebos.DirectMessage` module to make clear they are related only to direct messages+ * `Erebos.Storage.Merge.generations`/`generationsBy` return `NonEmpty`+ * Replaced `watchReceivedDirectMessages` with `watchDirectMessageThreads`+ * Return type of `sendMessage` and `sendDirectMessage` is now `()`+ * Some functions use `MonadStorage` instead of explicit `Storage` parameter:+ * `Erebos.Set.storeSetAdd`+ * `Erebos.State.makeSharedStateUpdate`+ * `Erebos.Identity.createIdentity`+ ## 0.1.9 -- 2025-07-08 * Option to show details or delete a conversation by giving index parameter without first selecting it
README.md view
@@ -216,6 +216,9 @@ : Drop the currently selected peer. Afterwards, the connection can be re-established by either side. +`/identity` +: Show details of current identity+ `/update-identity` : Interactively update current identity information @@ -226,8 +229,11 @@ Storage ------- -Data are by default stored within `.erebos` subdirectory of the current working-directory. This can be overriden by `EREBOS_DIR` environment variable.+Data are by default stored under `XDG_DATA_HOME`, typically+`$HOME/.local/share/erebos`, unless there is an erebos storage already+in `.erebos` subdirectory of the current working directory, in which case the+latter one in used instead. This can be overriden by `EREBOS_DIR` environment+variable. Private keys are currently stored in plaintext under the `keys` subdirectory of the erebos directory.
erebos.cabal view
@@ -1,7 +1,7 @@ Cabal-Version: 3.0 Name: erebos-Version: 0.1.9+Version: 0.2.0 Synopsis: Decentralized messaging and synchronization Description: Library and simple CLI interface implementing the Erebos identity@@ -100,30 +100,40 @@ hs-source-dirs: src exposed-modules: Erebos.Attach- Erebos.Channel Erebos.Chatroom Erebos.Contact Erebos.Conversation+ Erebos.DirectMessage Erebos.Discovery+ Erebos.Error Erebos.Identity- Erebos.Message Erebos.Network- Erebos.Network.Protocol+ Erebos.Object Erebos.Pairing Erebos.PubKey Erebos.Service+ Erebos.Service.Stream Erebos.Set Erebos.State+ Erebos.Storable Erebos.Storage+ Erebos.Storage.Backend+ Erebos.Storage.Head Erebos.Storage.Key Erebos.Storage.Merge Erebos.Sync - -- Used by test tool:- Erebos.Storage.Internal other-modules: Erebos.Flow+ Erebos.Network.Address+ Erebos.Network.Channel+ Erebos.Network.Protocol+ Erebos.Object.Internal+ Erebos.Storage.Disk+ Erebos.Storage.Internal+ Erebos.Storage.Memory Erebos.Storage.Platform+ Erebos.UUID Erebos.Util c-sources:@@ -134,7 +144,7 @@ src/Erebos/Network/ifaddrs.h if flag(ice)- exposed-modules:+ other-modules: Erebos.ICE c-sources: src/Erebos/ICE/pjproject.c@@ -164,7 +174,7 @@ stm >=2.5 && <2.6, text >= 1.2 && <2.2, time ^>= { 1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14 },- uuid >=1.3 && <1.4,+ uuid-types ^>= { 1.0.4 }, zlib >=0.6 && <0.8 if !flag(cryptonite)@@ -192,26 +202,31 @@ main-is: Main.hs other-modules: Paths_erebos+ State+ Terminal Test Test.Service Version Version.Git+ WebSocket autogen-modules: Paths_erebos build-depends:+ ansi-terminal ^>= { 0.11, 1.0, 1.1 }, bytestring, directory, erebos,- haskeline >=0.7 && <0.9, mtl, network, process >=1.6 && <1.7,+ stm, template-haskell ^>= { 2.17, 2.18, 2.19, 2.20, 2.21, 2.22, 2.23 }, text, time, transformers >= 0.5 && <0.7,- uuid,+ uuid-types,+ websockets ^>= { 0.12.7, 0.13 }, if !flag(cryptonite) build-depends:
main/Main.hs view
@@ -1,9 +1,7 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Main (main) where -import Control.Arrow (first) import Control.Concurrent import Control.Exception import Control.Monad@@ -11,11 +9,13 @@ import Control.Monad.Reader import Control.Monad.State import Control.Monad.Trans.Maybe+import Control.Monad.Writer import Crypto.Random -import qualified Data.ByteString.Char8 as BC-import qualified Data.ByteString.Lazy as BL+import Data.Bifunctor+import Data.ByteString.Char8 qualified as BC+import Data.ByteString.Lazy qualified as BL import Data.Char import Data.List import Data.Maybe@@ -31,7 +31,7 @@ import Network.Socket import System.Console.GetOpt-import System.Console.Haskeline+import System.Directory import System.Environment import System.Exit import System.IO@@ -40,30 +40,34 @@ import Erebos.Contact import Erebos.Chatroom import Erebos.Conversation+import Erebos.DirectMessage import Erebos.Discovery-#ifdef ENABLE_ICE_SUPPORT-import Erebos.ICE-#endif import Erebos.Identity-import Erebos.Message hiding (formatMessage) import Erebos.Network+import Erebos.Object import Erebos.PubKey import Erebos.Service import Erebos.Set import Erebos.State+import Erebos.Storable import Erebos.Storage import Erebos.Storage.Merge import Erebos.Sync +import State+import Terminal import Test import Version+import WebSocket data Options = Options { optServer :: ServerOptions , optServices :: [ServiceOption] , optStorage :: StorageOption+ , optCreateIdentity :: Maybe ( Maybe Text, [ Maybe Text ] ) , optChatroomAutoSubscribe :: Maybe Int , optDmBotEcho :: Maybe Text+ , optWebSocketServer :: Maybe Int , optShowHelp :: Bool , optShowVersion :: Bool }@@ -84,8 +88,10 @@ { optServer = defaultServerOptions , optServices = availableServices , optStorage = DefaultStorage+ , optCreateIdentity = Nothing , optChatroomAutoSubscribe = Nothing , optDmBotEcho = Nothing+ , optWebSocketServer = Nothing , optShowHelp = False , optShowVersion = False }@@ -106,7 +112,7 @@ True "peer discovery" ] -options :: [OptDescr (Options -> Options)]+options :: [ OptDescr (Options -> Writer [ String ] Options) ] options = [ Option ['p'] ["port"] (ReqArg (\p -> so $ \opts -> opts { serverPort = read p }) "<port>")@@ -115,57 +121,92 @@ (NoArg (so $ \opts -> opts { serverLocalDiscovery = False })) "do not send announce packets for local discovery" , Option [] [ "storage" ]- (ReqArg (\path -> \opts -> opts { optStorage = FilesystemStorage path }) "<path>")+ (ReqArg (\path -> \opts -> return opts { optStorage = FilesystemStorage path }) "<path>") "use storage in <path>" , Option [] [ "memory-storage" ]- (NoArg (\opts -> opts { optStorage = MemoryStorage }))+ (NoArg (\opts -> return opts { optStorage = MemoryStorage })) "use memory storage"+ , Option [] [ "create-identity" ]+ (OptArg (\value -> \opts -> return opts+ { optCreateIdentity =+ let devName = T.pack <$> value+ in maybe (Just ( devName, [] )) (Just . first (const devName)) (optCreateIdentity opts)+ }) "<name>")+ "create a new (device) identity in a new local state"+ , Option [] [ "create-owner" ]+ (OptArg (\value -> \opts -> return opts+ { optCreateIdentity =+ let ownerName = T.pack <$> value+ in maybe (Just ( Nothing, [ ownerName ] )) (Just . second (ownerName :)) (optCreateIdentity opts)+ }) "<name>")+ "create owner for a new device identity" , Option [] ["chatroom-auto-subscribe"]- (ReqArg (\count -> \opts -> opts { optChatroomAutoSubscribe = Just (read count) }) "<count>")+ (ReqArg (\count -> \opts -> return opts { optChatroomAutoSubscribe = Just (read count) }) "<count>") "automatically subscribe for up to <count> chatrooms"-#ifdef ENABLE_ICE_SUPPORT , Option [] [ "discovery-stun-port" ]- (ReqArg (\value -> serviceAttr $ \attrs -> attrs { discoveryStunPort = Just (read value) }) "<port>")+ (ReqArg (\value -> serviceAttr $ \attrs -> return attrs { discoveryStunPort = Just (read value) }) "<port>") "offer specified <port> to discovery peers for STUN protocol" , Option [] [ "discovery-stun-server" ]- (ReqArg (\value -> serviceAttr $ \attrs -> attrs { discoveryStunServer = Just (read value) }) "<server>")+ (ReqArg (\value -> serviceAttr $ \attrs -> return attrs { discoveryStunServer = Just (read value) }) "<server>") "offer <server> (domain name or IP address) to discovery peers for STUN protocol" , Option [] [ "discovery-turn-port" ]- (ReqArg (\value -> serviceAttr $ \attrs -> attrs { discoveryTurnPort = Just (read value) }) "<port>")+ (ReqArg (\value -> serviceAttr $ \attrs -> return attrs { discoveryTurnPort = Just (read value) }) "<port>") "offer specified <port> to discovery peers for TURN protocol" , Option [] [ "discovery-turn-server" ]- (ReqArg (\value -> serviceAttr $ \attrs -> attrs { discoveryTurnServer = Just (read value) }) "<server>")+ (ReqArg (\value -> serviceAttr $ \attrs -> return attrs { discoveryTurnServer = Just (read value) }) "<server>") "offer <server> (domain name or IP address) to discovery peers for TURN protocol"-#endif+ , Option [] [ "discovery-tunnel" ]+ (OptArg (\value -> \opts -> do+ fun <- provideTunnelFun value+ serviceAttr (\attrs -> return attrs { discoveryProvideTunnel = fun }) opts) "<peer-type>")+ "offer to provide tunnel for peers of given <peer-type>, possible values: all, none, websocket" , Option [] ["dm-bot-echo"]- (ReqArg (\prefix -> \opts -> opts { optDmBotEcho = Just (T.pack prefix) }) "<prefix>")+ (ReqArg (\prefix -> \opts -> return opts { optDmBotEcho = Just (T.pack prefix) }) "<prefix>") "automatically reply to direct messages with the same text prefixed with <prefix>"+ , Option [] [ "websocket-server" ]+ (ReqArg (\value -> \opts -> return opts { optWebSocketServer = Just (read value) }) "<port>")+ "start WebSocket server on given port" , Option ['h'] ["help"]- (NoArg $ \opts -> opts { optShowHelp = True })+ (NoArg $ \opts -> return opts { optShowHelp = True }) "show this help and exit" , Option ['V'] ["version"]- (NoArg $ \opts -> opts { optShowVersion = True })+ (NoArg $ \opts -> return opts { optShowVersion = True }) "show version and exit" ] where- so f opts = opts { optServer = f $ optServer opts }+ so f opts = return opts { optServer = f $ optServer opts } - updateService :: Service s => (ServiceAttributes s -> ServiceAttributes s) -> SomeService -> SomeService+ updateService :: (Service s, Monad m, Typeable m) => (ServiceAttributes s -> m (ServiceAttributes s)) -> SomeService -> m SomeService updateService f some@(SomeService proxy attrs)- | Just f' <- cast f = SomeService proxy (f' attrs)- | otherwise = some+ | Just f' <- cast f = SomeService proxy <$> f' attrs+ | otherwise = return some - serviceAttr :: Service s => (ServiceAttributes s -> ServiceAttributes s) -> Options -> Options- serviceAttr f opts = opts { optServices = map (\sopt -> sopt { soptService = updateService f (soptService sopt) }) (optServices opts) }+ serviceAttr :: (Service s, Monad m, Typeable m) => (ServiceAttributes s -> m (ServiceAttributes s)) -> Options -> m Options+ serviceAttr f opts = do+ services' <- forM (optServices opts) $ \sopt -> do+ service <- updateService f (soptService sopt)+ return sopt { soptService = service }+ return opts { optServices = services' } -servicesOptions :: [OptDescr (Options -> Options)]+ provideTunnelFun :: Maybe String -> Writer [ String ] (Peer -> PeerAddress -> Bool)+ provideTunnelFun Nothing = return $ \_ _ -> True+ provideTunnelFun (Just "all") = return $ \_ _ -> True+ provideTunnelFun (Just "none") = return $ \_ _ -> False+ provideTunnelFun (Just "websocket") = return $ \_ -> \case+ CustomPeerAddress addr | Just WebSocketAddress {} <- cast addr -> True+ _ -> False+ provideTunnelFun (Just name) = do+ tell [ "Invalid value of --discovery-tunnel: ‘" <> name <> "’\n" ]+ return $ \_ _ -> False++servicesOptions :: [ OptDescr (Options -> Writer [ String ] Options) ] servicesOptions = concatMap helper $ "all" : map soptName availableServices where helper name =- [ Option [] ["enable-" <> name] (NoArg $ so $ change name $ \sopt -> sopt { soptEnabled = True }) ""- , Option [] ["disable-" <> name] (NoArg $ so $ change name $ \sopt -> sopt { soptEnabled = False }) ""+ [ Option [] [ "enable-" <> name ] (NoArg $ so $ change name $ \sopt -> sopt { soptEnabled = True }) ""+ , Option [] [ "disable-" <> name ] (NoArg $ so $ change name $ \sopt -> sopt { soptEnabled = False }) "" ]- so f opts = opts { optServices = f $ optServices opts }+ so f opts = return opts { optServices = f $ optServices opts } change :: String -> (ServiceOption -> ServiceOption) -> [ServiceOption] -> [ServiceOption] change name f (s : ss) | soptName s == name || name == "all"@@ -173,18 +214,29 @@ | otherwise = s : change name f ss change _ _ [] = [] +getDefaultStorageDir :: IO FilePath+getDefaultStorageDir = do+ lookupEnv "EREBOS_DIR" >>= \case+ Just dir -> return dir+ Nothing -> doesFileExist "./.erebos/erebos-storage" >>= \case+ True -> return "./.erebos"+ False -> getXdgDirectory XdgData "erebos"+ main :: IO () main = do- (opts, args) <- (getOpt RequireOrder (options ++ servicesOptions) <$> getArgs) >>= \case- (o, args, []) -> do- return (foldl (flip id) defaultOptions o, args)- (_, _, errs) -> do+ let printErrors errs = do progName <- getProgName hPutStrLn stderr $ concat errs <> "Try `" <> progName <> " --help' for more information." exitFailure+ (opts, args) <- (getOpt RequireOrder (options ++ servicesOptions) <$> getArgs) >>= \case+ (wo, args, []) ->+ case runWriter (foldM (flip ($)) defaultOptions wo) of+ ( o, [] ) -> return ( o, args )+ ( _, errs ) -> printErrors errs+ (_, _, errs) -> printErrors errs st <- liftIO $ case optStorage opts of- DefaultStorage -> openStorage . fromMaybe "./.erebos" =<< lookupEnv "EREBOS_DIR"+ DefaultStorage -> openStorage =<< getDefaultStorageDir FilesystemStorage path -> openStorage path MemoryStorage -> memoryStorage @@ -224,17 +276,28 @@ Nothing -> error "ref does not exist" Just ref -> print $ storedGeneration (wrappedLoad ref :: Stored Object) - ["update-identity"] -> either fail return <=< runExceptT $ do- runReaderT updateSharedIdentity =<< loadLocalStateHead st+ [ "identity" ] -> do+ loadHeads st >>= \case+ (h : _) -> do+ T.putStr $ showIdentityDetails $ headLocalIdentity h+ [] -> do+ T.putStrLn "no local state head"+ exitFailure + ["update-identity"] -> do+ withTerminal noCompletion $ \term -> do+ either (fail . showErebosError) return <=< runExceptT $ do+ runReaderT (updateSharedIdentity term) =<< runReaderT (loadLocalStateHead term) st+ ("update-identity" : srefs) -> do- sequence <$> mapM (readRef st . BC.pack) srefs >>= \case- Nothing -> error "ref does not exist"- Just refs- | Just idt <- validateIdentityF $ map wrappedLoad refs -> do- BC.putStrLn . showRefDigest . refDigest . storedRef . idData =<<- (either fail return <=< runExceptT $ runReaderT (interactiveIdentityUpdate idt) st)- | otherwise -> error "invalid identity"+ withTerminal noCompletion $ \term -> do+ sequence <$> mapM (readRef st . BC.pack) srefs >>= \case+ Nothing -> error "ref does not exist"+ Just refs+ | Just idt <- validateIdentityF $ map wrappedLoad refs -> do+ BC.putStrLn . showRefDigest . refDigest . storedRef . idData =<<+ (either (fail . showErebosError) return <=< runExceptT $ runReaderT (interactiveIdentityUpdate term idt) st)+ | otherwise -> error "invalid identity" ["test"] -> runTestTool st @@ -264,27 +327,24 @@ exitFailure -inputSettings :: Settings IO-inputSettings = setComplete commandCompletion $ defaultSettings- interactiveLoop :: Storage -> Options -> IO ()-interactiveLoop st opts = runInputT inputSettings $ do- erebosHead <- liftIO $ loadLocalStateHead st- outputStrLn $ T.unpack $ displayIdentity $ headLocalIdentity erebosHead+interactiveLoop st opts = withTerminal commandCompletion $ \term -> do+ erebosHead <- either (fail . showErebosError) return <=< runExceptT . flip runReaderT st $ do+ case optCreateIdentity opts of+ Nothing -> loadLocalStateHead term+ Just ( devName, names ) -> createLocalStateHead (names ++ [ devName ])+ void $ printLine term $ T.unpack $ displayIdentity $ headLocalIdentity erebosHead - tui <- haveTerminalUI- extPrint <- getExternalPrint- let extPrintLn str = do- let str' = case reverse str of ('\n':_) -> str- _ -> str ++ "\n";- extPrint $! str' -- evaluate str before calling extPrint to avoid blinking+ let tui = hasTerminalUI term+ let extPrintLn = void . printLine term - let getInputLinesTui eprompt = do+ let getInputLinesTui :: Either CommandState String -> MaybeT IO String+ getInputLinesTui eprompt = do prompt <- case eprompt of Left cstate -> do pname <- case csContext cstate of NoContext -> return ""- SelectedPeer peer -> peerIdentity peer >>= return . \case+ SelectedPeer peer -> getPeerIdentity peer >>= return . \case PeerIdentityFull pid -> maybe "<unnamed>" T.unpack $ idName $ finalOwner pid PeerIdentityRef wref _ -> "<" ++ BC.unpack (showRefDigest $ wrDigest wref) ++ ">" PeerIdentityUnknown _ -> "<unknown>"@@ -293,69 +353,92 @@ SelectedConversation conv -> return $ T.unpack $ conversationName conv return $ pname ++ "> " Right prompt -> return prompt- Just input <- lift $ getInputLine prompt- case reverse input of- _ | all isSpace input -> getInputLinesTui eprompt- '\\':rest -> (reverse ('\n':rest) ++) <$> getInputLinesTui (Right ">> ")- _ -> return input+ lift $ setPrompt term prompt+ join $ lift $ getInputLine term $ \case+ Just input@('/' : _) -> KeepPrompt $ return input+ Just input -> ErasePrompt $ case reverse input of+ _ | all isSpace input -> getInputLinesTui eprompt+ '\\':rest -> (reverse ('\n':rest) ++) <$> getInputLinesTui (Right ">> ")+ _ -> return input+ Nothing+ | tui -> KeepPrompt mzero+ | otherwise -> KeepPrompt $ liftIO $ forever $ threadDelay 100000000 getInputCommandTui cstate = do- input <- getInputLinesTui cstate- let (CommandM cmd, line) = case input of- '/':rest -> let (scmd, args) = dropWhile isSpace <$> span (\c -> isAlphaNum c || c == '-') rest- in if not (null scmd) && all isDigit scmd- then (cmdSelectContext, scmd)- else (fromMaybe (cmdUnknown scmd) $ lookup scmd commands, args)- _ -> (cmdSend, input)- return (cmd, line)+ let parseCommand cmdline =+ case dropWhile isSpace <$> span (\c -> isAlphaNum c || c == '-') cmdline of+ ( scmd, args )+ | not (null scmd) && all isDigit scmd+ -> ( cmdSelectContext, scmd ) - getInputLinesPipe = do- lift (getInputLine "") >>= \case- Just input -> return input- Nothing -> liftIO $ forever $ threadDelay 100000000+ | otherwise+ -> ( fromMaybe (cmdUnknown scmd) $ lookup scmd commands, args ) - getInputCommandPipe _ = do- input <- getInputLinesPipe- let (scmd, args) = dropWhile isSpace <$> span (\c -> isAlphaNum c || c == '-') input- let (CommandM cmd, line) = (fromMaybe (cmdUnknown scmd) $ lookup scmd commands, args)- return (cmd, line)+ ( CommandM cmd, line ) <- getInputLinesTui cstate >>= return . \case+ '/' : input -> parseCommand input+ input | not tui -> parseCommand input+ input -> ( cmdSend, input )+ return ( cmd, line ) - let getInputCommand = if tui then getInputCommandTui . Left- else getInputCommandPipe+ let getInputCommand = getInputCommandTui . Left + contextVar <- liftIO $ newMVar NoContext+ _ <- liftIO $ do tzone <- getCurrentTimeZone- watchReceivedMessages erebosHead $ \smsg -> do- let msg = fromStored smsg- extPrintLn $ formatDirectMessage tzone msg- case optDmBotEcho opts of- Nothing -> return ()- Just prefix -> do- res <- runExceptT $ flip runReaderT erebosHead $ sendDirectMessage (msgFrom msg) (prefix <> msgText msg)- case res of- Right reply -> extPrintLn $ formatDirectMessage tzone $ fromStored reply- Left err -> extPrintLn $ "Failed to send dm echo: " <> err+ let self = finalOwner $ headLocalIdentity erebosHead+ watchDirectMessageThreads erebosHead $ \prev cur -> do+ forM_ (reverse $ dmThreadToListSince prev cur) $ \msg -> do+ withMVar contextVar $ \ctx -> do+ mbpid <- case ctx of+ SelectedPeer peer -> getPeerIdentity peer >>= return . \case+ PeerIdentityFull pid -> Just $ finalOwner pid+ _ -> Nothing+ SelectedContact contact+ | Just cid <- contactIdentity contact -> return (Just cid)+ SelectedConversation conv -> return $ conversationPeer conv+ _ -> return Nothing+ when (not tui || maybe False (msgPeer cur `sameIdentity`) mbpid) $ do+ extPrintLn $ formatDirectMessage tzone msg + case optDmBotEcho opts of+ Just prefix+ | not (msgFrom msg `sameIdentity` self)+ -> do+ void $ forkIO $ do+ res <- runExceptT $ flip runReaderT erebosHead $ sendDirectMessage (msgFrom msg) (prefix <> msgText msg)+ case res of+ Right _ -> return ()+ Left err -> extPrintLn $ "Failed to send dm echo: " <> err+ _ -> return ()+ peers <- liftIO $ newMVar []- contextOptions <- liftIO $ newMVar []+ contextOptions <- liftIO $ newMVar ( Nothing, [] ) chatroomSetVar <- liftIO $ newEmptyMVar let autoSubscribe = optChatroomAutoSubscribe opts chatroomList = fromSetBy (comparing roomStateData) . lookupSharedValue . lsShared . headObject $ erebosHead watched <- if isJust autoSubscribe || any roomStateSubscribe chatroomList- then fmap Just $ liftIO $ watchChatroomsForCli extPrintLn erebosHead chatroomSetVar contextOptions autoSubscribe- else return Nothing+ then do+ fmap Just $ liftIO $ watchChatroomsForCli tui extPrintLn erebosHead+ chatroomSetVar contextVar contextOptions autoSubscribe+ else do+ return Nothing server <- liftIO $ do startServer (optServer opts) erebosHead extPrintLn $ map soptService $ filter soptEnabled $ optServices opts + case optWebSocketServer opts of+ Just port -> startWebsocketServer server "::" port extPrintLn+ Nothing -> return ()+ void $ liftIO $ forkIO $ void $ forever $ do peer <- getNextPeerChange server- peerIdentity peer >>= \case+ getPeerIdentity peer >>= \case pid@(PeerIdentityFull _) -> do dropped <- isPeerDropped peer- let shown = showPeer pid $ peerAddress peer+ shown <- showPeer pid <$> getPeerAddress peer let update [] = ([(peer, shown)], (Nothing, "NEW")) update ((p,s):ps) | p == peer && dropped = (ps, (Nothing, "DEL"))@@ -367,36 +450,47 @@ | otherwise = first (ctx:) $ ctxUpdate (n + 1) ctxs (op, updateType) <- modifyMVar peers (return . update) let updateType' = if dropped then "DEL" else updateType- idx <- modifyMVar contextOptions (return . ctxUpdate (1 :: Int))- when (Just shown /= op) $ extPrintLn $ "[" <> show idx <> "] PEER " <> updateType' <> " " <> shown+ modifyMVar_ contextOptions $ \case+ ( watch, clist )+ | watch == Just WatchPeers || not tui+ -> do+ let ( clist', idx ) = ctxUpdate (1 :: Int) clist+ when (Just shown /= op) $ do+ extPrintLn $ "[" <> show idx <> "] PEER " <> updateType' <> " " <> shown+ return ( Just WatchPeers, clist' )+ cur -> return cur _ -> return () - let process :: CommandState -> MaybeT (InputT IO) CommandState+ let process :: CommandState -> MaybeT IO CommandState process cstate = do (cmd, line) <- getInputCommand cstate h <- liftIO (reloadHead $ csHead cstate) >>= \case Just h -> return h- Nothing -> do lift $ lift $ extPrintLn "current head deleted"+ Nothing -> do lift $ extPrintLn "current head deleted" mzero- res <- liftIO $ runExceptT $ flip execStateT cstate { csHead = h } $ runReaderT cmd CommandInput- { ciServer = server- , ciLine = line- , ciPrint = extPrintLn- , ciOptions = opts- , ciPeers = liftIO $ modifyMVar peers $ \ps -> do- ps' <- filterM (fmap not . isPeerDropped . fst) ps- return (ps', ps')- , ciContextOptions = liftIO $ readMVar contextOptions- , ciSetContextOptions = \ctxs -> liftIO $ modifyMVar_ contextOptions $ const $ return ctxs- , ciContextOptionsVar = contextOptions- , ciChatroomSetVar = chatroomSetVar- }+ res <- liftIO $ modifyMVar contextVar $ \ctx -> do+ res <- runExceptT $ flip execStateT cstate { csHead = h, csContext = ctx } $ runReaderT cmd CommandInput+ { ciServer = server+ , ciTerminal = term+ , ciLine = line+ , ciPrint = extPrintLn+ , ciOptions = opts+ , ciPeers = liftIO $ modifyMVar peers $ \ps -> do+ ps' <- filterM (fmap not . isPeerDropped . fst) ps+ return (ps', ps')+ , ciContextOptions = liftIO $ snd <$> readMVar contextOptions+ , ciSetContextOptions = \watch ctxs -> liftIO $ modifyMVar_ contextOptions $ const $ return ( Just watch, ctxs )+ , ciContextVar = contextVar+ , ciContextOptionsVar = contextOptions+ , ciChatroomSetVar = chatroomSetVar+ }+ return ( either (const ctx) csContext res, res ) case res of Right cstate' | csQuit cstate' -> mzero | otherwise -> return cstate' Left err -> do- lift $ lift $ extPrintLn $ "Error: " ++ err+ lift $ extPrintLn $ "Error: " ++ showErebosError err return cstate let loop (Just cstate) = runMaybeT (process cstate) >>= loop@@ -404,10 +498,6 @@ loop $ Just $ CommandState { csHead = erebosHead , csContext = NoContext-#ifdef ENABLE_ICE_SUPPORT- , csIceSessions = []-#endif- , csIcePeer = Nothing , csWatchChatrooms = watched , csQuit = False }@@ -415,42 +505,48 @@ data CommandInput = CommandInput { ciServer :: Server+ , ciTerminal :: Terminal , ciLine :: String , ciPrint :: String -> IO () , ciOptions :: Options , ciPeers :: CommandM [(Peer, String)]- , ciContextOptions :: CommandM [CommandContext]- , ciSetContextOptions :: [CommandContext] -> Command- , ciContextOptionsVar :: MVar [ CommandContext ]+ , ciContextOptions :: CommandM [ CommandContext ]+ , ciSetContextOptions :: ContextWatchOptions -> [ CommandContext ] -> Command+ , ciContextVar :: MVar CommandContext+ , ciContextOptionsVar :: MVar ( Maybe ContextWatchOptions, [ CommandContext ] ) , ciChatroomSetVar :: MVar (Set ChatroomState) } data CommandState = CommandState { csHead :: Head LocalState , csContext :: CommandContext-#ifdef ENABLE_ICE_SUPPORT- , csIceSessions :: [IceSession]-#endif- , csIcePeer :: Maybe Peer , csWatchChatrooms :: Maybe WatchedHead , csQuit :: Bool } -data CommandContext = NoContext- | SelectedPeer Peer- | SelectedContact Contact- | SelectedChatroom ChatroomState- | SelectedConversation Conversation+data CommandContext+ = NoContext+ | SelectedPeer Peer+ | SelectedContact Contact+ | SelectedChatroom ChatroomState+ | SelectedConversation Conversation -newtype CommandM a = CommandM (ReaderT CommandInput (StateT CommandState (ExceptT String IO)) a)- deriving (Functor, Applicative, Monad, MonadReader CommandInput, MonadState CommandState, MonadError String)+data ContextWatchOptions+ = WatchPeers+ | WatchContacts+ | WatchChatrooms+ | WatchConversations+ deriving (Eq) +newtype CommandM a = CommandM (ReaderT CommandInput (StateT CommandState (ExceptT ErebosError IO)) a)+ deriving (Functor, Applicative, Monad, MonadReader CommandInput, MonadState CommandState, MonadError ErebosError)+ instance MonadFail CommandM where- fail = throwError+ fail = throwOtherError instance MonadIO CommandM where liftIO act = CommandM (liftIO (try act)) >>= \case- Left (e :: SomeException) -> throwError (show e)+ Left (e :: SomeException) -> throwOtherError (show e) Right x -> return x instance MonadRandom CommandM where@@ -471,41 +567,42 @@ getSelectedPeer :: CommandM Peer getSelectedPeer = gets csContext >>= \case SelectedPeer peer -> return peer- _ -> throwError "no peer selected"+ _ -> throwOtherError "no peer selected" getSelectedChatroom :: CommandM ChatroomState getSelectedChatroom = gets csContext >>= \case SelectedChatroom rstate -> return rstate- _ -> throwError "no chatroom selected"+ _ -> throwOtherError "no chatroom selected" getSelectedConversation :: CommandM Conversation getSelectedConversation = gets csContext >>= getConversationFromContext getConversationFromContext :: CommandContext -> CommandM Conversation getConversationFromContext = \case- SelectedPeer peer -> peerIdentity peer >>= \case+ SelectedPeer peer -> getPeerIdentity peer >>= \case PeerIdentityFull pid -> directMessageConversation $ finalOwner pid- _ -> throwError "incomplete peer identity"+ _ -> throwOtherError "incomplete peer identity" SelectedContact contact -> case contactIdentity contact of Just cid -> directMessageConversation cid- Nothing -> throwError "contact without erebos identity"+ Nothing -> throwOtherError "contact without erebos identity" SelectedChatroom rstate -> chatroomConversation rstate >>= \case Just conv -> return conv- Nothing -> throwError "invalid chatroom"+ Nothing -> throwOtherError "invalid chatroom" SelectedConversation conv -> reloadConversation conv- _ -> throwError "no contact, peer or conversation selected"+ _ -> throwOtherError "no contact, peer or conversation selected" getSelectedOrManualContext :: CommandM CommandContext getSelectedOrManualContext = do asks ciLine >>= \case "" -> gets csContext- str | all isDigit str -> getContextByIndex (read str)- _ -> throwError "invalid index"+ str | all isDigit str -> getContextByIndex id (read str)+ _ -> throwOtherError "invalid index" commands :: [(String, Command)] commands = [ ("history", cmdHistory)+ , ("identity", cmdIdentity) , ("peers", cmdPeers) , ("peer-add", cmdPeerAdd) , ("peer-add-public", cmdPeerAddPublic)@@ -524,15 +621,7 @@ , ("contact-reject", cmdContactReject) , ("conversations", cmdConversations) , ("details", cmdDetails)- , ("discovery-init", cmdDiscoveryInit) , ("discovery", cmdDiscovery)-#ifdef ENABLE_ICE_SUPPORT- , ("ice-create", cmdIceCreate)- , ("ice-destroy", cmdIceDestroy)- , ("ice-show", cmdIceShow)- , ("ice-connect", cmdIceConnect)- , ("ice-send", cmdIceSend)-#endif , ("join", cmdJoin) , ("join-as", cmdJoinAs) , ("leave", cmdLeave)@@ -549,16 +638,21 @@ sortedCommandNames = sort $ map fst commands +cmdPutStrLn :: String -> Command+cmdPutStrLn str = do+ term <- asks ciTerminal+ void $ liftIO $ printLine term str+ cmdUnknown :: String -> Command-cmdUnknown cmd = liftIO $ putStrLn $ "Unknown command: " ++ cmd+cmdUnknown cmd = cmdPutStrLn $ "Unknown command: " ++ cmd cmdPeers :: Command cmdPeers = do peers <- join $ asks ciPeers set <- asks ciSetContextOptions- set $ map (SelectedPeer . fst) peers+ set WatchPeers $ map (SelectedPeer . fst) peers forM_ (zip [1..] peers) $ \(i :: Int, (_, name)) -> do- liftIO $ putStrLn $ "[" ++ show i ++ "] " ++ name+ cmdPutStrLn $ "[" ++ show i ++ "] " ++ name cmdPeerAdd :: Command cmdPeerAdd = void $ do@@ -566,13 +660,17 @@ (hostname, port) <- (words <$> asks ciLine) >>= \case hostname:p:_ -> return (hostname, p) [hostname] -> return (hostname, show discoveryPort)- [] -> throwError "missing peer address"+ [] -> throwOtherError "missing peer address" addr:_ <- liftIO $ getAddrInfo (Just $ defaultHints { addrSocketType = Datagram }) (Just hostname) (Just port)+ contextOptsVar <- asks ciContextOptionsVar+ liftIO $ modifyMVar_ contextOptsVar $ return . first (const $ Just WatchPeers) liftIO $ serverPeer server (addrAddress addr) cmdPeerAddPublic :: Command cmdPeerAddPublic = do server <- asks ciServer+ contextOptsVar <- asks ciContextOptionsVar+ liftIO $ modifyMVar_ contextOptsVar $ return . first (const $ Just WatchPeers) liftIO $ mapM_ (serverPeer server . addrAddress) =<< gather 'a' where gather c@@ -606,8 +704,7 @@ cmdJoinAs :: Command cmdJoinAs = do name <- asks ciLine- st <- getStorage- identity <- liftIO $ createIdentity st (Just $ T.pack name) Nothing+ identity <- createIdentity (Just $ T.pack name) Nothing joinChatroomAs identity =<< getSelectedChatroom cmdLeave :: Command@@ -617,18 +714,22 @@ cmdMembers = do Just room <- findChatroomByStateData . head . roomStateData =<< getSelectedChatroom forM_ (chatroomMembers room) $ \x -> do- liftIO $ putStrLn $ maybe "<unnamed>" T.unpack $ idName x+ cmdPutStrLn $ maybe "<unnamed>" T.unpack $ idName x -getContextByIndex :: Int -> CommandM CommandContext-getContextByIndex n = do- join (asks ciContextOptions) >>= \ctxs -> if- | n > 0, (ctx : _) <- drop (n - 1) ctxs -> return ctx- | otherwise -> throwError "invalid index"+getContextByIndex :: (Maybe ContextWatchOptions -> Maybe ContextWatchOptions) -> Int -> CommandM CommandContext+getContextByIndex f n = do+ contextOptsVar <- asks ciContextOptionsVar+ join $ liftIO $ modifyMVar contextOptsVar $ \cur@( watch, ctxs ) -> if+ | n > 0, (ctx : _) <- drop (n - 1) ctxs+ -> return ( ( f watch, ctxs ), return ctx ) + | otherwise+ -> return ( cur, throwOtherError "invalid index" )+ cmdSelectContext :: Command cmdSelectContext = do n <- read <$> asks ciLine- ctx <- getContextByIndex n+ ctx <- getContextByIndex (const Nothing) n modify $ \s -> s { csContext = ctx } case ctx of SelectedChatroom rstate -> do@@ -640,11 +741,7 @@ cmdSend = void $ do text <- asks ciLine conv <- getSelectedConversation- sendMessage conv (T.pack text) >>= \case- Just msg -> do- tzone <- liftIO $ getCurrentTimeZone- liftIO $ putStrLn $ formatMessage tzone msg- Nothing -> return ()+ sendMessage conv (T.pack text) cmdDelete :: Command cmdDelete = void $ do@@ -657,13 +754,32 @@ case conversationHistory conv of thread@(_:_) -> do tzone <- liftIO $ getCurrentTimeZone- liftIO $ mapM_ (putStrLn . formatMessage tzone) $ reverse $ take 50 thread+ mapM_ (cmdPutStrLn . formatMessage tzone) $ reverse $ take 50 thread [] -> do- liftIO $ putStrLn $ "<empty history>"+ cmdPutStrLn $ "<empty history>" +showIdentityDetails :: Foldable f => Identity f -> Text+showIdentityDetails identity = T.unlines $ go $ reverse $ unfoldOwners identity+ where+ go (i : is) = concat+ [ maybeToList $ ("Name: " <>) <$> idName i+ , map (("Ref: " <>) . T.pack . show . refDigest . storedRef) $ idDataF i+ , map (("ExtRef: " <>) . T.pack . show . refDigest . storedRef) $ filter isExtension $ idExtDataF i+ , do guard $ not (null is)+ "" : "Device:" : map (" " <>) (go is)+ ]+ go [] = []+ isExtension x = case fromSigned x of BaseIdentityData {} -> False+ _ -> True++cmdIdentity :: Command+cmdIdentity = do+ cmdPutStrLn . T.unpack . showIdentityDetails . localIdentity . fromStored =<< getLocalHead+ cmdUpdateIdentity :: Command cmdUpdateIdentity = void $ do- runReaderT updateSharedIdentity =<< gets csHead+ term <- asks ciTerminal+ runReaderT (updateSharedIdentity term) =<< gets csHead cmdAttach :: Command cmdAttach = attachToOwner =<< getSelectedPeer@@ -674,8 +790,11 @@ cmdAttachReject :: Command cmdAttachReject = attachReject =<< getSelectedPeer -watchChatroomsForCli :: (String -> IO ()) -> Head LocalState -> MVar (Set ChatroomState) -> MVar [ CommandContext ] -> Maybe Int -> IO WatchedHead-watchChatroomsForCli eprint h chatroomSetVar contextVar autoSubscribe = do+watchChatroomsForCli+ :: Bool -> (String -> IO ()) -> Head LocalState -> MVar (Set ChatroomState)+ -> MVar CommandContext -> MVar ( Maybe ContextWatchOptions, [ CommandContext ] )+ -> Maybe Int -> IO WatchedHead+watchChatroomsForCli tui eprint h chatroomSetVar contextVar contextOptsVar autoSubscribe = do subscribedNumVar <- newEmptyMVar let ctxUpdate updateType (idx :: Int) rstate = \case@@ -710,32 +829,48 @@ forM_ (take (num - subscribedNum) notSubscribed) $ \rstate -> do (runExceptT $ flip runReaderT h $ chatroomSetSubscribe (head $ roomStateData rstate) True) >>= \case Right () -> return ()- Left err -> eprint err+ Left err -> eprint (showErebosError err) Just diff -> do modifyMVar_ chatroomSetVar $ return . const set+ modifyMVar_ contextOptsVar $ \case+ ( watch, clist )+ | watch == Just WatchChatrooms || not tui+ -> do+ let upd c = \case+ AddedChatroom rstate -> ctxUpdate "NEW" 1 rstate c+ RemovedChatroom rstate -> ctxUpdate "DEL" 1 rstate c+ UpdatedChatroom _ rstate+ | any ((\rsd -> not (null (rsdRoom rsd))) . fromStored) (roomStateData rstate)+ -> do+ ctxUpdate "UPD" 1 rstate c+ | otherwise -> return c+ ( watch, ) <$> foldM upd clist diff+ cur -> return cur+ forM_ diff $ \case AddedChatroom rstate -> do- modifyMVar_ contextVar $ ctxUpdate "NEW" 1 rstate modifyMVar_ subscribedNumVar $ return . if roomStateSubscribe rstate then (+ 1) else id RemovedChatroom rstate -> do- modifyMVar_ contextVar $ ctxUpdate "DEL" 1 rstate modifyMVar_ subscribedNumVar $ return . if roomStateSubscribe rstate then subtract 1 else id UpdatedChatroom oldroom rstate -> do- when (any ((\rsd -> not (null (rsdRoom rsd))) . fromStored) (roomStateData rstate)) $ do- modifyMVar_ contextVar $ ctxUpdate "UPD" 1 rstate when (any (not . null . rsdMessages . fromStored) (roomStateData rstate)) $ do- tzone <- getCurrentTimeZone- forM_ (reverse $ getMessagesSinceState rstate oldroom) $ \msg -> do- eprint $ concat $- [ maybe "<unnamed>" T.unpack $ roomName =<< cmsgRoom msg- , formatTime defaultTimeLocale " [%H:%M] " $ utcToLocalTime tzone $ zonedTimeToUTC $ cmsgTime msg- , maybe "<unnamed>" T.unpack $ idName $ cmsgFrom msg- , if cmsgLeave msg then " left" else ""- , maybe (if cmsgLeave msg then "" else " joined") ((": " ++) . T.unpack) $ cmsgText msg- ]+ withMVar contextVar $ \ctx -> do+ isSelected <- case ctx of+ SelectedChatroom rstate' -> return $ isSameChatroom rstate' rstate+ SelectedConversation conv -> return $ isChatroomStateConversation rstate conv+ _ -> return False+ when (not tui || isSelected) $ do+ tzone <- getCurrentTimeZone+ forM_ (reverse $ getMessagesSinceState rstate oldroom) $ \msg -> do+ eprint $ concat $+ [ formatTime defaultTimeLocale "[%H:%M] " $ utcToLocalTime tzone $ zonedTimeToUTC $ cmsgTime msg+ , maybe "<unnamed>" T.unpack $ idName $ cmsgFrom msg+ , if cmsgLeave msg then " left" else ""+ , maybe (if cmsgLeave msg then "" else " joined") ((": " ++) . T.unpack) $ cmsgText msg+ ] modifyMVar_ subscribedNumVar $ return . (if roomStateSubscribe rstate then (+ 1) else id) . (if roomStateSubscribe oldroom then subtract 1 else id)@@ -747,9 +882,11 @@ eprint <- asks ciPrint h <- gets csHead chatroomSetVar <- asks ciChatroomSetVar- contextVar <- asks ciContextOptionsVar+ contextVar <- asks ciContextVar+ contextOptsVar <- asks ciContextOptionsVar autoSubscribe <- asks $ optChatroomAutoSubscribe . ciOptions- watched <- liftIO $ watchChatroomsForCli eprint h chatroomSetVar contextVar autoSubscribe+ tui <- asks $ hasTerminalUI . ciTerminal+ watched <- liftIO $ watchChatroomsForCli tui eprint h chatroomSetVar contextVar contextOptsVar autoSubscribe modify $ \s -> s { csWatchChatrooms = Just watched } Just _ -> return () @@ -759,20 +896,22 @@ chatroomSetVar <- asks ciChatroomSetVar chatroomList <- filter (not . roomStateDeleted) . fromSetBy (comparing roomStateData) <$> liftIO (readMVar chatroomSetVar) set <- asks ciSetContextOptions- set $ map SelectedChatroom chatroomList+ set WatchChatrooms $ map SelectedChatroom chatroomList forM_ (zip [1..] chatroomList) $ \(i :: Int, rstate) -> do- liftIO $ putStrLn $ "[" ++ show i ++ "] " ++ maybe "<unnamed>" T.unpack (roomName =<< roomStateRoom rstate)+ cmdPutStrLn $ "[" ++ show i ++ "] " ++ maybe "<unnamed>" T.unpack (roomName =<< roomStateRoom rstate) cmdChatroomCreatePublic :: Command cmdChatroomCreatePublic = do+ term <- asks ciTerminal name <- asks ciLine >>= \case line | not (null line) -> return $ T.pack line _ -> liftIO $ do- T.putStr $ T.pack "Name: "- hFlush stdout- T.getLine+ setPrompt term "Name: "+ getInputLine term $ KeepPrompt . maybe T.empty T.pack ensureWatchedChatrooms+ contextOptsVar <- asks ciContextOptionsVar+ liftIO $ modifyMVar_ contextOptsVar $ return . first (const $ Just WatchChatrooms) void $ createChatroom (if T.null name then Nothing else Just name) Nothing@@ -785,9 +924,9 @@ let contacts = fromSetBy (comparing contactName) $ lookupSharedValue $ lsShared $ headObject ehead verbose = "-v" `elem` args set <- asks ciSetContextOptions- set $ map SelectedContact contacts- forM_ (zip [1..] contacts) $ \(i :: Int, c) -> liftIO $ do- T.putStrLn $ T.concat+ set WatchContacts $ map SelectedContact contacts+ forM_ (zip [1..] contacts) $ \(i :: Int, c) -> do+ cmdPutStrLn $ T.unpack $ T.concat [ "[", T.pack (show i), "] ", contactName c , case contactIdentity c of Just idt | cname <- displayIdentity idt@@ -811,38 +950,39 @@ cmdConversations = do conversations <- lookupConversations set <- asks ciSetContextOptions- set $ map SelectedConversation conversations+ set WatchConversations $ map SelectedConversation conversations forM_ (zip [1..] conversations) $ \(i :: Int, conv) -> do- liftIO $ putStrLn $ "[" ++ show i ++ "] " ++ T.unpack (conversationName conv)+ cmdPutStrLn $ "[" ++ show i ++ "] " ++ T.unpack (conversationName conv) cmdDetails :: Command cmdDetails = do getSelectedOrManualContext >>= \case SelectedPeer peer -> do- liftIO $ putStr $ unlines+ paddr <- getPeerAddress peer+ cmdPutStrLn $ unlines [ "Network peer:"- , " " <> show (peerAddress peer)+ , " " <> show paddr ]- peerIdentity peer >>= \case- PeerIdentityUnknown _ -> liftIO $ do- putStrLn $ "unknown identity"- PeerIdentityRef wref _ -> liftIO $ do- putStrLn $ "Identity ref:"- putStrLn $ " " <> BC.unpack (showRefDigest $ wrDigest wref)+ getPeerIdentity peer >>= \case+ PeerIdentityUnknown _ -> do+ cmdPutStrLn $ "unknown identity"+ PeerIdentityRef wref _ -> do+ cmdPutStrLn $ "Identity ref:"+ cmdPutStrLn $ " " <> BC.unpack (showRefDigest $ wrDigest wref) PeerIdentityFull pid -> printContactOrIdentityDetails pid SelectedContact contact -> do printContactDetails contact SelectedChatroom rstate -> do- liftIO $ putStrLn $ "Chatroom: " <> (T.unpack $ fromMaybe (T.pack "<unnamed>") $ roomName =<< roomStateRoom rstate)+ cmdPutStrLn $ "Chatroom: " <> (T.unpack $ fromMaybe (T.pack "<unnamed>") $ roomName =<< roomStateRoom rstate) SelectedConversation conv -> do case conversationPeer conv of Just pid -> printContactOrIdentityDetails pid- Nothing -> liftIO $ putStrLn $ "(conversation without peer)"+ Nothing -> cmdPutStrLn $ "(conversation without peer)" - NoContext -> liftIO $ putStrLn "nothing selected"+ NoContext -> cmdPutStrLn "nothing selected" where printContactOrIdentityDetails cid = do contacts <- fromSetBy (comparing contactName) . lookupSharedValue . lsShared . fromStored <$> getLocalHead@@ -850,11 +990,11 @@ Just contact -> printContactDetails contact Nothing -> printIdentityDetails cid - printContactDetails contact = liftIO $ do- putStrLn $ "Contact:"+ printContactDetails contact = do+ cmdPutStrLn $ "Contact:" prefix <- case contactCustomName contact of Just name -> do- putStrLn $ " " <> T.unpack name+ cmdPutStrLn $ " " <> T.unpack name return $ Just "alias of" Nothing -> do return $ Nothing@@ -863,117 +1003,28 @@ Just cid -> do printIdentityDetailsBody prefix cid Nothing -> do- putStrLn $ " (without erebos identity)"+ cmdPutStrLn $ " (without erebos identity)" - printIdentityDetails identity = liftIO $ do- putStrLn $ "Identity:"+ printIdentityDetails identity = do+ cmdPutStrLn $ "Identity:" printIdentityDetailsBody Nothing identity printIdentityDetailsBody prefix identity = do forM_ (zip (False : repeat True) $ unfoldOwners identity) $ \(owned, cpid) -> do- putStrLn $ unwords $ concat+ cmdPutStrLn $ unwords $ concat [ [ " " ] , if owned then [ "owned by" ] else maybeToList prefix , [ maybe "<unnamed>" T.unpack (idName cpid) ] , map (BC.unpack . showRefDigest . refDigest . storedRef) $ idExtDataF cpid ] -cmdDiscoveryInit :: Command-cmdDiscoveryInit = void $ do- server <- asks ciServer-- (hostname, port) <- (words <$> asks ciLine) >>= return . \case- hostname:p:_ -> (hostname, p)- [hostname] -> (hostname, show discoveryPort)- [] -> ("discovery.erebosprotocol.net", show discoveryPort)- addr:_ <- liftIO $ getAddrInfo (Just $ defaultHints { addrSocketType = Datagram }) (Just hostname) (Just port)- peer <- liftIO $ serverPeer server (addrAddress addr)- sendToPeer peer $ DiscoverySelf [ T.pack "ICE" ] Nothing- modify $ \s -> s { csIcePeer = Just peer }- cmdDiscovery :: Command cmdDiscovery = void $ do server <- asks ciServer- st <- getStorage sref <- asks ciLine- eprint <- asks ciPrint- liftIO $ readRef st (BC.pack sref) >>= \case- Nothing -> error "ref does not exist"- Just ref -> do- res <- runExceptT $ discoverySearch server ref- case res of- Right _ -> return ()- Left err -> eprint err--#ifdef ENABLE_ICE_SUPPORT--cmdIceCreate :: Command-cmdIceCreate = do- let getRole = \case- 'm':_ -> PjIceSessRoleControlling- 's':_ -> PjIceSessRoleControlled- _ -> PjIceSessRoleUnknown-- ( role, stun, turn ) <- asks (words . ciLine) >>= \case- [] -> return ( PjIceSessRoleControlling, Nothing, Nothing )- [ role ] -> return- ( getRole role, Nothing, Nothing )- [ role, server ] -> return- ( getRole role- , Just ( T.pack server, 0 )- , Just ( T.pack server, 0 )- )- [ role, server, port ] -> return- ( getRole role- , Just ( T.pack server, read port )- , Just ( T.pack server, read port )- )- [ role, stunServer, stunPort, turnServer, turnPort ] -> return- ( getRole role- , Just ( T.pack stunServer, read stunPort )- , Just ( T.pack turnServer, read turnPort )- )- _ -> throwError "invalid parameters"-- eprint <- asks ciPrint- Just cfg <- liftIO $ iceCreateConfig stun turn- sess <- liftIO $ iceCreateSession cfg role $ eprint <=< iceShow- modify $ \s -> s { csIceSessions = sess : csIceSessions s }--cmdIceDestroy :: Command-cmdIceDestroy = do- s:ss <- gets csIceSessions- modify $ \st -> st { csIceSessions = ss }- liftIO $ iceDestroy s--cmdIceShow :: Command-cmdIceShow = do- sess <- gets csIceSessions- eprint <- asks ciPrint- liftIO $ forM_ (zip [1::Int ..] sess) $ \(i, s) -> do- eprint $ "[" ++ show i ++ "]"- eprint =<< iceShow s--cmdIceConnect :: Command-cmdIceConnect = do- s:_ <- gets csIceSessions- server <- asks ciServer- let loadInfo = BC.getLine >>= \case line | BC.null line -> return []- | otherwise -> (line:) <$> loadInfo- Right remote <- liftIO $ do- st <- memoryStorage- pst <- derivePartialStorage st- rbytes <- (BL.fromStrict . BC.unlines) <$> loadInfo- copyRef st =<< storeRawBytes pst (BL.fromChunks [ BC.pack "rec ", BC.pack (show (BL.length rbytes)), BC.singleton '\n' ] `BL.append` rbytes)- liftIO $ iceConnect s (load remote) $ void $ serverPeerIce server s--cmdIceSend :: Command-cmdIceSend = void $ do- s:_ <- gets csIceSessions- server <- asks ciServer- liftIO $ serverPeerIce server s--#endif+ case readRefDigest (BC.pack sref) of+ Nothing -> throwOtherError "failed to parse ref"+ Just dgst -> discoverySearch server dgst cmdQuit :: Command cmdQuit = modify $ \s -> s { csQuit = True }
+ main/State.hs view
@@ -0,0 +1,115 @@+module State (+ loadLocalStateHead,+ createLocalStateHead,+ updateSharedIdentity,+ interactiveIdentityUpdate,+) where++import Control.Monad+import Control.Monad.Except+import Control.Monad.IO.Class++import Data.Foldable+import Data.Proxy+import Data.Text (Text)+import Data.Text qualified as T++import Erebos.Error+import Erebos.Identity+import Erebos.PubKey+import Erebos.State+import Erebos.Storable+import Erebos.Storage++import Terminal+++loadLocalStateHead+ :: (MonadStorage m, MonadError e m, FromErebosError e, MonadIO m)+ => Terminal -> m (Head LocalState)+loadLocalStateHead term = getStorage >>= loadHeads >>= \case+ (h : _) -> return h+ [] -> do+ name <- liftIO $ do+ setPrompt term "Name: "+ getInputLine term $ KeepPrompt . maybe T.empty T.pack++ devName <- liftIO $ do+ setPrompt term "Device: "+ getInputLine term $ KeepPrompt . maybe T.empty T.pack++ ( owner, shared ) <- if+ | T.null name -> do+ return ( Nothing, [] )+ | otherwise -> do+ owner <- createIdentity (Just name) Nothing+ shared <- mstore SharedState+ { ssPrev = []+ , ssType = Just $ sharedTypeID @(Maybe ComposedIdentity) Proxy+ , ssValue = [ storedRef $ idExtData owner ]+ }+ return ( Just owner, [ shared ] )++ identity <- createIdentity (if T.null devName then Nothing else Just devName) owner++ st <- getStorage+ storeHead st $ LocalState+ { lsPrev = Nothing+ , lsIdentity = idExtData identity+ , lsShared = shared+ , lsOther = []+ }++createLocalStateHead+ :: (MonadStorage m, MonadError e m, FromErebosError e, MonadIO m)+ => [ Maybe Text ] -> m (Head LocalState)+createLocalStateHead [] = throwOtherError "createLocalStateHead: empty name list"+createLocalStateHead ( ownerName : names ) = do+ owner <- createIdentity ownerName Nothing+ identity <- foldM createSingleIdentity owner names+ shared <- case names of+ [] -> return []+ _ : _ -> do+ fmap (: []) $ mstore SharedState+ { ssPrev = []+ , ssType = Just $ sharedTypeID @(Maybe ComposedIdentity) Proxy+ , ssValue = [ storedRef $ idExtData owner ]+ }+ st <- getStorage+ storeHead st $ LocalState+ { lsPrev = Nothing+ , lsIdentity = idExtData identity+ , lsShared = shared+ , lsOther = []+ }+ where+ createSingleIdentity owner name = createIdentity name (Just owner)+++updateSharedIdentity :: (MonadHead LocalState m, MonadError e m, FromErebosError e) => Terminal -> m ()+updateSharedIdentity term = updateLocalState_ $ updateSharedState_ $ \case+ Just identity -> do+ Just . toComposedIdentity <$> interactiveIdentityUpdate term identity+ Nothing -> throwOtherError "no existing shared identity"++interactiveIdentityUpdate :: (Foldable f, MonadStorage m, MonadIO m, MonadError e m, FromErebosError e) => Terminal -> Identity f -> m UnifiedIdentity+interactiveIdentityUpdate term fidentity = do+ identity <- mergeIdentity fidentity+ name <- liftIO $ do+ setPrompt term $ T.unpack $ T.concat $ concat+ [ [ T.pack "Name" ]+ , case idName identity of+ Just name -> [T.pack " [", name, T.pack "]"]+ Nothing -> []+ , [ T.pack ": " ]+ ]+ getInputLine term $ KeepPrompt . maybe T.empty T.pack++ if | T.null name -> return identity+ | otherwise -> do+ secret <- loadKey $ idKeyIdentity identity+ maybe (throwOtherError "created invalid identity") return . validateExtendedIdentity =<<+ mstore =<< sign secret =<< mstore . ExtendedIdentityData =<< return (emptyIdentityExtension $ idData identity)+ { idePrev = toList $ idExtDataF identity+ , ideName = Just name+ }
+ main/Terminal.hs view
@@ -0,0 +1,421 @@+{-# LANGUAGE CPP #-}++module Terminal (+ Terminal,+ hasTerminalUI,+ withTerminal,+ setPrompt,+ getInputLine,+ InputHandling(..),++ TerminalLine,+ printLine,++ printBottomLines,+ clearBottomLines,++ CompletionFunc, Completion,+ noCompletion,+ simpleCompletion,+ completeWordWithPrev,+) where++import Control.Arrow+import Control.Concurrent+import Control.Concurrent.STM+import Control.Exception+import Control.Monad++import Data.Char+import Data.List+import Data.Text (Text)+import Data.Text qualified as T++import System.Console.ANSI+import System.IO+import System.IO.Error+++data Terminal = Terminal+ { termLock :: MVar ()+ , termAnsi :: Bool+ , termCompletionFunc :: CompletionFunc IO+ , termPrompt :: TVar String+ , termShowPrompt :: TVar Bool+ , termInput :: TVar ( String, String )+ , termBottomLines :: TVar [ String ]+ , termHistory :: TVar [ String ]+ , termHistoryPos :: TVar Int+ , termHistoryStash :: TVar ( String, String )+ }++data TerminalLine = TerminalLine+ { tlTerminal :: Terminal+ , tlLineCount :: Int+ }++data Input+ = InputChar Char+ | InputMoveUp+ | InputMoveDown+ | InputMoveRight+ | InputMoveLeft+ | InputMoveEnd+ | InputMoveStart+ | InputBackspace+ | InputClear+ | InputBackWord+ | InputEnd+ | InputEscape String+ deriving (Eq, Ord, Show)+++data InputHandling a+ = KeepPrompt a+ | ErasePrompt a+++hasTerminalUI :: Terminal -> Bool+hasTerminalUI = termAnsi++initTerminal :: CompletionFunc IO -> IO Terminal+initTerminal termCompletionFunc = do+ termLock <- newMVar ()+#if MIN_VERSION_ansi_terminal(1, 0, 1)+ termAnsi <- hNowSupportsANSI stdout+#else+ termAnsi <- hSupportsANSI stdout+#endif+ termPrompt <- newTVarIO ""+ termShowPrompt <- newTVarIO False+ termInput <- newTVarIO ( "", "" )+ termBottomLines <- newTVarIO []+ termHistory <- newTVarIO []+ termHistoryPos <- newTVarIO 0+ termHistoryStash <- newTVarIO ( "", "" )+ return Terminal {..}++bracketSet :: IO a -> (a -> IO b) -> a -> IO c -> IO c+bracketSet get set val = bracket (get <* set val) set . const++withTerminal :: CompletionFunc IO -> (Terminal -> IO a) -> IO a+withTerminal compl act = do+ term <- initTerminal compl++ bracketSet (hGetEcho stdin) (hSetEcho stdin) False $+ bracketSet (hGetBuffering stdin) (hSetBuffering stdin) NoBuffering $+ bracketSet (hGetBuffering stdout) (hSetBuffering stdout) (BlockBuffering Nothing) $+ act term+++termPutStr :: Terminal -> String -> IO ()+termPutStr Terminal {..} str = do+ withMVar termLock $ \_ -> do+ putStr str+ hFlush stdout+++getInput :: IO Input+getInput = do+ handleJust (guard . isEOFError) (\() -> return InputEnd) $ getChar >>= \case+ '\ESC' -> do+ esc <- readEsc+ case parseEsc esc of+ Just ( 'A' , [] ) -> return InputMoveUp+ Just ( 'B' , [] ) -> return InputMoveDown+ Just ( 'C' , [] ) -> return InputMoveRight+ Just ( 'D' , [] ) -> return InputMoveLeft+ _ -> return (InputEscape esc)+ '\b' -> return InputBackspace+ '\DEL' -> return InputBackspace+ '\NAK' -> return InputClear+ '\ETB' -> return InputBackWord+ '\DLE' -> return InputMoveUp+ '\SO' -> return InputMoveDown+ '\SOH' -> return InputMoveStart+ '\ENQ' -> return InputMoveEnd+ '\EOT' -> return InputEnd+ c -> return (InputChar c)+ where+ readEsc = getChar >>= \case+ c | c == '\ESC' || isAlpha c -> return [ c ]+ | otherwise -> (c :) <$> readEsc++ parseEsc = \case+ '[' : c : [] -> do+ Just ( c, [] )+ _ -> Nothing+++getInputLine :: Terminal -> (Maybe String -> InputHandling a) -> IO a+getInputLine term@Terminal {..} handleResult = do+ when termAnsi $ do+ withMVar termLock $ \_ -> do+ prompt <- atomically $ do+ writeTVar termShowPrompt True+ readTVar termPrompt+ putStr $ prompt <> "\ESC[K"+ drawBottomLines term+ hFlush stdout++ mbLine <- go+ forM_ mbLine $ \line -> do+ let addLine xs+ | null line = xs+ | (x : _) <- xs, x == line = xs+ | otherwise = line : xs+ atomically $ do+ writeTVar termHistory . addLine =<< readTVar termHistory+ writeTVar termHistoryPos 0++ case handleResult mbLine of+ KeepPrompt x -> do+ when termAnsi $ do+ termPutStr term "\n\ESC[J"+ return x+ ErasePrompt x -> do+ when termAnsi $ do+ termPutStr term "\r\ESC[J"+ return x+ where+ go = getInput >>= \case+ InputChar '\n' -> do+ atomically $ do+ ( pre, post ) <- readTVar termInput+ writeTVar termInput ( "", "" )+ when termAnsi $ do+ writeTVar termShowPrompt False+ writeTVar termBottomLines []+ return $ Just $ pre ++ post++ InputChar '\t' | termAnsi -> do+ options <- withMVar termLock $ const $ do+ ( pre, post ) <- atomically $ readTVar termInput+ let updatePrompt pre' = do+ prompt <- atomically $ do+ writeTVar termInput ( pre', post )+ getCurrentPromptLine term+ putStr $ "\r" <> prompt+ hFlush stdout++ termCompletionFunc ( T.pack pre, T.pack post ) >>= \case++ ( unused, [ compl ] ) -> do+ updatePrompt $ T.unpack unused ++ T.unpack (replacement compl) ++ if isFinished compl then " " else ""+ return []++ ( unused, completions@(c : cs) ) -> do+ let commonPrefixes' x y = fmap (\( common, _, _ ) -> common) $ T.commonPrefixes x y+ case foldl' (\mbcommon cur -> commonPrefixes' cur =<< mbcommon) (Just $ replacement c) (fmap replacement cs) of+ Just common | T.unpack common /= pre -> do+ updatePrompt $ T.unpack unused ++ T.unpack common+ return []+ _ -> do+ return $ map replacement completions++ ( _, [] ) -> do+ return []++ printBottomLines term $ T.unpack $ T.unlines options+ go++ InputChar c | isPrint c -> withInput $ \case+ ( _, post ) -> do+ writeTVar termInput . first (++ [ c ]) =<< readTVar termInput+ return $ c : (if null post then "" else "\ESC[s" <> post <> "\ESC[u")++ InputChar _ -> go++ InputMoveUp -> withInput $ \prepost -> do+ hist <- readTVar termHistory+ pos <- readTVar termHistoryPos+ case drop pos hist of+ ( h : _ ) -> do+ when (pos == 0) $ do+ writeTVar termHistoryStash prepost+ writeTVar termHistoryPos (pos + 1)+ writeTVar termInput ( h, "" )+ ("\r\ESC[K" <>) <$> getCurrentPromptLine term+ [] -> do+ return ""++ InputMoveDown -> withInput $ \_ -> do+ readTVar termHistoryPos >>= \case+ 0 -> do+ return ""+ 1 -> do+ writeTVar termHistoryPos 0+ writeTVar termInput =<< readTVar termHistoryStash+ ("\r\ESC[K" <>) <$> getCurrentPromptLine term+ pos -> do+ writeTVar termHistoryPos (pos - 1)+ hist <- readTVar termHistory+ case drop (pos - 2) hist of+ ( h : _ ) -> do+ writeTVar termInput ( h, "" )+ ("\r\ESC[K" <>) <$> getCurrentPromptLine term+ [] -> do+ return ""++ InputMoveRight -> withInput $ \case+ ( pre, c : post ) -> do+ writeTVar termInput ( pre ++ [ c ], post )+ return $ "\ESC[C"+ _ -> return ""++ InputMoveLeft -> withInput $ \case+ ( pre@(_ : _), post ) -> do+ writeTVar termInput ( init pre, last pre : post )+ return $ "\ESC[D"+ _ -> return ""++ InputBackspace -> withInput $ \case+ ( pre@(_ : _), post ) -> do+ writeTVar termInput ( init pre, post )+ return $ "\b\ESC[K" <> (if null post then "" else "\ESC[s" <> post <> "\ESC[u")+ _ -> return ""++ InputClear -> withInput $ \_ -> do+ writeTVar termInput ( "", "" )+ ("\r\ESC[K" <>) <$> getCurrentPromptLine term++ InputBackWord -> withInput $ \( pre, post ) -> do+ let pre' = reverse $ dropWhile (not . isSpace) $ dropWhile isSpace $ reverse pre+ writeTVar termInput ( pre', post )+ ("\r\ESC[K" <>) <$> getCurrentPromptLine term++ InputMoveStart -> withInput $ \( pre, post ) -> do+ writeTVar termInput ( "", pre <> post )+ return $ "\ESC[" <> show (length pre) <> "D"++ InputMoveEnd -> withInput $ \( pre, post ) -> do+ writeTVar termInput ( pre <> post, "" )+ return $ "\ESC[" <> show (length post) <> "C"++ InputEnd -> do+ atomically (readTVar termInput) >>= \case+ ( "", "" ) -> return Nothing+ _ -> go++ InputEscape _ -> go++ withInput f = do+ withMVar termLock $ const $ do+ str <- atomically $ f =<< readTVar termInput+ when (termAnsi && not (null str)) $ do+ putStr str+ hFlush stdout+ go+++getCurrentPromptLine :: Terminal -> STM String+getCurrentPromptLine Terminal {..} = do+ prompt <- readTVar termPrompt+ ( pre, post ) <- readTVar termInput+ return $ prompt <> pre <> "\ESC[s" <> post <> "\ESC[u"++setPrompt :: Terminal -> String -> IO ()+setPrompt Terminal { termAnsi = False } _ = do+ return ()+setPrompt term@Terminal {..} prompt = do+ withMVar termLock $ \_ -> do+ join $ atomically $ do+ writeTVar termPrompt prompt+ readTVar termShowPrompt >>= \case+ True -> do+ promptLine <- getCurrentPromptLine term+ return $ do+ putStr $ "\r\ESC[K" <> promptLine+ hFlush stdout+ False -> return $ return ()++printLine :: Terminal -> String -> IO TerminalLine+printLine tlTerminal@Terminal {..} str = do+ withMVar termLock $ \_ -> do+ let strLines = lines str+ tlLineCount = length strLines+ if termAnsi+ then do+ promptLine <- atomically $ do+ readTVar termShowPrompt >>= \case+ True -> getCurrentPromptLine tlTerminal+ False -> return ""+ putStr $ "\r\ESC[K" <> unlines strLines <> "\ESC[K" <> promptLine+ drawBottomLines tlTerminal+ else do+ putStr $ unlines strLines++ hFlush stdout+ return TerminalLine {..}+++printBottomLines :: Terminal -> String -> IO ()+printBottomLines Terminal { termAnsi = False } _ = do+ return ()+printBottomLines term@Terminal {..} str = do+ case lines str of+ [] -> clearBottomLines term+ blines -> do+ withMVar termLock $ \_ -> do+ atomically $ writeTVar termBottomLines blines+ drawBottomLines term+ hFlush stdout++clearBottomLines :: Terminal -> IO ()+clearBottomLines Terminal { termAnsi = False } = do+ return ()+clearBottomLines Terminal {..} = do+ withMVar termLock $ \_ -> do+ atomically (readTVar termBottomLines) >>= \case+ [] -> return ()+ _:_ -> do+ atomically $ writeTVar termBottomLines []+ putStr $ "\ESC[s\n\ESC[J\ESC[u"+ hFlush stdout++drawBottomLines :: Terminal -> IO ()+drawBottomLines Terminal {..} = do+ atomically (readTVar termBottomLines) >>= \case+ blines@( firstLine : otherLines ) -> do+ ( shift ) <- atomically $ do+ readTVar termShowPrompt >>= \case+ True -> do+ prompt <- readTVar termPrompt+ ( pre, _ ) <- readTVar termInput+ return (displayWidth (prompt <> pre) + 1)+ False -> do+ return 0+ putStr $ concat+ [ "\n\ESC[J", firstLine, concat (map ('\n' :) otherLines)+ , "\ESC[", show (length blines), "F"+ , "\ESC[", show shift, "G"+ ]+ [] -> return ()+++displayWidth :: String -> Int+displayWidth = \case+ ('\ESC' : '[' : rest) -> displayWidth $ drop 1 $ dropWhile (not . isAlpha) rest+ ('\ESC' : _ : rest) -> displayWidth rest+ (_ : rest) -> 1 + displayWidth rest+ [] -> 0+++type CompletionFunc m = ( Text, Text ) -> m ( Text, [ Completion ] )++data Completion = Completion+ { replacement :: Text+ , isFinished :: Bool+ }++noCompletion :: Monad m => CompletionFunc m+noCompletion ( l, _ ) = return ( l, [] )++completeWordWithPrev :: Monad m => Maybe Char -> [ Char ] -> (String -> String -> m [ Completion ]) -> CompletionFunc m+completeWordWithPrev _ spaceChars fun ( l, _ ) = do+ let lastSpaceIndex = snd $ T.foldl' (\( i, found ) c -> if c `elem` spaceChars then ( i + 1, i ) else ( i + 1, found )) ( 1, 0 ) l+ let ( pre, word ) = T.splitAt lastSpaceIndex l+ ( pre, ) <$> fun (T.unpack pre) (T.unpack word)++simpleCompletion :: String -> Completion+simpleCompletion str = Completion (T.pack str) True
main/Test.hs view
@@ -26,7 +26,7 @@ import Data.Text.Encoding import Data.Text.IO qualified as T import Data.Typeable-import Data.UUID qualified as U+import Data.UUID.Types qualified as U import Network.Socket @@ -36,17 +36,20 @@ import Erebos.Attach import Erebos.Chatroom import Erebos.Contact+import Erebos.DirectMessage import Erebos.Discovery import Erebos.Identity-import Erebos.Message import Erebos.Network+import Erebos.Object import Erebos.Pairing import Erebos.PubKey import Erebos.Service+import Erebos.Service.Stream import Erebos.Set import Erebos.State+import Erebos.Storable import Erebos.Storage-import Erebos.Storage.Internal (unsafeStoreRawBytes)+import Erebos.Storage.Head import Erebos.Storage.Merge import Erebos.Sync @@ -64,10 +67,17 @@ data RunningServer = RunningServer { rsServer :: Server- , rsPeers :: MVar (Int, [(Int, Peer)])+ , rsPeers :: MVar ( Int, [ TestPeer ] ) , rsPeerThread :: ThreadId } +data TestPeer = TestPeer+ { tpIndex :: Int+ , tpPeer :: Peer+ , tpStreamReaders :: MVar [ (Int, StreamReader ) ]+ , tpStreamWriters :: MVar [ (Int, StreamWriter ) ]+ }+ initTestState :: TestState initTestState = TestState { tsHead = Nothing@@ -101,7 +111,7 @@ Nothing -> return () runExceptT (evalStateT testLoop initTestState) >>= \case- Left x -> B.hPutStr stderr $ (`BC.snoc` '\n') $ BC.pack x+ Left x -> B.hPutStr stderr $ (`BC.snoc` '\n') $ BC.pack (showErebosError x) Right () -> return () getLineMb :: MonadIO m => m (Maybe Text)@@ -135,17 +145,20 @@ getPeer :: Text -> CommandM Peer-getPeer spidx = do+getPeer spidx = tpPeer <$> getTestPeer spidx++getTestPeer :: Text -> CommandM TestPeer+getTestPeer spidx = do Just RunningServer {..} <- gets tsServer- Just peer <- lookup (read $ T.unpack spidx) . snd <$> liftIO (readMVar rsPeers)+ Just peer <- find (((read $ T.unpack spidx) ==) . tpIndex) . snd <$> liftIO (readMVar rsPeers) return peer -getPeerIndex :: MVar (Int, [(Int, Peer)]) -> ServiceHandler (PairingService a) Int+getPeerIndex :: MVar ( Int, [ TestPeer ] ) -> ServiceHandler s Int getPeerIndex pmvar = do peer <- asks svcPeer- maybe 0 fst . find ((==peer) . snd) . snd <$> liftIO (readMVar pmvar)+ maybe 0 tpIndex . find ((peer ==) . tpPeer) . snd <$> liftIO (readMVar pmvar) -pairingAttributes :: PairingResult a => proxy (PairingService a) -> Output -> MVar (Int, [(Int, Peer)]) -> String -> PairingAttributes a+pairingAttributes :: PairingResult a => proxy (PairingService a) -> Output -> MVar ( Int, [ TestPeer ] ) -> String -> PairingAttributes a pairingAttributes _ out peers prefix = PairingAttributes { pairingHookRequest = return () @@ -173,7 +186,7 @@ , pairingHookFailed = \case PairingUserRejected -> failed "user" PairingUnexpectedMessage pstate packet -> failed $ "unexpected " ++ strState pstate ++ " " ++ strPacket packet- PairingFailedOther str -> failed $ "other " ++ str+ PairingFailedOther err -> failed $ "other " ++ showErebosError err , pairingHookVerifyFailed = failed "verify" , pairingHookRejected = failed "rejected" }@@ -214,21 +227,28 @@ { dmOwnerMismatch = afterCommit $ outLine out "dm-owner-mismatch" } -dmReceivedWatcher :: Output -> Stored DirectMessage -> IO ()-dmReceivedWatcher out smsg = do- let msg = fromStored smsg- outLine out $ unwords- [ "dm-received"- , "from", maybe "<unnamed>" T.unpack $ idName $ msgFrom msg- , "text", T.unpack $ msgText msg- ]+discoveryAttributes :: DiscoveryAttributes+discoveryAttributes = (defaultServiceAttributes Proxy)+ { discoveryProvideTunnel = \_ _ -> False+ } +dmThreadWatcher :: ComposedIdentity -> Output -> DirectMessageThread -> DirectMessageThread -> IO ()+dmThreadWatcher self out prev cur = do+ forM_ (reverse $ dmThreadToListSince prev cur) $ \msg -> do+ outLine out $ unwords+ [ if sameIdentity self (msgFrom msg)+ then "dm-sent"+ else "dm-received"+ , "from", maybe "<unnamed>" T.unpack $ idName $ msgFrom msg+ , "text", T.unpack $ msgText msg+ ] -newtype CommandM a = CommandM (ReaderT TestInput (StateT TestState (ExceptT String IO)) a)- deriving (Functor, Applicative, Monad, MonadIO, MonadReader TestInput, MonadState TestState, MonadError String) +newtype CommandM a = CommandM (ReaderT TestInput (StateT TestState (ExceptT ErebosError IO)) a)+ deriving (Functor, Applicative, Monad, MonadIO, MonadReader TestInput, MonadState TestState, MonadError ErebosError)+ instance MonadFail CommandM where- fail = throwError+ fail = throwOtherError instance MonadRandom CommandM where getRandomBytes = liftIO . getRandomBytes@@ -245,72 +265,85 @@ type Command = CommandM () -commands :: [(Text, Command)]-commands = map (T.pack *** id)- [ ("store", cmdStore)- , ("load", cmdLoad)- , ("stored-generation", cmdStoredGeneration)- , ("stored-roots", cmdStoredRoots)- , ("stored-set-add", cmdStoredSetAdd)- , ("stored-set-list", cmdStoredSetList)- , ("head-create", cmdHeadCreate)- , ("head-replace", cmdHeadReplace)- , ("head-watch", cmdHeadWatch)- , ("head-unwatch", cmdHeadUnwatch)- , ("create-identity", cmdCreateIdentity)- , ("identity-info", cmdIdentityInfo)- , ("start-server", cmdStartServer)- , ("stop-server", cmdStopServer)- , ("peer-add", cmdPeerAdd)- , ("peer-drop", cmdPeerDrop)- , ("peer-list", cmdPeerList)- , ("test-message-send", cmdTestMessageSend)- , ("local-state-get", cmdLocalStateGet)- , ("local-state-replace", cmdLocalStateReplace)- , ("local-state-wait", cmdLocalStateWait)- , ("shared-state-get", cmdSharedStateGet)- , ("shared-state-wait", cmdSharedStateWait)- , ("watch-local-identity", cmdWatchLocalIdentity)- , ("watch-shared-identity", cmdWatchSharedIdentity)- , ("update-local-identity", cmdUpdateLocalIdentity)- , ("update-shared-identity", cmdUpdateSharedIdentity)- , ("attach-to", cmdAttachTo)- , ("attach-accept", cmdAttachAccept)- , ("attach-reject", cmdAttachReject)- , ("contact-request", cmdContactRequest)- , ("contact-accept", cmdContactAccept)- , ("contact-reject", cmdContactReject)- , ("contact-list", cmdContactList)- , ("contact-set-name", cmdContactSetName)- , ("dm-send-peer", cmdDmSendPeer)- , ("dm-send-contact", cmdDmSendContact)- , ("dm-send-identity", cmdDmSendIdentity)- , ("dm-list-peer", cmdDmListPeer)- , ("dm-list-contact", cmdDmListContact)- , ("chatroom-create", cmdChatroomCreate)- , ("chatroom-delete", cmdChatroomDelete)- , ("chatroom-list-local", cmdChatroomListLocal)- , ("chatroom-watch-local", cmdChatroomWatchLocal)- , ("chatroom-set-name", cmdChatroomSetName)- , ("chatroom-subscribe", cmdChatroomSubscribe)- , ("chatroom-unsubscribe", cmdChatroomUnsubscribe)- , ("chatroom-members", cmdChatroomMembers)- , ("chatroom-join", cmdChatroomJoin)- , ("chatroom-join-as", cmdChatroomJoinAs)- , ("chatroom-leave", cmdChatroomLeave)- , ("chatroom-message-send", cmdChatroomMessageSend)- , ("discovery-connect", cmdDiscoveryConnect)+commands :: [ ( Text, Command ) ]+commands =+ [ ( "store", cmdStore )+ , ( "load", cmdLoad )+ , ( "stored-generation", cmdStoredGeneration )+ , ( "stored-roots", cmdStoredRoots )+ , ( "stored-set-add", cmdStoredSetAdd )+ , ( "stored-set-list", cmdStoredSetList )+ , ( "stored-difference", cmdStoredDifference )+ , ( "head-create", cmdHeadCreate )+ , ( "head-replace", cmdHeadReplace )+ , ( "head-watch", cmdHeadWatch )+ , ( "head-unwatch", cmdHeadUnwatch )+ , ( "create-identity", cmdCreateIdentity )+ , ( "identity-info", cmdIdentityInfo )+ , ( "start-server", cmdStartServer )+ , ( "stop-server", cmdStopServer )+ , ( "peer-add", cmdPeerAdd )+ , ( "peer-drop", cmdPeerDrop )+ , ( "peer-list", cmdPeerList )+ , ( "test-message-send", cmdTestMessageSend )+ , ( "test-stream-open", cmdTestStreamOpen )+ , ( "test-stream-close", cmdTestStreamClose )+ , ( "test-stream-send", cmdTestStreamSend )+ , ( "local-state-get", cmdLocalStateGet )+ , ( "local-state-replace", cmdLocalStateReplace )+ , ( "local-state-wait", cmdLocalStateWait )+ , ( "shared-state-get", cmdSharedStateGet )+ , ( "shared-state-wait", cmdSharedStateWait )+ , ( "watch-local-identity", cmdWatchLocalIdentity )+ , ( "watch-shared-identity", cmdWatchSharedIdentity )+ , ( "update-local-identity", cmdUpdateLocalIdentity )+ , ( "update-shared-identity", cmdUpdateSharedIdentity )+ , ( "attach-to", cmdAttachTo )+ , ( "attach-accept", cmdAttachAccept )+ , ( "attach-reject", cmdAttachReject )+ , ( "contact-request", cmdContactRequest )+ , ( "contact-accept", cmdContactAccept )+ , ( "contact-reject", cmdContactReject )+ , ( "contact-list", cmdContactList )+ , ( "contact-set-name", cmdContactSetName )+ , ( "dm-send-peer", cmdDmSendPeer )+ , ( "dm-send-contact", cmdDmSendContact )+ , ( "dm-send-identity", cmdDmSendIdentity )+ , ( "dm-list-peer", cmdDmListPeer )+ , ( "dm-list-contact", cmdDmListContact )+ , ( "chatroom-create", cmdChatroomCreate )+ , ( "chatroom-delete", cmdChatroomDelete )+ , ( "chatroom-list-local", cmdChatroomListLocal )+ , ( "chatroom-watch-local", cmdChatroomWatchLocal )+ , ( "chatroom-set-name", cmdChatroomSetName )+ , ( "chatroom-subscribe", cmdChatroomSubscribe )+ , ( "chatroom-unsubscribe", cmdChatroomUnsubscribe )+ , ( "chatroom-members", cmdChatroomMembers )+ , ( "chatroom-join", cmdChatroomJoin )+ , ( "chatroom-join-as", cmdChatroomJoinAs )+ , ( "chatroom-leave", cmdChatroomLeave )+ , ( "chatroom-message-send", cmdChatroomMessageSend )+ , ( "discovery-connect", cmdDiscoveryConnect )+ , ( "discovery-tunnel", cmdDiscoveryTunnel ) ] cmdStore :: Command cmdStore = do st <- asks tiStorage+ pst <- liftIO $ derivePartialStorage st [otype] <- asks tiParams ls <- getLines let cnt = encodeUtf8 $ T.unlines ls- ref <- liftIO $ unsafeStoreRawBytes st $ BL.fromChunks [encodeUtf8 otype, BC.singleton ' ', BC.pack (show $ B.length cnt), BC.singleton '\n', cnt]- cmdOut $ "store-done " ++ show (refDigest ref)+ full = BL.fromChunks+ [ encodeUtf8 otype+ , BC.singleton ' '+ , BC.pack (show $ B.length cnt)+ , BC.singleton '\n', cnt+ ]+ liftIO (copyRef st =<< storeRawBytes pst full) >>= \case+ Right ref -> cmdOut $ "store-done " ++ show (refDigest ref)+ Left _ -> cmdOut $ "store-failed" cmdLoad :: Command cmdLoad = do@@ -345,7 +378,7 @@ [Just iref, Just sref] -> return (wrappedLoad iref, loadSet @[Stored Object] sref) [Just iref] -> return (wrappedLoad iref, emptySet) _ -> fail "unexpected parameters"- set' <- storeSetAdd st [item] set+ set' <- storeSetAdd [ item ] set cmdOut $ "stored-set-add" ++ concatMap ((' ':) . show . refDigest . storedRef) (toComponents set') cmdStoredSetList :: Command@@ -358,6 +391,19 @@ cmdOut $ "stored-set-item" ++ concatMap ((' ':) . show . refDigest . storedRef) item cmdOut $ "stored-set-done" +cmdStoredDifference :: Command+cmdStoredDifference = do+ st <- asks tiStorage+ ( trefs1, "|" : trefs2 ) <- span (/= "|") <$> asks tiParams++ let loadObjs = mapM (maybe (fail "invalid ref") (return . wrappedLoad @Object) <=< liftIO . readRef st . encodeUtf8)+ objs1 <- loadObjs trefs1+ objs2 <- loadObjs trefs2++ forM_ (storedDifference objs1 objs2) $ \item -> do+ cmdOut $ "stored-difference-item " ++ (show $ refDigest $ storedRef item)+ cmdOut $ "stored-difference-done"+ cmdHeadCreate :: Command cmdHeadCreate = do [ ttid, tref ] <- asks tiParams@@ -412,7 +458,8 @@ initTestHead :: Head LocalState -> Command initTestHead h = do- _ <- liftIO . watchReceivedMessages h . dmReceivedWatcher =<< asks tiOutput+ let self = finalOwner $ headLocalIdentity h+ _ <- liftIO . watchDirectMessageThreads h . dmThreadWatcher self =<< asks tiOutput modify $ \s -> s { tsHead = Just h } loadTestHead :: CommandM (Head LocalState)@@ -435,17 +482,18 @@ st <- asks tiStorage names <- asks tiParams - h <- liftIO $ do+ h <- do Just identity <- if null names- then Just <$> createIdentity st Nothing Nothing- else foldrM (\n o -> Just <$> createIdentity st (Just n) o) Nothing names+ then Just <$> createIdentity Nothing Nothing+ else foldrM (\n o -> Just <$> createIdentity (Just n) o) Nothing names shared <- case names of- _:_:_ -> (:[]) <$> makeSharedStateUpdate st (Just $ finalOwner identity) []+ _:_:_ -> (: []) <$> makeSharedStateUpdate (Just $ finalOwner identity) [] _ -> return [] storeHead st $ LocalState- { lsIdentity = idExtData identity+ { lsPrev = Nothing+ , lsIdentity = idExtData identity , lsShared = shared , lsOther = [] }@@ -473,43 +521,79 @@ let parseParams = \case (name : value : rest)- | name == "services" -> T.splitOn "," value+ | name == "services" -> second ( map splitServiceParams (T.splitOn "," value) ++ ) (parseParams rest)+ (name : rest)+ | name == "test-log" -> first (\o -> o { serverTestLog = True }) (parseParams rest) | otherwise -> parseParams rest- _ -> []- serviceNames <- parseParams <$> asks tiParams+ _ -> ( defaultServerOptions { serverErrorPrefix = "server-error-message " }, [] ) + splitServiceParams svc =+ case T.splitOn ":" svc of+ name : params -> ( name, params )+ _ -> ( svc, [] )++ ( serverOptions, serviceNames ) <- parseParams <$> asks tiParams+ h <- getOrLoadHead rsPeers <- liftIO $ newMVar (1, []) services <- forM serviceNames $ \case- "attach" -> return $ someServiceAttr $ pairingAttributes (Proxy @AttachService) out rsPeers "attach"- "chatroom" -> return $ someService @ChatroomService Proxy- "contact" -> return $ someServiceAttr $ pairingAttributes (Proxy @ContactService) out rsPeers "contact"- "discovery" -> return $ someService @DiscoveryService Proxy- "dm" -> return $ someServiceAttr $ directMessageAttributes out- "sync" -> return $ someService @SyncService Proxy- "test" -> return $ someServiceAttr $ (defaultServiceAttributes Proxy)+ ( "attach", _ ) -> return $ someServiceAttr $ pairingAttributes (Proxy @AttachService) out rsPeers "attach"+ ( "chatroom", _ ) -> return $ someService @ChatroomService Proxy+ ( "contact", _ ) -> return $ someServiceAttr $ pairingAttributes (Proxy @ContactService) out rsPeers "contact"+ ( "discovery", params ) -> return $ someServiceAttr $ discoveryAttributes+ { discoveryProvideTunnel = \_ _ -> "tunnel" `elem` params+ }+ ( "dm", _ ) -> return $ someServiceAttr $ directMessageAttributes out+ ( "sync", _ ) -> return $ someService @SyncService Proxy+ ( "test", _ ) -> return $ someServiceAttr $ (defaultServiceAttributes Proxy) { testMessageReceived = \obj otype len sref -> do liftIO $ do void $ store (headStorage h) obj- outLine out $ unwords ["test-message-received", otype, len, sref]+ outLine out $ unwords [ "test-message-received", otype, len, sref ]+ , testStreamsReceived = \streams -> do+ pidx <- getPeerIndex rsPeers+ liftIO $ do+ nums <- mapM getStreamReaderNumber streams+ outLine out $ unwords $ "test-stream-open-from" : show pidx : map show nums+ forM_ (zip nums streams) $ \( num, stream ) -> void $ forkIO $ do+ let go = readStreamPacket stream >>= \case+ StreamData seqNum bytes -> do+ outLine out $ unwords [ "test-stream-received", show pidx, show num, show seqNum, BC.unpack bytes ]+ go+ StreamClosed seqNum -> do+ outLine out $ unwords [ "test-stream-closed-from", show pidx, show num, show seqNum ]+ go }- sname -> throwError $ "unknown service `" <> T.unpack sname <> "'"+ ( sname, _ ) -> throwOtherError $ "unknown service `" <> T.unpack sname <> "'" - rsServer <- liftIO $ startServer defaultServerOptions h (B.hPutStr stderr . (`BC.snoc` '\n') . BC.pack) services+ let logPrint str = do BC.hPutStrLn stdout (BC.pack str)+ hFlush stdout+ rsServer <- liftIO $ startServer serverOptions h logPrint services rsPeerThread <- liftIO $ forkIO $ void $ forever $ do peer <- getNextPeerChange rsServer - let printPeer (idx, p) = do- params <- peerIdentity p >>= return . \case- PeerIdentityFull pid -> ("id":) $ map (maybe "<unnamed>" T.unpack . idName) (unfoldOwners pid)- _ -> [ "addr", show (peerAddress p) ]- outLine out $ unwords $ [ "peer", show idx ] ++ params+ let printPeer TestPeer {..} = do+ params <- getPeerIdentity tpPeer >>= \case+ PeerIdentityFull pid -> do+ return $ ("id":) $ map (maybe "<unnamed>" T.unpack . idName) (unfoldOwners pid)+ _ -> do+ paddr <- getPeerAddress tpPeer+ return $ [ "addr", show paddr ]+ outLine out $ unwords $ [ "peer", show tpIndex ] ++ params - update (nid, []) = printPeer (nid, peer) >> return (nid + 1, [(nid, peer)])- update cur@(nid, p:ps) | snd p == peer = printPeer p >> return cur- | otherwise = fmap (p:) <$> update (nid, ps)+ update ( tpIndex, [] ) = do+ tpPeer <- return peer+ tpStreamReaders <- newMVar []+ tpStreamWriters <- newMVar []+ let tp = TestPeer {..}+ printPeer tp+ return ( tpIndex + 1, [ tp ] ) + update cur@( nid, p : ps )+ | tpPeer p == peer = printPeer p >> return cur+ | otherwise = fmap (p :) <$> update ( nid, ps )+ modifyMVar_ rsPeers update modify $ \s -> s { tsServer = Just RunningServer {..} }@@ -545,11 +629,12 @@ peers <- liftIO $ getCurrentPeerList rsServer tpeers <- liftIO $ readMVar rsPeers forM_ peers $ \peer -> do- Just (n, _) <- return $ find ((peer==).snd) . snd $ tpeers- mbpid <- peerIdentity peer+ Just tp <- return $ find ((peer ==) . tpPeer) . snd $ tpeers+ mbpid <- getPeerIdentity peer+ paddr <- getPeerAddress peer cmdOut $ unwords $ concat- [ [ "peer-list-item", show n ]- , [ "addr", show (peerAddress peer) ]+ [ [ "peer-list-item", show (tpIndex tp) ]+ , [ "addr", show paddr ] , case mbpid of PeerIdentityFull pid -> ("id":) $ map (maybe "<unnamed>" T.unpack . idName) (unfoldOwners pid) _ -> [] ]@@ -565,6 +650,40 @@ sendManyToPeer peer $ map (TestMessage . wrappedLoad) refs cmdOut "test-message-send done" +cmdTestStreamOpen :: Command+cmdTestStreamOpen = do+ spidx : rest <- asks tiParams+ tp <- getTestPeer spidx+ count <- case rest of+ [] -> return 1+ tcount : _ -> return $ read $ T.unpack tcount++ out <- asks tiOutput+ runPeerService (tpPeer tp) $ do+ streams <- openTestStreams count+ afterCommit $ do+ nums <- mapM getStreamWriterNumber streams+ modifyMVar_ (tpStreamWriters tp) $ return . (++ zip nums streams)+ outLine out $ unwords $ "test-stream-open-done"+ : T.unpack spidx+ : map show nums++cmdTestStreamClose :: Command+cmdTestStreamClose = do+ [ spidx, sid ] <- asks tiParams+ tp <- getTestPeer spidx+ Just stream <- lookup (read $ T.unpack sid) <$> liftIO (readMVar (tpStreamWriters tp))+ liftIO $ closeStream stream+ cmdOut $ unwords [ "test-stream-close-done", T.unpack spidx, T.unpack sid ]++cmdTestStreamSend :: Command+cmdTestStreamSend = do+ [ spidx, sid, content ] <- asks tiParams+ tp <- getTestPeer spidx+ Just stream <- lookup (read $ T.unpack sid) <$> liftIO (readMVar (tpStreamWriters tp))+ liftIO $ writeStream stream $ encodeUtf8 content+ cmdOut $ unwords [ "test-stream-send-done", T.unpack spidx, T.unpack sid ]+ cmdLocalStateGet :: Command cmdLocalStateGet = do h <- getHead@@ -637,7 +756,7 @@ cmdUpdateLocalIdentity :: Command cmdUpdateLocalIdentity = do [name] <- asks tiParams- updateLocalHead_ $ \ls -> do+ updateLocalState_ $ \ls -> do Just identity <- return $ validateExtendedIdentity $ lsIdentity $ fromStored ls let public = idKeyIdentity identity @@ -652,8 +771,8 @@ cmdUpdateSharedIdentity :: Command cmdUpdateSharedIdentity = do [name] <- asks tiParams- updateLocalHead_ $ updateSharedState_ $ \case- Nothing -> throwError "no existing shared identity"+ updateLocalState_ $ updateSharedState_ $ \case+ Nothing -> throwOtherError "no existing shared identity" Just identity -> do let public = idKeyIdentity identity secret <- loadKey public@@ -722,13 +841,13 @@ cmdContactSetName = do [cid, name] <- asks tiParams contact <- getContact cid- updateLocalHead_ $ updateSharedState_ $ contactSetName contact name+ updateLocalState_ $ updateSharedState_ $ contactSetName contact name cmdOut "contact-set-name-done" cmdDmSendPeer :: Command cmdDmSendPeer = do [spidx, msg] <- asks tiParams- PeerIdentityFull to <- peerIdentity =<< getPeer spidx+ PeerIdentityFull to <- getPeerIdentity =<< getPeer spidx void $ sendDirectMessage to msg cmdDmSendContact :: Command@@ -747,10 +866,10 @@ dmList :: Foldable f => Identity f -> Command dmList peer = do- threads <- toThreadList . lookupSharedValue . lsShared . headObject <$> getHead+ threads <- dmThreadList . lookupSharedValue . lsShared . headObject <$> getHead case find (sameIdentity peer . msgPeer) threads of Just thread -> do- forM_ (reverse $ threadToList thread) $ \DirectMessage {..} -> cmdOut $ "dm-list-item"+ forM_ (reverse $ dmThreadToList thread) $ \DirectMessage {..} -> cmdOut $ "dm-list-item" <> " from " <> (maybe "<unnamed>" T.unpack $ idName msgFrom) <> " text " <> (T.unpack msgText) Nothing -> return ()@@ -759,7 +878,7 @@ cmdDmListPeer :: Command cmdDmListPeer = do [spidx] <- asks tiParams- PeerIdentityFull to <- peerIdentity =<< getPeer spidx+ PeerIdentityFull to <- getPeerIdentity =<< getPeer spidx dmList to cmdDmListContact :: Command@@ -867,8 +986,7 @@ cmdChatroomJoinAs :: Command cmdChatroomJoinAs = do [ cid, name ] <- asks tiParams- st <- asks tiStorage- identity <- liftIO $ createIdentity st (Just name) Nothing+ identity <- createIdentity (Just name) Nothing joinChatroomAsByStateData identity =<< getChatroomStateData cid cmdOut $ unwords [ "chatroom-join-as-done", T.unpack cid ] @@ -886,8 +1004,14 @@ cmdDiscoveryConnect :: Command cmdDiscoveryConnect = do- st <- asks tiStorage [ tref ] <- asks tiParams- Just ref <- liftIO $ readRef st $ encodeUtf8 tref+ Just dgst <- return $ readRefDigest $ encodeUtf8 tref Just RunningServer {..} <- gets tsServer- discoverySearch rsServer ref+ discoverySearch rsServer dgst++cmdDiscoveryTunnel :: Command+cmdDiscoveryTunnel = do+ [ tvia, ttarget ] <- asks tiParams+ via <- getPeer tvia+ Just target <- return $ readRefDigest $ encodeUtf8 ttarget+ liftIO $ discoverySetupTunnel via target
main/Test/Service.hs view
@@ -1,20 +1,26 @@ module Test.Service ( TestMessage(..), TestMessageAttributes(..),++ openTestStreams, ) where +import Control.Monad import Control.Monad.Reader import Data.ByteString.Lazy.Char8 qualified as BL import Erebos.Network+import Erebos.Object import Erebos.Service-import Erebos.Storage+import Erebos.Service.Stream+import Erebos.Storable data TestMessage = TestMessage (Stored Object) data TestMessageAttributes = TestMessageAttributes { testMessageReceived :: Object -> String -> String -> String -> ServiceHandler TestMessage ()+ , testStreamsReceived :: [ StreamReader ] -> ServiceHandler TestMessage () } instance Storable TestMessage where@@ -25,7 +31,10 @@ serviceID _ = mkServiceID "cb46b92c-9203-4694-8370-8742d8ac9dc8" type ServiceAttributes TestMessage = TestMessageAttributes- defaultServiceAttributes _ = TestMessageAttributes (\_ _ _ _ -> return ())+ defaultServiceAttributes _ = TestMessageAttributes+ { testMessageReceived = \_ _ _ _ -> return ()+ , testStreamsReceived = \_ -> return ()+ } serviceHandler smsg = do let TestMessage sobj = fromStored smsg@@ -35,3 +44,14 @@ cb <- asks $ testMessageReceived . svcAttributes cb obj otype len (show $ refDigest $ storedRef sobj) _ -> return ()++ streams <- receivedStreams+ when (not $ null streams) $ do+ cb <- asks $ testStreamsReceived . svcAttributes+ cb streams+++openTestStreams :: Int -> ServiceHandler TestMessage [ StreamWriter ]+openTestStreams count = do+ replyPacket . TestMessage =<< mstore (Rec [])+ replicateM count openStream
+ main/WebSocket.hs view
@@ -0,0 +1,48 @@+module WebSocket (+ WebSocketAddress(..),+ startWebsocketServer,+) where++import Control.Concurrent+import Control.Exception+import Control.Monad++import Data.ByteString.Lazy qualified as BL+import Data.Unique++import Erebos.Network++import Network.WebSockets qualified as WS+++data WebSocketAddress = WebSocketAddress Unique WS.Connection++instance Eq WebSocketAddress where+ WebSocketAddress u _ == WebSocketAddress u' _ = u == u'++instance Ord WebSocketAddress where+ compare (WebSocketAddress u _) (WebSocketAddress u' _) = compare u u'++instance Show WebSocketAddress where+ show (WebSocketAddress _ _) = "websocket"++instance PeerAddressType WebSocketAddress where+ sendBytesToAddress (WebSocketAddress _ conn) msg = do+ WS.sendDataMessage conn $ WS.Binary $ BL.fromStrict msg+ connectionToAddressClosed (WebSocketAddress _ conn) = do+ WS.sendClose conn BL.empty++startWebsocketServer :: Server -> String -> Int -> (String -> IO ()) -> IO ()+startWebsocketServer server addr port logd = do+ void $ forkIO $ do+ WS.runServer addr port $ \pending -> do+ conn <- WS.acceptRequest pending+ u <- newUnique+ let paddr = WebSocketAddress u conn+ void $ serverPeerCustom server paddr+ handle (\(e :: SomeException) -> logd $ "WebSocket thread exception: " ++ show e) $ do+ WS.withPingThread conn 30 (return ()) $ do+ forever $ do+ WS.receiveDataMessage conn >>= \case+ WS.Binary msg -> receivedFromCustomAddress server paddr $ BL.toStrict msg+ WS.Text {} -> logd $ "unexpected websocket text message"
src/Erebos/Attach.hs view
@@ -20,7 +20,7 @@ import Erebos.PubKey import Erebos.Service import Erebos.State-import Erebos.Storage+import Erebos.Storable import Erebos.Storage.Key type AttachService = PairingService AttachIdentity@@ -52,14 +52,14 @@ guard $ iddPrev (fromSigned $ idData identity) == [eiddStoredBase curid] return (identity, keys) - pairingFinalizeRequest (identity, keys) = updateLocalHead_ $ \slocal -> do+ pairingFinalizeRequest (identity, keys) = updateLocalState_ $ \slocal -> do let owner = finalOwner identity st <- getStorage pkeys <- mapM (copyStored st) [ idKeyIdentity owner, idKeyMessage owner ] liftIO $ mapM_ storeKey $ catMaybes [ keyFromData sec pub | sec <- keys, pub <- pkeys ] identity' <- mergeIdentity $ updateIdentity [ lsIdentity $ fromStored slocal ] identity- shared <- makeSharedStateUpdate st (Just owner) (lsShared $ fromStored slocal)+ shared <- makeSharedStateUpdate (Just owner) (lsShared $ fromStored slocal) mstore (fromStored slocal) { lsIdentity = idExtData identity' , lsShared = [ shared ]@@ -113,11 +113,11 @@ svcPrint $ "Attachement failed" } -attachToOwner :: (MonadIO m, MonadError String m) => Peer -> m ()+attachToOwner :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m () attachToOwner = pairingRequest @AttachIdentity Proxy -attachAccept :: (MonadIO m, MonadError String m) => Peer -> m ()+attachAccept :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m () attachAccept = pairingAccept @AttachIdentity Proxy -attachReject :: (MonadIO m, MonadError String m) => Peer -> m ()+attachReject :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m () attachReject = pairingReject @AttachIdentity Proxy
− src/Erebos/Channel.hs
@@ -1,175 +0,0 @@-module Erebos.Channel (- Channel,- ChannelRequest, ChannelRequestData(..),- ChannelAccept, ChannelAcceptData(..),-- createChannelRequest,- acceptChannelRequest,- acceptedChannel,-- channelEncrypt,- channelDecrypt,-) where--import Control.Concurrent.MVar-import Control.Monad-import Control.Monad.Except-import Control.Monad.IO.Class--import Crypto.Cipher.ChaChaPoly1305-import Crypto.Error--import Data.Binary-import Data.ByteArray (ByteArray, Bytes, ScrubbedBytes, convert)-import Data.ByteArray qualified as BA-import Data.ByteString.Lazy qualified as BL-import Data.List--import Erebos.Identity-import Erebos.PubKey-import Erebos.Storage--data Channel = Channel- { chPeers :: [Stored (Signed IdentityData)]- , chKey :: ScrubbedBytes- , chNonceFixedOur :: Bytes- , chNonceFixedPeer :: Bytes- , chCounterNextOut :: MVar Word64- , chCounterNextIn :: MVar Word64- }--type ChannelRequest = Signed ChannelRequestData--data ChannelRequestData = ChannelRequest- { crPeers :: [Stored (Signed IdentityData)]- , crKey :: Stored PublicKexKey- }- deriving (Show)--type ChannelAccept = Signed ChannelAcceptData--data ChannelAcceptData = ChannelAccept- { caRequest :: Stored ChannelRequest- , caKey :: Stored PublicKexKey- }---instance Storable ChannelRequestData where- store' cr = storeRec $ do- mapM_ (storeRef "peer") $ crPeers cr- storeRef "key" $ crKey cr-- load' = loadRec $ do- ChannelRequest- <$> loadRefs "peer"- <*> loadRef "key"--instance Storable ChannelAcceptData where- store' ca = storeRec $ do- storeRef "req" $ caRequest ca- storeRef "key" $ caKey ca-- load' = loadRec $ do- ChannelAccept- <$> loadRef "req"- <*> loadRef "key"---keySize :: Int-keySize = 32--createChannelRequest :: (MonadStorage m, MonadIO m, MonadError String m) => UnifiedIdentity -> UnifiedIdentity -> m (Stored ChannelRequest)-createChannelRequest self peer = do- (_, xpublic) <- liftIO . generateKeys =<< getStorage- skey <- loadKey $ idKeyMessage self- mstore =<< sign skey =<< mstore ChannelRequest { crPeers = sort [idData self, idData peer], crKey = xpublic }--acceptChannelRequest :: (MonadStorage m, MonadIO m, MonadError String m) => UnifiedIdentity -> UnifiedIdentity -> Stored ChannelRequest -> m (Stored ChannelAccept, Channel)-acceptChannelRequest self peer req = do- case sequence $ map validateIdentity $ crPeers $ fromStored $ signedData $ fromStored req of- Nothing -> throwError $ "invalid peers in channel request"- Just peers -> do- when (not $ any (self `sameIdentity`) peers) $- throwError $ "self identity missing in channel request peers"- when (not $ any (peer `sameIdentity`) peers) $- throwError $ "peer identity missing in channel request peers"- when (idKeyMessage peer `notElem` (map (sigKey . fromStored) $ signedSignature $ fromStored req)) $- throwError $ "channel requent not signed by peer"-- (xsecret, xpublic) <- liftIO . generateKeys =<< getStorage- skey <- loadKey $ idKeyMessage self- acc <- mstore =<< sign skey =<< mstore ChannelAccept { caRequest = req, caKey = xpublic }- liftIO $ do- let chPeers = crPeers $ fromStored $ signedData $ fromStored req- chKey = BA.take keySize $ dhSecret xsecret $- fromStored $ crKey $ fromStored $ signedData $ fromStored req- chNonceFixedOur = BA.pack [ 2, 0, 0, 0 ]- chNonceFixedPeer = BA.pack [ 1, 0, 0, 0 ]- chCounterNextOut <- newMVar 0- chCounterNextIn <- newMVar 0-- return (acc, Channel {..})--acceptedChannel :: (MonadIO m, MonadError String m) => UnifiedIdentity -> UnifiedIdentity -> Stored ChannelAccept -> m Channel-acceptedChannel self peer acc = do- let req = caRequest $ fromStored $ signedData $ fromStored acc- case sequence $ map validateIdentity $ crPeers $ fromStored $ signedData $ fromStored req of- Nothing -> throwError $ "invalid peers in channel accept"- Just peers -> do- when (not $ any (self `sameIdentity`) peers) $- throwError $ "self identity missing in channel accept peers"- when (not $ any (peer `sameIdentity`) peers) $- throwError $ "peer identity missing in channel accept peers"- when (idKeyMessage peer `notElem` (map (sigKey . fromStored) $ signedSignature $ fromStored acc)) $- throwError $ "channel accept not signed by peer"- when (idKeyMessage self `notElem` (map (sigKey . fromStored) $ signedSignature $ fromStored req)) $- throwError $ "original channel request not signed by us"-- xsecret <- loadKey $ crKey $ fromStored $ signedData $ fromStored req- let chPeers = crPeers $ fromStored $ signedData $ fromStored req- chKey = BA.take keySize $ dhSecret xsecret $- fromStored $ caKey $ fromStored $ signedData $ fromStored acc- chNonceFixedOur = BA.pack [ 1, 0, 0, 0 ]- chNonceFixedPeer = BA.pack [ 2, 0, 0, 0 ]- chCounterNextOut <- liftIO $ newMVar 0- chCounterNextIn <- liftIO $ newMVar 0-- return Channel {..}---channelEncrypt :: (ByteArray ba, MonadIO m, MonadError String m) => Channel -> ba -> m (ba, Word64)-channelEncrypt Channel {..} plain = do- count <- liftIO $ modifyMVar chCounterNextOut $ \c -> return (c + 1, c)- let cbytes = convert $ BL.toStrict $ encode count- nonce = nonce8 chNonceFixedOur cbytes- state <- case initialize chKey =<< nonce of- CryptoPassed state -> return state- CryptoFailed err -> throwError $ "failed to init chacha-poly1305 cipher: " <> show err-- let (ctext, state') = encrypt plain state- tag = finalize state'- return (BA.concat [ convert $ BA.drop 7 cbytes, ctext, convert tag ], count)--channelDecrypt :: (ByteArray ba, MonadIO m, MonadError String m) => Channel -> ba -> m (ba, Word64)-channelDecrypt Channel {..} body = do- when (BA.length body < 17) $ do- throwError $ "invalid encrypted data length"-- expectedCount <- liftIO $ readMVar chCounterNextIn- let countByte = body `BA.index` 0- body' = BA.dropView body 1- guessedCount = expectedCount - 128 + fromIntegral (countByte - fromIntegral expectedCount + 128 :: Word8)- nonce = nonce8 chNonceFixedPeer $ convert $ BL.toStrict $ encode guessedCount- blen = BA.length body' - 16- ctext = BA.takeView body' blen- tag = BA.dropView body' blen- state <- case initialize chKey =<< nonce of- CryptoPassed state -> return state- CryptoFailed err -> throwError $ "failed to init chacha-poly1305 cipher: " <> show err-- let (plain, state') = decrypt (convert ctext) state- when (not $ tag `BA.constEq` finalize state') $ do- throwError $ "tag validation falied"-- liftIO $ modifyMVar_ chCounterNextIn $ return . max (guessedCount + 1)- return (plain, guessedCount)
src/Erebos/Chatroom.hs view
@@ -17,6 +17,7 @@ joinChatroomAs, joinChatroomAsByStateData, leaveChatroom, leaveChatroomByStateData, getMessagesSinceState,+ isSameChatroom, ChatroomSetChange(..), watchChatrooms,@@ -54,7 +55,8 @@ import Erebos.Service import Erebos.Set import Erebos.State-import Erebos.Storage+import Erebos.Storable+import Erebos.Storage.Head import Erebos.Storage.Merge import Erebos.Util @@ -180,17 +182,17 @@ cmpView msg = (zonedTimeToUTC $ mdTime $ fromSigned msg, msg) sendChatroomMessage- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => ChatroomState -> Text -> m () sendChatroomMessage rstate msg = sendChatroomMessageByStateData (head $ roomStateData rstate) msg sendChatroomMessageByStateData- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> Text -> m () sendChatroomMessageByStateData lookupData msg = sendRawChatroomMessageByStateData lookupData Nothing Nothing (Just msg) False sendRawChatroomMessageByStateData- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> Maybe UnifiedIdentity -> Maybe (Stored (Signed ChatMessageData)) -> Maybe Text -> Bool -> m () sendRawChatroomMessageByStateData lookupData mbIdentity mdReplyTo mdText mdLeave = void $ findAndUpdateChatroomState $ \cstate -> do guard $ any (lookupData `precedesOrEquals`) $ roomStateData cstate@@ -282,7 +284,7 @@ instance SharedType (Set ChatroomState) where sharedTypeID _ = mkSharedTypeID "7bc71cbf-bc43-42b1-b413-d3a2c9a2aae0" -createChatroom :: (MonadStorage m, MonadHead LocalState m, MonadIO m, MonadError String m) => Maybe Text -> Maybe Text -> m ChatroomState+createChatroom :: (MonadStorage m, MonadHead LocalState m, MonadIO m, MonadError e m, FromErebosError e) => Maybe Text -> Maybe Text -> m ChatroomState createChatroom rdName rdDescription = do (secret, rdKey) <- liftIO . generateKeys =<< getStorage let rdPrev = []@@ -292,31 +294,29 @@ , rsdSubscribe = Just True } - updateLocalHead $ updateSharedState $ \rooms -> do- st <- getStorage- (, cstate) <$> storeSetAdd st cstate rooms+ updateLocalState $ updateSharedState $ \rooms -> do+ (, cstate) <$> storeSetAdd cstate rooms findAndUpdateChatroomState :: (MonadStorage m, MonadHead LocalState m) => (ChatroomState -> Maybe (m ChatroomState)) -> m (Maybe ChatroomState) findAndUpdateChatroomState f = do- updateLocalHead $ updateSharedState $ \roomSet -> do+ updateLocalState $ updateSharedState $ \roomSet -> do let roomList = fromSetBy (comparing $ roomName <=< roomStateRoom) roomSet case catMaybes $ map (\x -> (x,) <$> f x) roomList of ((orig, act) : _) -> do upd <- act if roomStateData orig /= roomStateData upd then do- st <- getStorage- roomSet' <- storeSetAdd st upd roomSet+ roomSet' <- storeSetAdd upd roomSet return (roomSet', Just upd) else do return (roomSet, Just upd) [] -> return (roomSet, Nothing) deleteChatroomByStateData- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> m () deleteChatroomByStateData lookupData = void $ findAndUpdateChatroomState $ \cstate -> do guard $ any (lookupData `precedesOrEquals`) $ roomStateData cstate@@ -327,7 +327,7 @@ } updateChatroomByStateData- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> Maybe Text -> Maybe Text@@ -368,7 +368,7 @@ findChatroomByStateData cdata = findChatroom $ any (cdata `precedesOrEquals`) . roomStateData chatroomSetSubscribe- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> Bool -> m () chatroomSetSubscribe lookupData subscribe = void $ findAndUpdateChatroomState $ \cstate -> do guard $ any (lookupData `precedesOrEquals`) $ roomStateData cstate@@ -389,39 +389,44 @@ toList $ ancestors $ roomStateMessageData joinChatroom- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => ChatroomState -> m () joinChatroom rstate = joinChatroomByStateData (head $ roomStateData rstate) joinChatroomByStateData- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> m () joinChatroomByStateData lookupData = sendRawChatroomMessageByStateData lookupData Nothing Nothing Nothing False joinChatroomAs- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => UnifiedIdentity -> ChatroomState -> m () joinChatroomAs identity rstate = joinChatroomAsByStateData identity (head $ roomStateData rstate) joinChatroomAsByStateData- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => UnifiedIdentity -> Stored ChatroomStateData -> m () joinChatroomAsByStateData identity lookupData = sendRawChatroomMessageByStateData lookupData (Just identity) Nothing Nothing False leaveChatroom- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => ChatroomState -> m () leaveChatroom rstate = leaveChatroomByStateData (head $ roomStateData rstate) leaveChatroomByStateData- :: (MonadStorage m, MonadHead LocalState m, MonadError String m)+ :: (MonadStorage m, MonadHead LocalState m, MonadError e m, FromErebosError e) => Stored ChatroomStateData -> m () leaveChatroomByStateData lookupData = sendRawChatroomMessageByStateData lookupData Nothing Nothing Nothing True getMessagesSinceState :: ChatroomState -> ChatroomState -> [ChatMessage] getMessagesSinceState cur old = threadToListSince (roomStateMessageData old) (roomStateMessageData cur) +isSameChatroom :: ChatroomState -> ChatroomState -> Bool+isSameChatroom rstate rstate' =+ let roots = filterAncestors . concatMap storedRoots . roomStateData+ in intersectsSorted (roots rstate) (roots rstate') + data ChatroomSetChange = AddedChatroom ChatroomState | RemovedChatroom ChatroomState | UpdatedChatroom ChatroomState ChatroomState@@ -522,7 +527,7 @@ } when (not $ null chatRoomInfo) $ do- updateLocalHead_ $ updateSharedState_ $ \roomSet -> do+ updateLocalState_ $ updateSharedState_ $ \roomSet -> do let rooms = fromSetBy (comparing $ roomName <=< roomStateRoom) roomSet upd set (roomInfo :: Stored (Signed ChatroomData)) = do let currentRoots = storedRoots roomInfo@@ -561,7 +566,7 @@ svcModify $ \ps -> ps { psSubscribedTo = filter (/= leastRoot) (psSubscribedTo ps) } when (not (null chatRoomMessage)) $ do- updateLocalHead_ $ updateSharedState_ $ \roomSet -> do+ updateLocalState_ $ updateSharedState_ $ \roomSet -> do let rooms = fromSetBy (comparing $ roomName <=< roomStateRoom) roomSet upd set (msgData :: Stored (Signed ChatMessageData)) | Just msg <- validateSingleMessage msgData = do
src/Erebos/Contact.hs view
@@ -28,7 +28,7 @@ import Erebos.Service import Erebos.Set import Erebos.State-import Erebos.Storage+import Erebos.Storable import Erebos.Storage.Merge data Contact = Contact@@ -83,13 +83,12 @@ contactSetName :: MonadHead LocalState m => Contact -> Text -> Set Contact -> m (Set Contact) contactSetName contact name set = do- st <- getStorage- cdata <- wrappedStore st ContactData+ cdata <- mstore ContactData { cdPrev = toComponents contact , cdIdentity = [] , cdName = Just name }- storeSetAdd st (mergeSorted @Contact [cdata]) set+ storeSetAdd (mergeSorted @Contact [cdata]) set type ContactService = PairingService ContactAccepted@@ -155,21 +154,20 @@ svcPrint $ "Contact failed" } -contactRequest :: (MonadIO m, MonadError String m) => Peer -> m ()+contactRequest :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m () contactRequest = pairingRequest @ContactAccepted Proxy -contactAccept :: (MonadIO m, MonadError String m) => Peer -> m ()+contactAccept :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m () contactAccept = pairingAccept @ContactAccepted Proxy -contactReject :: (MonadIO m, MonadError String m) => Peer -> m ()+contactReject :: (MonadIO m, MonadError e m, FromErebosError e) => Peer -> m () contactReject = pairingReject @ContactAccepted Proxy finalizeContact :: MonadHead LocalState m => UnifiedIdentity -> m ()-finalizeContact identity = updateLocalHead_ $ updateSharedState_ $ \contacts -> do- st <- getStorage- cdata <- wrappedStore st ContactData+finalizeContact identity = updateLocalState_ $ updateSharedState_ $ \contacts -> do+ cdata <- mstore ContactData { cdPrev = [] , cdIdentity = idExtDataF $ finalOwner identity , cdName = Nothing }- storeSetAdd st (mergeSorted @Contact [cdata]) contacts+ storeSetAdd (mergeSorted @Contact [cdata]) contacts
src/Erebos/Conversation.hs view
@@ -7,9 +7,11 @@ formatMessage, Conversation,+ isSameConversation, directMessageConversation, chatroomConversation, chatroomConversationByStateData,+ isChatroomStateConversation, reloadConversation, lookupConversations, @@ -30,11 +32,11 @@ import Data.Time.Format import Data.Time.LocalTime -import Erebos.Identity import Erebos.Chatroom-import Erebos.Message hiding (formatMessage)+import Erebos.DirectMessage+import Erebos.Identity import Erebos.State-import Erebos.Storage+import Erebos.Storable data Message = DirectMessageMessage DirectMessage Bool@@ -64,12 +66,20 @@ ] -data Conversation = DirectMessageConversation DirectMessageThread- | ChatroomConversation ChatroomState+data Conversation+ = DirectMessageConversation DirectMessageThread+ | ChatroomConversation ChatroomState +isSameConversation :: Conversation -> Conversation -> Bool+isSameConversation (DirectMessageConversation t) (DirectMessageConversation t')+ = sameIdentity (msgPeer t) (msgPeer t')+isSameConversation (ChatroomConversation rstate) (ChatroomConversation rstate') = isSameChatroom rstate rstate'+isSameConversation _ _ = False+ directMessageConversation :: MonadHead LocalState m => ComposedIdentity -> m Conversation directMessageConversation peer = do- (find (sameIdentity peer . msgPeer) . toThreadList . lookupSharedValue . lsShared . fromStored <$> getLocalHead) >>= \case+ createOrUpdateDirectMessagePeer peer+ (find (sameIdentity peer . msgPeer) . dmThreadList . lookupSharedValue . lsShared . fromStored <$> getLocalHead) >>= \case Just thread -> return $ DirectMessageConversation thread Nothing -> return $ DirectMessageConversation $ DirectMessageThread peer [] [] [] [] @@ -79,13 +89,17 @@ chatroomConversationByStateData :: MonadHead LocalState m => Stored ChatroomStateData -> m (Maybe Conversation) chatroomConversationByStateData sdata = fmap ChatroomConversation <$> findChatroomByStateData sdata +isChatroomStateConversation :: ChatroomState -> Conversation -> Bool+isChatroomStateConversation rstate (ChatroomConversation rstate') = isSameChatroom rstate rstate'+isChatroomStateConversation _ _ = False+ reloadConversation :: MonadHead LocalState m => Conversation -> m Conversation reloadConversation (DirectMessageConversation thread) = directMessageConversation (msgPeer thread) reloadConversation cur@(ChatroomConversation rstate) = fromMaybe cur <$> chatroomConversation rstate -lookupConversations :: MonadHead LocalState m => m [Conversation]-lookupConversations = map DirectMessageConversation . toThreadList . lookupSharedValue . lsShared . fromStored <$> getLocalHead+lookupConversations :: MonadHead LocalState m => m [ Conversation ]+lookupConversations = map DirectMessageConversation . dmThreadList . lookupSharedValue . lsShared . fromStored <$> getLocalHead conversationName :: Conversation -> Text@@ -96,15 +110,15 @@ conversationPeer (DirectMessageConversation thread) = Just $ msgPeer thread conversationPeer (ChatroomConversation _) = Nothing -conversationHistory :: Conversation -> [Message]-conversationHistory (DirectMessageConversation thread) = map (\msg -> DirectMessageMessage msg False) $ threadToList thread+conversationHistory :: Conversation -> [ Message ]+conversationHistory (DirectMessageConversation thread) = map (\msg -> DirectMessageMessage msg False) $ dmThreadToList thread conversationHistory (ChatroomConversation rstate) = map (\msg -> ChatroomMessage msg False) $ roomStateMessages rstate -sendMessage :: (MonadHead LocalState m, MonadError String m) => Conversation -> Text -> m (Maybe Message)-sendMessage (DirectMessageConversation thread) text = fmap Just $ DirectMessageMessage <$> (fromStored <$> sendDirectMessage (msgPeer thread) text) <*> pure False-sendMessage (ChatroomConversation rstate) text = sendChatroomMessage rstate text >> return Nothing+sendMessage :: (MonadHead LocalState m, MonadError e m, FromErebosError e) => Conversation -> Text -> m ()+sendMessage (DirectMessageConversation thread) text = sendDirectMessage (msgPeer thread) text+sendMessage (ChatroomConversation rstate) text = sendChatroomMessage rstate text -deleteConversation :: (MonadHead LocalState m, MonadError String m) => Conversation -> m ()-deleteConversation (DirectMessageConversation _) = throwError "deleting direct message conversation is not supported"+deleteConversation :: (MonadHead LocalState m, MonadError e m, FromErebosError e) => Conversation -> m ()+deleteConversation (DirectMessageConversation _) = throwOtherError "deleting direct message conversation is not supported" deleteConversation (ChatroomConversation rstate) = deleteChatroomByStateData (head $ roomStateData rstate)
+ src/Erebos/DirectMessage.hs view
@@ -0,0 +1,345 @@+module Erebos.DirectMessage (+ DirectMessage(..),+ sendDirectMessage,+ updateDirectMessagePeer,+ createOrUpdateDirectMessagePeer,++ DirectMessageAttributes(..),+ defaultDirectMessageAttributes,++ DirectMessageThreads,+ dmThreadList,++ DirectMessageThread(..),+ dmThreadToList, dmThreadToListSince,+ dmThreadView,++ watchDirectMessageThreads,+ formatDirectMessage,+) where++import Control.Concurrent.MVar+import Control.Monad+import Control.Monad.Except+import Control.Monad.Reader++import Data.List+import Data.Ord+import Data.Set (Set)+import Data.Set qualified as S+import Data.Text (Text)+import Data.Text qualified as T+import Data.Time.Format+import Data.Time.LocalTime++import Erebos.Discovery+import Erebos.Identity+import Erebos.Network+import Erebos.Object+import Erebos.Service+import Erebos.State+import Erebos.Storable+import Erebos.Storage.Head+import Erebos.Storage.Merge++data DirectMessage = DirectMessage+ { msgFrom :: ComposedIdentity+ , msgPrev :: [ Stored DirectMessage ]+ , msgTime :: ZonedTime+ , msgText :: Text+ }++instance Storable DirectMessage where+ store' msg = storeRec $ do+ mapM_ (storeRef "from") $ idExtDataF $ msgFrom msg+ mapM_ (storeRef "PREV") $ msgPrev msg+ storeDate "time" $ msgTime msg+ storeText "text" $ msgText msg++ load' = loadRec $ DirectMessage+ <$> loadIdentity "from"+ <*> loadRefs "PREV"+ <*> loadDate "time"+ <*> loadText "text"++data DirectMessageAttributes = DirectMessageAttributes+ { dmOwnerMismatch :: ServiceHandler DirectMessage ()+ }++defaultDirectMessageAttributes :: DirectMessageAttributes+defaultDirectMessageAttributes = DirectMessageAttributes+ { dmOwnerMismatch = svcPrint "Owner mismatch"+ }++instance Service DirectMessage where+ serviceID _ = mkServiceID "c702076c-4928-4415-8b6b-3e839eafcb0d"++ type ServiceAttributes DirectMessage = DirectMessageAttributes+ defaultServiceAttributes _ = defaultDirectMessageAttributes++ serviceHandler smsg = do+ let msg = fromStored smsg+ powner <- asks $ finalOwner . svcPeerIdentity+ erb <- svcGetLocal+ let DirectMessageThreads prev _ = lookupSharedValue $ lsShared $ fromStored erb+ sent = findMsgProperty powner msSent prev+ received = findMsgProperty powner msReceived prev+ received' = filterAncestors $ smsg : received+ if powner `sameIdentity` msgFrom msg ||+ filterAncestors sent == filterAncestors (smsg : sent)+ then do+ when (received' /= received) $ do+ next <- mstore MessageState+ { msPrev = prev+ , msPeer = powner+ , msReady = []+ , msSent = []+ , msReceived = received'+ , msSeen = []+ }+ let threads = DirectMessageThreads [ next ] (dmThreadView [ next ])+ shared <- makeSharedStateUpdate threads (lsShared $ fromStored erb)+ svcSetLocal =<< mstore (fromStored erb) { lsShared = [ shared ] }++ when (powner `sameIdentity` msgFrom msg) $ do+ replyStoredRef smsg++ else join $ asks $ dmOwnerMismatch . svcAttributes++ serviceNewPeer = do+ syncDirectMessageToPeer . lookupSharedValue . lsShared . fromStored =<< svcGetLocal++ serviceUpdatedPeer = do+ updateDirectMessagePeer . finalOwner =<< asks svcPeerIdentity++ serviceStorageWatchers _ =+ [ SomeStorageWatcher (lookupSharedValue . lsShared . fromStored) syncDirectMessageToPeer+ , GlobalStorageWatcher (lookupSharedValue . lsShared . fromStored) findMissingPeers+ ]+++data MessageState = MessageState+ { msPrev :: [ Stored MessageState ]+ , msPeer :: ComposedIdentity+ , msReady :: [ Stored DirectMessage ]+ , msSent :: [ Stored DirectMessage ]+ , msReceived :: [ Stored DirectMessage ]+ , msSeen :: [ Stored DirectMessage ]+ }++data DirectMessageThreads = DirectMessageThreads [ Stored MessageState ] [ DirectMessageThread ]++instance Eq DirectMessageThreads where+ DirectMessageThreads mss _ == DirectMessageThreads mss' _ = mss == mss'++dmThreadList :: DirectMessageThreads -> [ DirectMessageThread ]+dmThreadList (DirectMessageThreads _ threads) = threads++instance Storable MessageState where+ store' MessageState {..} = storeRec $ do+ mapM_ (storeRef "PREV") msPrev+ mapM_ (storeRef "peer") $ idExtDataF msPeer+ mapM_ (storeRef "ready") msReady+ mapM_ (storeRef "sent") msSent+ mapM_ (storeRef "received") msReceived+ mapM_ (storeRef "seen") msSeen++ load' = loadRec $ do+ msPrev <- loadRefs "PREV"+ msPeer <- loadIdentity "peer"+ msReady <- loadRefs "ready"+ msSent <- loadRefs "sent"+ msReceived <- loadRefs "received"+ msSeen <- loadRefs "seen"+ return MessageState {..}++instance Mergeable DirectMessageThreads where+ type Component DirectMessageThreads = MessageState+ mergeSorted mss = DirectMessageThreads mss (dmThreadView mss)+ toComponents (DirectMessageThreads mss _) = mss++instance SharedType DirectMessageThreads where+ sharedTypeID _ = mkSharedTypeID "ee793681-5976-466a-b0f0-4e1907d3fade"++findMsgProperty :: Foldable m => Identity m -> (MessageState -> [ a ]) -> [ Stored MessageState ] -> [ a ]+findMsgProperty pid sel mss = concat $ flip findProperty mss $ \x -> do+ guard $ msPeer x `sameIdentity` pid+ guard $ not $ null $ sel x+ return $ sel x+++sendDirectMessage :: (Foldable f, Applicative f, MonadHead LocalState m)+ => Identity f -> Text -> m ()+sendDirectMessage pid text = updateLocalState_ $ \ls -> do+ let self = localIdentity $ fromStored ls+ powner = finalOwner pid+ flip updateSharedState_ ls $ \(DirectMessageThreads prev _) -> do+ let ready = findMsgProperty powner msReady prev+ received = findMsgProperty powner msReceived prev++ time <- liftIO getZonedTime+ smsg <- mstore DirectMessage+ { msgFrom = toComposedIdentity $ finalOwner self+ , msgPrev = filterAncestors $ ready ++ received+ , msgTime = time+ , msgText = text+ }+ next <- mstore MessageState+ { msPrev = prev+ , msPeer = powner+ , msReady = [ smsg ]+ , msSent = []+ , msReceived = []+ , msSeen = []+ }+ return $ DirectMessageThreads [ next ] (dmThreadView [ next ])++updateDirectMessagePeer+ :: (Foldable f, Applicative f, MonadHead LocalState m)+ => Identity f -> m ()+updateDirectMessagePeer = createOrUpdateDirectMessagePeer' False++createOrUpdateDirectMessagePeer+ :: (Foldable f, Applicative f, MonadHead LocalState m)+ => Identity f -> m ()+createOrUpdateDirectMessagePeer = createOrUpdateDirectMessagePeer' True++createOrUpdateDirectMessagePeer'+ :: (Foldable f, Applicative f, MonadHead LocalState m)+ => Bool -> Identity f -> m ()+createOrUpdateDirectMessagePeer' create pid = do+ let powner = finalOwner pid+ updateLocalState_ $ updateSharedState_ $ \old@(DirectMessageThreads prev threads) -> do+ let updatePeerThread = do+ next <- mstore MessageState+ { msPrev = prev+ , msPeer = powner+ , msReady = []+ , msSent = []+ , msReceived = []+ , msSeen = []+ }+ return $ DirectMessageThreads [ next ] (dmThreadView [ next ])+ case find (sameIdentity powner . msgPeer) threads of+ Nothing+ | create+ -> updatePeerThread++ Just thread+ | oldPeer <- msgPeer thread+ , newPeer <- updateIdentity (idExtDataF powner) oldPeer+ , oldPeer /= newPeer+ -> updatePeerThread++ _ -> return old+++syncDirectMessageToPeer :: DirectMessageThreads -> ServiceHandler DirectMessage ()+syncDirectMessageToPeer (DirectMessageThreads mss _) = do+ pid <- finalOwner <$> asks svcPeerIdentity+ peer <- asks svcPeer+ let thread = messageThreadFor pid mss+ mapM_ (sendToPeerStored peer) $ msgHead thread+ updateLocalState_ $ \ls -> do+ let powner = finalOwner pid+ flip updateSharedState_ ls $ \unchanged@(DirectMessageThreads prev _) -> do+ let ready = findMsgProperty powner msReady prev+ sent = findMsgProperty powner msSent prev+ sent' = filterAncestors (ready ++ sent)++ if sent' /= sent+ then do+ next <- mstore MessageState+ { msPrev = prev+ , msPeer = powner+ , msReady = []+ , msSent = sent'+ , msReceived = []+ , msSeen = []+ }+ return $ DirectMessageThreads [ next ] (dmThreadView [ next ])+ else do+ return unchanged++findMissingPeers :: Server -> DirectMessageThreads -> ExceptT ErebosError IO ()+findMissingPeers server threads = do+ forM_ (dmThreadList threads) $ \thread -> do+ when (msgHead thread /= msgReceived thread) $ do+ mapM_ (discoverySearch server) $ map (refDigest . storedRef) $ idDataF $ msgPeer thread+++data DirectMessageThread = DirectMessageThread+ { msgPeer :: ComposedIdentity+ , msgHead :: [ Stored DirectMessage ]+ , msgSent :: [ Stored DirectMessage ]+ , msgSeen :: [ Stored DirectMessage ]+ , msgReceived :: [ Stored DirectMessage ]+ }++dmThreadToList :: DirectMessageThread -> [ DirectMessage ]+dmThreadToList thread = threadToListHelper S.empty $ msgHead thread++dmThreadToListSince :: DirectMessageThread -> DirectMessageThread -> [ DirectMessage ]+dmThreadToListSince since thread = threadToListHelper (S.fromAscList $ msgHead since) (msgHead thread)++threadToListHelper :: Set (Stored DirectMessage) -> [ Stored DirectMessage ] -> [ DirectMessage ]+threadToListHelper seen msgs+ | msg : msgs' <- filter (`S.notMember` seen) $ reverse $ sortBy (comparing cmpView) msgs =+ fromStored msg : threadToListHelper (S.insert msg seen) (msgs' ++ msgPrev (fromStored msg))+ | otherwise = []+ where+ cmpView msg = (zonedTimeToUTC $ msgTime $ fromStored msg, msg)++dmThreadView :: [ Stored MessageState ] -> [ DirectMessageThread ]+dmThreadView = helper []+ where helper used ms' = case filterAncestors ms' of+ mss@(sms : rest)+ | any (sameIdentity $ msPeer $ fromStored sms) used ->+ helper used $ msPrev (fromStored sms) ++ rest+ | otherwise ->+ let peer = msPeer $ fromStored sms+ in messageThreadFor peer mss : helper (peer : used) (msPrev (fromStored sms) ++ rest)+ _ -> []++messageThreadFor :: ComposedIdentity -> [ Stored MessageState ] -> DirectMessageThread+messageThreadFor peer mss =+ let ready = findMsgProperty peer msReady mss+ sent = findMsgProperty peer msSent mss+ received = findMsgProperty peer msReceived mss+ seen = findMsgProperty peer msSeen mss++ in DirectMessageThread+ { msgPeer = peer+ , msgHead = filterAncestors $ ready ++ received+ , msgSent = filterAncestors $ sent ++ received+ , msgSeen = filterAncestors $ ready ++ seen+ , msgReceived = filterAncestors $ received+ }+++watchDirectMessageThreads :: Head LocalState -> (DirectMessageThread -> DirectMessageThread -> IO ()) -> IO WatchedHead+watchDirectMessageThreads h f = do+ prevVar <- newMVar Nothing+ watchHeadWith h (lookupSharedValue . lsShared . headObject) $ \(DirectMessageThreads sms _) -> do+ modifyMVar_ prevVar $ \case+ Just prev -> do+ let addPeer (p : ps) p'+ | p `sameIdentity` p' = p : ps+ | otherwise = p : addPeer ps p'+ addPeer [] p' = [ p' ]++ let peers = foldl' addPeer [] $ map (msPeer . fromStored) $ storedDifference prev sms+ forM_ peers $ \peer -> do+ f (messageThreadFor peer prev) (messageThreadFor peer sms)+ return (Just sms)++ Nothing -> do+ return (Just sms)++formatDirectMessage :: TimeZone -> DirectMessage -> String+formatDirectMessage tzone msg = concat+ [ formatTime defaultTimeLocale "[%H:%M] " $ utcToLocalTime tzone $ zonedTimeToUTC $ msgTime msg+ , maybe "<unnamed>" T.unpack $ idName $ msgFrom msg+ , ": "+ , T.unpack $ msgText msg+ ]
src/Erebos/Discovery.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-} module Erebos.Discovery ( DiscoveryService(..),@@ -6,6 +7,7 @@ DiscoveryConnection(..), discoverySearch,+ discoverySetupTunnel, ) where import Control.Concurrent@@ -13,7 +15,6 @@ import Control.Monad.Except import Control.Monad.Reader -import Data.IP qualified as IP import Data.List import Data.Map.Strict (Map) import Data.Map.Strict qualified as M@@ -25,15 +26,18 @@ import Data.Text qualified as T import Data.Word -import Network.Socket+import Text.Read #ifdef ENABLE_ICE_SUPPORT import Erebos.ICE #endif import Erebos.Identity import Erebos.Network+import Erebos.Network.Address+import Erebos.Object import Erebos.Service-import Erebos.Storage+import Erebos.Service.Stream+import Erebos.Storable #ifndef ENABLE_ICE_SUPPORT@@ -44,18 +48,25 @@ data DiscoveryService- = DiscoverySelf [ Text ] (Maybe Int)- | DiscoveryAcknowledged [ Text ] (Maybe Text) (Maybe Word16) (Maybe Text) (Maybe Word16)- | DiscoverySearch Ref- | DiscoveryResult Ref [ Text ]+ = DiscoverySelf [ DiscoveryAddress ] (Maybe Int)+ | DiscoveryAcknowledged [ DiscoveryAddress ] (Maybe Text) (Maybe Word16) (Maybe Text) (Maybe Word16)+ | DiscoverySearch (Either Ref RefDigest)+ | DiscoveryResult (Either Ref RefDigest) [ DiscoveryAddress ] | DiscoveryConnectionRequest DiscoveryConnection | DiscoveryConnectionResponse DiscoveryConnection +data DiscoveryAddress+ = DiscoveryIP InetAddress PortNumber+ | DiscoveryICE+ | DiscoveryTunnel+ | DiscoveryOther Text+ data DiscoveryAttributes = DiscoveryAttributes { discoveryStunPort :: Maybe Word16 , discoveryStunServer :: Maybe Text , discoveryTurnPort :: Maybe Word16 , discoveryTurnServer :: Maybe Text+ , discoveryProvideTunnel :: Peer -> PeerAddress -> Bool } defaultDiscoveryAttributes :: DiscoveryAttributes@@ -64,19 +75,22 @@ , discoveryStunServer = Nothing , discoveryTurnPort = Nothing , discoveryTurnServer = Nothing+ , discoveryProvideTunnel = \_ _ -> False } data DiscoveryConnection = DiscoveryConnection- { dconnSource :: Ref- , dconnTarget :: Ref+ { dconnSource :: Either Ref RefDigest+ , dconnTarget :: Either Ref RefDigest , dconnAddress :: Maybe Text+ , dconnTunnel :: Bool , dconnIceInfo :: Maybe IceRemoteInfo } -emptyConnection :: Ref -> Ref -> DiscoveryConnection+emptyConnection :: Either Ref RefDigest -> Either Ref RefDigest -> DiscoveryConnection emptyConnection dconnSource dconnTarget = DiscoveryConnection {..} where dconnAddress = Nothing+ dconnTunnel = False dconnIceInfo = Nothing instance Storable DiscoveryService where@@ -92,19 +106,20 @@ storeMbInt "stun-port" stunPort storeMbText "turn-server" turnServer storeMbInt "turn-port" turnPort- DiscoverySearch ref -> storeRawRef "search" ref- DiscoveryResult ref addr -> do- storeRawRef "result" ref+ DiscoverySearch edgst -> either (storeRawRef "search") (storeRawWeak "search") edgst+ DiscoveryResult edgst addr -> do+ either (storeRawRef "result") (storeRawWeak "result") edgst mapM_ (storeText "address") addr DiscoveryConnectionRequest conn -> storeConnection "request" conn DiscoveryConnectionResponse conn -> storeConnection "response" conn where- storeConnection ctype DiscoveryConnection {..} = do+ storeConnection (ctype :: Text) DiscoveryConnection {..} = do storeText "connection" $ ctype- storeRawRef "source" dconnSource- storeRawRef "target" dconnTarget+ either (storeRawRef "source") (storeRawWeak "source") dconnSource+ either (storeRawRef "target") (storeRawWeak "target") dconnTarget storeMbText "address" dconnAddress+ when dconnTunnel $ storeEmpty "tunnel" storeMbRef "ice-info" dconnIceInfo load' = loadRec $ msum@@ -123,27 +138,63 @@ <*> loadMbInt "stun-port" <*> loadMbText "turn-server" <*> loadMbInt "turn-port"- , DiscoverySearch <$> loadRawRef "search"+ , DiscoverySearch <$> msum+ [ Left <$> loadRawRef "search"+ , Right <$> loadRawWeak "search"+ ] , DiscoveryResult- <$> loadRawRef "result"+ <$> msum+ [ Left <$> loadRawRef "result"+ , Right <$> loadRawWeak "result"+ ] <*> loadTexts "address" , loadConnection "request" DiscoveryConnectionRequest , loadConnection "response" DiscoveryConnectionResponse ] where- loadConnection ctype ctor = do+ loadConnection (ctype :: Text) ctor = do ctype' <- loadText "connection" guard $ ctype == ctype'- dconnSource <- loadRawRef "source"- dconnTarget <- loadRawRef "target"+ dconnSource <- msum+ [ Left <$> loadRawRef "source"+ , Right <$> loadRawWeak "source"+ ]+ dconnTarget <- msum+ [ Left <$> loadRawRef "target"+ , Right <$> loadRawWeak "target"+ ] dconnAddress <- loadMbText "address"+ dconnTunnel <- isJust <$> loadMbEmpty "tunnel" dconnIceInfo <- loadMbRef "ice-info" return $ ctor DiscoveryConnection {..} +instance StorableText DiscoveryAddress where+ toText = \case+ DiscoveryIP addr port -> T.unwords [ T.pack $ show addr, T.pack $ show port ]+ DiscoveryICE -> "ICE"+ DiscoveryTunnel -> "tunnel"+ DiscoveryOther str -> str++ fromText str = return $ if+ | [ addrStr, portStr ] <- T.words str+ , Just addr <- readMaybe $ T.unpack addrStr+ , Just port <- readMaybe $ T.unpack portStr+ -> DiscoveryIP addr port++ | "ice" <- T.toLower str+ -> DiscoveryICE++ | "tunnel" <- str+ -> DiscoveryTunnel++ | otherwise+ -> DiscoveryOther str++ data DiscoveryPeer = DiscoveryPeer { dpPriority :: Int , dpPeer :: Maybe Peer- , dpAddress :: [ Text ]+ , dpAddress :: [ DiscoveryAddress ] , dpIceSession :: Maybe IceSession } @@ -156,7 +207,11 @@ } data DiscoveryPeerState = DiscoveryPeerState- { dpsStunServer :: Maybe ( Text, Word16 )+ { dpsOurTunnelRequests :: [ ( RefDigest, StreamWriter ) ]+ -- ( original target, our write stream )+ , dpsRelayedTunnelRequests :: [ ( RefDigest, ( StreamReader, StreamWriter )) ]+ -- ( original source, ( from source, to target ))+ , dpsStunServer :: Maybe ( Text, Word16 ) , dpsTurnServer :: Maybe ( Text, Word16 ) , dpsIceConfig :: Maybe IceConfig }@@ -174,7 +229,9 @@ type ServiceState DiscoveryService = DiscoveryPeerState emptyServiceState _ = DiscoveryPeerState- { dpsStunServer = Nothing+ { dpsOurTunnelRequests = []+ , dpsRelayedTunnelRequests = []+ , dpsStunServer = Nothing , dpsTurnServer = Nothing , dpsIceConfig = Nothing }@@ -189,26 +246,22 @@ DiscoverySelf addrs priority -> do pid <- asks svcPeerIdentity peer <- asks svcPeer+ paddrs <- getPeerAddresses peer+ let insertHelper new old | dpPriority new > dpPriority old = new | otherwise = old- matchedAddrs <- fmap catMaybes $ forM addrs $ \addr -> if- | addr == T.pack "ICE" -> do- return $ Just addr - | [ ipaddr, port ] <- words (T.unpack addr)- , DatagramAddress paddr <- peerAddress peer -> do- saddr <- liftIO $ head <$> getAddrInfo (Just $ defaultHints { addrSocketType = Datagram }) (Just ipaddr) (Just port)- return $ if paddr == addrAddress saddr- then Just addr- else Nothing-- | otherwise -> return Nothing+ let matchedAddrs = flip filter addrs $ \case+ DiscoveryICE -> True+ DiscoveryIP ipaddr port ->+ DatagramAddress (inetToSockAddr ( ipaddr, port )) `elem` paddrs+ _ -> False forM_ (idDataF =<< unfoldOwners pid) $ \sdata -> do let dp = DiscoveryPeer { dpPriority = fromMaybe 0 priority , dpPeer = Just peer- , dpAddress = addrs+ , dpAddress = matchedAddrs , dpIceSession = Nothing } svcModifyGlobal $ \s -> s { dgsPeers = M.insertWith insertHelper (refDigest $ storedRef sdata) dp $ dgsPeers s }@@ -220,14 +273,8 @@ (discoveryTurnPort attrs) DiscoveryAcknowledged _ stunServer stunPort turnServer turnPort -> do- paddr <- asks (peerAddress . svcPeer) >>= return . \case- (DatagramAddress saddr) -> case IP.fromSockAddr saddr of- Just (IP.IPv6 ipv6, _)- | (0, 0, 0xffff, ipv4) <- IP.fromIPv6w ipv6- -> Just $ T.pack $ show (IP.toIPv4w ipv4)- Just (addr, _)- -> Just $ T.pack $ show addr- _ -> Nothing+ paddr <- asks svcPeerAddress >>= return . \case+ (DatagramAddress saddr) -> T.pack . show . fst <$> inetFromSockAddr saddr _ -> Nothing let toIceServer Nothing Nothing = Nothing@@ -240,65 +287,105 @@ , dpsTurnServer = toIceServer turnServer turnPort } - DiscoverySearch ref -> do- dpeer <- M.lookup (refDigest ref) . dgsPeers <$> svcGetGlobal- replyPacket $ DiscoveryResult ref $ maybe [] dpAddress dpeer+ DiscoverySearch edgst -> do+ dpeer <- M.lookup (either refDigest id edgst) . dgsPeers <$> svcGetGlobal+ peer <- asks svcPeer+ paddr <- asks svcPeerAddress+ attrs <- asks svcAttributes+ let offerTunnel+ | discoveryProvideTunnel attrs peer paddr = (++ [ DiscoveryTunnel ])+ | otherwise = id+ replyPacket $ DiscoveryResult edgst $ maybe [] (offerTunnel . dpAddress) dpeer DiscoveryResult _ [] -> do -- not found return () - DiscoveryResult ref addrs -> do- let dgst = refDigest ref+ DiscoveryResult edgst addrs -> do+ let dgst = either refDigest id edgst -- TODO: check if we really requested that server <- asks svcServer+ st <- getStorage self <- svcSelf discoveryPeer <- asks svcPeer let runAsService = runPeerService @DiscoveryService discoveryPeer - forM_ addrs $ \addr -> if- | addr == T.pack "ICE"- -> do+ let tryAddresses = \case+ DiscoveryIP ipaddr port : _ -> do+ void $ liftIO $ forkIO $ do+ let saddr = inetToSockAddr ( ipaddr, port )+ peer <- serverPeer server saddr+ runAsService $ do+ let upd dp = dp { dpPeer = Just peer }+ svcModifyGlobal $ \s -> s { dgsPeers = M.alter (Just . upd . fromMaybe emptyPeer) dgst $ dgsPeers s }++ DiscoveryICE : rest -> do #ifdef ENABLE_ICE_SUPPORT- getIceConfig >>= \case- Just config -> void $ liftIO $ forkIO $ do- ice <- iceCreateSession config PjIceSessRoleControlling $ \ice -> do- rinfo <- iceRemoteInfo ice+ getIceConfig >>= \case+ Just config -> do+ void $ liftIO $ forkIO $ do+ ice <- iceCreateSession config PjIceSessRoleControlling $ \ice -> do+ rinfo <- iceRemoteInfo ice - res <- runExceptT $ sendToPeer discoveryPeer $- DiscoveryConnectionRequest (emptyConnection (storedRef $ idData self) ref) { dconnIceInfo = Just rinfo }- case res of- Right _ -> return ()- Left err -> putStrLn $ "Discovery: failed to send connection request: " ++ err+ -- Try to promote weak ref to normal one for older peers:+ edgst' <- case edgst of+ Left r -> return (Left r)+ Right d -> refFromDigest st d >>= \case+ Just r -> return (Left r)+ Nothing -> return (Right d) - runAsService $ do- let upd dp = dp { dpIceSession = Just ice }- svcModifyGlobal $ \s -> s { dgsPeers = M.alter (Just . upd . fromMaybe emptyPeer) dgst $ dgsPeers s }+ res <- runExceptT $ sendToPeer discoveryPeer $+ DiscoveryConnectionRequest (emptyConnection (Left $ storedRef $ idData self) edgst') { dconnIceInfo = Just rinfo }+ case res of+ Right _ -> return ()+ Left err -> putStrLn $ "Discovery: failed to send connection request: " ++ err - Nothing -> do- return ()+ runAsService $ do+ let upd dp = dp { dpIceSession = Just ice }+ svcModifyGlobal $ \s -> s { dgsPeers = M.alter (Just . upd . fromMaybe emptyPeer) dgst $ dgsPeers s }++ Nothing -> do #endif- return ()+ tryAddresses rest - | [ ipaddr, port ] <- words (T.unpack addr) -> do- void $ liftIO $ forkIO $ do- saddr <- head <$>- getAddrInfo (Just $ defaultHints { addrSocketType = Datagram }) (Just ipaddr) (Just port)- peer <- serverPeer server (addrAddress saddr)- runAsService $ do- let upd dp = dp { dpPeer = Just peer }- svcModifyGlobal $ \s -> s { dgsPeers = M.alter (Just . upd . fromMaybe emptyPeer) dgst $ dgsPeers s }+ DiscoveryTunnel : _ -> do+ discoverySetupTunnelResponse dgst - | otherwise -> do- svcPrint $ "Discovery: invalid address in result: " ++ T.unpack addr+ addr : rest -> do+ svcPrint $ "Discovery: unsupported address in result: " ++ T.unpack (toText addr)+ tryAddresses rest + [] -> svcPrint $ "Discovery: no (supported) address received for " <> show dgst++ tryAddresses addrs+ DiscoveryConnectionRequest conn -> do self <- svcSelf+ attrs <- asks svcAttributes let rconn = emptyConnection (dconnSource conn) (dconnTarget conn)- if refDigest (dconnTarget conn) `elem` identityDigests self+ if either refDigest id (dconnTarget conn) `elem` identityDigests self then if+ -- request for us, create ICE sesssion or tunnel+ | dconnTunnel conn -> do+ receivedStreams >>= \case+ (tunnelReader : _) -> do+ tunnelWriter <- openStream+ replyPacket $ DiscoveryConnectionResponse rconn+ { dconnTunnel = True+ }+ tunnelVia <- asks svcPeer+ tunnelIdentity <- asks svcPeerIdentity+ server <- asks svcServer+ void $ liftIO $ forkIO $ do+ tunnelStreamNumber <- getStreamWriterNumber tunnelWriter+ let addr = TunnelAddress {..}+ void $ serverPeerCustom server addr+ receiveFromTunnel server addr++ [] -> do+ svcPrint $ "Discovery: missing stream on tunnel request (endpoint)"+ #ifdef ENABLE_ICE_SUPPORT- -- request for us, create ICE sesssion | Just prinfo <- dconnIceInfo conn -> do server <- asks svcServer peer <- asks svcPeer@@ -318,58 +405,123 @@ svcPrint $ "Discovery: unsupported connection request" else do- -- request to some of our peers, relay- mbdp <- M.lookup (refDigest $ dconnTarget conn) . dgsPeers <$> svcGetGlobal- case mbdp of+ -- request to some of our peers, relay+ peer <- asks svcPeer+ paddr <- asks svcPeerAddress+ mbdp <- M.lookup (either refDigest id $ dconnTarget conn) . dgsPeers <$> svcGetGlobal+ streams <- receivedStreams+ case mbdp of Nothing -> replyPacket $ DiscoveryConnectionResponse rconn Just dp- | Just dpeer <- dpPeer dp -> do- sendToPeer dpeer $ DiscoveryConnectionRequest conn+ | Just dpeer <- dpPeer dp -> if+ | dconnTunnel conn -> if+ | not (discoveryProvideTunnel attrs peer paddr) -> do+ replyPacket $ DiscoveryConnectionResponse rconn+ | fromSource : _ <- streams -> do+ void $ liftIO $ forkIO $ runPeerService @DiscoveryService dpeer $ do+ toTarget <- openStream+ svcModify $ \s -> s { dpsRelayedTunnelRequests =+ ( either refDigest id $ dconnSource conn, ( fromSource, toTarget )) : dpsRelayedTunnelRequests s }+ replyPacket $ DiscoveryConnectionRequest conn+ | otherwise -> do+ svcPrint $ "Discovery: missing stream on tunnel request (relay)"+ | otherwise -> do+ sendToPeer dpeer $ DiscoveryConnectionRequest conn | otherwise -> svcPrint $ "Discovery: failed to relay connection request" DiscoveryConnectionResponse conn -> do self <- svcSelf+ dps <- svcGet dpeers <- dgsPeers <$> svcGetGlobal- if refDigest (dconnSource conn) `elem` identityDigests self- then do++ if either refDigest id (dconnSource conn) `elem` identityDigests self+ then do -- response to our request, try to connect to the peer server <- asks svcServer- if | Just addr <- dconnAddress conn- , [ipaddr, port] <- words (T.unpack addr) -> do- saddr <- liftIO $ head <$>- getAddrInfo (Just $ defaultHints { addrSocketType = Datagram }) (Just ipaddr) (Just port)- peer <- liftIO $ serverPeer server (addrAddress saddr)+ if+ | Just addr <- dconnAddress conn+ , [ addrStr, portStr ] <- words (T.unpack addr)+ , Just ipaddr <- readMaybe addrStr+ , Just port <- readMaybe portStr+ -> do+ let saddr = inetToSockAddr ( ipaddr, port )+ peer <- liftIO $ serverPeer server saddr let upd dp = dp { dpPeer = Just peer } svcModifyGlobal $ \s -> s- { dgsPeers = M.alter (Just . upd . fromMaybe emptyPeer) (refDigest $ dconnTarget conn) $ dgsPeers s }+ { dgsPeers = M.alter (Just . upd . fromMaybe emptyPeer) (either refDigest id $ dconnTarget conn) $ dgsPeers s } + | dconnTunnel conn+ , Just tunnelWriter <- lookup (either refDigest id (dconnTarget conn)) (dpsOurTunnelRequests dps)+ -> do+ receivedStreams >>= \case+ tunnelReader : _ -> do+ tunnelVia <- asks svcPeer+ tunnelIdentity <- asks svcPeerIdentity+ void $ liftIO $ forkIO $ do+ tunnelStreamNumber <- getStreamWriterNumber tunnelWriter+ let addr = TunnelAddress {..}+ void $ serverPeerCustom server addr+ receiveFromTunnel server addr+ [] -> do+ svcPrint $ "Discovery: missing stream in tunnel response"+ liftIO $ closeStream tunnelWriter++ | Just tunnelWriter <- lookup (either refDigest id (dconnTarget conn)) (dpsOurTunnelRequests dps)+ -> do+ svcPrint $ "Discovery: tunnel request failed"+ liftIO $ closeStream tunnelWriter+ #ifdef ENABLE_ICE_SUPPORT- | Just dp <- M.lookup (refDigest $ dconnTarget conn) dpeers+ | Just dp <- M.lookup (either refDigest id $ dconnTarget conn) dpeers , Just ice <- dpIceSession dp , Just rinfo <- dconnIceInfo conn -> do liftIO $ iceConnect ice rinfo $ void $ serverPeerIce server ice #endif | otherwise -> svcPrint $ "Discovery: connection request failed"- else do- -- response to relayed request- case M.lookup (refDigest $ dconnSource conn) dpeers of- Just dp | Just dpeer <- dpPeer dp -> do+ else do+ -- response to relayed request+ streams <- receivedStreams+ svcModify $ \s -> s { dpsRelayedTunnelRequests =+ filter ((either refDigest id (dconnSource conn) /=) . fst) (dpsRelayedTunnelRequests s) }++ case M.lookup (either refDigest id $ dconnSource conn) dpeers of+ Just dp | Just dpeer <- dpPeer dp -> if+ -- successful tunnel request+ | dconnTunnel conn+ , Just ( fromSource, toTarget ) <- lookup (either refDigest id (dconnSource conn)) (dpsRelayedTunnelRequests dps)+ , fromTarget : _ <- streams+ -> liftIO $ do+ toSourceVar <- newEmptyMVar+ void $ forkIO $ runPeerService @DiscoveryService dpeer $ do+ liftIO . putMVar toSourceVar =<< openStream+ svcModify $ \s -> s { dpsRelayedTunnelRequests =+ ( either refDigest id $ dconnSource conn, ( fromSource, toTarget )) : dpsRelayedTunnelRequests s }+ replyPacket $ DiscoveryConnectionResponse conn+ void $ forkIO $ do+ relayStream fromSource toTarget+ void $ forkIO $ do+ toSource <- readMVar toSourceVar+ relayStream fromTarget toSource++ -- failed tunnel request+ | Just ( _, toTarget ) <- lookup (either refDigest id (dconnSource conn)) (dpsRelayedTunnelRequests dps)+ -> do+ liftIO $ closeStream toTarget sendToPeer dpeer $ DiscoveryConnectionResponse conn- _ -> svcPrint $ "Discovery: failed to relay connection response" + | otherwise -> do+ sendToPeer dpeer $ DiscoveryConnectionResponse conn+ _ -> svcPrint $ "Discovery: failed to relay connection response"+ serviceNewPeer = do server <- asks svcServer peer <- asks svcPeer- st <- getStorage - let addrToText saddr = do- ( addr, port ) <- IP.fromSockAddr saddr- Just $ T.pack $ show addr <> " " <> show port addrs <- concat <$> sequence- [ catMaybes . map addrToText <$> liftIO (getServerAddresses server)+ [ catMaybes . map (fmap (uncurry DiscoveryIP) . inetFromSockAddr) <$> liftIO (getServerAddresses server) #ifdef ENABLE_ICE_SUPPORT- , return [ T.pack "ICE" ]+ , return [ DiscoveryICE ] #endif ] @@ -381,9 +533,7 @@ when (not $ null addrs) $ do sendToPeer peer $ DiscoverySelf addrs Nothing forM_ searchingFor $ \dgst -> do- liftIO (refFromDigest st dgst) >>= \case- Just ref -> sendToPeer peer $ DiscoverySearch ref- Nothing -> return ()+ sendToPeer peer $ DiscoverySearch (Right dgst) #ifdef ENABLE_ICE_SUPPORT serviceStopServer _ _ _ pstates = do@@ -416,17 +566,84 @@ #endif -discoverySearch :: (MonadIO m, MonadError String m) => Server -> Ref -> m ()-discoverySearch server ref = do+discoverySearch :: (MonadIO m, MonadError e m, FromErebosError e) => Server -> RefDigest -> m ()+discoverySearch server dgst = do peers <- liftIO $ getCurrentPeerList server match <- forM peers $ \peer -> do- peerIdentity peer >>= \case+ getPeerIdentity peer >>= \case PeerIdentityFull pid -> do- return $ refDigest ref `elem` identityDigests pid+ return $ dgst `elem` identityDigests pid _ -> return False when (not $ or match) $ do modifyServiceGlobalState server (Proxy @DiscoveryService) $ \s -> (, ()) s- { dgsSearchingFor = S.insert (refDigest ref) $ dgsSearchingFor s+ { dgsSearchingFor = S.insert dgst $ dgsSearchingFor s } forM_ peers $ \peer -> do- sendToPeer peer $ DiscoverySearch ref+ sendToPeer peer $ DiscoverySearch $ Right dgst+++data TunnelAddress = TunnelAddress+ { tunnelVia :: Peer+ , tunnelIdentity :: UnifiedIdentity+ , tunnelStreamNumber :: Int+ , tunnelReader :: StreamReader+ , tunnelWriter :: StreamWriter+ }++instance Eq TunnelAddress where+ x == y = (==)+ (idData (tunnelIdentity x), tunnelStreamNumber x)+ (idData (tunnelIdentity y), tunnelStreamNumber y)++instance Ord TunnelAddress where+ compare x y = compare+ (idData (tunnelIdentity x), tunnelStreamNumber x)+ (idData (tunnelIdentity y), tunnelStreamNumber y)++instance Show TunnelAddress where+ show tunnel = concat+ [ "tunnel@"+ , show $ refDigest $ storedRef $ idData $ tunnelIdentity tunnel+ , "/" <> show (tunnelStreamNumber tunnel)+ ]++instance PeerAddressType TunnelAddress where+ sendBytesToAddress TunnelAddress {..} bytes = do+ writeStream tunnelWriter bytes++ connectionToAddressClosed TunnelAddress {..} = do+ closeStream tunnelWriter++relayStream :: StreamReader -> StreamWriter -> IO ()+relayStream r w = do+ p <- readStreamPacket r+ writeStreamPacket w p+ case p of+ StreamClosed {} -> return ()+ _ -> relayStream r w++receiveFromTunnel :: Server -> TunnelAddress -> IO ()+receiveFromTunnel server taddr = do+ p <- readStreamPacket (tunnelReader taddr)+ case p of+ StreamData {..} -> do+ receivedFromCustomAddress server taddr stpData+ receiveFromTunnel server taddr+ StreamClosed {} -> do+ return ()+++discoverySetupTunnel :: Peer -> RefDigest -> IO ()+discoverySetupTunnel via target = do+ runPeerService via $ do+ discoverySetupTunnelResponse target++discoverySetupTunnelResponse :: RefDigest -> ServiceHandler DiscoveryService ()+discoverySetupTunnelResponse target = do+ self <- refDigest . storedRef . idData <$> svcSelf+ stream <- openStream+ svcModify $ \s -> s { dpsOurTunnelRequests = ( target, stream ) : dpsOurTunnelRequests s }+ replyPacket $ DiscoveryConnectionRequest+ (emptyConnection (Right self) (Right target))+ { dconnTunnel = True+ }
+ src/Erebos/Error.hs view
@@ -0,0 +1,39 @@+module Erebos.Error (+ ErebosError(..),+ showErebosError,++ FromErebosError(..),+ throwOtherError,+) where++import Control.Monad.Except+++data ErebosError+ = ManyErrors [ ErebosError ]+ | OtherError String++showErebosError :: ErebosError -> String+showErebosError (ManyErrors errs) = unlines $ map showErebosError errs+showErebosError (OtherError str) = str++instance Semigroup ErebosError where+ ManyErrors [] <> b = b+ a <> ManyErrors [] = a+ ManyErrors a <> ManyErrors b = ManyErrors (a ++ b)+ ManyErrors a <> b = ManyErrors (a ++ [ b ])+ a <> ManyErrors b = ManyErrors (a : b)+ a@OtherError {} <> b@OtherError {} = ManyErrors [ a, b ]++instance Monoid ErebosError where+ mempty = ManyErrors []+++class FromErebosError e where+ fromErebosError :: ErebosError -> e++instance FromErebosError ErebosError where+ fromErebosError = id++throwOtherError :: (MonadError e m, FromErebosError e) => String -> m a+throwOtherError = throwError . fromErebosError . OtherError
src/Erebos/ICE.chs view
@@ -16,13 +16,12 @@ iceConnect, iceSend, - iceSetChan,+ serverPeerIce, ) where import Control.Arrow import Control.Concurrent import Control.Monad-import Control.Monad.Except import Control.Monad.Identity import Data.ByteString (ByteString, packCStringLen, useAsCString)@@ -33,7 +32,6 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Read qualified as T-import Data.Void import Data.Word import Foreign.C.String@@ -44,7 +42,9 @@ import Foreign.Ptr import Foreign.StablePtr -import Erebos.Flow+import Erebos.Network+import Erebos.Object+import Erebos.Storable import Erebos.Storage #include "pjproject.h"@@ -52,7 +52,7 @@ data IceSession = IceSession { isStrans :: PjIceStrans , _isConfig :: IceConfig- , isChan :: MVar (Either [ByteString] (Flow Void ByteString))+ , isChan :: MVar (Either [ ByteString ] (ByteString -> IO ())) } instance Eq IceSession where@@ -64,7 +64,11 @@ instance Show IceSession where show _ = "<ICE>" +instance PeerAddressType IceSession where+ sendBytesToAddress = iceSend+ connectionToAddressClosed = iceDestroy + data IceRemoteInfo = IceRemoteInfo { iriUsernameFrament :: Text , iriPassword :: Text@@ -117,7 +121,7 @@ , icandPort = port , icandType = ctype }- _ -> throwError "failed to parse candidate"+ _ -> throwOtherError "failed to parse candidate" {#enum pj_ice_sess_role as IceSessionRole {underscoreToCase} deriving (Show, Eq) #}@@ -125,9 +129,9 @@ data PjIceStransCfg newtype IceConfig = IceConfig (ForeignPtr PjIceStransCfg) -foreign import ccall unsafe "pjproject.h &ice_cfg_free"+foreign import ccall unsafe "pjproject.h &erebos_ice_cfg_free" ice_cfg_free :: FunPtr (Ptr PjIceStransCfg -> IO ())-foreign import ccall unsafe "pjproject.h ice_cfg_create"+foreign import ccall unsafe "pjproject.h erebos_ice_cfg_create" ice_cfg_create :: CString -> Word16 -> CString -> Word16 -> IO (Ptr PjIceStransCfg) iceCreateConfig :: Maybe ( Text, Word16 ) -> Maybe ( Text, Word16 ) -> IO (Maybe IceConfig)@@ -139,7 +143,7 @@ then return Nothing else Just . IceConfig <$> newForeignPtr ice_cfg_free cfg -foreign import ccall unsafe "pjproject.h ice_cfg_stop_thread"+foreign import ccall unsafe "pjproject.h erebos_ice_cfg_stop_thread" ice_cfg_stop_thread :: Ptr PjIceStransCfg -> IO () iceStopThread :: IceConfig -> IO ()@@ -157,13 +161,13 @@ forkIO $ cb sess sess <- IceSession <$> (withForeignPtr fcfg $ \cfg ->- {#call ice_create #} (castPtr cfg) (fromIntegral $ fromEnum role) (castStablePtrToPtr sptr) (castStablePtrToPtr cbptr)+ {#call erebos_ice_create #} (castPtr cfg) (fromIntegral $ fromEnum role) (castStablePtrToPtr sptr) (castStablePtrToPtr cbptr) ) <*> pure icfg <*> (newMVar $ Left []) return $ sess -{#fun ice_destroy as ^ { isStrans `IceSession' } -> `()' #}+{#fun erebos_ice_destroy as iceDestroy { isStrans `IceSession' } -> `()' #} iceRemoteInfo :: IceSession -> IO IceRemoteInfo iceRemoteInfo sess = do@@ -178,7 +182,7 @@ let cptrs = take maxcand $ iterate (`plusPtr` maxlen) bytes pokeArray carr $ take maxcand cptrs - ncand <- {#call ice_encode_session #} (isStrans sess) ufrag pass def carr (fromIntegral maxlen) (fromIntegral maxcand)+ ncand <- {#call erebos_ice_encode_session #} (isStrans sess) ufrag pass def carr (fromIntegral maxlen) (fromIntegral maxcand) if ncand < 0 then fail "failed to generate ICE remote info" else IceRemoteInfo <$> (T.pack <$> peekCString ufrag)@@ -195,13 +199,13 @@ iceConnect :: IceSession -> IceRemoteInfo -> (IO ()) -> IO () iceConnect sess remote cb = do cbptr <- newStablePtr $ cb- ice_connect sess cbptr+ erebos_ice_connect sess cbptr (iriUsernameFrament remote) (iriPassword remote) (iriDefaultCandidate remote) (iriCandidates remote) -{#fun ice_connect { isStrans `IceSession', castStablePtrToPtr `StablePtr (IO ())',+{#fun erebos_ice_connect { isStrans `IceSession', castStablePtrToPtr `StablePtr (IO ())', withText* `Text', withText* `Text', withText* `Text', withTextArray* `[Text]'& } -> `()' #} withText :: Text -> (Ptr CChar -> IO a) -> IO a@@ -217,19 +221,19 @@ withByteStringLen :: Num n => ByteString -> ((Ptr CChar, n) -> IO a) -> IO a withByteStringLen t f = unsafeUseAsCStringLen t (f . (id *** fromIntegral)) -{#fun ice_send as ^ { isStrans `IceSession', withByteStringLen* `ByteString'& } -> `()' #}+{#fun erebos_ice_send as iceSend { isStrans `IceSession', withByteStringLen* `ByteString'& } -> `()' #} foreign export ccall ice_call_cb :: StablePtr (IO ()) -> IO () ice_call_cb :: StablePtr (IO ()) -> IO () ice_call_cb = join . deRefStablePtr -iceSetChan :: IceSession -> Flow Void ByteString -> IO ()-iceSetChan sess chan = do+iceSetServer :: IceSession -> Server -> IO ()+iceSetServer sess server = do modifyMVar_ (isChan sess) $ \orig -> do case orig of- Left buf -> mapM_ (writeFlowIO chan) $ reverse buf+ Left buf -> mapM_ (receivedFromCustomAddress server sess) $ reverse buf Right _ -> return ()- return $ Right chan+ return $ Right $ receivedFromCustomAddress server sess foreign export ccall ice_rx_data :: StablePtr IceSession -> Ptr CChar -> Int -> IO () ice_rx_data :: StablePtr IceSession -> Ptr CChar -> Int -> IO ()@@ -237,5 +241,12 @@ sess <- deRefStablePtr sptr bs <- packCStringLen (buf, len) modifyMVar_ (isChan sess) $ \case- mc@(Right chan) -> writeFlowIO chan bs >> return mc- Left bss -> return $ Left (bs:bss)+ mc@(Right sendToServer) -> sendToServer bs >> return mc+ Left bss -> return $ Left (bs : bss)+++serverPeerIce :: Server -> IceSession -> IO Peer+serverPeerIce server ice = do+ peer <- serverPeerCustom server ice+ iceSetServer ice server+ return peer
src/Erebos/ICE/pjproject.c view
@@ -78,7 +78,7 @@ { if (status != PJ_SUCCESS) { ice_perror("cb_on_ice_complete", status);- ice_destroy(strans);+ erebos_ice_destroy(strans); return; } @@ -139,7 +139,7 @@ pthread_mutex_unlock(&mutex); } -struct erebos_ice_cfg * ice_cfg_create( const char * stun_server, uint16_t stun_port,+struct erebos_ice_cfg * erebos_ice_cfg_create( const char * stun_server, uint16_t stun_port, const char * turn_server, uint16_t turn_port ) { ice_init();@@ -189,11 +189,11 @@ return ecfg; fail:- ice_cfg_free( ecfg );+ erebos_ice_cfg_free( ecfg ); return NULL; } -void ice_cfg_free( struct erebos_ice_cfg * ecfg )+void erebos_ice_cfg_free( struct erebos_ice_cfg * ecfg ) { if( ! ecfg ) return;@@ -216,14 +216,14 @@ free( ecfg ); } -void ice_cfg_stop_thread( struct erebos_ice_cfg * ecfg )+void erebos_ice_cfg_stop_thread( struct erebos_ice_cfg * ecfg ) { if( ! ecfg ) return; ecfg->exit = true; } -pj_ice_strans * ice_create( const struct erebos_ice_cfg * ecfg, pj_ice_sess_role role,+pj_ice_strans * erebos_ice_create( const struct erebos_ice_cfg * ecfg, pj_ice_sess_role role, HsStablePtr sptr, HsStablePtr cb ) { ice_init();@@ -249,7 +249,7 @@ return res; } -void ice_destroy(pj_ice_strans * strans)+void erebos_ice_destroy(pj_ice_strans * strans) { struct user_data * udata = pj_ice_strans_get_user_data(strans); if (udata->sptr)@@ -264,7 +264,7 @@ pj_ice_strans_destroy(strans); } -ssize_t ice_encode_session(pj_ice_strans * strans, char * ufrag, char * pass,+ssize_t erebos_ice_encode_session(pj_ice_strans * strans, char * ufrag, char * pass, char * def, char * candidates[], size_t maxlen, size_t maxcand) { int n;@@ -318,7 +318,7 @@ return cand_cnt; } -void ice_connect(pj_ice_strans * strans, HsStablePtr cb,+void erebos_ice_connect(pj_ice_strans * strans, HsStablePtr cb, const char * ufrag, const char * pass, const char * defcand, const char * tcandidates[], size_t ncand) {@@ -409,7 +409,7 @@ } } -void ice_send(pj_ice_strans * strans, const char * data, size_t len)+void erebos_ice_send(pj_ice_strans * strans, const char * data, size_t len) { if (!pj_ice_strans_sess_is_complete(strans)) { fprintf(stderr, "ICE: negotiation has not been started or is in progress\n");
src/Erebos/ICE/pjproject.h view
@@ -3,18 +3,18 @@ #include <pjnath.h> #include <HsFFI.h> -struct erebos_ice_cfg * ice_cfg_create( const char * stun_server, uint16_t stun_port,+struct erebos_ice_cfg * erebos_ice_cfg_create( const char * stun_server, uint16_t stun_port, const char * turn_server, uint16_t turn_port );-void ice_cfg_free( struct erebos_ice_cfg * cfg );-void ice_cfg_stop_thread( struct erebos_ice_cfg * cfg );+void erebos_ice_cfg_free( struct erebos_ice_cfg * cfg );+void erebos_ice_cfg_stop_thread( struct erebos_ice_cfg * cfg ); -pj_ice_strans * ice_create( const struct erebos_ice_cfg *, pj_ice_sess_role role,+pj_ice_strans * erebos_ice_create( const struct erebos_ice_cfg *, pj_ice_sess_role role, HsStablePtr sptr, HsStablePtr cb );-void ice_destroy(pj_ice_strans * strans);+void erebos_ice_destroy(pj_ice_strans * strans); -ssize_t ice_encode_session(pj_ice_strans *, char * ufrag, char * pass,+ssize_t erebos_ice_encode_session(pj_ice_strans *, char * ufrag, char * pass, char * def, char * candidates[], size_t maxlen, size_t maxcand);-void ice_connect(pj_ice_strans * strans, HsStablePtr cb,+void erebos_ice_connect(pj_ice_strans * strans, HsStablePtr cb, const char * ufrag, const char * pass, const char * defcand, const char * candidates[], size_t ncand);-void ice_send(pj_ice_strans *, const char * data, size_t len);+void erebos_ice_send(pj_ice_strans *, const char * data, size_t len);
src/Erebos/Identity.hs view
@@ -41,7 +41,7 @@ import qualified Data.Text as T import Erebos.PubKey-import Erebos.Storage+import Erebos.Storable import Erebos.Storage.Merge import Erebos.Util @@ -214,29 +214,33 @@ _ -> True -createIdentity :: Storage -> Maybe Text -> Maybe UnifiedIdentity -> IO UnifiedIdentity-createIdentity st name owner = do- (secret, public) <- generateKeys st- (_secretMsg, publicMsg) <- generateKeys st+createIdentity+ :: forall m e. (MonadStorage m, MonadError e m, FromErebosError e, MonadIO m)+ => Maybe Text -> Maybe UnifiedIdentity -> m UnifiedIdentity+createIdentity name owner = do+ st <- getStorage+ ( secret, public ) <- liftIO $ generateKeys st+ ( _secretMsg, publicMsg ) <- liftIO $ generateKeys st - let signOwner :: Signed a -> ReaderT Storage IO (Signed a)+ let signOwner :: Signed a -> m (Signed a) signOwner idd | Just o <- owner = do- Just ownerSecret <- loadKeyMb (iddKeyIdentity $ fromSigned $ idData o)+ ownerSecret <- maybe (throwOtherError "failed to load private key") return =<<+ loadKeyMb (iddKeyIdentity $ fromSigned $ idData o) signAdd ownerSecret idd | otherwise = return idd - Just identity <- flip runReaderT st $ do- baseData <- mstore =<< signOwner =<< sign secret =<<- mstore (emptyIdentityData public)- { iddOwner = idData <$> owner- , iddKeyMessage = Just publicMsg- }- let extOwner = do- odata <- idExtData <$> owner- guard $ isExtension odata- return odata+ baseData <- mstore =<< signOwner =<< sign secret =<<+ mstore (emptyIdentityData public)+ { iddOwner = idData <$> owner+ , iddKeyMessage = Just publicMsg+ }+ let extOwner = do+ odata <- idExtData <$> owner+ guard $ isExtension odata+ return odata + maybe (throwOtherError "created invalid identity") return =<< do validateExtendedIdentityF . I.Identity <$> if isJust name || isJust extOwner then mstore =<< signOwner =<< sign secret =<<@@ -245,7 +249,6 @@ , ideOwner = extOwner } else return $ baseToExtended baseData- return identity validateIdentity :: Stored (Signed IdentityData) -> Maybe UnifiedIdentity validateIdentity = validateIdentityF . I.Identity@@ -280,13 +283,13 @@ Just mk -> return mk loadIdentity :: String -> LoadRec ComposedIdentity-loadIdentity name = maybe (throwError "identity validation failed") return . validateExtendedIdentityF =<< loadRefs name+loadIdentity name = maybe (throwOtherError "identity validation failed") return . validateExtendedIdentityF =<< loadRefs name loadMbIdentity :: String -> LoadRec (Maybe ComposedIdentity) loadMbIdentity name = return . validateExtendedIdentityF =<< loadRefs name loadUnifiedIdentity :: String -> LoadRec UnifiedIdentity-loadUnifiedIdentity name = maybe (throwError "identity validation failed") return . validateExtendedIdentity =<< loadRef name+loadUnifiedIdentity name = maybe (throwOtherError "identity validation failed") return . validateExtendedIdentity =<< loadRef name loadMbUnifiedIdentity :: String -> LoadRec (Maybe UnifiedIdentity) loadMbUnifiedIdentity name = return . (validateExtendedIdentity =<<) =<< loadMbRef name@@ -322,7 +325,7 @@ findResult [] = Nothing findResult xs = sel $ fromSigned $ minimum xs -mergeIdentity :: (MonadStorage m, MonadError String m, MonadIO m) => Identity f -> m UnifiedIdentity+mergeIdentity :: (MonadStorage m, MonadError e m, FromErebosError e, MonadIO m) => Identity f -> m UnifiedIdentity mergeIdentity idt | Just idt' <- toUnifiedIdentity idt = return idt' mergeIdentity idt@Identity {..} = do (owner, ownerData) <- case idOwner_ of
− src/Erebos/Message.hs
@@ -1,283 +0,0 @@-module Erebos.Message (- DirectMessage(..),- sendDirectMessage,-- DirectMessageAttributes(..),- defaultDirectMessageAttributes,-- DirectMessageThreads,- toThreadList,-- DirectMessageThread(..),- threadToList,- messageThreadView,-- watchReceivedMessages,- formatMessage,- formatDirectMessage,-) where--import Control.Monad-import Control.Monad.Except-import Control.Monad.Reader--import Data.List-import Data.Ord-import qualified Data.Set as S-import Data.Text (Text)-import qualified Data.Text as T-import Data.Time.Format-import Data.Time.LocalTime--import Erebos.Discovery-import Erebos.Identity-import Erebos.Network-import Erebos.Service-import Erebos.State-import Erebos.Storage-import Erebos.Storage.Merge--data DirectMessage = DirectMessage- { msgFrom :: ComposedIdentity- , msgPrev :: [Stored DirectMessage]- , msgTime :: ZonedTime- , msgText :: Text- }--instance Storable DirectMessage where- store' msg = storeRec $ do- mapM_ (storeRef "from") $ idExtDataF $ msgFrom msg- mapM_ (storeRef "PREV") $ msgPrev msg- storeDate "time" $ msgTime msg- storeText "text" $ msgText msg-- load' = loadRec $ DirectMessage- <$> loadIdentity "from"- <*> loadRefs "PREV"- <*> loadDate "time"- <*> loadText "text"--data DirectMessageAttributes = DirectMessageAttributes- { dmOwnerMismatch :: ServiceHandler DirectMessage ()- }--defaultDirectMessageAttributes :: DirectMessageAttributes-defaultDirectMessageAttributes = DirectMessageAttributes- { dmOwnerMismatch = svcPrint "Owner mismatch"- }--instance Service DirectMessage where- serviceID _ = mkServiceID "c702076c-4928-4415-8b6b-3e839eafcb0d"-- type ServiceAttributes DirectMessage = DirectMessageAttributes- defaultServiceAttributes _ = defaultDirectMessageAttributes-- serviceHandler smsg = do- let msg = fromStored smsg- powner <- asks $ finalOwner . svcPeerIdentity- erb <- svcGetLocal- st <- getStorage- let DirectMessageThreads prev _ = lookupSharedValue $ lsShared $ fromStored erb- sent = findMsgProperty powner msSent prev- received = findMsgProperty powner msReceived prev- received' = filterAncestors $ smsg : received- if powner `sameIdentity` msgFrom msg ||- filterAncestors sent == filterAncestors (smsg : sent)- then do- when (received' /= received) $ do- next <- wrappedStore st $ MessageState- { msPrev = prev- , msPeer = powner- , msReady = []- , msSent = []- , msReceived = received'- , msSeen = []- }- let threads = DirectMessageThreads [next] (messageThreadView [next])- shared <- makeSharedStateUpdate st threads (lsShared $ fromStored erb)- svcSetLocal =<< wrappedStore st (fromStored erb) { lsShared = [shared] }-- when (powner `sameIdentity` msgFrom msg) $ do- replyStoredRef smsg-- else join $ asks $ dmOwnerMismatch . svcAttributes-- serviceNewPeer = syncDirectMessageToPeer . lookupSharedValue . lsShared . fromStored =<< svcGetLocal-- serviceStorageWatchers _ =- [ SomeStorageWatcher (lookupSharedValue . lsShared . fromStored) syncDirectMessageToPeer- , GlobalStorageWatcher (lookupSharedValue . lsShared . fromStored) findMissingPeers- ]---data MessageState = MessageState- { msPrev :: [Stored MessageState]- , msPeer :: ComposedIdentity- , msReady :: [Stored DirectMessage]- , msSent :: [Stored DirectMessage]- , msReceived :: [Stored DirectMessage]- , msSeen :: [Stored DirectMessage]- }--data DirectMessageThreads = DirectMessageThreads [Stored MessageState] [DirectMessageThread]--instance Eq DirectMessageThreads where- DirectMessageThreads mss _ == DirectMessageThreads mss' _ = mss == mss'--toThreadList :: DirectMessageThreads -> [DirectMessageThread]-toThreadList (DirectMessageThreads _ threads) = threads--instance Storable MessageState where- store' MessageState {..} = storeRec $ do- mapM_ (storeRef "PREV") msPrev- mapM_ (storeRef "peer") $ idExtDataF msPeer- mapM_ (storeRef "ready") msReady- mapM_ (storeRef "sent") msSent- mapM_ (storeRef "received") msReceived- mapM_ (storeRef "seen") msSeen-- load' = loadRec $ do- msPrev <- loadRefs "PREV"- msPeer <- loadIdentity "peer"- msReady <- loadRefs "ready"- msSent <- loadRefs "sent"- msReceived <- loadRefs "received"- msSeen <- loadRefs "seen"- return MessageState {..}--instance Mergeable DirectMessageThreads where- type Component DirectMessageThreads = MessageState- mergeSorted mss = DirectMessageThreads mss (messageThreadView mss)- toComponents (DirectMessageThreads mss _) = mss--instance SharedType DirectMessageThreads where- sharedTypeID _ = mkSharedTypeID "ee793681-5976-466a-b0f0-4e1907d3fade"--findMsgProperty :: Foldable m => Identity m -> (MessageState -> [a]) -> [Stored MessageState] -> [a]-findMsgProperty pid sel mss = concat $ flip findProperty mss $ \x -> do- guard $ msPeer x `sameIdentity` pid- guard $ not $ null $ sel x- return $ sel x---sendDirectMessage :: (Foldable f, Applicative f, MonadHead LocalState m, MonadError String m)- => Identity f -> Text -> m (Stored DirectMessage)-sendDirectMessage pid text = updateLocalHead $ \ls -> do- let self = localIdentity $ fromStored ls- powner = finalOwner pid- flip updateSharedState ls $ \(DirectMessageThreads prev _) -> do- let ready = findMsgProperty powner msReady prev- received = findMsgProperty powner msReceived prev-- time <- liftIO getZonedTime- smsg <- mstore DirectMessage- { msgFrom = toComposedIdentity $ finalOwner self- , msgPrev = filterAncestors $ ready ++ received- , msgTime = time- , msgText = text- }- next <- mstore MessageState- { msPrev = prev- , msPeer = powner- , msReady = [smsg]- , msSent = []- , msReceived = []- , msSeen = []- }- return (DirectMessageThreads [next] (messageThreadView [next]), smsg)--syncDirectMessageToPeer :: DirectMessageThreads -> ServiceHandler DirectMessage ()-syncDirectMessageToPeer (DirectMessageThreads mss _) = do- pid <- finalOwner <$> asks svcPeerIdentity- peer <- asks svcPeer- let thread = messageThreadFor pid mss- mapM_ (sendToPeerStored peer) $ msgHead thread- updateLocalHead_ $ \ls -> do- let powner = finalOwner pid- flip updateSharedState_ ls $ \unchanged@(DirectMessageThreads prev _) -> do- let ready = findMsgProperty powner msReady prev- sent = findMsgProperty powner msSent prev- sent' = filterAncestors (ready ++ sent)-- if sent' /= sent- then do- next <- mstore MessageState- { msPrev = prev- , msPeer = powner- , msReady = []- , msSent = sent'- , msReceived = []- , msSeen = []- }- return $ DirectMessageThreads [next] (messageThreadView [next])- else do- return unchanged--findMissingPeers :: Server -> DirectMessageThreads -> ExceptT String IO ()-findMissingPeers server threads = do- forM_ (toThreadList threads) $ \thread -> do- when (msgHead thread /= msgReceived thread) $ do- mapM_ (discoverySearch server) $ map storedRef $ idDataF $ msgPeer thread---data DirectMessageThread = DirectMessageThread- { msgPeer :: ComposedIdentity- , msgHead :: [ Stored DirectMessage ]- , msgSent :: [ Stored DirectMessage ]- , msgSeen :: [ Stored DirectMessage ]- , msgReceived :: [ Stored DirectMessage ]- }--threadToList :: DirectMessageThread -> [DirectMessage]-threadToList thread = helper S.empty $ msgHead thread- where helper seen msgs- | msg : msgs' <- filter (`S.notMember` seen) $ reverse $ sortBy (comparing cmpView) msgs =- fromStored msg : helper (S.insert msg seen) (msgs' ++ msgPrev (fromStored msg))- | otherwise = []- cmpView msg = (zonedTimeToUTC $ msgTime $ fromStored msg, msg)--messageThreadView :: [Stored MessageState] -> [DirectMessageThread]-messageThreadView = helper []- where helper used ms' = case filterAncestors ms' of- mss@(sms : rest)- | any (sameIdentity $ msPeer $ fromStored sms) used ->- helper used $ msPrev (fromStored sms) ++ rest- | otherwise ->- let peer = msPeer $ fromStored sms- in messageThreadFor peer mss : helper (peer : used) (msPrev (fromStored sms) ++ rest)- _ -> []--messageThreadFor :: ComposedIdentity -> [Stored MessageState] -> DirectMessageThread-messageThreadFor peer mss =- let ready = findMsgProperty peer msReady mss- sent = findMsgProperty peer msSent mss- received = findMsgProperty peer msReceived mss- seen = findMsgProperty peer msSeen mss-- in DirectMessageThread- { msgPeer = peer- , msgHead = filterAncestors $ ready ++ received- , msgSent = filterAncestors $ sent ++ received- , msgSeen = filterAncestors $ ready ++ seen- , msgReceived = filterAncestors $ received- }---watchReceivedMessages :: Head LocalState -> (Stored DirectMessage -> IO ()) -> IO WatchedHead-watchReceivedMessages h f = do- let self = finalOwner $ localIdentity $ headObject h- watchHeadWith h (lookupSharedValue . lsShared . headObject) $ \(DirectMessageThreads sms _) -> do- forM_ (map fromStored sms) $ \ms -> do- mapM_ f $ filter (not . sameIdentity self . msgFrom . fromStored) $ msReceived ms--{-# DEPRECATED formatMessage "use formatDirectMessage instead" #-}-formatMessage :: TimeZone -> DirectMessage -> String-formatMessage = formatDirectMessage--formatDirectMessage :: TimeZone -> DirectMessage -> String-formatDirectMessage tzone msg = concat- [ formatTime defaultTimeLocale "[%H:%M] " $ utcToLocalTime tzone $ zonedTimeToUTC $ msgTime msg- , maybe "<unnamed>" T.unpack $ idName $ msgFrom msg- , ": "- , T.unpack $ msgText msg- ]
src/Erebos/Network.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE CPP #-}- module Erebos.Network ( Server, startServer,@@ -10,14 +8,17 @@ ServerOptions(..), serverIdentity, defaultServerOptions, Peer, peerServer, peerStorage,- PeerAddress(..), peerAddress,- PeerIdentity(..), peerIdentity,+ PeerAddress(..), getPeerAddress, getPeerAddresses,+ PeerIdentity(..), getPeerIdentity, WaitingRef, wrDigest, Service(..),++ PeerAddressType(..),+ receivedFromCustomAddress,+ serverPeer,-#ifdef ENABLE_ICE_SUPPORT- serverPeerIce,-#endif+ serverPeerCustom,+ findPeer, dropPeer, isPeerDropped, sendToPeer, sendManyToPeer,@@ -37,13 +38,14 @@ import Control.Monad.Reader import Control.Monad.State +import Data.ByteString (ByteString) import Data.ByteString.Char8 qualified as BC import Data.ByteString.Lazy qualified as BL import Data.Function import Data.IP qualified as IP import Data.List import Data.Map (Map)-import qualified Data.Map as M+import Data.Map qualified as M import Data.Maybe import Data.Typeable import Data.Word@@ -57,14 +59,14 @@ import GHC.Conc.Sync (unsafeIOToSTM) import Network.Socket hiding (ControlMessage)-import qualified Network.Socket.ByteString as S+import Network.Socket.ByteString qualified as S -import Erebos.Channel-#ifdef ENABLE_ICE_SUPPORT-import Erebos.ICE-#endif+import Erebos.Error import Erebos.Identity+import Erebos.Network.Address+import Erebos.Network.Channel import Erebos.Network.Protocol+import Erebos.Object.Internal import Erebos.PubKey import Erebos.Service import Erebos.State@@ -93,7 +95,7 @@ , serverRawPath :: SymFlow (PeerAddress, BC.ByteString) , serverControlFlow :: Flow (ControlMessage PeerAddress) (ControlRequest PeerAddress) , serverDataResponse :: TQueue (Peer, Maybe PartialRef)- , serverIOActions :: TQueue (ExceptT String IO ())+ , serverIOActions :: TQueue (ExceptT ErebosError IO ()) , serverServices :: [SomeService] , serverServiceStates :: TMVar (M.Map ServiceID SomeServiceGlobalState) , serverPeers :: MVar (Map PeerAddress Peer)@@ -113,12 +115,16 @@ data ServerOptions = ServerOptions { serverPort :: PortNumber , serverLocalDiscovery :: Bool+ , serverErrorPrefix :: String+ , serverTestLog :: Bool } defaultServerOptions :: ServerOptions defaultServerOptions = ServerOptions { serverPort = discoveryPort , serverLocalDiscovery = True+ , serverErrorPrefix = ""+ , serverTestLog = False } @@ -133,6 +139,14 @@ , peerWaitingRefs :: TMVar [WaitingRef] } +-- | Get current main address of the peer (used to send new packets).+getPeerAddress :: MonadIO m => Peer -> m PeerAddress+getPeerAddress = liftIO . return . peerAddress++-- | Get all known addresses of given peer.+getPeerAddresses :: MonadIO m => Peer -> m [ PeerAddress ]+getPeerAddresses = fmap (: []) . getPeerAddress+ peerServer :: Peer -> Server peerServer = peerServer_ @@ -156,50 +170,52 @@ instance Eq Peer where (==) = (==) `on` peerIdentityVar -data PeerAddress = DatagramAddress SockAddr-#ifdef ENABLE_ICE_SUPPORT- | PeerIceSession IceSession-#endif+class (Eq addr, Ord addr, Show addr, Typeable addr) => PeerAddressType addr where+ sendBytesToAddress :: addr -> ByteString -> IO ()+ connectionToAddressClosed :: addr -> IO () +data PeerAddress+ = forall addr. PeerAddressType addr => CustomPeerAddress addr+ | DatagramAddress SockAddr+ instance Show PeerAddress where- show (DatagramAddress saddr) = unwords $ case IP.fromSockAddr saddr of- Just (IP.IPv6 ipv6, port)- | (0, 0, 0xffff, ipv4) <- IP.fromIPv6w ipv6- -> [show (IP.toIPv4w ipv4), show port]- Just (addr, port)- -> [show addr, show port]- _ -> [show saddr]-#ifdef ENABLE_ICE_SUPPORT- show (PeerIceSession ice) = show ice-#endif+ show (CustomPeerAddress addr) = show addr + show (DatagramAddress saddr) =+ case inetFromSockAddr saddr of+ Just ( addr, port ) -> unwords [ show addr, show port ]+ _ -> show saddr+ instance Eq PeerAddress where+ CustomPeerAddress addr == CustomPeerAddress addr'+ | Just addr'' <- cast addr' = addr == addr'' DatagramAddress addr == DatagramAddress addr' = addr == addr'-#ifdef ENABLE_ICE_SUPPORT- PeerIceSession ice == PeerIceSession ice' = ice == ice' _ == _ = False-#endif instance Ord PeerAddress where+ compare (CustomPeerAddress addr) (CustomPeerAddress addr')+ | Just addr'' <- cast addr' = compare addr addr''+ | otherwise = compare (typeOf addr) (typeOf addr')+ compare (CustomPeerAddress _ ) _ = LT+ compare _ (CustomPeerAddress _ ) = GT+ compare (DatagramAddress addr) (DatagramAddress addr') = compare addr addr'-#ifdef ENABLE_ICE_SUPPORT- compare (DatagramAddress _ ) _ = LT- compare _ (DatagramAddress _ ) = GT- compare (PeerIceSession ice ) (PeerIceSession ice') = compare ice ice'-#endif -data PeerIdentity = PeerIdentityUnknown (TVar [UnifiedIdentity -> ExceptT String IO ()])- | PeerIdentityRef WaitingRef (TVar [UnifiedIdentity -> ExceptT String IO ()])- | PeerIdentityFull UnifiedIdentity+data PeerIdentity+ = PeerIdentityUnknown (TVar [ UnifiedIdentity -> ExceptT ErebosError IO () ])+ | PeerIdentityRef WaitingRef (TVar [ UnifiedIdentity -> ExceptT ErebosError IO () ])+ | PeerIdentityFull UnifiedIdentity -peerIdentity :: MonadIO m => Peer -> m PeerIdentity-peerIdentity = liftIO . atomically . readTVar . peerIdentityVar+-- | Get currently known identity of the given peer+getPeerIdentity :: MonadIO m => Peer -> m PeerIdentity+getPeerIdentity = liftIO . atomically . readTVar . peerIdentityVar -data PeerState = PeerInit [(SecurityRequirement, TransportPacket Ref, [TransportHeaderItem])]- | PeerConnected (Connection PeerAddress)- | PeerDropped+data PeerState+ = PeerInit [ ( SecurityRequirement, TransportPacket Ref, [ TransportHeaderItem ] ) ]+ | PeerConnected (Connection PeerAddress)+ | PeerDropped lookupServiceType :: [TransportHeaderItem] -> Maybe ServiceID@@ -251,11 +267,20 @@ let logd = writeTQueue serverErrorLog forkServerThread server $ forever $ do- logd' =<< atomically (readTQueue serverErrorLog)+ logd' . (serverErrorPrefix serverOptions <>) =<< atomically (readTQueue serverErrorLog) + logt <- if+ | serverTestLog serverOptions -> do+ serverTestLog <- newTQueueIO+ forkServerThread server $ forever $ do+ logd' =<< atomically (readTQueue serverTestLog)+ return $ writeTQueue serverTestLog+ | otherwise -> do+ return $ \_ -> return ()+ forkServerThread server $ dataResponseWorker server forkServerThread server $ forever $ do- either (atomically . logd) return =<< runExceptT =<<+ either (atomically . logd . showErebosError) return =<< runExceptT =<< atomically (readTQueue serverIOActions) let open addr = do@@ -320,12 +345,10 @@ forkServerThread server $ forever $ do (paddr, msg) <- readFlowIO serverRawPath- handle (\(e :: IOException) -> atomically . logd $ "failed to send packet to " ++ show paddr ++ ": " ++ show e) $ do+ handle (\(e :: SomeException) -> atomically . logd $ "failed to send packet to " ++ show paddr ++ ": " ++ show e) $ do case paddr of+ CustomPeerAddress addr -> sendBytesToAddress addr msg DatagramAddress addr -> void $ S.sendTo sock msg addr-#ifdef ENABLE_ICE_SUPPORT- PeerIceSession ice -> iceSend ice msg-#endif forkServerThread server $ forever $ do readFlowIO serverControlFlow >>= \case@@ -367,9 +390,13 @@ prefs <- forM objs $ storeObject $ peerInStorage peer identity <- readMVar serverIdentity_ let svcs = map someServiceID serverServices- handlePacket identity secure peer chanSvc svcs header prefs+ handlePacket paddr identity secure peer chanSvc svcs header prefs peerLoop Nothing -> do+ case paddr of+ DatagramAddress _ -> return ()+ CustomPeerAddress caddr -> connectionToAddressClosed caddr+ dropPeer peer atomically $ writeTChan serverChanPeer peer peerLoop@@ -377,7 +404,7 @@ ReceivedAnnounce addr _ -> do void $ serverPeer' server addr - erebosNetworkProtocol (headLocalIdentity serverOrigHead) logd protocolRawPath protocolControlFlow+ erebosNetworkProtocol (headLocalIdentity serverOrigHead) logd logt protocolRawPath protocolControlFlow forkServerThread server $ withSocketsDo $ do let hints = defaultHints@@ -389,9 +416,9 @@ bracket (open addr) close loop forkServerThread server $ forever $ do- (peer, svc, ref) <- atomically $ readTQueue chanSvc+ ( peer, paddr, svc, ref, streams ) <- atomically $ readTQueue chanSvc case find ((svc ==) . someServiceID) serverServices of- Just service@(SomeService (_ :: Proxy s) attr) -> runPeerServiceOn (Just (service, attr)) peer (serviceHandler $ wrappedLoad @s ref)+ Just service@(SomeService (_ :: Proxy s) attr) -> runPeerServiceOn (Just ( service, attr )) streams paddr peer (serviceHandler $ wrappedLoad @s ref) _ -> atomically $ logd $ "unhandled service '" ++ show (toUUID svc) ++ "'" return server@@ -425,7 +452,7 @@ Right ref -> do atomically (writeTVar tvar $ Right ref) forkServerThread server $ runExceptT (wrefAction wr ref) >>= \case- Left err -> atomically $ writeTQueue (serverErrorLog server) err+ Left err -> atomically $ writeTQueue (serverErrorLog server) (showErebosError err) Right () -> return () return (Nothing, [])@@ -519,9 +546,7 @@ conn <- readTVarP peerState >>= \case PeerConnected conn -> return conn _ -> throwError "can't open stream without established connection"- (hdr, writer, handler) <- liftSTM (connAddWriteStream conn) >>= \case- Right res -> return res- Left err -> throwError err+ (hdr, writer, handler) <- liftEither =<< liftSTM (connAddWriteStream conn) liftSTM $ writeTQueue (serverIOActions peerServer_) (liftIO $ forkServerThread peerServer_ handler) addHeader hdr@@ -540,10 +565,10 @@ | otherwise = y : appendDistinct x ys appendDistinct x [] = [x] -handlePacket :: UnifiedIdentity -> Bool- -> Peer -> TQueue (Peer, ServiceID, Ref) -> [ServiceID]- -> TransportHeader -> [PartialRef] -> IO ()-handlePacket identity secure peer chanSvc svcs (TransportHeader headers) prefs = atomically $ do+handlePacket :: PeerAddress -> UnifiedIdentity -> Bool+ -> Peer -> TQueue ( Peer, PeerAddress, ServiceID, Ref, [ RawStreamReader ] ) -> [ ServiceID ]+ -> TransportHeader -> [ PartialRef ] -> IO ()+handlePacket paddr identity secure peer chanSvc svcs (TransportHeader headers) prefs = atomically $ do let server = peerServer peer ochannel <- getPeerChannel peer let sidentity = idData identity@@ -604,7 +629,7 @@ liftSTM $ writeTQueue (serverIOActions server) $ void $ liftIO $ forkIO $ do (runExcept <$> readObjectsFromStream (peerInStorage peer) streamReader) >>= \case Left err -> atomically $ writeTQueue (serverErrorLog server) $- "failed to receive object from stream: " <> err+ "failed to receive object from stream: " <> showErebosError err Right objs -> do forM_ objs $ \obj -> do pref <- storeObject (peerInStorage peer) obj@@ -676,17 +701,18 @@ | Just svc <- lookupServiceType headers -> if | svc `elem` svcs -> do if dgst `elem` map refDigest prefs || True {- TODO: used by Message service to confirm receive -}- then do- void $ newWaitingRef dgst $ \ref ->- liftIO $ atomically $ writeTQueue chanSvc (peer, svc, ref)- else throwError $ "missing service object " ++ show dgst+ then do+ streamReaders <- mapM acceptStream $ lookupNewStreams headers+ void $ newWaitingRef dgst $ \ref ->+ liftIO $ atomically $ writeTQueue chanSvc ( peer, paddr, svc, ref, streamReaders )+ else throwError $ "missing service object " ++ show dgst | otherwise -> addHeader $ Rejected dgst | otherwise -> throwError $ "service ref without type" _ -> return () -withPeerIdentity :: MonadIO m => Peer -> (UnifiedIdentity -> ExceptT String IO ()) -> m ()+withPeerIdentity :: MonadIO m => Peer -> (UnifiedIdentity -> ExceptT ErebosError IO ()) -> m () withPeerIdentity peer act = liftIO $ atomically $ readTVar (peerIdentityVar peer) >>= \case PeerIdentityUnknown tvar -> modifyTVar' tvar (act:) PeerIdentityRef _ tvar -> modifyTVar' tvar (act:)@@ -742,7 +768,7 @@ sendToPeerS peer [] $ TransportPacket (TransportHeader [Acknowledged $ refDigest accref]) [] finalizedChannel peer ch identity - Left dgst -> throwError $ "missing accept data " ++ BC.unpack (showRefDigest dgst)+ Left dgst -> throwOtherError $ "missing accept data " ++ BC.unpack (showRefDigest dgst) finalizedChannel :: Peer -> Channel -> UnifiedIdentity -> STM ()@@ -758,7 +784,7 @@ -- Notify services about new peer readTVar peerIdentityVar >>= \case- PeerIdentityFull _ -> notifyServicesOfPeer peer+ PeerIdentityFull _ -> notifyServicesOfPeer True peer _ -> return () @@ -784,7 +810,7 @@ PeerIdentityFull pid | idData pid `precedes` wrappedLoad ref -> validateAndUpdate (idUpdates pid) $ \_ -> do- notifyServicesOfPeer peer+ notifyServicesOfPeer False peer _ -> return () @@ -796,17 +822,24 @@ -> do writeTVar (peerIdentityVar peer) $ PeerIdentityFull pid' writeTChan (serverChanPeer $ peerServer peer) peer- when (idData pid /= idData pid') $ notifyServicesOfPeer peer+ when (pid /= pid') $ do+ notifyServicesOfPeer False peer | otherwise -> return () -notifyServicesOfPeer :: Peer -> STM ()-notifyServicesOfPeer peer@Peer { peerServer_ = Server {..} } = do+notifyServicesOfPeer :: Bool -> Peer -> STM ()+notifyServicesOfPeer new peer@Peer { peerServer_ = Server {..} } = do writeTQueue serverIOActions $ do+ paddr <- getPeerAddress peer forM_ serverServices $ \service@(SomeService _ attrs) ->- runPeerServiceOn (Just (service, attrs)) peer serviceNewPeer+ runPeerServiceOn (Just ( service, attrs )) [] paddr peer $+ if new then serviceNewPeer else serviceUpdatedPeer +receivedFromCustomAddress :: PeerAddressType addr => Server -> addr -> ByteString -> IO ()+receivedFromCustomAddress Server {..} addr msg = do+ writeFlowIO serverRawPath ( CustomPeerAddress addr, msg )+ mkPeer :: Server -> PeerAddress -> IO Peer mkPeer peerServer_ peerAddress = do peerState <- newTVarIO (PeerInit [])@@ -825,14 +858,8 @@ _ -> paddr serverPeer' server (DatagramAddress paddr') -#ifdef ENABLE_ICE_SUPPORT-serverPeerIce :: Server -> IceSession -> IO Peer-serverPeerIce server@Server {..} ice = do- let paddr = PeerIceSession ice- peer <- serverPeer' server paddr- iceSetChan ice $ mapFlow undefined (paddr,) serverRawPath- return peer-#endif+serverPeerCustom :: PeerAddressType addr => Server -> addr -> IO Peer+serverPeerCustom server addr = serverPeer' server (CustomPeerAddress addr) serverPeer' :: Server -> PeerAddress -> IO Peer serverPeer' server paddr = do@@ -846,6 +873,13 @@ writeFlow (serverControlFlow server) (RequestConnection paddr) return peer +findPeer :: Server -> (Peer -> IO Bool) -> IO (Maybe Peer)+findPeer server test = withMVar (serverPeers server) (helper . M.elems)+ where+ helper (p : ps) = test p >>= \case True -> return (Just p)+ False -> helper ps+ helper [] = return Nothing+ dropPeer :: MonadIO m => Peer -> m () dropPeer peer = liftIO $ do modifyMVar_ (serverPeers $ peerServer peer) $ \pvalue -> do@@ -873,20 +907,50 @@ sendManyToPeerStored :: (Service s, MonadIO m) => Peer -> [ Stored s ] -> m () sendManyToPeerStored peer = sendToPeerList peer . map (\part -> ServiceReply (Right part) True) -sendToPeerList :: (Service s, MonadIO m) => Peer -> [ServiceReply s] -> m ()+sendToPeerList :: (Service s, MonadIO m) => Peer -> [ ServiceReply s ] -> m () sendToPeerList peer parts = do let st = peerStorage peer- srefs <- liftIO $ fmap catMaybes $ forM parts $ \case- ServiceReply (Left x) use -> Just . (,use) <$> store st x- ServiceReply (Right sx) use -> return $ Just (storedRef sx, use)- ServiceFinally act -> act >> return Nothing- let dgsts = map (refDigest . fst) srefs- let content = map fst $ filter (\(ref, use) -> use && BL.length (lazyLoadBytes ref) < 500) srefs -- TODO: MTU- header = TransportHeader (ServiceType (serviceID $ head parts) : map ServiceRef dgsts)- packet = TransportPacket header content- ackedBy = concat [[ Acknowledged r, Rejected r, DataRequest r ] | r <- dgsts ]- liftIO $ atomically $ sendToPeerS peer ackedBy packet+ res <- runExceptT $ do+ srefs <- liftIO $ fmap catMaybes $ forM parts $ \case+ ServiceReply (Left x) use -> Just . (,use) <$> store st x+ ServiceReply (Right sx) use -> return $ Just (storedRef sx, use)+ _ -> return Nothing + streamHeaders <- concat <$> do+ (liftEither =<<) $ liftIO $ atomically $ runExceptT $ do+ forM parts $ \case+ ServiceOpenStream cb -> do+ conn <- lift (readTVar (peerState peer)) >>= \case+ PeerConnected conn -> return conn+ _ -> throwError "can't open stream without established connection"+ (hdr, writer, handler) <- liftEither =<< lift (connAddWriteStream conn)++ lift $ writeTQueue (serverIOActions (peerServer peer)) $ do+ liftIO $ forkServerThread (peerServer peer) handler+ return [ ( hdr, cb writer ) ]+ _ -> return []+ liftIO $ sequence_ $ map snd streamHeaders++ liftIO $ forM_ parts $ \case+ ServiceFinally act -> act+ _ -> return ()++ let dgsts = map (refDigest . fst) srefs+ let content = map fst $ filter (\(ref, use) -> use && BL.length (lazyLoadBytes ref) < 500) srefs -- TODO: MTU+ header = TransportHeader $ concat+ [ [ ServiceType (serviceID $ head parts) ]+ , map ServiceRef dgsts+ , map fst streamHeaders+ ]+ packet = TransportPacket header content+ ackedBy = concat [[ Acknowledged r, Rejected r, DataRequest r ] | r <- dgsts ]+ liftIO $ atomically $ sendToPeerS peer ackedBy packet++ case res of+ Right () -> return ()+ Left err -> liftIO $ atomically $ writeTQueue (serverErrorLog $ peerServer peer) $+ "failed to send packet to " <> show (peerAddress peer) <> ": " <> err+ sendToPeerS' :: SecurityRequirement -> Peer -> [TransportHeaderItem] -> TransportPacket Ref -> STM () sendToPeerS' secure Peer {..} ackedBy packet = do readTVar peerState >>= \case@@ -900,7 +964,7 @@ sendToPeerPlain :: Peer -> [TransportHeaderItem] -> TransportPacket Ref -> STM () sendToPeerPlain = sendToPeerS' PlaintextAllowed -sendToPeerWith :: forall s m. (Service s, MonadIO m, MonadError String m) => Peer -> (ServiceState s -> ExceptT String IO (Maybe s, ServiceState s)) -> m ()+sendToPeerWith :: forall s m e. (Service s, MonadIO m, MonadError e m, FromErebosError e) => Peer -> (ServiceState s -> ExceptT ErebosError IO (Maybe s, ServiceState s)) -> m () sendToPeerWith peer fobj = do let sproxy = Proxy @s sid = serviceID sproxy@@ -915,7 +979,7 @@ case res of Right (Just obj) -> sendToPeer peer obj Right Nothing -> return ()- Left err -> throwError err+ Left err -> throwError $ fromErebosError err lookupService :: forall s proxy. Service s => proxy s -> [SomeService] -> Maybe (SomeService, ServiceAttributes s)@@ -925,10 +989,12 @@ lookupService _ [] = Nothing runPeerService :: forall s m. (Service s, MonadIO m) => Peer -> ServiceHandler s () -> m ()-runPeerService = runPeerServiceOn Nothing+runPeerService peer handler = do+ paddr <- getPeerAddress peer+ runPeerServiceOn Nothing [] paddr peer handler -runPeerServiceOn :: forall s m. (Service s, MonadIO m) => Maybe (SomeService, ServiceAttributes s) -> Peer -> ServiceHandler s () -> m ()-runPeerServiceOn mbservice peer handler = liftIO $ do+runPeerServiceOn :: forall s m. (Service s, MonadIO m) => Maybe ( SomeService, ServiceAttributes s ) -> [ RawStreamReader ] -> PeerAddress -> Peer -> ServiceHandler s () -> m ()+runPeerServiceOn mbservice newStreams paddr peer handler = liftIO $ do let server = peerServer peer proxy = Proxy @s svc = serviceID proxy@@ -950,9 +1016,11 @@ let inp = ServiceInput { svcAttributes = attr , svcPeer = peer+ , svcPeerAddress = paddr , svcPeerIdentity = peerId , svcServer = server , svcPrintOp = atomically . logd+ , svcNewStreams = newStreams } reloadHead (serverOrigHead server) >>= \case Nothing -> atomically $ do@@ -968,13 +1036,13 @@ putTMVar (peerServiceState peer) $ M.insert svc (SomeServiceState proxy s') svcs putTMVar (serverServiceStates server) $ M.insert svc (SomeServiceGlobalState proxy gs') global _ -> do- atomically $ logd $ "can't run service handler on peer with incomplete identity " ++ show (peerAddress peer)+ atomically $ logd $ "can't run service handler on peer with incomplete identity " ++ show paddr _ -> atomically $ do logd $ "unhandled service '" ++ show (toUUID svc) ++ "'" modifyServiceGlobalState- :: forall s a m proxy. (Service s, MonadIO m, MonadError String m)+ :: forall s a m e proxy. (Service s, MonadIO m, MonadError e m, FromErebosError e) => Server -> proxy s -> (ServiceGlobalState s -> ( ServiceGlobalState s, a )) -> m a@@ -992,33 +1060,14 @@ putTMVar (serverServiceStates server) global' return res Nothing -> do- throwError $ "unhandled service '" ++ show (toUUID svc) ++ "'"+ throwOtherError $ "unhandled service '" ++ show (toUUID svc) ++ "'" -foreign import ccall unsafe "Network/ifaddrs.h join_multicast" cJoinMulticast :: CInt -> Ptr CSize -> IO (Ptr Word32)-foreign import ccall unsafe "Network/ifaddrs.h local_addresses" cLocalAddresses :: Ptr CSize -> IO (Ptr InetAddress)-foreign import ccall unsafe "Network/ifaddrs.h broadcast_addresses" cBroadcastAddresses :: IO (Ptr Word32)+foreign import ccall unsafe "Network/ifaddrs.h erebos_join_multicast" cJoinMulticast :: CInt -> Ptr CSize -> IO (Ptr Word32)+foreign import ccall unsafe "Network/ifaddrs.h erebos_local_addresses" cLocalAddresses :: Ptr CSize -> IO (Ptr InetAddress)+foreign import ccall unsafe "Network/ifaddrs.h erebos_broadcast_addresses" cBroadcastAddresses :: IO (Ptr Word32) foreign import ccall unsafe "stdlib.h free" cFree :: Ptr a -> IO () -data InetAddress = InetAddress { fromInetAddress :: IP.IP }--instance F.Storable InetAddress where- sizeOf _ = sizeOf (undefined :: CInt) + 16- alignment _ = 8-- peek ptr = (unpackFamily <$> peekByteOff ptr 0) >>= \case- AF_INET -> InetAddress . IP.IPv4 . IP.fromHostAddress <$> peekByteOff ptr (sizeOf (undefined :: CInt))- AF_INET6 -> InetAddress . IP.IPv6 . IP.toIPv6b . map fromIntegral <$> peekArray 16 (ptr `plusPtr` sizeOf (undefined :: CInt) :: Ptr Word8)- _ -> fail "InetAddress: unknown family"-- poke ptr (InetAddress addr) = case addr of- IP.IPv4 ip -> do- pokeByteOff ptr 0 (packFamily AF_INET)- pokeByteOff ptr (sizeOf (undefined :: CInt)) (IP.toHostAddress ip)- IP.IPv6 ip -> do- pokeByteOff ptr 0 (packFamily AF_INET6)- pokeArray (ptr `plusPtr` sizeOf (undefined :: CInt) :: Ptr Word8) (map fromIntegral $ IP.fromIPv6b ip)- joinMulticast :: Socket -> IO [ Word32 ] joinMulticast sock = withFdSocket sock $ \fd ->@@ -1045,7 +1094,7 @@ count <- fromIntegral <$> peek pcount res <- peekArray count ptr cFree ptr- return $ map (IP.toSockAddr . (, serverPort serverOptions ) . fromInetAddress) res+ return $ map (inetToSockAddr . (, serverPort serverOptions )) res getBroadcastAddresses :: PortNumber -> IO [SockAddr] getBroadcastAddresses port = do
src/Erebos/Network.hs-boot view
@@ -1,8 +1,9 @@ module Erebos.Network where -import Erebos.Storage+import Erebos.Object.Internal data Server data Peer+data PeerAddress peerStorage :: Peer -> Storage
+ src/Erebos/Network/Address.hs view
@@ -0,0 +1,65 @@+module Erebos.Network.Address (+ InetAddress(..),+ inetFromSockAddr,+ inetToSockAddr,++ SockAddr, PortNumber,+) where++import Data.Bifunctor+import Data.IP qualified as IP+import Data.Word++import Foreign.C.Types+import Foreign.Marshal.Array+import Foreign.Ptr+import Foreign.Storable as F++import Network.Socket++import Text.Read+++newtype InetAddress = InetAddress { fromInetAddress :: IP.IP }+ deriving (Eq, Ord)++instance Show InetAddress where+ show (InetAddress ipaddr)+ | IP.IPv6 ipv6 <- ipaddr+ , ( 0, 0, 0xffff, ipv4 ) <- IP.fromIPv6w ipv6+ = show (IP.toIPv4w ipv4)++ | otherwise+ = show ipaddr++instance Read InetAddress where+ readPrec = do+ readPrec >>= return . InetAddress . \case+ IP.IPv4 ipv4 -> IP.IPv6 $ IP.toIPv6w ( 0, 0, 0xffff, IP.fromIPv4w ipv4 )+ ipaddr -> ipaddr++ readListPrec = readListPrecDefault++instance F.Storable InetAddress where+ sizeOf _ = sizeOf (undefined :: CInt) + 16+ alignment _ = 8++ peek ptr = (unpackFamily <$> peekByteOff ptr 0) >>= \case+ AF_INET -> InetAddress . IP.IPv4 . IP.fromHostAddress <$> peekByteOff ptr (sizeOf (undefined :: CInt))+ AF_INET6 -> InetAddress . IP.IPv6 . IP.toIPv6b . map fromIntegral <$> peekArray 16 (ptr `plusPtr` sizeOf (undefined :: CInt) :: Ptr Word8)+ _ -> fail "InetAddress: unknown family"++ poke ptr (InetAddress addr) = case addr of+ IP.IPv4 ip -> do+ pokeByteOff ptr 0 (packFamily AF_INET)+ pokeByteOff ptr (sizeOf (undefined :: CInt)) (IP.toHostAddress ip)+ IP.IPv6 ip -> do+ pokeByteOff ptr 0 (packFamily AF_INET6)+ pokeArray (ptr `plusPtr` sizeOf (undefined :: CInt) :: Ptr Word8) (map fromIntegral $ IP.fromIPv6b ip)+++inetFromSockAddr :: SockAddr -> Maybe ( InetAddress, PortNumber )+inetFromSockAddr saddr = first InetAddress <$> IP.fromSockAddr saddr++inetToSockAddr :: ( InetAddress, PortNumber ) -> SockAddr+inetToSockAddr = IP.toSockAddr . first fromInetAddress
+ src/Erebos/Network/Channel.hs view
@@ -0,0 +1,175 @@+module Erebos.Network.Channel (+ Channel,+ ChannelRequest, ChannelRequestData(..),+ ChannelAccept, ChannelAcceptData(..),++ createChannelRequest,+ acceptChannelRequest,+ acceptedChannel,++ channelEncrypt,+ channelDecrypt,+) where++import Control.Concurrent.MVar+import Control.Monad+import Control.Monad.Except+import Control.Monad.IO.Class++import Crypto.Cipher.ChaChaPoly1305+import Crypto.Error++import Data.Binary+import Data.ByteArray (ByteArray, Bytes, ScrubbedBytes, convert)+import Data.ByteArray qualified as BA+import Data.ByteString.Lazy qualified as BL+import Data.List++import Erebos.Identity+import Erebos.PubKey+import Erebos.Storable++data Channel = Channel+ { chPeers :: [Stored (Signed IdentityData)]+ , chKey :: ScrubbedBytes+ , chNonceFixedOur :: Bytes+ , chNonceFixedPeer :: Bytes+ , chCounterNextOut :: MVar Word64+ , chCounterNextIn :: MVar Word64+ }++type ChannelRequest = Signed ChannelRequestData++data ChannelRequestData = ChannelRequest+ { crPeers :: [Stored (Signed IdentityData)]+ , crKey :: Stored PublicKexKey+ }+ deriving (Show)++type ChannelAccept = Signed ChannelAcceptData++data ChannelAcceptData = ChannelAccept+ { caRequest :: Stored ChannelRequest+ , caKey :: Stored PublicKexKey+ }+++instance Storable ChannelRequestData where+ store' cr = storeRec $ do+ mapM_ (storeRef "peer") $ crPeers cr+ storeRef "key" $ crKey cr++ load' = loadRec $ do+ ChannelRequest+ <$> loadRefs "peer"+ <*> loadRef "key"++instance Storable ChannelAcceptData where+ store' ca = storeRec $ do+ storeRef "req" $ caRequest ca+ storeRef "key" $ caKey ca++ load' = loadRec $ do+ ChannelAccept+ <$> loadRef "req"+ <*> loadRef "key"+++keySize :: Int+keySize = 32++createChannelRequest :: (MonadStorage m, MonadIO m, MonadError e m, FromErebosError e) => UnifiedIdentity -> UnifiedIdentity -> m (Stored ChannelRequest)+createChannelRequest self peer = do+ (_, xpublic) <- liftIO . generateKeys =<< getStorage+ skey <- loadKey $ idKeyMessage self+ mstore =<< sign skey =<< mstore ChannelRequest { crPeers = sort [idData self, idData peer], crKey = xpublic }++acceptChannelRequest :: (MonadStorage m, MonadIO m, MonadError e m, FromErebosError e) => UnifiedIdentity -> UnifiedIdentity -> Stored ChannelRequest -> m (Stored ChannelAccept, Channel)+acceptChannelRequest self peer req = do+ case sequence $ map validateIdentity $ crPeers $ fromStored $ signedData $ fromStored req of+ Nothing -> throwOtherError $ "invalid peers in channel request"+ Just peers -> do+ when (not $ any (self `sameIdentity`) peers) $+ throwOtherError $ "self identity missing in channel request peers"+ when (not $ any (peer `sameIdentity`) peers) $+ throwOtherError $ "peer identity missing in channel request peers"+ when (idKeyMessage peer `notElem` (map (sigKey . fromStored) $ signedSignature $ fromStored req)) $+ throwOtherError $ "channel requent not signed by peer"++ (xsecret, xpublic) <- liftIO . generateKeys =<< getStorage+ skey <- loadKey $ idKeyMessage self+ acc <- mstore =<< sign skey =<< mstore ChannelAccept { caRequest = req, caKey = xpublic }+ liftIO $ do+ let chPeers = crPeers $ fromStored $ signedData $ fromStored req+ chKey = BA.take keySize $ dhSecret xsecret $+ fromStored $ crKey $ fromStored $ signedData $ fromStored req+ chNonceFixedOur = BA.pack [ 2, 0, 0, 0 ]+ chNonceFixedPeer = BA.pack [ 1, 0, 0, 0 ]+ chCounterNextOut <- newMVar 0+ chCounterNextIn <- newMVar 0++ return (acc, Channel {..})++acceptedChannel :: (MonadIO m, MonadError e m, FromErebosError e) => UnifiedIdentity -> UnifiedIdentity -> Stored ChannelAccept -> m Channel+acceptedChannel self peer acc = do+ let req = caRequest $ fromStored $ signedData $ fromStored acc+ case sequence $ map validateIdentity $ crPeers $ fromStored $ signedData $ fromStored req of+ Nothing -> throwOtherError $ "invalid peers in channel accept"+ Just peers -> do+ when (not $ any (self `sameIdentity`) peers) $+ throwOtherError $ "self identity missing in channel accept peers"+ when (not $ any (peer `sameIdentity`) peers) $+ throwOtherError $ "peer identity missing in channel accept peers"+ when (idKeyMessage peer `notElem` (map (sigKey . fromStored) $ signedSignature $ fromStored acc)) $+ throwOtherError $ "channel accept not signed by peer"+ when (idKeyMessage self `notElem` (map (sigKey . fromStored) $ signedSignature $ fromStored req)) $+ throwOtherError $ "original channel request not signed by us"++ xsecret <- loadKey $ crKey $ fromStored $ signedData $ fromStored req+ let chPeers = crPeers $ fromStored $ signedData $ fromStored req+ chKey = BA.take keySize $ dhSecret xsecret $+ fromStored $ caKey $ fromStored $ signedData $ fromStored acc+ chNonceFixedOur = BA.pack [ 1, 0, 0, 0 ]+ chNonceFixedPeer = BA.pack [ 2, 0, 0, 0 ]+ chCounterNextOut <- liftIO $ newMVar 0+ chCounterNextIn <- liftIO $ newMVar 0++ return Channel {..}+++channelEncrypt :: (ByteArray ba, MonadIO m, MonadError e m, FromErebosError e) => Channel -> ba -> m (ba, Word64)+channelEncrypt Channel {..} plain = do+ count <- liftIO $ modifyMVar chCounterNextOut $ \c -> return (c + 1, c)+ let cbytes = convert $ BL.toStrict $ encode count+ nonce = nonce8 chNonceFixedOur cbytes+ state <- case initialize chKey =<< nonce of+ CryptoPassed state -> return state+ CryptoFailed err -> throwOtherError $ "failed to init chacha-poly1305 cipher: " <> show err++ let (ctext, state') = encrypt plain state+ tag = finalize state'+ return (BA.concat [ convert $ BA.drop 7 cbytes, ctext, convert tag ], count)++channelDecrypt :: (ByteArray ba, MonadIO m, MonadError e m, FromErebosError e) => Channel -> ba -> m (ba, Word64)+channelDecrypt Channel {..} body = do+ when (BA.length body < 17) $ do+ throwOtherError $ "invalid encrypted data length"++ expectedCount <- liftIO $ readMVar chCounterNextIn+ let countByte = body `BA.index` 0+ body' = BA.dropView body 1+ guessedCount = expectedCount - 128 + fromIntegral (countByte - fromIntegral expectedCount + 128 :: Word8)+ nonce = nonce8 chNonceFixedPeer $ convert $ BL.toStrict $ encode guessedCount+ blen = BA.length body' - 16+ ctext = BA.takeView body' blen+ tag = BA.dropView body' blen+ state <- case initialize chKey =<< nonce of+ CryptoPassed state -> return state+ CryptoFailed err -> throwOtherError $ "failed to init chacha-poly1305 cipher: " <> show err++ let (plain, state') = decrypt (convert ctext) state+ when (not $ tag `BA.constEq` finalize state') $ do+ throwOtherError $ "tag validation falied"++ liftIO $ modifyMVar_ chCounterNextIn $ return . max (guessedCount + 1)+ return (plain, guessedCount)
src/Erebos/Network/Protocol.hs view
@@ -3,6 +3,7 @@ transportToObject, TransportHeader(..), TransportHeaderItem(..),+ ServiceID(..), SecurityRequirement(..), WaitingRef(..),@@ -22,7 +23,8 @@ connSetChannel, connClose, - RawStreamReader, RawStreamWriter,+ RawStreamReader(..), RawStreamWriter(..),+ StreamPacket(..), connAddWriteStream, connAddReadStream, readStreamToList,@@ -65,11 +67,13 @@ import System.Clock -import Erebos.Channel import Erebos.Flow import Erebos.Identity-import Erebos.Service+import Erebos.Network.Channel+import Erebos.Object+import Erebos.Storable import Erebos.Storage+import Erebos.UUID (UUID) protocolVersion :: Text@@ -106,6 +110,9 @@ | StreamOpen Word8 deriving (Eq, Show) +newtype ServiceID = ServiceID UUID+ deriving (Eq, Ord, Show, StorableUUID)+ newtype Cookie = Cookie ByteString deriving (Eq, Show) @@ -206,6 +213,7 @@ , gControlFlow :: Flow (ControlRequest addr) (ControlMessage addr) , gNextUp :: TMVar (Connection addr, (Bool, TransportPacket PartialObject)) , gLog :: String -> STM ()+ , gTestLog :: String -> STM () , gStorage :: PartialStorage , gStartTime :: TimeSpec , gNowVar :: TVar TimeSpec@@ -242,6 +250,12 @@ connAddress :: Connection addr -> addr connAddress = cAddress +showConnAddress :: forall addr. Connection addr -> String+showConnAddress Connection {..} = helper cGlobalState cAddress+ where+ helper :: GlobalState addr -> addr -> String+ helper GlobalState {} = show+ connData :: Connection addr -> Flow (Maybe (Bool, TransportPacket PartialObject)) (SecurityRequirement, TransportPacket Ref, [TransportHeaderItem])@@ -266,6 +280,7 @@ connAddWriteStream :: Connection addr -> STM (Either String (TransportHeaderItem, RawStreamWriter, IO ())) connAddWriteStream conn@Connection {..} = do+ let GlobalState {..} = cGlobalState outStreams <- readTVar cOutStreams let doInsert :: Word8 -> [(Word8, Stream)] -> ExceptT String STM ((Word8, Stream), [(Word8, Stream)]) doInsert n (s@(n', _) : rest) | n == n' =@@ -282,10 +297,16 @@ runExceptT $ do ((streamNumber, stream), outStreams') <- doInsert 1 outStreams lift $ writeTVar cOutStreams outStreams'- return (StreamOpen streamNumber, sFlowIn stream, go cGlobalState streamNumber stream)+ lift $ gTestLog $ "net-ostream-open " <> showConnAddress conn <> " " <> show streamNumber <> " " <> show (length outStreams')+ return+ ( StreamOpen streamNumber+ , RawStreamWriter (fromIntegral streamNumber) (sFlowIn stream)+ , go streamNumber stream+ ) where- go gs@GlobalState {..} streamNumber stream = do+ go streamNumber stream = do+ let GlobalState {..} = cGlobalState (reserved, msg) <- atomically $ do readTVar (sState stream) >>= \case StreamRunning -> return ()@@ -298,6 +319,8 @@ return (stpData, True, return ()) StreamClosed {} -> do atomically $ do+ gTestLog $ "net-ostream-close-send " <> showConnAddress conn <> " " <> show streamNumber+ atomically $ do -- wait for ack on all sent stream data waits <- readTVar (sWaitingForAck stream) when (waits > 0) retry@@ -322,7 +345,7 @@ Right (ctext, counter) -> do let isAcked = True return $ Just (0x80 `B.cons` ctext, if isAcked then [ AcknowledgedSingle $ fromIntegral counter ] else [])- Left err -> do atomically $ gLog $ "Failed to encrypt data: " ++ err+ Left err -> do atomically $ gLog $ "Failed to encrypt data: " ++ showErebosError err return Nothing Nothing | secure -> return Nothing | otherwise -> return $ Just (plain, plainAckedBy)@@ -341,7 +364,7 @@ sendBytes conn mbReserved' bs Nothing -> return () - when cont $ go gs streamNumber stream+ when cont $ go streamNumber stream connAddReadStream :: Connection addr -> Word8 -> STM RawStreamReader connAddReadStream Connection {..} streamNumber = do@@ -355,15 +378,22 @@ sNextSequence <- newTVar 0 sWaitingForAck <- newTVar 0 let stream = Stream {..}- return (stream, (streamNumber, stream) : streams)- (stream, inStreams') <- doInsert inStreams+ return ( streamNumber, stream, (streamNumber, stream) : streams )+ ( num, stream, inStreams' ) <- doInsert inStreams writeTVar cInStreams inStreams'- return $ sFlowOut stream+ return $ RawStreamReader (fromIntegral num) (sFlowOut stream) -type RawStreamReader = Flow StreamPacket Void-type RawStreamWriter = Flow Void StreamPacket+data RawStreamReader = RawStreamReader+ { rsrNum :: Int+ , rsrFlow :: Flow StreamPacket Void+ } +data RawStreamWriter = RawStreamWriter+ { rswNum :: Int+ , rswFlow :: Flow Void StreamPacket+ }+ data Stream = Stream { sState :: TVar StreamState , sFlowIn :: Flow Void StreamPacket@@ -393,24 +423,26 @@ Nothing -> return () streamClosed :: Connection addr -> Word8 -> IO ()-streamClosed Connection {..} snum = atomically $ do- modifyTVar' cOutStreams $ filter ((snum /=) . fst)+streamClosed conn@Connection {..} snum = atomically $ do+ streams <- filter ((snum /=) . fst) <$> readTVar cOutStreams+ writeTVar cOutStreams streams+ gTestLog cGlobalState $ "net-ostream-close-ack " <> showConnAddress conn <> " " <> show snum <> " " <> show (length streams) readStreamToList :: RawStreamReader -> IO (Word64, [(Word64, BC.ByteString)])-readStreamToList stream = readFlowIO stream >>= \case+readStreamToList stream = readFlowIO (rsrFlow stream) >>= \case StreamData sq bytes -> fmap ((sq, bytes) :) <$> readStreamToList stream StreamClosed sqEnd -> return (sqEnd, []) -readObjectsFromStream :: PartialStorage -> RawStreamReader -> IO (Except String [PartialObject])+readObjectsFromStream :: PartialStorage -> RawStreamReader -> IO (Except ErebosError [PartialObject]) readObjectsFromStream st stream = do (seqEnd, list) <- readStreamToList stream let validate s ((s', bytes) : rest) | s == s' = (bytes : ) <$> validate (s + 1) rest | s > s' = validate s rest- | otherwise = throwError "missing object chunk"+ | otherwise = throwOtherError "missing object chunk" validate s [] | s == seqEnd = return []- | otherwise = throwError "content length mismatch"+ | otherwise = throwOtherError "content length mismatch" return $ do content <- BL.fromChunks <$> validate 0 list deserializeObjects st content@@ -419,10 +451,10 @@ writeByteStringToStream stream = go 0 where go seqNum bstr- | BL.null bstr = writeFlowIO stream $ StreamClosed seqNum+ | BL.null bstr = writeFlowIO (rswFlow stream) $ StreamClosed seqNum | otherwise = do let (cur, rest) = BL.splitAt 500 bstr -- TODO: MTU- writeFlowIO stream $ StreamData seqNum (BL.toStrict cur)+ writeFlowIO (rswFlow stream) $ StreamData seqNum (BL.toStrict cur) go (seqNum + 1) rest @@ -433,7 +465,7 @@ , wrefStatus :: TVar (Either [RefDigest] Ref) } -type WaitingRefCallback = ExceptT String IO ()+type WaitingRefCallback = ExceptT ErebosError IO () wrDigest :: WaitingRef -> RefDigest wrDigest = refDigest . wrefPartial@@ -476,10 +508,11 @@ erebosNetworkProtocol :: (Eq addr, Ord addr, Show addr) => UnifiedIdentity -> (String -> STM ())+ -> (String -> STM ()) -> SymFlow (addr, ByteString) -> Flow (ControlRequest addr) (ControlMessage addr) -> IO ()-erebosNetworkProtocol initialIdentity gLog gDataFlow gControlFlow = do+erebosNetworkProtocol initialIdentity gLog gTestLog gDataFlow gControlFlow = do gIdentity <- newTVarIO (initialIdentity, []) gConnections <- newTVarIO [] gNextUp <- newEmptyTMVarIO@@ -543,6 +576,7 @@ cOutStreams <- newTVar [] let conn = Connection {..} + gTestLog $ "net-conn-new " <> show cAddress writeTVar gConnections (conn : conns) return conn @@ -572,7 +606,7 @@ let parse = case B.uncons msg of Just (b, enc) | b .&. 0xE0 == 0x80 -> do- ch <- maybe (throwError "unexpected encrypted packet") return mbch+ ch <- maybe (throwOtherError "unexpected encrypted packet") return mbch (dec, counter) <- channelDecrypt ch enc case B.uncons dec of@@ -587,18 +621,18 @@ return $ Right (snum, seq8, content, counter) Just (_, _) -> do- throwError "unexpected stream header"+ throwOtherError "unexpected stream header" Nothing -> do- throwError "empty decrypted content"+ throwOtherError "empty decrypted content" | b .&. 0xE0 == 0x60 -> do objs <- deserialize msg return $ Left (False, objs, Nothing) - | otherwise -> throwError "invalid packet"+ | otherwise -> throwOtherError "invalid packet" - Nothing -> throwError "empty packet"+ Nothing -> throwOtherError "empty packet" now <- getTime Monotonic runExceptT parse >>= \case@@ -649,7 +683,7 @@ atomically $ gLog $ show addr <> ": stream packet without connection" Left err -> do- atomically $ gLog $ show addr <> ": failed to parse packet: " <> err+ atomically $ gLog $ show addr <> ": failed to parse packet: " <> showErebosError err processPacket :: GlobalState addr -> Either addr (Connection addr) -> Bool -> TransportPacket a -> IO (Maybe (Connection addr, Maybe (TransportPacket a))) processPacket gs@GlobalState {..} econn secure packet@(TransportPacket (TransportHeader header) _) = if@@ -883,7 +917,7 @@ Right (ctext, counter) -> do let isAcked = any isHeaderItemAcknowledged hitems return $ Just (0x80 `B.cons` ctext, if isAcked then [ AcknowledgedSingle $ fromIntegral counter ] else [])- Left err -> do atomically $ gLog $ "Failed to encrypt data: " ++ err+ Left err -> do atomically $ gLog $ "Failed to encrypt data: " ++ showErebosError err return Nothing mbs <- case (secure, mbch) of@@ -899,7 +933,10 @@ , rsOnAck = rsOnAck rs >> onAck }) <$> mbReserved sendBytes conn mbReserved' bs- Nothing -> return ()+ Nothing -> do+ when (isJust mbReserved) $ do+ atomically $ do+ modifyTVar' cReservedPackets (subtract 1) let waitUntil :: TimeSpec -> TimeSpec -> STM () waitUntil now till = do
src/Erebos/Network/ifaddrs.c view
@@ -22,7 +22,7 @@ #define DISCOVERY_MULTICAST_GROUP "ff12:b6a4:6b1f:969:caee:acc2:5c93:73e1" -uint32_t * join_multicast(int fd, size_t * count)+uint32_t * erebos_join_multicast(int fd, size_t * count) { size_t capacity = 16; *count = 0;@@ -117,7 +117,7 @@ #ifndef _WIN32 -struct InetAddress * local_addresses( size_t * count )+struct InetAddress * erebos_local_addresses( size_t * count ) { struct ifaddrs * addrs; if( getifaddrs( &addrs ) < 0 )@@ -153,7 +153,7 @@ return ret; } -uint32_t * broadcast_addresses(void)+uint32_t * erebos_broadcast_addresses(void) { struct ifaddrs * addrs; if (getifaddrs(&addrs) < 0)@@ -196,7 +196,7 @@ #pragma comment(lib, "ws2_32.lib") -struct InetAddress * local_addresses( size_t * count )+struct InetAddress * erebos_local_addresses( size_t * count ) { * count = 0; struct InetAddress * ret = NULL;@@ -237,7 +237,7 @@ return ret; } -uint32_t * broadcast_addresses(void)+uint32_t * erebos_broadcast_addresses(void) { uint32_t * ret = NULL; SOCKET wsock = INVALID_SOCKET;
src/Erebos/Network/ifaddrs.h view
@@ -13,6 +13,6 @@ uint8_t addr[16]; } __attribute__((packed)); -uint32_t * join_multicast(int fd, size_t * count);-struct InetAddress * local_addresses( size_t * count );-uint32_t * broadcast_addresses(void);+uint32_t * erebos_join_multicast(int fd, size_t * count);+struct InetAddress * erebos_local_addresses( size_t * count );+uint32_t * erebos_broadcast_addresses(void);
+ src/Erebos/Object.hs view
@@ -0,0 +1,23 @@+{-|+Description: Core Erebos objects and references++Data types and functions for working with "raw" Erebos objects and references.+-}++module Erebos.Object (+ Object, PartialObject, Object'(..),+ serializeObject, deserializeObject, deserializeObjects,+ ioLoadObject, ioLoadBytes,+ storeRawBytes, lazyLoadBytes,++ RecItem, RecItem'(..),++ Ref, PartialRef, RefDigest,+ refDigest, refFromDigest,+ readRef, showRef,+ readRefDigest, showRefDigest,+ refDigestFromByteString, hashToRefDigest,+ copyRef, partialRef, partialRefFromDigest,+) where++import Erebos.Object.Internal
+ src/Erebos/Object/Internal.hs view
@@ -0,0 +1,772 @@+module Erebos.Object.Internal (+ Storage, PartialStorage, StorageCompleteness,++ Ref, PartialRef, RefDigest,+ refDigest, refFromDigest,+ readRef, showRef,+ readRefDigest, showRefDigest,+ refDigestFromByteString, hashToRefDigest,+ copyRef, partialRef, partialRefFromDigest,++ Object, PartialObject, Object'(..), RecItem, RecItem'(..),+ serializeObject, deserializeObject, deserializeObjects,+ ioLoadObject, ioLoadBytes,+ storeRawBytes, lazyLoadBytes,+ storeObject,+ collectObjects, collectStoredObjects,++ MonadStorage(..),++ Storable(..), ZeroStorable(..),+ StorableText(..), StorableDate(..), StorableUUID(..),++ Store, StoreRec,+ evalStore, evalStoreObject,+ storeBlob, storeRec, storeZero,+ storeEmpty, storeInt, storeNum, storeText, storeBinary, storeDate, storeUUID, storeRef, storeRawRef, storeWeak, storeRawWeak,+ storeMbEmpty, storeMbInt, storeMbNum, storeMbText, storeMbBinary, storeMbDate, storeMbUUID, storeMbRef, storeMbRawRef, storeMbWeak, storeMbRawWeak,+ storeZRef, storeZWeak,+ storeRecItems,++ Load, LoadRec,+ evalLoad,+ loadCurrentRef, loadCurrentObject,+ loadRecCurrentRef, loadRecItems,++ loadBlob, loadRec, loadZero,+ loadEmpty, loadInt, loadNum, loadText, loadBinary, loadDate, loadUUID, loadRef, loadRawRef, loadRawWeak,+ loadMbEmpty, loadMbInt, loadMbNum, loadMbText, loadMbBinary, loadMbDate, loadMbUUID, loadMbRef, loadMbRawRef, loadMbRawWeak,+ loadTexts, loadBinaries, loadRefs, loadRawRefs, loadRawWeaks,+ loadZRef,++ Stored,+ fromStored, storedRef,+ wrappedStore, wrappedLoad,+ copyStored,+ unsafeMapStored,+) where++import Control.Applicative+import Control.Monad+import Control.Monad.Except+import Control.Monad.Reader+import Control.Monad.Writer++import Crypto.Hash++import Data.Bifunctor+import Data.ByteString (ByteString)+import qualified Data.ByteArray as BA+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BLC+import Data.Char+import Data.Function+import Data.Maybe+import Data.Ratio+import Data.Set (Set)+import qualified Data.Set as S+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding+import Data.Text.Encoding.Error+import Data.Time.Calendar+import Data.Time.Clock+import Data.Time.Format+import Data.Time.LocalTime++import System.IO.Unsafe++import Erebos.Error+import Erebos.Storage.Internal+import Erebos.UUID (UUID)+import Erebos.UUID qualified as U+import Erebos.Util+++zeroRef :: Storage' c -> Ref' c+zeroRef s = Ref s (RefDigest h)+ where h = case digestFromByteString $ B.replicate (hashDigestSize $ digestAlgo h) 0 of+ Nothing -> error $ "Failed to create zero hash"+ Just h' -> h'+ digestAlgo :: Digest a -> a+ digestAlgo = undefined++isZeroRef :: Ref' c -> Bool+isZeroRef (Ref _ h) = all (==0) $ BA.unpack h+++refFromDigest :: Storage' c -> RefDigest -> IO (Maybe (Ref' c))+refFromDigest st dgst = fmap (const $ Ref st dgst) <$> ioLoadBytesFromStorage st dgst++readRef :: Storage -> ByteString -> IO (Maybe Ref)+readRef s b =+ case readRefDigest b of+ Nothing -> return Nothing+ Just dgst -> refFromDigest s dgst++copyRef' :: forall c c'. (StorageCompleteness c, StorageCompleteness c') => Storage' c' -> Ref' c -> IO (c (Ref' c'))+copyRef' st ref'@(Ref _ dgst) = refFromDigest st dgst >>= \case Just ref -> return $ return ref+ Nothing -> doCopy+ where doCopy = do mbobj' <- ioLoadObject ref'+ mbobj <- sequence $ copyObject' st <$> mbobj'+ sequence $ unsafeStoreObject st <$> join mbobj++copyRecItem' :: forall c c'. (StorageCompleteness c, StorageCompleteness c') => Storage' c' -> RecItem' c -> IO (c (RecItem' c'))+copyRecItem' st = \case+ RecEmpty -> return $ return $ RecEmpty+ RecInt x -> return $ return $ RecInt x+ RecNum x -> return $ return $ RecNum x+ RecText x -> return $ return $ RecText x+ RecBinary x -> return $ return $ RecBinary x+ RecDate x -> return $ return $ RecDate x+ RecUUID x -> return $ return $ RecUUID x+ RecRef x -> fmap RecRef <$> copyRef' st x+ RecWeak x -> return $ return $ RecWeak x+ RecUnknown t x -> return $ return $ RecUnknown t x++copyObject' :: forall c c'. (StorageCompleteness c, StorageCompleteness c') => Storage' c' -> Object' c -> IO (c (Object' c'))+copyObject' _ (Blob bs) = return $ return $ Blob bs+copyObject' st (Rec rs) = fmap Rec . sequence <$> mapM (\( n, item ) -> fmap ( n, ) <$> copyRecItem' st item) rs+copyObject' _ ZeroObject = return $ return ZeroObject+copyObject' _ (UnknownObject otype content) = return $ return $ UnknownObject otype content++copyRef :: forall c c' m. (StorageCompleteness c, StorageCompleteness c', MonadIO m) => Storage' c' -> Ref' c -> m (LoadResult c (Ref' c'))+copyRef st ref' = liftIO $ returnLoadResult <$> copyRef' st ref'++copyRecItem :: forall c c' m. (StorageCompleteness c, StorageCompleteness c', MonadIO m) => Storage' c' -> RecItem' c -> m (LoadResult c (RecItem' c'))+copyRecItem st item' = liftIO $ returnLoadResult <$> copyRecItem' st item'++copyObject :: forall c c'. (StorageCompleteness c, StorageCompleteness c') => Storage' c' -> Object' c -> IO (LoadResult c (Object' c'))+copyObject st obj' = returnLoadResult <$> copyObject' st obj'++partialRef :: PartialStorage -> Ref -> PartialRef+partialRef st (Ref _ dgst) = Ref st dgst++partialRefFromDigest :: PartialStorage -> RefDigest -> PartialRef+partialRefFromDigest st dgst = Ref st dgst+++data Object' c+ = Blob ByteString+ | Rec [(ByteString, RecItem' c)]+ | ZeroObject+ | UnknownObject ByteString ByteString+ deriving (Show)++type Object = Object' Complete+type PartialObject = Object' Partial++data RecItem' c+ = RecEmpty+ | RecInt Integer+ | RecNum Rational+ | RecText Text+ | RecBinary ByteString+ | RecDate ZonedTime+ | RecUUID UUID+ | RecRef (Ref' c)+ | RecWeak RefDigest+ | RecUnknown ByteString ByteString+ deriving (Show)++type RecItem = RecItem' Complete++serializeObject :: Object' c -> BL.ByteString+serializeObject = \case+ Blob cnt -> BL.fromChunks [BC.pack "blob ", BC.pack (show $ B.length cnt), BC.singleton '\n', cnt]+ Rec rec -> let cnt = BL.fromChunks $ concatMap (uncurry serializeRecItem) rec+ in BL.fromChunks [BC.pack "rec ", BC.pack (show $ BL.length cnt), BC.singleton '\n'] `BL.append` cnt+ ZeroObject -> BL.empty+ UnknownObject otype cnt -> BL.fromChunks [ otype, BC.singleton ' ', BC.pack (show $ B.length cnt), BC.singleton '\n', cnt ]++-- |Serializes and stores object data without ony dependencies, so is safe only+-- if all the referenced objects are already stored or reference is partial.+unsafeStoreObject :: Storage' c -> Object' c -> IO (Ref' c)+unsafeStoreObject storage = \case+ ZeroObject -> return $ zeroRef storage+ obj -> unsafeStoreRawBytes storage $ serializeObject obj++storeObject :: PartialStorage -> PartialObject -> IO PartialRef+storeObject = unsafeStoreObject++storeRawBytes :: PartialStorage -> BL.ByteString -> IO PartialRef+storeRawBytes = unsafeStoreRawBytes++serializeRecItem :: ByteString -> RecItem' c -> [ByteString]+serializeRecItem name (RecEmpty) = [name, BC.pack ":e", BC.singleton ' ', BC.singleton '\n']+serializeRecItem name (RecInt x) = [name, BC.pack ":i", BC.singleton ' ', BC.pack (show x), BC.singleton '\n']+serializeRecItem name (RecNum x) = [name, BC.pack ":n", BC.singleton ' ', BC.pack (showRatio x), BC.singleton '\n']+serializeRecItem name (RecText x) = [name, BC.pack ":t", BC.singleton ' ', escaped, BC.singleton '\n']+ where escaped = BC.concatMap escape $ encodeUtf8 x+ escape '\n' = BC.pack "\n\t"+ escape c = BC.singleton c+serializeRecItem name (RecBinary x) = [name, BC.pack ":b ", showHex x, BC.singleton '\n']+serializeRecItem name (RecDate x) = [name, BC.pack ":d", BC.singleton ' ', BC.pack (formatTime defaultTimeLocale "%s %z" x), BC.singleton '\n']+serializeRecItem name (RecUUID x) = [name, BC.pack ":u", BC.singleton ' ', U.toASCIIBytes x, BC.singleton '\n']+serializeRecItem name (RecRef x) = [name, BC.pack ":r ", showRef x, BC.singleton '\n']+serializeRecItem name (RecWeak x) = [name, BC.pack ":w ", showRefDigest x, BC.singleton '\n']+serializeRecItem name (RecUnknown t x) = [ name, BC.singleton ':', t, BC.singleton ' ', x, BC.singleton '\n' ]++lazyLoadObject :: forall c. StorageCompleteness c => Ref' c -> LoadResult c (Object' c)+lazyLoadObject = returnLoadResult . unsafePerformIO . ioLoadObject++ioLoadObject :: forall c. StorageCompleteness c => Ref' c -> IO (c (Object' c))+ioLoadObject ref | isZeroRef ref = return $ return ZeroObject+ioLoadObject ref@(Ref st rhash) = do+ file' <- ioLoadBytes ref+ return $ do+ file <- file'+ let chash = hashToRefDigest file+ when (chash /= rhash) $ error $ "Hash mismatch on object " ++ BC.unpack (showRef ref) {- TODO throw -}+ return $ case runExcept $ unsafeDeserializeObject st file of+ Left err -> error $ showErebosError err ++ ", ref " ++ BC.unpack (showRef ref) {- TODO throw -}+ Right (x, rest) | BL.null rest -> x+ | otherwise -> error $ "Superfluous content after " ++ BC.unpack (showRef ref) {- TODO throw -}++lazyLoadBytes :: forall c. StorageCompleteness c => Ref' c -> LoadResult c BL.ByteString+lazyLoadBytes ref | isZeroRef ref = returnLoadResult (return BL.empty :: c BL.ByteString)+lazyLoadBytes ref = returnLoadResult $ unsafePerformIO $ ioLoadBytes ref++unsafeDeserializeObject :: Storage' c -> BL.ByteString -> Except ErebosError (Object' c, BL.ByteString)+unsafeDeserializeObject _ bytes | BL.null bytes = return (ZeroObject, bytes)+unsafeDeserializeObject st bytes =+ case BLC.break (=='\n') bytes of+ (line, rest) | Just (otype, len) <- splitObjPrefix line -> do+ let (content, next) = first BL.toStrict $ BL.splitAt (fromIntegral len) $ BL.drop 1 rest+ guard $ B.length content == len+ (,next) <$> case otype of+ _ | otype == BC.pack "blob" -> return $ Blob content+ | otype == BC.pack "rec" -> maybe (throwOtherError $ "malformed record item ")+ (return . Rec) $ sequence $ map parseRecLine $ mergeCont [] $ BC.lines content+ | otherwise -> return $ UnknownObject otype content+ _ -> throwOtherError $ "malformed object"+ where splitObjPrefix line = do+ [otype, tlen] <- return $ BLC.words line+ (len, rest) <- BLC.readInt tlen+ guard $ BL.null rest+ return (BL.toStrict otype, len)++ mergeCont cs (a:b:rest) | Just ('\t', b') <- BC.uncons b = mergeCont (b':BC.pack "\n":cs) (a:rest)+ mergeCont cs (a:rest) = B.concat (a : reverse cs) : mergeCont [] rest+ mergeCont _ [] = []++ parseRecLine line = do+ colon <- BC.elemIndex ':' line+ space <- BC.elemIndex ' ' line+ guard $ colon < space+ let name = B.take colon line+ itype = B.take (space-colon-1) $ B.drop (colon+1) line+ content = B.drop (space+1) line++ let val = fromMaybe (RecUnknown itype content) $+ case BC.unpack itype of+ "e" -> do guard $ B.null content+ return RecEmpty+ "i" -> do (num, rest) <- BC.readInteger content+ guard $ B.null rest+ return $ RecInt num+ "n" -> RecNum <$> parseRatio content+ "t" -> return $ RecText $ decodeUtf8With lenientDecode content+ "b" -> RecBinary <$> readHex content+ "d" -> RecDate <$> parseTimeM False defaultTimeLocale "%s %z" (BC.unpack content)+ "u" -> RecUUID <$> U.fromASCIIBytes content+ "r" -> RecRef . Ref st <$> readRefDigest content+ "w" -> RecWeak <$> readRefDigest content+ _ -> Nothing+ return (name, val)++deserializeObject :: PartialStorage -> BL.ByteString -> Except ErebosError (PartialObject, BL.ByteString)+deserializeObject = unsafeDeserializeObject++deserializeObjects :: PartialStorage -> BL.ByteString -> Except ErebosError [PartialObject]+deserializeObjects _ bytes | BL.null bytes = return []+deserializeObjects st bytes = do (obj, rest) <- deserializeObject st bytes+ (obj:) <$> deserializeObjects st rest+++collectObjects :: Object -> [Object]+collectObjects obj = obj : map fromStored (fst $ collectOtherStored S.empty obj)++collectStoredObjects :: Stored Object -> [Stored Object]+collectStoredObjects obj = obj : (fst $ collectOtherStored S.empty $ fromStored obj)++collectOtherStored :: Set RefDigest -> Object -> ([Stored Object], Set RefDigest)+collectOtherStored seen (Rec items) = foldr helper ([], seen) $ map snd items+ where helper (RecRef ref) (xs, s) | r <- refDigest ref+ , r `S.notMember` s+ = let o = wrappedLoad ref+ (xs', s') = collectOtherStored (S.insert r s) $ fromStored o+ in ((o : xs') ++ xs, s')+ helper _ (xs, s) = (xs, s)+collectOtherStored seen _ = ([], seen)+++deriving instance StorableUUID HeadID+deriving instance StorableUUID HeadTypeID+++class Monad m => MonadStorage m where+ getStorage :: m Storage+ mstore :: Storable a => a -> m (Stored a)++ default mstore :: MonadIO m => Storable a => a -> m (Stored a)+ mstore x = do+ st <- getStorage+ wrappedStore st x++instance MonadIO m => MonadStorage (ReaderT Storage m) where+ getStorage = ask+++class Storable a where+ store' :: a -> Store+ load' :: Load a++ store :: StorageCompleteness c => Storage' c -> a -> IO (Ref' c)+ store st = evalStore st . store'+ load :: Ref -> a+ load = evalLoad load'++class Storable a => ZeroStorable a where+ fromZero :: Storage -> a++data Store = StoreBlob ByteString+ | StoreRec (forall c. StorageCompleteness c => Storage' c -> [IO [(ByteString, RecItem' c)]])+ | StoreZero+ | StoreUnknown ByteString ByteString++evalStore :: StorageCompleteness c => Storage' c -> Store -> IO (Ref' c)+evalStore st = unsafeStoreObject st <=< evalStoreObject st++evalStoreObject :: StorageCompleteness c => Storage' c -> Store -> IO (Object' c)+evalStoreObject _ (StoreBlob x) = return $ Blob x+evalStoreObject s (StoreRec f) = Rec . concat <$> sequence (f s)+evalStoreObject _ StoreZero = return ZeroObject+evalStoreObject _ (StoreUnknown otype content) = return $ UnknownObject otype content++newtype StoreRecM c a = StoreRecM (ReaderT (Storage' c) (Writer [IO [(ByteString, RecItem' c)]]) a)+ deriving (Functor, Applicative, Monad)++type StoreRec c = StoreRecM c ()++newtype Load a = Load (ReaderT (Ref, Object) (Except ErebosError) a)+ deriving (Functor, Applicative, Alternative, Monad, MonadPlus, MonadError ErebosError)++evalLoad :: Load a -> Ref -> a+evalLoad (Load f) ref = either (error {- TODO throw -} . ((BC.unpack (showRef ref) ++ ": ") ++) . showErebosError) id $+ runExcept $ runReaderT f (ref, lazyLoadObject ref)++loadCurrentRef :: Load Ref+loadCurrentRef = Load $ asks fst++loadCurrentObject :: Load Object+loadCurrentObject = Load $ asks snd++newtype LoadRec a = LoadRec (ReaderT (Ref, [(ByteString, RecItem)]) (Except ErebosError) a)+ deriving (Functor, Applicative, Alternative, Monad, MonadPlus, MonadError ErebosError)++loadRecCurrentRef :: LoadRec Ref+loadRecCurrentRef = LoadRec $ asks fst++loadRecItems :: LoadRec [(ByteString, RecItem)]+loadRecItems = LoadRec $ asks snd+++instance Storable Object where+ store' (Blob bs) = StoreBlob bs+ store' (Rec xs) = StoreRec $ \st -> return $ do+ Rec xs' <- copyObject st (Rec xs)+ return xs'+ store' ZeroObject = StoreZero+ store' (UnknownObject otype content) = StoreUnknown otype content++ load' = loadCurrentObject++ store st = unsafeStoreObject st <=< copyObject st+ load = lazyLoadObject++instance Storable ByteString where+ store' = storeBlob+ load' = loadBlob id++instance Storable a => Storable [a] where+ store' [] = storeZero+ store' (x:xs) = storeRec $ do+ storeRef "i" x+ storeRef "n" xs++ load' = loadCurrentObject >>= \case+ ZeroObject -> return []+ _ -> loadRec $ (:)+ <$> loadRef "i"+ <*> loadRef "n"++instance Storable a => ZeroStorable [a] where+ fromZero _ = []+++storeBlob :: ByteString -> Store+storeBlob = StoreBlob++storeRec :: (forall c. StorageCompleteness c => StoreRec c) -> Store+storeRec sr = StoreRec $ do+ let StoreRecM r = sr+ execWriter . runReaderT r++storeZero :: Store+storeZero = StoreZero+++class StorableText a where+ toText :: a -> Text+ fromText :: MonadError ErebosError m => Text -> m a++instance StorableText Text where+ toText = id; fromText = return++instance StorableText [Char] where+ toText = T.pack; fromText = return . T.unpack+++class StorableDate a where+ toDate :: a -> ZonedTime+ fromDate :: ZonedTime -> a++instance StorableDate ZonedTime where+ toDate = id; fromDate = id++instance StorableDate UTCTime where+ toDate = utcToZonedTime utc+ fromDate = zonedTimeToUTC++instance StorableDate Day where+ toDate day = toDate $ UTCTime day 0+ fromDate = utctDay . fromDate+++class StorableUUID a where+ toUUID :: a -> UUID+ fromUUID :: UUID -> a++instance StorableUUID UUID where+ toUUID = id; fromUUID = id+++storeEmpty :: String -> StoreRec c+storeEmpty name = StoreRecM $ tell [return [(BC.pack name, RecEmpty)]]++storeMbEmpty :: String -> Maybe () -> StoreRec c+storeMbEmpty name = maybe (return ()) (const $ storeEmpty name)++storeInt :: Integral a => String -> a -> StoreRec c+storeInt name x = StoreRecM $ tell [return [(BC.pack name, RecInt $ toInteger x)]]++storeMbInt :: Integral a => String -> Maybe a -> StoreRec c+storeMbInt name = maybe (return ()) (storeInt name)++storeNum :: (Real a, Fractional a) => String -> a -> StoreRec c+storeNum name x = StoreRecM $ tell [return [(BC.pack name, RecNum $ toRational x)]]++storeMbNum :: (Real a, Fractional a) => String -> Maybe a -> StoreRec c+storeMbNum name = maybe (return ()) (storeNum name)++storeText :: StorableText a => String -> a -> StoreRec c+storeText name x = StoreRecM $ tell [return [(BC.pack name, RecText $ toText x)]]++storeMbText :: StorableText a => String -> Maybe a -> StoreRec c+storeMbText name = maybe (return ()) (storeText name)++storeBinary :: BA.ByteArrayAccess a => String -> a -> StoreRec c+storeBinary name x = StoreRecM $ tell [return [(BC.pack name, RecBinary $ BA.convert x)]]++storeMbBinary :: BA.ByteArrayAccess a => String -> Maybe a -> StoreRec c+storeMbBinary name = maybe (return ()) (storeBinary name)++storeDate :: StorableDate a => String -> a -> StoreRec c+storeDate name x = StoreRecM $ tell [return [(BC.pack name, RecDate $ toDate x)]]++storeMbDate :: StorableDate a => String -> Maybe a -> StoreRec c+storeMbDate name = maybe (return ()) (storeDate name)++storeUUID :: StorableUUID a => String -> a -> StoreRec c+storeUUID name x = StoreRecM $ tell [return [(BC.pack name, RecUUID $ toUUID x)]]++storeMbUUID :: StorableUUID a => String -> Maybe a -> StoreRec c+storeMbUUID name = maybe (return ()) (storeUUID name)++storeRef :: Storable a => StorageCompleteness c => String -> a -> StoreRec c+storeRef name x = StoreRecM $ do+ s <- ask+ tell $ (:[]) $ do+ ref <- store s x+ return [(BC.pack name, RecRef ref)]++storeMbRef :: Storable a => StorageCompleteness c => String -> Maybe a -> StoreRec c+storeMbRef name = maybe (return ()) (storeRef name)++storeRawRef :: StorageCompleteness c => String -> Ref -> StoreRec c+storeRawRef name ref = StoreRecM $ do+ st <- ask+ tell $ (:[]) $ do+ ref' <- copyRef st ref+ return [(BC.pack name, RecRef ref')]++storeMbRawRef :: StorageCompleteness c => String -> Maybe Ref -> StoreRec c+storeMbRawRef name = maybe (return ()) (storeRawRef name)++storeZRef :: (ZeroStorable a, StorageCompleteness c) => String -> a -> StoreRec c+storeZRef name x = StoreRecM $ do+ s <- ask+ tell $ (:[]) $ do+ ref <- store s x+ return $ if isZeroRef ref then []+ else [(BC.pack name, RecRef ref)]++storeWeak :: Storable a => StorageCompleteness c => String -> a -> StoreRec c+storeWeak name x = StoreRecM $ do+ s <- ask+ tell $ (:[]) $ do+ ref <- store s x+ return [ ( BC.pack name, RecWeak $ refDigest ref ) ]++storeMbWeak :: Storable a => StorageCompleteness c => String -> Maybe a -> StoreRec c+storeMbWeak name = maybe (return ()) (storeWeak name)++storeRawWeak :: StorageCompleteness c => String -> RefDigest -> StoreRec c+storeRawWeak name dgst = StoreRecM $ do+ tell $ (:[]) $ do+ return [ ( BC.pack name, RecWeak dgst ) ]++storeMbRawWeak :: StorageCompleteness c => String -> Maybe RefDigest -> StoreRec c+storeMbRawWeak name = maybe (return ()) (storeRawWeak name)++storeZWeak :: (ZeroStorable a, StorageCompleteness c) => String -> a -> StoreRec c+storeZWeak name x = StoreRecM $ do+ s <- ask+ tell $ (:[]) $ do+ ref <- store s x+ return $ if isZeroRef ref then []+ else [ ( BC.pack name, RecWeak $ refDigest ref ) ]+++storeRecItems :: StorageCompleteness c => [ ( ByteString, RecItem ) ] -> StoreRec c+storeRecItems items = StoreRecM $ do+ st <- ask+ tell $ flip map items $ \( name, value ) -> do+ value' <- copyRecItem st value+ return [ ( name, value' ) ]++loadBlob :: (ByteString -> a) -> Load a+loadBlob f = loadCurrentObject >>= \case+ Blob x -> return $ f x+ _ -> throwOtherError "Expecting blob"++loadRec :: LoadRec a -> Load a+loadRec (LoadRec lrec) = loadCurrentObject >>= \case+ Rec rs -> do+ ref <- loadCurrentRef+ either throwError return $ runExcept $ runReaderT lrec (ref, rs)+ _ -> throwOtherError "Expecting record"++loadZero :: a -> Load a+loadZero x = loadCurrentObject >>= \case+ ZeroObject -> return x+ _ -> throwOtherError "Expecting zero"+++loadEmpty :: String -> LoadRec ()+loadEmpty name = maybe (throwOtherError $ "Missing record item '"++name++"'") return =<< loadMbEmpty name++loadMbEmpty :: String -> LoadRec (Maybe ())+loadMbEmpty name = listToMaybe . mapMaybe p <$> loadRecItems+ where+ bname = BC.pack name+ p ( name', RecEmpty ) | name' == bname+ = Just ()+ p _ = Nothing++loadInt :: Num a => String -> LoadRec a+loadInt name = maybe (throwOtherError $ "Missing record item '"++name++"'") return =<< loadMbInt name++loadMbInt :: Num a => String -> LoadRec (Maybe a)+loadMbInt name = listToMaybe . mapMaybe p <$> loadRecItems+ where+ bname = BC.pack name+ p ( name', RecInt x ) | name' == bname+ = Just (fromInteger x)+ p _ = Nothing++loadNum :: (Real a, Fractional a) => String -> LoadRec a+loadNum name = maybe (throwOtherError $ "Missing record item '"++name++"'") return =<< loadMbNum name++loadMbNum :: (Real a, Fractional a) => String -> LoadRec (Maybe a)+loadMbNum name = listToMaybe . mapMaybe p <$> loadRecItems+ where+ bname = BC.pack name+ p ( name', RecNum x ) | name' == bname+ = Just (fromRational x)+ p _ = Nothing++loadText :: StorableText a => String -> LoadRec a+loadText name = maybe (throwOtherError $ "Missing record item '"++name++"'") return =<< loadMbText name++loadMbText :: StorableText a => String -> LoadRec (Maybe a)+loadMbText name = listToMaybe <$> loadTexts name++loadTexts :: StorableText a => String -> LoadRec [a]+loadTexts name = sequence . mapMaybe p =<< loadRecItems+ where+ bname = BC.pack name+ p ( name', RecText x ) | name' == bname+ = Just (fromText x)+ p _ = Nothing++loadBinary :: BA.ByteArray a => String -> LoadRec a+loadBinary name = maybe (throwOtherError $ "Missing record item '"++name++"'") return =<< loadMbBinary name++loadMbBinary :: BA.ByteArray a => String -> LoadRec (Maybe a)+loadMbBinary name = listToMaybe <$> loadBinaries name++loadBinaries :: BA.ByteArray a => String -> LoadRec [a]+loadBinaries name = mapMaybe p <$> loadRecItems+ where+ bname = BC.pack name+ p ( name', RecBinary x ) | name' == bname+ = Just (BA.convert x)+ p _ = Nothing++loadDate :: StorableDate a => String -> LoadRec a+loadDate name = maybe (throwOtherError $ "Missing record item '"++name++"'") return =<< loadMbDate name++loadMbDate :: StorableDate a => String -> LoadRec (Maybe a)+loadMbDate name = listToMaybe . mapMaybe p <$> loadRecItems+ where+ bname = BC.pack name+ p ( name', RecDate x ) | name' == bname+ = Just (fromDate x)+ p _ = Nothing++loadUUID :: StorableUUID a => String -> LoadRec a+loadUUID name = maybe (throwOtherError $ "Missing record iteem '"++name++"'") return =<< loadMbUUID name++loadMbUUID :: StorableUUID a => String -> LoadRec (Maybe a)+loadMbUUID name = listToMaybe . mapMaybe p <$> loadRecItems+ where+ bname = BC.pack name+ p ( name', RecUUID x ) | name' == bname+ = Just (fromUUID x)+ p _ = Nothing++loadRawRef :: String -> LoadRec Ref+loadRawRef name = maybe (throwOtherError $ "Missing record item '"++name++"'") return =<< loadMbRawRef name++loadMbRawRef :: String -> LoadRec (Maybe Ref)+loadMbRawRef name = listToMaybe <$> loadRawRefs name++loadRawRefs :: String -> LoadRec [Ref]+loadRawRefs name = mapMaybe p <$> loadRecItems+ where+ bname = BC.pack name+ p ( name', RecRef x ) | name' == bname = Just x+ p _ = Nothing++loadRef :: Storable a => String -> LoadRec a+loadRef name = load <$> loadRawRef name++loadMbRef :: Storable a => String -> LoadRec (Maybe a)+loadMbRef name = fmap load <$> loadMbRawRef name++loadRefs :: Storable a => String -> LoadRec [a]+loadRefs name = map load <$> loadRawRefs name++loadZRef :: ZeroStorable a => String -> LoadRec a+loadZRef name = loadMbRef name >>= \case+ Nothing -> do Ref st _ <- loadRecCurrentRef+ return $ fromZero st+ Just x -> return x++loadRawWeak :: String -> LoadRec RefDigest+loadRawWeak name = maybe (throwOtherError $ "Missing record item '"++name++"'") return =<< loadMbRawWeak name++loadMbRawWeak :: String -> LoadRec (Maybe RefDigest)+loadMbRawWeak name = listToMaybe <$> loadRawWeaks name++loadRawWeaks :: String -> LoadRec [ RefDigest ]+loadRawWeaks name = mapMaybe p <$> loadRecItems+ where+ bname = BC.pack name+ p ( name', RecRef x ) | name' == bname = Just (refDigest x)+ p ( name', RecWeak x ) | name' == bname = Just x+ p _ = Nothing++++instance Storable a => Storable (Stored a) where+ store st = copyRef st . storedRef+ store' (Stored _ x) = store' x+ load' = Stored <$> loadCurrentRef <*> load'++instance ZeroStorable a => ZeroStorable (Stored a) where+ fromZero st = Stored (zeroRef st) $ fromZero st++fromStored :: Stored a -> a+fromStored = storedObject'++storedRef :: Stored a -> Ref+storedRef = storedRef'++wrappedStore :: MonadIO m => Storable a => Storage -> a -> m (Stored a)+wrappedStore st x = do ref <- liftIO $ store st x+ return $ Stored ref x++wrappedLoad :: Storable a => Ref -> Stored a+wrappedLoad ref = Stored ref (load ref)++copyStored :: forall m a. MonadIO m => Storage -> Stored a -> m (Stored a)+copyStored st (Stored ref' x) = liftIO $ returnLoadResult . fmap (\r -> Stored r x) <$> copyRef' st ref'++-- |Passed function needs to preserve the object representation to be safe+unsafeMapStored :: (a -> b) -> Stored a -> Stored b+unsafeMapStored f (Stored ref x) = Stored ref (f x)+++showRatio :: Rational -> String+showRatio r = case decimalRatio r of+ Just (n, 1) -> show n+ Just (n', d) -> let n = abs n'+ in (if n' < 0 then "-" else "") ++ show (n `div` d) ++ "." +++ (concatMap (show.(`mod` 10).snd) $ reverse $ takeWhile ((>1).fst) $ zip (iterate (`div` 10) d) (iterate (`div` 10) (n `mod` d)))+ Nothing -> show (numerator r) ++ "/" ++ show (denominator r)++decimalRatio :: Rational -> Maybe (Integer, Integer)+decimalRatio r = do+ let n = numerator r+ d = denominator r+ (c2, d') = takeFactors 2 d+ (c5, d'') = takeFactors 5 d'+ guard $ d'' == 1+ let m = if c2 > c5 then 5 ^ (c2 - c5)+ else 2 ^ (c5 - c2)+ return (n * m, d * m)++takeFactors :: Integer -> Integer -> (Integer, Integer)+takeFactors f n | n `mod` f == 0 = let (c, n') = takeFactors f (n `div` f)+ in (c+1, n')+ | otherwise = (0, n)++parseRatio :: ByteString -> Maybe Rational+parseRatio bs = case BC.groupBy ((==) `on` isNumber) bs of+ (m:xs) | m == BC.pack "-" -> negate <$> positive xs+ xs -> positive xs+ where positive = \case+ [bx] -> fromInteger . fst <$> BC.readInteger bx+ [bx, op, by] -> do+ (x, _) <- BC.readInteger bx+ (y, _) <- BC.readInteger by+ case BC.unpack op of+ "." -> return $ (x % 1) + (y % (10 ^ BC.length by))+ "/" -> return $ x % y+ _ -> Nothing+ _ -> Nothing
src/Erebos/Pairing.hs view
@@ -17,9 +17,10 @@ import Crypto.Random import Data.Bits-import Data.ByteArray (Bytes, convert)-import qualified Data.ByteArray as BA-import qualified Data.ByteString.Char8 as BC+import Data.ByteArray qualified as BA+import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.ByteString.Char8 qualified as BC import Data.Kind import Data.Maybe import Data.Typeable@@ -27,28 +28,29 @@ import Erebos.Identity import Erebos.Network+import Erebos.Object import Erebos.PubKey import Erebos.Service import Erebos.State-import Erebos.Storage+import Erebos.Storable data PairingService a = PairingRequest (Stored (Signed IdentityData)) (Stored (Signed IdentityData)) RefDigest- | PairingResponse Bytes- | PairingRequestNonce Bytes+ | PairingResponse ByteString+ | PairingRequestNonce ByteString | PairingAccept a | PairingReject data PairingState a = NoPairing- | OurRequest UnifiedIdentity UnifiedIdentity Bytes+ | OurRequest UnifiedIdentity UnifiedIdentity ByteString | OurRequestConfirm (Maybe (PairingVerifiedResult a)) | OurRequestReady- | PeerRequest UnifiedIdentity UnifiedIdentity Bytes RefDigest+ | PeerRequest UnifiedIdentity UnifiedIdentity ByteString RefDigest | PeerRequestConfirm | PairingDone data PairingFailureReason a = PairingUserRejected | PairingUnexpectedMessage (PairingState a) (PairingService a)- | PairingFailedOther String+ | PairingFailedOther ErebosError data PairingAttributes a = PairingAttributes { pairingHookRequest :: ServiceHandler (PairingService a) ()@@ -87,7 +89,7 @@ load' = do res <- loadRec $ do- (req :: Maybe Bytes) <- loadMbBinary "request"+ (req :: Maybe ByteString) <- loadMbBinary "request" idReq <- loadMbRef "id-req" idRsp <- loadMbRef "id-rsp" rsp <- loadMbBinary "response"@@ -115,16 +117,16 @@ serviceHandler spacket = ((,fromStored spacket) <$> svcGet) >>= \case (NoPairing, PairingRequest pdata sdata confirm) -> do- self <- maybe (throwError "failed to validate received identity") return $ validateIdentity sdata- self' <- maybe (throwError "failed to validate own identity") return .+ self <- maybe (throwOtherError "failed to validate received identity") return $ validateIdentity sdata+ self' <- maybe (throwOtherError "failed to validate own identity") return . validateExtendedIdentity . lsIdentity . fromStored =<< svcGetLocal when (not $ self `sameIdentity` self') $ do- throwError "pairing request to different identity"+ throwOtherError "pairing request to different identity" - peer <- maybe (throwError "failed to validate received peer identity") return $ validateIdentity pdata+ peer <- maybe (throwOtherError "failed to validate received peer identity") return $ validateIdentity pdata peer' <- asks $ svcPeerIdentity when (not $ peer `sameIdentity` peer') $ do- throwError "pairing request from different identity"+ throwOtherError "pairing request from different identity" join $ asks $ pairingHookRequest . svcAttributes nonce <- liftIO $ getRandomBytes 32@@ -166,11 +168,11 @@ svcSet $ PairingDone Nothing -> do join $ asks $ pairingHookVerifyFailed . svcAttributes- throwError ""+ throwOtherError "" x@(OurRequestReady, _) -> reject $ uncurry PairingUnexpectedMessage x (PeerRequest peer self nonce dgst, PairingRequestNonce pnonce) -> do- if dgst == nonceDigest peer self pnonce BA.empty+ if dgst == nonceDigest peer self pnonce BS.empty then do hook <- asks $ pairingHookRequestNonce . svcAttributes hook $ confirmationNumber $ nonceDigest peer self pnonce nonce svcSet PeerRequestConfirm@@ -187,12 +189,12 @@ replyPacket PairingReject -nonceDigest :: UnifiedIdentity -> UnifiedIdentity -> Bytes -> Bytes -> RefDigest+nonceDigest :: UnifiedIdentity -> UnifiedIdentity -> ByteString -> ByteString -> RefDigest nonceDigest idReq idRsp nonceReq nonceRsp = hashToRefDigest $ serializeObject $ Rec [ (BC.pack "id-req", RecRef $ storedRef $ idData idReq) , (BC.pack "id-rsp", RecRef $ storedRef $ idData idRsp)- , (BC.pack "nonce-req", RecBinary $ convert nonceReq)- , (BC.pack "nonce-rsp", RecBinary $ convert nonceRsp)+ , (BC.pack "nonce-req", RecBinary nonceReq)+ , (BC.pack "nonce-rsp", RecBinary nonceRsp) ] confirmationNumber :: RefDigest -> String@@ -203,22 +205,22 @@ _ -> "" where len = 6 -pairingRequest :: forall a m proxy. (PairingResult a, MonadIO m, MonadError String m) => proxy a -> Peer -> m ()+pairingRequest :: forall a m e proxy. (PairingResult a, MonadIO m, MonadError e m, FromErebosError e) => proxy a -> Peer -> m () pairingRequest _ peer = do self <- liftIO $ serverIdentity $ peerServer peer nonce <- liftIO $ getRandomBytes 32- pid <- peerIdentity peer >>= \case+ pid <- getPeerIdentity peer >>= \case PeerIdentityFull pid -> return pid- _ -> throwError "incomplete peer identity"+ _ -> throwOtherError "incomplete peer identity" sendToPeerWith @(PairingService a) peer $ \case- NoPairing -> return (Just $ PairingRequest (idData self) (idData pid) (nonceDigest self pid nonce BA.empty), OurRequest self pid nonce)- _ -> throwError "already in progress"+ NoPairing -> return (Just $ PairingRequest (idData self) (idData pid) (nonceDigest self pid nonce BS.empty), OurRequest self pid nonce)+ _ -> throwOtherError "already in progress" -pairingAccept :: forall a m proxy. (PairingResult a, MonadIO m, MonadError String m) => proxy a -> Peer -> m ()+pairingAccept :: forall a m e proxy. (PairingResult a, MonadIO m, MonadError e m, FromErebosError e) => proxy a -> Peer -> m () pairingAccept _ peer = runPeerService @(PairingService a) peer $ do svcGet >>= \case- NoPairing -> throwError $ "none in progress"- OurRequest {} -> throwError $ "waiting for peer"+ NoPairing -> throwOtherError $ "none in progress"+ OurRequest {} -> throwOtherError $ "waiting for peer" OurRequestConfirm Nothing -> do join $ asks $ pairingHookConfirmedResponse . svcAttributes svcSet OurRequestReady@@ -226,17 +228,17 @@ join $ asks $ pairingHookAcceptedResponse . svcAttributes pairingFinalizeRequest verified svcSet PairingDone- OurRequestReady -> throwError $ "already accepted, waiting for peer"- PeerRequest {} -> throwError $ "waiting for peer"+ OurRequestReady -> throwOtherError $ "already accepted, waiting for peer"+ PeerRequest {} -> throwOtherError $ "waiting for peer" PeerRequestConfirm -> do join $ asks $ pairingHookAcceptedRequest . svcAttributes replyPacket . PairingAccept =<< pairingFinalizeResponse svcSet PairingDone- PairingDone -> throwError $ "already done"+ PairingDone -> throwOtherError $ "already done" -pairingReject :: forall a m proxy. (PairingResult a, MonadIO m, MonadError String m) => proxy a -> Peer -> m ()+pairingReject :: forall a m e proxy. (PairingResult a, MonadIO m, MonadError e m, FromErebosError e) => proxy a -> Peer -> m () pairingReject _ peer = runPeerService @(PairingService a) peer $ do svcGet >>= \case- NoPairing -> throwError $ "none in progress"- PairingDone -> throwError $ "already done"+ NoPairing -> throwOtherError $ "none in progress"+ PairingDone -> throwOtherError $ "already done" _ -> reject PairingUserRejected
src/Erebos/PubKey.hs view
@@ -11,7 +11,6 @@ ) where import Control.Monad-import Control.Monad.Except import Crypto.Error import qualified Crypto.PubKey.Ed25519 as ED@@ -21,7 +20,7 @@ import Data.ByteString (ByteString) import qualified Data.Text as T -import Erebos.Storage+import Erebos.Storable import Erebos.Storage.Key data PublicKey = PublicKey ED.PublicKey@@ -70,7 +69,7 @@ load' = loadRec $ do ktype <- loadText "type" guard $ ktype == "ed25519"- maybe (throwError "Public key decoding failed") (return . PublicKey) .+ maybe (throwOtherError "public key decoding failed") (return . PublicKey) . maybeCryptoError . (ED.publicKey :: ByteString -> CryptoFailable ED.PublicKey) =<< loadBinary "pubkey" @@ -82,7 +81,7 @@ load' = loadRec $ Signature <$> loadRef "key" <*> loadSignature "sig"- where loadSignature = maybe (throwError "Signature decoding failed") return .+ where loadSignature = maybe (throwOtherError "signature decoding failed") return . maybeCryptoError . (ED.signature :: ByteString -> CryptoFailable ED.Signature) <=< loadBinary instance Storable a => Storable (Signed a) where@@ -96,7 +95,7 @@ forM_ sigs $ \sig -> do let PublicKey pubkey = fromStored $ sigKey $ fromStored sig when (not $ ED.verify pubkey (storedRef sdata) $ sigSignature $ fromStored sig) $- throwError "signature verification failed"+ throwOtherError "signature verification failed" return $ Signed sdata sigs sign :: MonadStorage m => SecretKey -> Stored a -> m (Signed a)@@ -148,7 +147,7 @@ load' = loadRec $ do ktype <- loadText "type" guard $ ktype == "x25519"- maybe (throwError "public key decoding failed") (return . PublicKexKey) .+ maybe (throwOtherError "public key decoding failed") (return . PublicKexKey) . maybeCryptoError . (CX.publicKey :: ScrubbedBytes -> CryptoFailable CX.PublicKey) =<< loadBinary "pubkey"
src/Erebos/Service.hs view
@@ -29,13 +29,14 @@ import Data.Kind import Data.Typeable-import Data.UUID (UUID)-import qualified Data.UUID as U import Erebos.Identity import {-# SOURCE #-} Erebos.Network+import Erebos.Network.Protocol import Erebos.State-import Erebos.Storage+import Erebos.Storable+import Erebos.Storage.Head+import Erebos.UUID qualified as U class ( Typeable s, Storable s,@@ -50,6 +51,9 @@ serviceNewPeer :: ServiceHandler s () serviceNewPeer = return () + serviceUpdatedPeer :: ServiceHandler s ()+ serviceUpdatedPeer = return ()+ type ServiceAttributes s = attr | attr -> s type ServiceAttributes s = Proxy s defaultServiceAttributes :: proxy s -> ServiceAttributes s@@ -105,25 +109,26 @@ data SomeStorageWatcher s = forall a. Eq a => SomeStorageWatcher (Stored LocalState -> a) (a -> ServiceHandler s ())- | forall a. Eq a => GlobalStorageWatcher (Stored LocalState -> a) (Server -> a -> ExceptT String IO ())+ | forall a. Eq a => GlobalStorageWatcher (Stored LocalState -> a) (Server -> a -> ExceptT ErebosError IO ()) -newtype ServiceID = ServiceID UUID- deriving (Eq, Ord, Show, StorableUUID)- mkServiceID :: String -> ServiceID mkServiceID = maybe (error "Invalid service ID") ServiceID . U.fromString data ServiceInput s = ServiceInput { svcAttributes :: ServiceAttributes s , svcPeer :: Peer+ , svcPeerAddress :: PeerAddress , svcPeerIdentity :: UnifiedIdentity , svcServer :: Server , svcPrintOp :: String -> IO ()+ , svcNewStreams :: [ RawStreamReader ] } -data ServiceReply s = ServiceReply (Either s (Stored s)) Bool- | ServiceFinally (IO ())+data ServiceReply s+ = ServiceReply (Either s (Stored s)) Bool+ | ServiceOpenStream (RawStreamWriter -> IO ())+ | ServiceFinally (IO ()) data ServiceHandlerState s = ServiceHandlerState { svcValue :: ServiceState s@@ -131,8 +136,8 @@ , svcLocal :: Stored LocalState } -newtype ServiceHandler s a = ServiceHandler (ReaderT (ServiceInput s) (WriterT [ServiceReply s] (StateT (ServiceHandlerState s) (ExceptT String IO))) a)- deriving (Functor, Applicative, Monad, MonadReader (ServiceInput s), MonadWriter [ServiceReply s], MonadState (ServiceHandlerState s), MonadError String, MonadIO)+newtype ServiceHandler s a = ServiceHandler (ReaderT (ServiceInput s) (WriterT [ServiceReply s] (StateT (ServiceHandlerState s) (ExceptT ErebosError IO))) a)+ deriving (Functor, Applicative, Monad, MonadReader (ServiceInput s), MonadWriter [ServiceReply s], MonadState (ServiceHandlerState s), MonadError ErebosError, MonadIO) instance MonadStorage (ServiceHandler s) where getStorage = asks $ peerStorage . svcPeer@@ -149,7 +154,7 @@ ServiceHandler handler = shandler (runExceptT $ flip runStateT sstate $ execWriterT $ flip runReaderT input $ handler) >>= \case Left err -> do- svcPrintOp input $ "service failed: " ++ err+ svcPrintOp input $ "service failed: " ++ showErebosError err return ([], (svc, global)) Right (rsp, sstate') | svcLocal sstate' == svcLocal sstate -> return (rsp, (svcValue sstate', svcGlobal sstate'))@@ -182,7 +187,7 @@ svcSetLocal x = modify $ \st -> st { svcLocal = x } svcSelf :: ServiceHandler s UnifiedIdentity-svcSelf = maybe (throwError "failed to validate own identity") return .+svcSelf = maybe (throwOtherError "failed to validate own identity") return . validateExtendedIdentity . lsIdentity . fromStored =<< svcGetLocal svcPrint :: String -> ServiceHandler s ()
+ src/Erebos/Service/Stream.hs view
@@ -0,0 +1,74 @@+module Erebos.Service.Stream (+ StreamPacket(..),+ StreamReader, getStreamReaderNumber,+ StreamWriter, getStreamWriterNumber,+ openStream, receivedStreams,+ readStreamPacket, writeStreamPacket,+ writeStream,+ closeStream, +) where++import Control.Concurrent.MVar+import Control.Monad.Reader+import Control.Monad.Writer++import Data.ByteString (ByteString)+import Data.Word++import Erebos.Flow+import Erebos.Network+import Erebos.Network.Protocol+import Erebos.Service+++data StreamReader = StreamReader RawStreamReader++getStreamReaderNumber :: StreamReader -> IO Int+getStreamReaderNumber (StreamReader stream) = return $ rsrNum stream++data StreamWriter = StreamWriter (MVar StreamWriterData)++data StreamWriterData = StreamWriterData+ { swdStream :: RawStreamWriter+ , swdSequence :: Maybe Word64+ }++getStreamWriterNumber :: StreamWriter -> IO Int+getStreamWriterNumber (StreamWriter stream) = rswNum . swdStream <$> readMVar stream+++openStream :: Service s => ServiceHandler s StreamWriter+openStream = do+ mvar <- liftIO newEmptyMVar+ tell [ ServiceOpenStream $ \stream -> putMVar mvar $ StreamWriterData stream (Just 0) ]+ return $ StreamWriter mvar++receivedStreams :: Service s => ServiceHandler s [ StreamReader ]+receivedStreams = do+ map StreamReader <$> asks svcNewStreams++readStreamPacket :: StreamReader -> IO StreamPacket+readStreamPacket (StreamReader stream) = do+ readFlowIO (rsrFlow stream)++writeStreamPacket :: StreamWriter -> StreamPacket -> IO ()+writeStreamPacket (StreamWriter mvar) packet = do+ withMVar mvar $ \swd -> do+ writeFlowIO (rswFlow $ swdStream swd) packet++writeStream :: StreamWriter -> ByteString -> IO ()+writeStream (StreamWriter mvar) bytes = do+ modifyMVar_ mvar $ \swd -> do+ case swdSequence swd of+ Just seqNum -> do+ writeFlowIO (rswFlow $ swdStream swd) $ StreamData seqNum bytes+ return swd { swdSequence = Just (seqNum + 1) }+ Nothing -> do+ fail "writeStream: stream closed"++closeStream :: StreamWriter -> IO ()+closeStream (StreamWriter mvar) = do+ withMVar mvar $ \swd -> do+ case swdSequence swd of+ Just seqNum -> writeFlowIO (rswFlow $ swdStream swd) $ StreamClosed seqNum+ Nothing -> fail "closeStream: stream already closed"
src/Erebos/Set.hs view
@@ -10,7 +10,6 @@ ) where import Control.Arrow-import Control.Monad.IO.Class import Data.Function import Data.List@@ -19,7 +18,8 @@ import Data.Maybe import Data.Ord -import Erebos.Storage+import Erebos.Object+import Erebos.Storable import Erebos.Storage.Merge import Erebos.Util @@ -52,14 +52,14 @@ loadSet :: Mergeable a => Ref -> Set a loadSet = mergeSorted . (:[]) . wrappedLoad -storeSetAdd :: (Mergeable a, MonadIO m) => Storage -> a -> Set a -> m (Set a)-storeSetAdd st x (Set prev) = Set . (:[]) <$> wrappedStore st SetItem+storeSetAdd :: (Mergeable a, MonadStorage m) => a -> Set a -> m (Set a)+storeSetAdd x (Set prev) = Set . (: []) <$> mstore SetItem { siPrev = prev , siItem = toComponents x } -storeSetAddComponent :: (Mergeable a, MonadStorage m, MonadIO m) => Stored (Component a) -> Set a -> m (Set a)-storeSetAddComponent component (Set prev) = Set . (:[]) <$> mstore SetItem+storeSetAddComponent :: (Mergeable a, MonadStorage m) => Stored (Component a) -> Set a -> m (Set a)+storeSetAddComponent component (Set prev) = Set . (: []) <$> mstore SetItem { siPrev = prev , siItem = [ component ] }
src/Erebos/State.hs view
@@ -1,13 +1,14 @@ module Erebos.State ( LocalState(..),- SharedState, SharedType(..),+ SharedState(..), SharedType(..), SharedTypeID, mkSharedTypeID, + MonadStorage(..), MonadHead(..), updateLocalHead_,-- loadLocalStateHead,+ LocalHeadT(..), + updateLocalState, updateLocalState_, updateSharedState, updateSharedState_, lookupSharedValue, makeSharedStateUpdate, @@ -15,32 +16,29 @@ headLocalIdentity, mergeSharedIdentity,- updateSharedIdentity,- interactiveIdentityUpdate, ) where +import Control.Monad import Control.Monad.Except import Control.Monad.Reader +import Data.Bifunctor import Data.ByteString (ByteString) import Data.ByteString.Char8 qualified as BC-import Data.Foldable-import Data.Maybe-import Data.Text qualified as T-import Data.Text.IO qualified as T import Data.Typeable-import Data.UUID (UUID)-import Data.UUID qualified as U -import System.IO- import Erebos.Identity+import Erebos.Object import Erebos.PubKey-import Erebos.Storage+import Erebos.Storable+import Erebos.Storage.Head import Erebos.Storage.Merge+import Erebos.UUID (UUID)+import Erebos.UUID qualified as U data LocalState = LocalState- { lsIdentity :: Stored (Signed ExtendedIdentityData)+ { lsPrev :: Maybe RefDigest+ , lsIdentity :: Stored (Signed ExtendedIdentityData) , lsShared :: [Stored SharedState] , lsOther :: [ ( ByteString, RecItem ) ] }@@ -62,14 +60,16 @@ instance Storable LocalState where store' LocalState {..} = storeRec $ do+ mapM_ (storeRawWeak "PREV") lsPrev storeRef "id" lsIdentity mapM_ (storeRef "shared") lsShared storeRecItems lsOther load' = loadRec $ do+ lsPrev <- loadMbRawWeak "PREV" lsIdentity <- loadRef "id" lsShared <- loadRefs "shared"- lsOther <- filter ((`notElem` [ BC.pack "id", BC.pack "shared" ]) . fst) <$> loadRecItems+ lsOther <- filter ((`notElem` [ BC.pack "PREV", BC.pack "id", BC.pack "shared" ]) . fst) <$> loadRecItems return LocalState {..} instance HeadType LocalState where@@ -104,35 +104,35 @@ snd <$> updateHead h f -loadLocalStateHead :: MonadIO m => Storage -> m (Head LocalState)-loadLocalStateHead st = loadHeads st >>= \case- (h:_) -> return h- [] -> liftIO $ do- putStr "Name: "- hFlush stdout- name <- T.getLine+newtype LocalHeadT h m a = LocalHeadT { runLocalHeadT :: Storage -> Stored h -> m ( a, Stored h ) } - putStr "Device: "- hFlush stdout- devName <- T.getLine+instance Functor m => Functor (LocalHeadT h m) where+ fmap f (LocalHeadT act) = LocalHeadT $ \st h -> first f <$> act st h - owner <- if- | T.null name -> return Nothing- | otherwise -> Just <$> createIdentity st (Just name) Nothing+instance Monad m => Applicative (LocalHeadT h m) where+ pure x = LocalHeadT $ \_ h -> pure ( x, h )+ (<*>) = ap - identity <- createIdentity st (if T.null devName then Nothing else Just devName) owner+instance Monad m => Monad (LocalHeadT h m) where+ return = pure+ LocalHeadT act >>= f = LocalHeadT $ \st h -> do+ ( x, h' ) <- act st h+ let (LocalHeadT act') = f x+ act' st h' - shared <- wrappedStore st $ SharedState- { ssPrev = []- , ssType = Just $ sharedTypeID @(Maybe ComposedIdentity) Proxy- , ssValue = [storedRef $ idExtData $ fromMaybe identity owner]- }- storeHead st $ LocalState- { lsIdentity = idExtData identity- , lsShared = [ shared ]- , lsOther = []- }+instance MonadIO m => MonadIO (LocalHeadT h m) where+ liftIO act = LocalHeadT $ \_ h -> ( , h ) <$> liftIO act +instance MonadIO m => MonadStorage (LocalHeadT h m) where+ getStorage = LocalHeadT $ \st h -> return ( st, h )++instance (HeadType h, MonadIO m) => MonadHead h (LocalHeadT h m) where+ updateLocalHead f = LocalHeadT $ \st h -> do+ let LocalHeadT act = f h+ ( ( h', x ), _ ) <- act st h+ return ( x, h' )++ localIdentity :: LocalState -> UnifiedIdentity localIdentity ls = maybe (error "failed to verify local identity") (updateOwners $ maybe [] idExtDataF $ lookupSharedValue $ lsShared ls)@@ -142,6 +142,17 @@ headLocalIdentity = localIdentity . headObject +updateLocalState :: forall m b. MonadHead LocalState m => (Stored LocalState -> m ( Stored LocalState, b )) -> m b+updateLocalState f = updateLocalHead $ \ls -> do+ ( ls', x ) <- f ls+ (, x) <$> if ls' == ls+ then return ls'+ else mstore (fromStored ls') { lsPrev = Just $ refDigest (storedRef ls) }++updateLocalState_ :: forall m. MonadHead LocalState m => (Stored LocalState -> m (Stored LocalState)) -> m ()+updateLocalState_ f = updateLocalState (fmap (,()) . f)++ updateSharedState_ :: forall a m. (SharedType a, MonadHead LocalState m) => (a -> m a) -> Stored LocalState -> m (Stored LocalState) updateSharedState_ f = fmap fst <$> updateSharedState (fmap (,()) . f) @@ -149,12 +160,11 @@ updateSharedState f = \ls -> do let shared = lsShared $ fromStored ls val = lookupSharedValue shared- st <- getStorage (val', x) <- f val (,x) <$> if toComponents val' == toComponents val then return ls- else do shared' <- makeSharedStateUpdate st val' shared- wrappedStore st (fromStored ls) { lsShared = [shared'] }+ else do shared' <- makeSharedStateUpdate val' shared+ mstore (fromStored ls) { lsShared = [shared'] } lookupSharedValue :: forall a. SharedType a => [Stored SharedState] -> a lookupSharedValue = mergeSorted . filterAncestors . map wrappedLoad . concatMap (ssValue . fromStored) . filterAncestors . helper@@ -162,46 +172,17 @@ | otherwise = helper $ ssPrev (fromStored x) ++ xs helper [] = [] -makeSharedStateUpdate :: forall a m. MonadIO m => SharedType a => Storage -> a -> [Stored SharedState] -> m (Stored SharedState)-makeSharedStateUpdate st val prev = liftIO $ wrappedStore st SharedState+makeSharedStateUpdate :: forall a m. (SharedType a, MonadStorage m) => a -> [ Stored SharedState ] -> m (Stored SharedState)+makeSharedStateUpdate val prev = mstore SharedState { ssPrev = prev , ssType = Just $ sharedTypeID @a Proxy , ssValue = storedRef <$> toComponents val } -mergeSharedIdentity :: (MonadHead LocalState m, MonadError String m) => m UnifiedIdentity-mergeSharedIdentity = updateLocalHead $ updateSharedState $ \case+mergeSharedIdentity :: (MonadHead LocalState m, MonadError e m, FromErebosError e) => m UnifiedIdentity+mergeSharedIdentity = updateLocalState $ updateSharedState $ \case Just cidentity -> do identity <- mergeIdentity cidentity return (Just $ toComposedIdentity identity, identity)- Nothing -> throwError "no existing shared identity"--updateSharedIdentity :: (MonadHead LocalState m, MonadError String m) => m ()-updateSharedIdentity = updateLocalHead_ $ updateSharedState_ $ \case- Just identity -> do- Just . toComposedIdentity <$> interactiveIdentityUpdate identity- Nothing -> throwError "no existing shared identity"--interactiveIdentityUpdate :: (Foldable f, MonadStorage m, MonadIO m, MonadError String m) => Identity f -> m UnifiedIdentity-interactiveIdentityUpdate fidentity = do- identity <- mergeIdentity fidentity- name <- liftIO $ do- T.putStr $ T.concat $ concat- [ [ T.pack "Name" ]- , case idName identity of- Just name -> [T.pack " [", name, T.pack "]"]- Nothing -> []- , [ T.pack ": " ]- ]- hFlush stdout- T.getLine-- if | T.null name -> return identity- | otherwise -> do- secret <- loadKey $ idKeyIdentity identity- maybe (throwError "created invalid identity") return . validateExtendedIdentity =<<- mstore =<< sign secret =<< mstore . ExtendedIdentityData =<< return (emptyIdentityExtension $ idData identity)- { idePrev = toList $ idExtDataF identity- , ideName = Just name- }+ Nothing -> throwOtherError "no existing shared identity"
+ src/Erebos/Storable.hs view
@@ -0,0 +1,44 @@+{-|+Description: Encoding custom types into Erebos objects++Module provides the 'Storable' class for types that can be serialized to/from+Erebos objects, along with various helpers, mostly for encoding using records.++The 'Stored' wrapper for objects actually encoded and stored in some storage is+defined here as well.+-}++module Erebos.Storable (+ Storable(..), ZeroStorable(..),+ StorableText(..), StorableDate(..), StorableUUID(..),++ Store, StoreRec,+ storeBlob, storeRec, storeZero,+ storeEmpty, storeInt, storeNum, storeText, storeBinary, storeDate, storeUUID, storeRef, storeRawRef, storeWeak, storeRawWeak,+ storeMbEmpty, storeMbInt, storeMbNum, storeMbText, storeMbBinary, storeMbDate, storeMbUUID, storeMbRef, storeMbRawRef, storeMbWeak, storeMbRawWeak,+ storeZRef, storeZWeak,+ storeRecItems,++ Load, LoadRec,+ loadCurrentRef, loadCurrentObject,+ loadRecCurrentRef, loadRecItems,++ loadBlob, loadRec, loadZero,+ loadEmpty, loadInt, loadNum, loadText, loadBinary, loadDate, loadUUID, loadRef, loadRawRef, loadRawWeak,+ loadMbEmpty, loadMbInt, loadMbNum, loadMbText, loadMbBinary, loadMbDate, loadMbUUID, loadMbRef, loadMbRawRef, loadMbRawWeak,+ loadTexts, loadBinaries, loadRefs, loadRawRefs, loadRawWeaks,+ loadZRef,++ Stored,+ fromStored, storedRef,+ wrappedStore, wrappedLoad,+ copyStored,+ unsafeMapStored,++ Storage, MonadStorage(..),++ module Erebos.Error,+) where++import Erebos.Error+import Erebos.Object.Internal
src/Erebos/Storage.hs view
@@ -1,1087 +1,29 @@-module Erebos.Storage (- Storage, PartialStorage, StorageCompleteness,- openStorage, memoryStorage,- deriveEphemeralStorage, derivePartialStorage,-- Ref, PartialRef, RefDigest,- refDigest, refFromDigest,- readRef, showRef, showRefDigest,- refDigestFromByteString, hashToRefDigest,- copyRef, partialRef, partialRefFromDigest,-- Object, PartialObject, Object'(..), RecItem, RecItem'(..),- serializeObject, deserializeObject, deserializeObjects,- ioLoadObject, ioLoadBytes,- storeRawBytes, lazyLoadBytes,- storeObject,- collectObjects, collectStoredObjects,-- Head, HeadType(..),- HeadTypeID, mkHeadTypeID,- headId, headStorage, headRef, headObject, headStoredObject,- loadHeads, loadHead, reloadHead,- storeHead, replaceHead, updateHead, updateHead_,- loadHeadRaw, storeHeadRaw, replaceHeadRaw,-- WatchedHead,- watchHead, watchHeadWith, unwatchHead,- watchHeadRaw,-- MonadStorage(..),-- Storable(..), ZeroStorable(..),- StorableText(..), StorableDate(..), StorableUUID(..),-- Store, StoreRec,- evalStore, evalStoreObject,- storeBlob, storeRec, storeZero,- storeEmpty, storeInt, storeNum, storeText, storeBinary, storeDate, storeUUID, storeRef, storeRawRef,- storeMbEmpty, storeMbInt, storeMbNum, storeMbText, storeMbBinary, storeMbDate, storeMbUUID, storeMbRef, storeMbRawRef,- storeZRef,- storeRecItems,-- Load, LoadRec,- evalLoad,- loadCurrentRef, loadCurrentObject,- loadRecCurrentRef, loadRecItems,-- loadBlob, loadRec, loadZero,- loadEmpty, loadInt, loadNum, loadText, loadBinary, loadDate, loadUUID, loadRef, loadRawRef,- loadMbEmpty, loadMbInt, loadMbNum, loadMbText, loadMbBinary, loadMbDate, loadMbUUID, loadMbRef, loadMbRawRef,- loadTexts, loadBinaries, loadRefs, loadRawRefs,- loadZRef,-- Stored,- fromStored, storedRef,- wrappedStore, wrappedLoad,- copyStored,- unsafeMapStored,-- StoreInfo(..), makeStoreInfo,-- StoredHistory,- fromHistory, fromHistoryAt, storedFromHistory, storedHistoryList,- beginHistory, modifyHistory,-) where--import Control.Applicative-import Control.Concurrent-import Control.Exception-import Control.Monad-import Control.Monad.Except-import Control.Monad.Reader-import Control.Monad.Writer--import Crypto.Hash--import Data.Bifunctor-import Data.ByteString (ByteString)-import qualified Data.ByteArray as BA-import qualified Data.ByteString as B-import qualified Data.ByteString.Char8 as BC-import qualified Data.ByteString.Lazy as BL-import qualified Data.ByteString.Lazy.Char8 as BLC-import Data.Char-import Data.Function-import qualified Data.HashTable.IO as HT-import Data.List-import qualified Data.Map as M-import Data.Maybe-import Data.Ratio-import Data.Set (Set)-import qualified Data.Set as S-import Data.Text (Text)-import qualified Data.Text as T-import Data.Text.Encoding-import Data.Text.Encoding.Error-import Data.Time.Calendar-import Data.Time.Clock-import Data.Time.Format-import Data.Time.LocalTime-import Data.Typeable-import Data.UUID (UUID)-import qualified Data.UUID as U-import qualified Data.UUID.V4 as U--import System.Directory-import System.FSNotify-import System.FilePath-import System.IO.Error-import System.IO.Unsafe--import Erebos.Storage.Internal---type Storage = Storage' Complete-type PartialStorage = Storage' Partial--storageVersion :: String-storageVersion = "0.1"--openStorage :: FilePath -> IO Storage-openStorage path = modifyIOError annotate $ do- let versionFileName = "erebos-storage"- let versionPath = path </> versionFileName- let writeVersionFile = writeFileOnce versionPath $ BLC.pack $ storageVersion <> "\n"-- maybeVersion <- handleJust (guard . isDoesNotExistError) (const $ return Nothing) $- Just <$> readFile versionPath- version <- case maybeVersion of- Just versionContent -> do- return $ takeWhile (/= '\n') versionContent-- Nothing -> do- files <- handleJust (guard . isDoesNotExistError) (const $ return []) $- listDirectory path- when (not $ or- [ null files- , versionFileName `elem` files- , (versionFileName ++ ".lock") `elem` files- , "objects" `elem` files && "heads" `elem` files- ]) $ do- fail "directory is neither empty, nor an existing erebos storage"-- createDirectoryIfMissing True $ path- writeVersionFile- takeWhile (/= '\n') <$> readFile versionPath-- when (version /= storageVersion) $ do- fail $ "unsupported storage version " <> version-- createDirectoryIfMissing True $ path </> "objects"- createDirectoryIfMissing True $ path </> "heads"- watchers <- newMVar (Nothing, [], WatchList 1 [])- refgen <- newMVar =<< HT.new- refroots <- newMVar =<< HT.new- return $ Storage- { stBacking = StorageDir path watchers- , stParent = Nothing- , stRefGeneration = refgen- , stRefRoots = refroots- }- where- annotate e = annotateIOError e "failed to open storage" Nothing (Just path)--memoryStorage' :: IO (Storage' c')-memoryStorage' = do- backing <- StorageMemory <$> newMVar [] <*> newMVar M.empty <*> newMVar M.empty <*> newMVar (WatchList 1 [])- refgen <- newMVar =<< HT.new- refroots <- newMVar =<< HT.new- return $ Storage- { stBacking = backing- , stParent = Nothing- , stRefGeneration = refgen- , stRefRoots = refroots- }--memoryStorage :: IO Storage-memoryStorage = memoryStorage'--deriveEphemeralStorage :: Storage -> IO Storage-deriveEphemeralStorage parent = do- st <- memoryStorage- return $ st { stParent = Just parent }--derivePartialStorage :: Storage -> IO PartialStorage-derivePartialStorage parent = do- st <- memoryStorage'- return $ st { stParent = Just parent }--type Ref = Ref' Complete-type PartialRef = Ref' Partial--zeroRef :: Storage' c -> Ref' c-zeroRef s = Ref s (RefDigest h)- where h = case digestFromByteString $ B.replicate (hashDigestSize $ digestAlgo h) 0 of- Nothing -> error $ "Failed to create zero hash"- Just h' -> h'- digestAlgo :: Digest a -> a- digestAlgo = undefined--isZeroRef :: Ref' c -> Bool-isZeroRef (Ref _ h) = all (==0) $ BA.unpack h---refFromDigest :: Storage' c -> RefDigest -> IO (Maybe (Ref' c))-refFromDigest st dgst = fmap (const $ Ref st dgst) <$> ioLoadBytesFromStorage st dgst--readRef :: Storage -> ByteString -> IO (Maybe Ref)-readRef s b =- case readRefDigest b of- Nothing -> return Nothing- Just dgst -> refFromDigest s dgst--copyRef' :: forall c c'. (StorageCompleteness c, StorageCompleteness c') => Storage' c' -> Ref' c -> IO (c (Ref' c'))-copyRef' st ref'@(Ref _ dgst) = refFromDigest st dgst >>= \case Just ref -> return $ return ref- Nothing -> doCopy- where doCopy = do mbobj' <- ioLoadObject ref'- mbobj <- sequence $ copyObject' st <$> mbobj'- sequence $ unsafeStoreObject st <$> join mbobj--copyRecItem' :: forall c c'. (StorageCompleteness c, StorageCompleteness c') => Storage' c' -> RecItem' c -> IO (c (RecItem' c'))-copyRecItem' st = \case- RecEmpty -> return $ return $ RecEmpty- RecInt x -> return $ return $ RecInt x- RecNum x -> return $ return $ RecNum x- RecText x -> return $ return $ RecText x- RecBinary x -> return $ return $ RecBinary x- RecDate x -> return $ return $ RecDate x- RecUUID x -> return $ return $ RecUUID x- RecRef x -> fmap RecRef <$> copyRef' st x- RecUnknown t x -> return $ return $ RecUnknown t x--copyObject' :: forall c c'. (StorageCompleteness c, StorageCompleteness c') => Storage' c' -> Object' c -> IO (c (Object' c'))-copyObject' _ (Blob bs) = return $ return $ Blob bs-copyObject' st (Rec rs) = fmap Rec . sequence <$> mapM (\( n, item ) -> fmap ( n, ) <$> copyRecItem' st item) rs-copyObject' _ ZeroObject = return $ return ZeroObject-copyObject' _ (UnknownObject otype content) = return $ return $ UnknownObject otype content--copyRef :: forall c c' m. (StorageCompleteness c, StorageCompleteness c', MonadIO m) => Storage' c' -> Ref' c -> m (LoadResult c (Ref' c'))-copyRef st ref' = liftIO $ returnLoadResult <$> copyRef' st ref'--copyRecItem :: forall c c' m. (StorageCompleteness c, StorageCompleteness c', MonadIO m) => Storage' c' -> RecItem' c -> m (LoadResult c (RecItem' c'))-copyRecItem st item' = liftIO $ returnLoadResult <$> copyRecItem' st item'--copyObject :: forall c c'. (StorageCompleteness c, StorageCompleteness c') => Storage' c' -> Object' c -> IO (LoadResult c (Object' c'))-copyObject st obj' = returnLoadResult <$> copyObject' st obj'--partialRef :: PartialStorage -> Ref -> PartialRef-partialRef st (Ref _ dgst) = Ref st dgst--partialRefFromDigest :: PartialStorage -> RefDigest -> PartialRef-partialRefFromDigest st dgst = Ref st dgst---data Object' c- = Blob ByteString- | Rec [(ByteString, RecItem' c)]- | ZeroObject- | UnknownObject ByteString ByteString- deriving (Show)--type Object = Object' Complete-type PartialObject = Object' Partial--data RecItem' c- = RecEmpty- | RecInt Integer- | RecNum Rational- | RecText Text- | RecBinary ByteString- | RecDate ZonedTime- | RecUUID UUID- | RecRef (Ref' c)- | RecUnknown ByteString ByteString- deriving (Show)--type RecItem = RecItem' Complete--serializeObject :: Object' c -> BL.ByteString-serializeObject = \case- Blob cnt -> BL.fromChunks [BC.pack "blob ", BC.pack (show $ B.length cnt), BC.singleton '\n', cnt]- Rec rec -> let cnt = BL.fromChunks $ concatMap (uncurry serializeRecItem) rec- in BL.fromChunks [BC.pack "rec ", BC.pack (show $ BL.length cnt), BC.singleton '\n'] `BL.append` cnt- ZeroObject -> BL.empty- UnknownObject otype cnt -> BL.fromChunks [ otype, BC.singleton ' ', BC.pack (show $ B.length cnt), BC.singleton '\n', cnt ]---- |Serializes and stores object data without ony dependencies, so is safe only--- if all the referenced objects are already stored or reference is partial.-unsafeStoreObject :: Storage' c -> Object' c -> IO (Ref' c)-unsafeStoreObject storage = \case- ZeroObject -> return $ zeroRef storage- obj -> unsafeStoreRawBytes storage $ serializeObject obj--storeObject :: PartialStorage -> PartialObject -> IO PartialRef-storeObject = unsafeStoreObject--storeRawBytes :: PartialStorage -> BL.ByteString -> IO PartialRef-storeRawBytes = unsafeStoreRawBytes--serializeRecItem :: ByteString -> RecItem' c -> [ByteString]-serializeRecItem name (RecEmpty) = [name, BC.pack ":e", BC.singleton ' ', BC.singleton '\n']-serializeRecItem name (RecInt x) = [name, BC.pack ":i", BC.singleton ' ', BC.pack (show x), BC.singleton '\n']-serializeRecItem name (RecNum x) = [name, BC.pack ":n", BC.singleton ' ', BC.pack (showRatio x), BC.singleton '\n']-serializeRecItem name (RecText x) = [name, BC.pack ":t", BC.singleton ' ', escaped, BC.singleton '\n']- where escaped = BC.concatMap escape $ encodeUtf8 x- escape '\n' = BC.pack "\n\t"- escape c = BC.singleton c-serializeRecItem name (RecBinary x) = [name, BC.pack ":b ", showHex x, BC.singleton '\n']-serializeRecItem name (RecDate x) = [name, BC.pack ":d", BC.singleton ' ', BC.pack (formatTime defaultTimeLocale "%s %z" x), BC.singleton '\n']-serializeRecItem name (RecUUID x) = [name, BC.pack ":u", BC.singleton ' ', U.toASCIIBytes x, BC.singleton '\n']-serializeRecItem name (RecRef x) = [name, BC.pack ":r ", showRef x, BC.singleton '\n']-serializeRecItem name (RecUnknown t x) = [ name, BC.singleton ':', t, BC.singleton ' ', x, BC.singleton '\n' ]--lazyLoadObject :: forall c. StorageCompleteness c => Ref' c -> LoadResult c (Object' c)-lazyLoadObject = returnLoadResult . unsafePerformIO . ioLoadObject--ioLoadObject :: forall c. StorageCompleteness c => Ref' c -> IO (c (Object' c))-ioLoadObject ref | isZeroRef ref = return $ return ZeroObject-ioLoadObject ref@(Ref st rhash) = do- file' <- ioLoadBytes ref- return $ do- file <- file'- let chash = hashToRefDigest file- when (chash /= rhash) $ error $ "Hash mismatch on object " ++ BC.unpack (showRef ref) {- TODO throw -}- return $ case runExcept $ unsafeDeserializeObject st file of- Left err -> error $ err ++ ", ref " ++ BC.unpack (showRef ref) {- TODO throw -}- Right (x, rest) | BL.null rest -> x- | otherwise -> error $ "Superfluous content after " ++ BC.unpack (showRef ref) {- TODO throw -}--lazyLoadBytes :: forall c. StorageCompleteness c => Ref' c -> LoadResult c BL.ByteString-lazyLoadBytes ref | isZeroRef ref = returnLoadResult (return BL.empty :: c BL.ByteString)-lazyLoadBytes ref = returnLoadResult $ unsafePerformIO $ ioLoadBytes ref--unsafeDeserializeObject :: Storage' c -> BL.ByteString -> Except String (Object' c, BL.ByteString)-unsafeDeserializeObject _ bytes | BL.null bytes = return (ZeroObject, bytes)-unsafeDeserializeObject st bytes =- case BLC.break (=='\n') bytes of- (line, rest) | Just (otype, len) <- splitObjPrefix line -> do- let (content, next) = first BL.toStrict $ BL.splitAt (fromIntegral len) $ BL.drop 1 rest- guard $ B.length content == len- (,next) <$> case otype of- _ | otype == BC.pack "blob" -> return $ Blob content- | otype == BC.pack "rec" -> maybe (throwError $ "Malformed record item ")- (return . Rec) $ sequence $ map parseRecLine $ mergeCont [] $ BC.lines content- | otherwise -> return $ UnknownObject otype content- _ -> throwError $ "Malformed object"- where splitObjPrefix line = do- [otype, tlen] <- return $ BLC.words line- (len, rest) <- BLC.readInt tlen- guard $ BL.null rest- return (BL.toStrict otype, len)-- mergeCont cs (a:b:rest) | Just ('\t', b') <- BC.uncons b = mergeCont (b':BC.pack "\n":cs) (a:rest)- mergeCont cs (a:rest) = B.concat (a : reverse cs) : mergeCont [] rest- mergeCont _ [] = []-- parseRecLine line = do- colon <- BC.elemIndex ':' line- space <- BC.elemIndex ' ' line- guard $ colon < space- let name = B.take colon line- itype = B.take (space-colon-1) $ B.drop (colon+1) line- content = B.drop (space+1) line-- let val = fromMaybe (RecUnknown itype content) $- case BC.unpack itype of- "e" -> do guard $ B.null content- return RecEmpty- "i" -> do (num, rest) <- BC.readInteger content- guard $ B.null rest- return $ RecInt num- "n" -> RecNum <$> parseRatio content- "t" -> return $ RecText $ decodeUtf8With lenientDecode content- "b" -> RecBinary <$> readHex content- "d" -> RecDate <$> parseTimeM False defaultTimeLocale "%s %z" (BC.unpack content)- "u" -> RecUUID <$> U.fromASCIIBytes content- "r" -> RecRef . Ref st <$> readRefDigest content- _ -> Nothing- return (name, val)--deserializeObject :: PartialStorage -> BL.ByteString -> Except String (PartialObject, BL.ByteString)-deserializeObject = unsafeDeserializeObject--deserializeObjects :: PartialStorage -> BL.ByteString -> Except String [PartialObject]-deserializeObjects _ bytes | BL.null bytes = return []-deserializeObjects st bytes = do (obj, rest) <- deserializeObject st bytes- (obj:) <$> deserializeObjects st rest---collectObjects :: Object -> [Object]-collectObjects obj = obj : map fromStored (fst $ collectOtherStored S.empty obj)--collectStoredObjects :: Stored Object -> [Stored Object]-collectStoredObjects obj = obj : (fst $ collectOtherStored S.empty $ fromStored obj)--collectOtherStored :: Set RefDigest -> Object -> ([Stored Object], Set RefDigest)-collectOtherStored seen (Rec items) = foldr helper ([], seen) $ map snd items- where helper (RecRef ref) (xs, s) | r <- refDigest ref- , r `S.notMember` s- = let o = wrappedLoad ref- (xs', s') = collectOtherStored (S.insert r s) $ fromStored o- in ((o : xs') ++ xs, s')- helper _ (xs, s) = (xs, s)-collectOtherStored seen _ = ([], seen)---type Head = Head' Complete--headId :: Head a -> HeadID-headId (Head uuid _) = uuid--headStorage :: Head a -> Storage-headStorage = refStorage . headRef--headRef :: Head a -> Ref-headRef (Head _ sx) = storedRef sx--headObject :: Head a -> a-headObject (Head _ sx) = fromStored sx--headStoredObject :: Head a -> Stored a-headStoredObject (Head _ sx) = sx--deriving instance StorableUUID HeadID-deriving instance StorableUUID HeadTypeID--mkHeadTypeID :: String -> HeadTypeID-mkHeadTypeID = maybe (error "Invalid head type ID") HeadTypeID . U.fromString--class Storable a => HeadType a where- headTypeID :: proxy a -> HeadTypeID---headTypePath :: FilePath -> HeadTypeID -> FilePath-headTypePath spath (HeadTypeID tid) = spath </> "heads" </> U.toString tid--headPath :: FilePath -> HeadTypeID -> HeadID -> FilePath-headPath spath tid (HeadID hid) = headTypePath spath tid </> U.toString hid--loadHeads :: forall a m. MonadIO m => HeadType a => Storage -> m [Head a]-loadHeads s@(Storage { stBacking = StorageDir { dirPath = spath }}) = liftIO $ do- let hpath = headTypePath spath $ headTypeID @a Proxy-- files <- filterM (doesFileExist . (hpath </>)) =<<- handleJust (\e -> guard (isDoesNotExistError e)) (const $ return [])- (getDirectoryContents hpath)- fmap catMaybes $ forM files $ \hname -> do- case U.fromString hname of- Just hid -> do- (h:_) <- BC.lines <$> B.readFile (hpath </> hname)- Just ref <- readRef s h- return $ Just $ Head (HeadID hid) $ wrappedLoad ref- Nothing -> return Nothing-loadHeads Storage { stBacking = StorageMemory { memHeads = theads } } = liftIO $ do- let toHead ((tid, hid), ref) | tid == headTypeID @a Proxy = Just $ Head hid $ wrappedLoad ref- | otherwise = Nothing- catMaybes . map toHead <$> readMVar theads--loadHead :: forall a m. (HeadType a, MonadIO m) => Storage -> HeadID -> m (Maybe (Head a))-loadHead st hid = fmap (Head hid . wrappedLoad) <$> loadHeadRaw st (headTypeID @a Proxy) hid--loadHeadRaw :: forall m. MonadIO m => Storage -> HeadTypeID -> HeadID -> m (Maybe Ref)-loadHeadRaw s@(Storage { stBacking = StorageDir { dirPath = spath }}) tid hid = liftIO $ do- handleJust (guard . isDoesNotExistError) (const $ return Nothing) $ do- (h:_) <- BC.lines <$> B.readFile (headPath spath tid hid)- Just ref <- readRef s h- return $ Just ref-loadHeadRaw Storage { stBacking = StorageMemory { memHeads = theads } } tid hid = liftIO $ do- lookup (tid, hid) <$> readMVar theads--reloadHead :: (HeadType a, MonadIO m) => Head a -> m (Maybe (Head a))-reloadHead (Head hid (Stored (Ref st _) _)) = loadHead st hid--storeHead :: forall a m. MonadIO m => HeadType a => Storage -> a -> m (Head a)-storeHead st obj = do- let tid = headTypeID @a Proxy- stored <- wrappedStore st obj- hid <- storeHeadRaw st tid (storedRef stored)- return $ Head hid stored--storeHeadRaw :: forall m. MonadIO m => Storage -> HeadTypeID -> Ref -> m HeadID-storeHeadRaw st tid ref = liftIO $ do- hid <- HeadID <$> U.nextRandom- case stBacking st of- StorageDir { dirPath = spath } -> do- Right () <- writeFileChecked (headPath spath tid hid) Nothing $- showRef ref `B.append` BC.singleton '\n'- return ()- StorageMemory { memHeads = theads } -> do- modifyMVar_ theads $ return . (((tid, hid), ref) :)- return hid--replaceHead :: forall a m. (HeadType a, MonadIO m) => Head a -> Stored a -> m (Either (Maybe (Head a)) (Head a))-replaceHead prev@(Head hid pobj) stored' = liftIO $ do- let st = headStorage prev- tid = headTypeID @a Proxy- stored <- copyStored st stored'- bimap (fmap $ Head hid . wrappedLoad) (const $ Head hid stored) <$>- replaceHeadRaw st tid hid (storedRef pobj) (storedRef stored)--replaceHeadRaw :: forall m. MonadIO m => Storage -> HeadTypeID -> HeadID -> Ref -> Ref -> m (Either (Maybe Ref) Ref)-replaceHeadRaw st tid hid prev new = liftIO $ do- case stBacking st of- StorageDir { dirPath = spath } -> do- let filename = headPath spath tid hid- showRefL r = showRef r `B.append` BC.singleton '\n'-- writeFileChecked filename (Just $ showRefL prev) (showRefL new) >>= \case- Left Nothing -> return $ Left Nothing- Left (Just bs) -> do Just oref <- readRef st $ BC.takeWhile (/='\n') bs- return $ Left $ Just oref- Right () -> return $ Right new-- StorageMemory { memHeads = theads, memWatchers = twatch } -> do- res <- modifyMVar theads $ \hs -> do- ws <- map wlFun . filter ((==(tid, hid)) . wlHead) . wlList <$> readMVar twatch- return $ case partition ((==(tid, hid)) . fst) hs of- ([] , _ ) -> (hs, Left Nothing)- ((_, r):_, hs') | r == prev -> (((tid, hid), new) : hs',- Right (new, ws))- | otherwise -> (hs, Left $ Just r)- case res of- Right (r, ws) -> mapM_ ($ r) ws >> return (Right r)- Left x -> return $ Left x--updateHead :: (HeadType a, MonadIO m) => Head a -> (Stored a -> m (Stored a, b)) -> m (Maybe (Head a), b)-updateHead h f = do- (o, x) <- f $ headStoredObject h- replaceHead h o >>= \case- Right h' -> return (Just h', x)- Left Nothing -> return (Nothing, x)- Left (Just h') -> updateHead h' f--updateHead_ :: (HeadType a, MonadIO m) => Head a -> (Stored a -> m (Stored a)) -> m (Maybe (Head a))-updateHead_ h = fmap fst . updateHead h . (fmap (,()) .)---data WatchedHead = forall a. WatchedHead Storage WatchID (MVar a)--watchHead :: forall a. HeadType a => Head a -> (Head a -> IO ()) -> IO WatchedHead-watchHead h = watchHeadWith h id--watchHeadWith :: forall a b. (HeadType a, Eq b) => Head a -> (Head a -> b) -> (b -> IO ()) -> IO WatchedHead-watchHeadWith (Head hid (Stored (Ref st _) _)) sel cb = do- watchHeadRaw st (headTypeID @a Proxy) hid (sel . Head hid . wrappedLoad) cb--watchHeadRaw :: forall b. Eq b => Storage -> HeadTypeID -> HeadID -> (Ref -> b) -> (b -> IO ()) -> IO WatchedHead-watchHeadRaw st tid hid sel cb = do- memo <- newEmptyMVar- let addWatcher wl = (wl', WatchedHead st (wlNext wl) memo)- where wl' = wl { wlNext = wlNext wl + 1- , wlList = WatchListItem- { wlID = wlNext wl- , wlHead = (tid, hid)- , wlFun = \r -> do- let x = sel r- modifyMVar_ memo $ \prev -> do- when (Just x /= prev) $ cb x- return $ Just x- } : wlList wl- }-- watched <- case stBacking st of- StorageDir { dirPath = spath, dirWatchers = mvar } -> modifyMVar mvar $ \(mbmanager, ilist, wl) -> do- manager <- maybe startManager return mbmanager- ilist' <- case tid `elem` ilist of- True -> return ilist- False -> do- void $ watchDir manager (headTypePath spath tid) (const True) $ \case- ev@Added {} | Just ihid <- HeadID <$> U.fromString (takeFileName (eventPath ev)) -> do- loadHeadRaw st tid ihid >>= \case- Just ref -> do- (_, _, iwl) <- readMVar mvar- mapM_ ($ ref) . map wlFun . filter ((== (tid, ihid)) . wlHead) . wlList $ iwl- Nothing -> return ()- _ -> return ()- return $ tid : ilist- return $ first ( Just manager, ilist', ) $ addWatcher wl-- StorageMemory { memWatchers = mvar } -> modifyMVar mvar $ return . addWatcher-- cur <- fmap sel <$> loadHeadRaw st tid hid- maybe (return ()) cb cur- putMVar memo cur-- return watched--unwatchHead :: WatchedHead -> IO ()-unwatchHead (WatchedHead st wid _) = do- let delWatcher wl = wl { wlList = filter ((/=wid) . wlID) $ wlList wl }- case stBacking st of- StorageDir { dirWatchers = mvar } -> modifyMVar_ mvar $ return . second delWatcher- StorageMemory { memWatchers = mvar } -> modifyMVar_ mvar $ return . delWatcher---class Monad m => MonadStorage m where- getStorage :: m Storage- mstore :: Storable a => a -> m (Stored a)-- default mstore :: MonadIO m => Storable a => a -> m (Stored a)- mstore x = do- st <- getStorage- wrappedStore st x--instance MonadIO m => MonadStorage (ReaderT Storage m) where- getStorage = ask--instance MonadIO m => MonadStorage (ReaderT (Head a) m) where- getStorage = asks $ headStorage---class Storable a where- store' :: a -> Store- load' :: Load a-- store :: StorageCompleteness c => Storage' c -> a -> IO (Ref' c)- store st = evalStore st . store'- load :: Ref -> a- load = evalLoad load'--class Storable a => ZeroStorable a where- fromZero :: Storage -> a--data Store = StoreBlob ByteString- | StoreRec (forall c. StorageCompleteness c => Storage' c -> [IO [(ByteString, RecItem' c)]])- | StoreZero- | StoreUnknown ByteString ByteString--evalStore :: StorageCompleteness c => Storage' c -> Store -> IO (Ref' c)-evalStore st = unsafeStoreObject st <=< evalStoreObject st--evalStoreObject :: StorageCompleteness c => Storage' c -> Store -> IO (Object' c)-evalStoreObject _ (StoreBlob x) = return $ Blob x-evalStoreObject s (StoreRec f) = Rec . concat <$> sequence (f s)-evalStoreObject _ StoreZero = return ZeroObject-evalStoreObject _ (StoreUnknown otype content) = return $ UnknownObject otype content--newtype StoreRecM c a = StoreRecM (ReaderT (Storage' c) (Writer [IO [(ByteString, RecItem' c)]]) a)- deriving (Functor, Applicative, Monad)--type StoreRec c = StoreRecM c ()--newtype Load a = Load (ReaderT (Ref, Object) (Except String) a)- deriving (Functor, Applicative, Alternative, Monad, MonadPlus, MonadError String)--evalLoad :: Load a -> Ref -> a-evalLoad (Load f) ref = either (error {- TODO throw -} . ((BC.unpack (showRef ref) ++ ": ")++)) id $ runExcept $ runReaderT f (ref, lazyLoadObject ref)--loadCurrentRef :: Load Ref-loadCurrentRef = Load $ asks fst--loadCurrentObject :: Load Object-loadCurrentObject = Load $ asks snd--newtype LoadRec a = LoadRec (ReaderT (Ref, [(ByteString, RecItem)]) (Except String) a)- deriving (Functor, Applicative, Alternative, Monad, MonadPlus, MonadError String)--loadRecCurrentRef :: LoadRec Ref-loadRecCurrentRef = LoadRec $ asks fst--loadRecItems :: LoadRec [(ByteString, RecItem)]-loadRecItems = LoadRec $ asks snd---instance Storable Object where- store' (Blob bs) = StoreBlob bs- store' (Rec xs) = StoreRec $ \st -> return $ do- Rec xs' <- copyObject st (Rec xs)- return xs'- store' ZeroObject = StoreZero- store' (UnknownObject otype content) = StoreUnknown otype content-- load' = loadCurrentObject-- store st = unsafeStoreObject st <=< copyObject st- load = lazyLoadObject--instance Storable ByteString where- store' = storeBlob- load' = loadBlob id--instance Storable a => Storable [a] where- store' [] = storeZero- store' (x:xs) = storeRec $ do- storeRef "i" x- storeRef "n" xs-- load' = loadCurrentObject >>= \case- ZeroObject -> return []- _ -> loadRec $ (:)- <$> loadRef "i"- <*> loadRef "n"--instance Storable a => ZeroStorable [a] where- fromZero _ = []---storeBlob :: ByteString -> Store-storeBlob = StoreBlob--storeRec :: (forall c. StorageCompleteness c => StoreRec c) -> Store-storeRec sr = StoreRec $ do- let StoreRecM r = sr- execWriter . runReaderT r--storeZero :: Store-storeZero = StoreZero---class StorableText a where- toText :: a -> Text- fromText :: MonadError String m => Text -> m a--instance StorableText Text where- toText = id; fromText = return--instance StorableText [Char] where- toText = T.pack; fromText = return . T.unpack---class StorableDate a where- toDate :: a -> ZonedTime- fromDate :: ZonedTime -> a--instance StorableDate ZonedTime where- toDate = id; fromDate = id--instance StorableDate UTCTime where- toDate = utcToZonedTime utc- fromDate = zonedTimeToUTC--instance StorableDate Day where- toDate day = toDate $ UTCTime day 0- fromDate = utctDay . fromDate---class StorableUUID a where- toUUID :: a -> UUID- fromUUID :: UUID -> a--instance StorableUUID UUID where- toUUID = id; fromUUID = id---storeEmpty :: String -> StoreRec c-storeEmpty name = StoreRecM $ tell [return [(BC.pack name, RecEmpty)]]--storeMbEmpty :: String -> Maybe () -> StoreRec c-storeMbEmpty name = maybe (return ()) (const $ storeEmpty name)--storeInt :: Integral a => String -> a -> StoreRec c-storeInt name x = StoreRecM $ tell [return [(BC.pack name, RecInt $ toInteger x)]]--storeMbInt :: Integral a => String -> Maybe a -> StoreRec c-storeMbInt name = maybe (return ()) (storeInt name)--storeNum :: (Real a, Fractional a) => String -> a -> StoreRec c-storeNum name x = StoreRecM $ tell [return [(BC.pack name, RecNum $ toRational x)]]--storeMbNum :: (Real a, Fractional a) => String -> Maybe a -> StoreRec c-storeMbNum name = maybe (return ()) (storeNum name)--storeText :: StorableText a => String -> a -> StoreRec c-storeText name x = StoreRecM $ tell [return [(BC.pack name, RecText $ toText x)]]--storeMbText :: StorableText a => String -> Maybe a -> StoreRec c-storeMbText name = maybe (return ()) (storeText name)--storeBinary :: BA.ByteArrayAccess a => String -> a -> StoreRec c-storeBinary name x = StoreRecM $ tell [return [(BC.pack name, RecBinary $ BA.convert x)]]--storeMbBinary :: BA.ByteArrayAccess a => String -> Maybe a -> StoreRec c-storeMbBinary name = maybe (return ()) (storeBinary name)--storeDate :: StorableDate a => String -> a -> StoreRec c-storeDate name x = StoreRecM $ tell [return [(BC.pack name, RecDate $ toDate x)]]--storeMbDate :: StorableDate a => String -> Maybe a -> StoreRec c-storeMbDate name = maybe (return ()) (storeDate name)--storeUUID :: StorableUUID a => String -> a -> StoreRec c-storeUUID name x = StoreRecM $ tell [return [(BC.pack name, RecUUID $ toUUID x)]]--storeMbUUID :: StorableUUID a => String -> Maybe a -> StoreRec c-storeMbUUID name = maybe (return ()) (storeUUID name)--storeRef :: Storable a => StorageCompleteness c => String -> a -> StoreRec c-storeRef name x = StoreRecM $ do- s <- ask- tell $ (:[]) $ do- ref <- store s x- return [(BC.pack name, RecRef ref)]--storeMbRef :: Storable a => StorageCompleteness c => String -> Maybe a -> StoreRec c-storeMbRef name = maybe (return ()) (storeRef name)--storeRawRef :: StorageCompleteness c => String -> Ref -> StoreRec c-storeRawRef name ref = StoreRecM $ do- st <- ask- tell $ (:[]) $ do- ref' <- copyRef st ref- return [(BC.pack name, RecRef ref')]--storeMbRawRef :: StorageCompleteness c => String -> Maybe Ref -> StoreRec c-storeMbRawRef name = maybe (return ()) (storeRawRef name)--storeZRef :: (ZeroStorable a, StorageCompleteness c) => String -> a -> StoreRec c-storeZRef name x = StoreRecM $ do- s <- ask- tell $ (:[]) $ do- ref <- store s x- return $ if isZeroRef ref then []- else [(BC.pack name, RecRef ref)]--storeRecItems :: StorageCompleteness c => [ ( ByteString, RecItem ) ] -> StoreRec c-storeRecItems items = StoreRecM $ do- st <- ask- tell $ flip map items $ \( name, value ) -> do- value' <- copyRecItem st value- return [ ( name, value' ) ]--loadBlob :: (ByteString -> a) -> Load a-loadBlob f = loadCurrentObject >>= \case- Blob x -> return $ f x- _ -> throwError "Expecting blob"--loadRec :: LoadRec a -> Load a-loadRec (LoadRec lrec) = loadCurrentObject >>= \case- Rec rs -> do- ref <- loadCurrentRef- either throwError return $ runExcept $ runReaderT lrec (ref, rs)- _ -> throwError "Expecting record"--loadZero :: a -> Load a-loadZero x = loadCurrentObject >>= \case- ZeroObject -> return x- _ -> throwError "Expecting zero"---loadEmpty :: String -> LoadRec ()-loadEmpty name = maybe (throwError $ "Missing record item '"++name++"'") return =<< loadMbEmpty name--loadMbEmpty :: String -> LoadRec (Maybe ())-loadMbEmpty name = listToMaybe . mapMaybe p <$> loadRecItems- where- bname = BC.pack name- p ( name', RecEmpty ) | name' == bname- = Just ()- p _ = Nothing--loadInt :: Num a => String -> LoadRec a-loadInt name = maybe (throwError $ "Missing record item '"++name++"'") return =<< loadMbInt name--loadMbInt :: Num a => String -> LoadRec (Maybe a)-loadMbInt name = listToMaybe . mapMaybe p <$> loadRecItems- where- bname = BC.pack name- p ( name', RecInt x ) | name' == bname- = Just (fromInteger x)- p _ = Nothing--loadNum :: (Real a, Fractional a) => String -> LoadRec a-loadNum name = maybe (throwError $ "Missing record item '"++name++"'") return =<< loadMbNum name--loadMbNum :: (Real a, Fractional a) => String -> LoadRec (Maybe a)-loadMbNum name = listToMaybe . mapMaybe p <$> loadRecItems- where- bname = BC.pack name- p ( name', RecNum x ) | name' == bname- = Just (fromRational x)- p _ = Nothing--loadText :: StorableText a => String -> LoadRec a-loadText name = maybe (throwError $ "Missing record item '"++name++"'") return =<< loadMbText name--loadMbText :: StorableText a => String -> LoadRec (Maybe a)-loadMbText name = listToMaybe <$> loadTexts name--loadTexts :: StorableText a => String -> LoadRec [a]-loadTexts name = sequence . mapMaybe p =<< loadRecItems- where- bname = BC.pack name- p ( name', RecText x ) | name' == bname- = Just (fromText x)- p _ = Nothing--loadBinary :: BA.ByteArray a => String -> LoadRec a-loadBinary name = maybe (throwError $ "Missing record item '"++name++"'") return =<< loadMbBinary name--loadMbBinary :: BA.ByteArray a => String -> LoadRec (Maybe a)-loadMbBinary name = listToMaybe <$> loadBinaries name--loadBinaries :: BA.ByteArray a => String -> LoadRec [a]-loadBinaries name = mapMaybe p <$> loadRecItems- where- bname = BC.pack name- p ( name', RecBinary x ) | name' == bname- = Just (BA.convert x)- p _ = Nothing--loadDate :: StorableDate a => String -> LoadRec a-loadDate name = maybe (throwError $ "Missing record item '"++name++"'") return =<< loadMbDate name--loadMbDate :: StorableDate a => String -> LoadRec (Maybe a)-loadMbDate name = listToMaybe . mapMaybe p <$> loadRecItems- where- bname = BC.pack name- p ( name', RecDate x ) | name' == bname- = Just (fromDate x)- p _ = Nothing--loadUUID :: StorableUUID a => String -> LoadRec a-loadUUID name = maybe (throwError $ "Missing record iteem '"++name++"'") return =<< loadMbUUID name--loadMbUUID :: StorableUUID a => String -> LoadRec (Maybe a)-loadMbUUID name = listToMaybe . mapMaybe p <$> loadRecItems- where- bname = BC.pack name- p ( name', RecUUID x ) | name' == bname- = Just (fromUUID x)- p _ = Nothing--loadRawRef :: String -> LoadRec Ref-loadRawRef name = maybe (throwError $ "Missing record item '"++name++"'") return =<< loadMbRawRef name--loadMbRawRef :: String -> LoadRec (Maybe Ref)-loadMbRawRef name = listToMaybe <$> loadRawRefs name--loadRawRefs :: String -> LoadRec [Ref]-loadRawRefs name = mapMaybe p <$> loadRecItems- where- bname = BC.pack name- p ( name', RecRef x ) | name' == bname = Just x- p _ = Nothing--loadRef :: Storable a => String -> LoadRec a-loadRef name = load <$> loadRawRef name--loadMbRef :: Storable a => String -> LoadRec (Maybe a)-loadMbRef name = fmap load <$> loadMbRawRef name--loadRefs :: Storable a => String -> LoadRec [a]-loadRefs name = map load <$> loadRawRefs name--loadZRef :: ZeroStorable a => String -> LoadRec a-loadZRef name = loadMbRef name >>= \case- Nothing -> do Ref st _ <- loadRecCurrentRef- return $ fromZero st- Just x -> return x---type Stored a = Stored' Complete a--instance Storable a => Storable (Stored a) where- store st = copyRef st . storedRef- store' (Stored _ x) = store' x- load' = Stored <$> loadCurrentRef <*> load'--instance ZeroStorable a => ZeroStorable (Stored a) where- fromZero st = Stored (zeroRef st) $ fromZero st--fromStored :: Stored a -> a-fromStored (Stored _ x) = x--storedRef :: Stored a -> Ref-storedRef (Stored ref _) = ref--wrappedStore :: MonadIO m => Storable a => Storage -> a -> m (Stored a)-wrappedStore st x = do ref <- liftIO $ store st x- return $ Stored ref x--wrappedLoad :: Storable a => Ref -> Stored a-wrappedLoad ref = Stored ref (load ref)--copyStored :: forall c c' m a. (StorageCompleteness c, StorageCompleteness c', MonadIO m) =>- Storage' c' -> Stored' c a -> m (LoadResult c (Stored' c' a))-copyStored st (Stored ref' x) = liftIO $ returnLoadResult . fmap (flip Stored x) <$> copyRef' st ref'---- |Passed function needs to preserve the object representation to be safe-unsafeMapStored :: (a -> b) -> Stored a -> Stored b-unsafeMapStored f (Stored ref x) = Stored ref (f x)---data StoreInfo = StoreInfo- { infoDate :: ZonedTime- , infoNote :: Maybe Text- }- deriving (Show)--makeStoreInfo :: IO StoreInfo-makeStoreInfo = StoreInfo- <$> getZonedTime- <*> pure Nothing--storeInfoRec :: StoreInfo -> StoreRec c-storeInfoRec info = do- storeDate "date" $ infoDate info- storeMbText "note" $ infoNote info--loadInfoRec :: LoadRec StoreInfo-loadInfoRec = StoreInfo- <$> loadDate "date"- <*> loadMbText "note"---data History a = History StoreInfo (Stored a) (Maybe (StoredHistory a))- deriving (Show)--type StoredHistory a = Stored (History a)--instance Storable a => Storable (History a) where- store' (History si x prev) = storeRec $ do- storeInfoRec si- storeMbRef "prev" prev- storeRef "item" x-- load' = loadRec $ History- <$> loadInfoRec- <*> loadRef "item"- <*> loadMbRef "prev"--fromHistory :: StoredHistory a -> a-fromHistory = fromStored . storedFromHistory--fromHistoryAt :: ZonedTime -> StoredHistory a -> Maybe a-fromHistoryAt zat = fmap (fromStored . snd) . listToMaybe . dropWhile ((at<) . zonedTimeToUTC . fst) . storedHistoryTimedList- where at = zonedTimeToUTC zat--storedFromHistory :: StoredHistory a -> Stored a-storedFromHistory sh = let History _ item _ = fromStored sh- in item--storedHistoryList :: StoredHistory a -> [Stored a]-storedHistoryList = map snd . storedHistoryTimedList--storedHistoryTimedList :: StoredHistory a -> [(ZonedTime, Stored a)]-storedHistoryTimedList sh = let History hinfo item prev = fromStored sh- in (infoDate hinfo, item) : maybe [] storedHistoryTimedList prev--beginHistory :: Storable a => Storage -> StoreInfo -> a -> IO (StoredHistory a)-beginHistory st si x = do sx <- wrappedStore st x- wrappedStore st $ History si sx Nothing--modifyHistory :: Storable a => StoreInfo -> (a -> a) -> StoredHistory a -> IO (StoredHistory a)-modifyHistory si f prev@(Stored (Ref st _) _) = do- sx <- wrappedStore st $ f $ fromHistory prev- wrappedStore st $ History si sx (Just prev)---showRatio :: Rational -> String-showRatio r = case decimalRatio r of- Just (n, 1) -> show n- Just (n', d) -> let n = abs n'- in (if n' < 0 then "-" else "") ++ show (n `div` d) ++ "." ++- (concatMap (show.(`mod` 10).snd) $ reverse $ takeWhile ((>1).fst) $ zip (iterate (`div` 10) d) (iterate (`div` 10) (n `mod` d)))- Nothing -> show (numerator r) ++ "/" ++ show (denominator r)--decimalRatio :: Rational -> Maybe (Integer, Integer)-decimalRatio r = do- let n = numerator r- d = denominator r- (c2, d') = takeFactors 2 d- (c5, d'') = takeFactors 5 d'- guard $ d'' == 1- let m = if c2 > c5 then 5 ^ (c2 - c5)- else 2 ^ (c5 - c2)- return (n * m, d * m)--takeFactors :: Integer -> Integer -> (Integer, Integer)-takeFactors f n | n `mod` f == 0 = let (c, n') = takeFactors f (n `div` f)- in (c+1, n')- | otherwise = (0, n)--parseRatio :: ByteString -> Maybe Rational-parseRatio bs = case BC.groupBy ((==) `on` isNumber) bs of- (m:xs) | m == BC.pack "-" -> negate <$> positive xs- xs -> positive xs- where positive = \case- [bx] -> fromInteger . fst <$> BC.readInteger bx- [bx, op, by] -> do- (x, _) <- BC.readInteger bx- (y, _) <- BC.readInteger by- case BC.unpack op of- "." -> return $ (x % 1) + (y % (10 ^ BC.length by))- "/" -> return $ x % y- _ -> Nothing- _ -> Nothing+{-|+Description: Working with storage and heads++Provides functions for opening 'Storage' backed either by disk or memory. For+conveniance also function for working with 'Head's are reexported here.+-}++module Erebos.Storage (+ Storage, PartialStorage,+ openStorage, memoryStorage,+ deriveEphemeralStorage, derivePartialStorage,++ Head, HeadType,+ HeadID, HeadTypeID,+ headId, headStorage, headRef, headObject, headStoredObject,+ loadHeads, loadHead, reloadHead,+ storeHead, replaceHead, updateHead, updateHead_,++ WatchedHead,+ watchHead, watchHeadWith, unwatchHead,+ watchHeadRaw,++ MonadStorage(..),+) where++import Erebos.Object.Internal+import Erebos.Storage.Disk+import Erebos.Storage.Head+import Erebos.Storage.Memory
+ src/Erebos/Storage/Backend.hs view
@@ -0,0 +1,28 @@+{-|+Description: Implement custom storage backend++Exports type class, which can be used to create custom 'Storage' backend.+-}++module Erebos.Storage.Backend (+ StorageBackend(..),+ Complete, Partial,+ Storage, PartialStorage,+ newStorage,++ WatchID, startWatchID, nextWatchID,+) where++import Control.Concurrent.MVar++import Data.HashTable.IO qualified as HT++import Erebos.Object.Internal+import Erebos.Storage.Internal+++newStorage :: StorageBackend bck => bck -> IO (Storage' (BackendCompleteness bck))+newStorage stBackend = do+ stRefGeneration <- newMVar =<< HT.new+ stRefRoots <- newMVar =<< HT.new+ return Storage {..}
+ src/Erebos/Storage/Disk.hs view
@@ -0,0 +1,230 @@+module Erebos.Storage.Disk (+ openStorage,+) where++import Codec.Compression.Zlib++import Control.Arrow+import Control.Concurrent+import Control.Exception+import Control.Monad++import Data.ByteArray qualified as BA+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.ByteString.Char8 qualified as BC+import Data.ByteString.Lazy qualified as BL+import Data.ByteString.Lazy.Char8 qualified as BLC+import Data.Function+import Data.List+import Data.Maybe++import System.Directory+import System.FSNotify+import System.FilePath+import System.IO+import System.IO.Error++import Erebos.Object+import Erebos.Storage.Backend+import Erebos.Storage.Head+import Erebos.Storage.Internal+import Erebos.Storage.Platform+import Erebos.UUID qualified as U+++data DiskStorage = StorageDir+ { dirPath :: FilePath+ , dirWatchers :: MVar ( Maybe WatchManager, [ HeadTypeID ], WatchList )+ }++instance Eq DiskStorage where+ (==) = (==) `on` dirPath++instance Show DiskStorage where+ show StorageDir { dirPath = path } = "dir:" ++ path++instance StorageBackend DiskStorage where+ backendLoadBytes StorageDir {..} dgst =+ handleJust (guard . isDoesNotExistError) (const $ return Nothing) $+ Just . decompress . BL.fromChunks . (:[]) <$> (B.readFile $ refPath dirPath dgst)+ backendStoreBytes StorageDir {..} dgst = writeFileOnce (refPath dirPath dgst) . compress+++ backendLoadHeads StorageDir {..} tid = do+ let hpath = headTypePath dirPath tid++ files <- filterM (doesFileExist . (hpath </>)) =<<+ handleJust (\e -> guard (isDoesNotExistError e)) (const $ return [])+ (getDirectoryContents hpath)+ fmap catMaybes $ forM files $ \hname -> do+ case U.fromString hname of+ Just hid -> do+ content <- B.readFile (hpath </> hname)+ return $ do+ (h : _) <- Just (BC.lines content)+ dgst <- readRefDigest h+ Just $ ( HeadID hid, dgst )+ Nothing -> return Nothing++ backendLoadHead StorageDir {..} tid hid = do+ handleJust (guard . isDoesNotExistError) (const $ return Nothing) $ do+ (h:_) <- BC.lines <$> B.readFile (headPath dirPath tid hid)+ return $ readRefDigest h++ backendStoreHead StorageDir {..} tid hid dgst = do+ Right () <- writeFileChecked (headPath dirPath tid hid) Nothing $+ showRefDigest dgst `B.append` BC.singleton '\n'+ return ()++ backendReplaceHead StorageDir {..} tid hid expected new = do+ let filename = headPath dirPath tid hid+ showDgstL r = showRefDigest r `B.append` BC.singleton '\n'++ writeFileChecked filename (Just $ showDgstL expected) (showDgstL new) >>= \case+ Left Nothing -> return $ Left Nothing+ Left (Just bs) -> do Just cur <- return $ readRefDigest $ BC.takeWhile (/='\n') bs+ return $ Left $ Just cur+ Right () -> return $ Right new++ backendWatchHead st@StorageDir {..} tid hid cb = do+ modifyMVar dirWatchers $ \( mbmanager, ilist, wl ) -> do+ manager <- maybe startManager return mbmanager+ ilist' <- case tid `elem` ilist of+ True -> return ilist+ False -> do+ void $ watchDir manager (headTypePath dirPath tid) (const True) $ \case+ ev@Added {} | Just ihid <- HeadID <$> U.fromString (takeFileName (eventPath ev)) -> do+ backendLoadHead st tid ihid >>= \case+ Just dgst -> do+ (_, _, iwl) <- readMVar dirWatchers+ mapM_ ($ dgst) . map wlFun . filter ((== (tid, ihid)) . wlHead) . wlList $ iwl+ Nothing -> return ()+ _ -> return ()+ return $ tid : ilist+ return $ first ( Just manager, ilist', ) $ watchListAdd tid hid cb wl++ backendUnwatchHead StorageDir {..} wid = do+ modifyMVar_ dirWatchers $ \( mbmanager, ilist, wl ) -> do+ return ( mbmanager, ilist, watchListDel wid wl )+++ backendListKeys StorageDir {..} = do+ catMaybes . map (readRefDigest . BC.pack) <$>+ listDirectory (keyDirPath dirPath)++ backendLoadKey StorageDir {..} dgst = do+ tryIOError (BC.readFile (keyFilePath dirPath dgst)) >>= \case+ Right kdata -> return $ Just $ BA.convert kdata+ Left _ -> return Nothing++ backendStoreKey StorageDir {..} dgst key = do+ writeFileOnce (keyFilePath dirPath dgst) (BL.fromStrict $ BA.convert key)++ backendRemoveKey StorageDir {..} dgst = do+ void $ tryIOError (removeFile $ keyFilePath dirPath dgst)+++storageVersion :: String+storageVersion = "0.1"++openStorage :: FilePath -> IO Storage+openStorage path = modifyIOError annotate $ do+ let versionFileName = "erebos-storage"+ let versionPath = path </> versionFileName+ let writeVersionFile = writeFileOnce versionPath $ BLC.pack $ storageVersion <> "\n"++ maybeVersion <- handleJust (guard . isDoesNotExistError) (const $ return Nothing) $+ Just <$> readFile versionPath+ version <- case maybeVersion of+ Just versionContent -> do+ return $ takeWhile (/= '\n') versionContent++ Nothing -> do+ files <- handleJust (guard . isDoesNotExistError) (const $ return []) $+ listDirectory path+ when (not $ or+ [ null files+ , versionFileName `elem` files+ , (versionFileName ++ ".lock") `elem` files+ , "objects" `elem` files && "heads" `elem` files+ ]) $ do+ fail "directory is neither empty, nor an existing erebos storage"++ createDirectoryIfMissing True $ path+ writeVersionFile+ takeWhile (/= '\n') <$> readFile versionPath++ when (version /= storageVersion) $ do+ fail $ "unsupported storage version " <> version++ createDirectoryIfMissing True $ path </> "objects"+ createDirectoryIfMissing True $ path </> "heads"+ watchers <- newMVar ( Nothing, [], WatchList startWatchID [] )+ newStorage $ StorageDir path watchers+ where+ annotate e = annotateIOError e "failed to open storage" Nothing (Just path)+++refPath :: FilePath -> RefDigest -> FilePath+refPath spath rdgst = intercalate "/" [ spath, "objects", BC.unpack alg, pref, rest ]+ where (alg, dgst) = showRefDigestParts rdgst+ (pref, rest) = splitAt 2 $ BC.unpack dgst++headTypePath :: FilePath -> HeadTypeID -> FilePath+headTypePath spath (HeadTypeID tid) = spath </> "heads" </> U.toString tid++headPath :: FilePath -> HeadTypeID -> HeadID -> FilePath+headPath spath tid (HeadID hid) = headTypePath spath tid </> U.toString hid++keyDirPath :: FilePath -> FilePath+keyDirPath sdir = sdir </> "keys"++keyFilePath :: FilePath -> RefDigest -> FilePath+keyFilePath sdir dgst = keyDirPath sdir </> (BC.unpack $ showRefDigest dgst)+++openLockFile :: FilePath -> IO Handle+openLockFile path = do+ createDirectoryIfMissing True (takeDirectory path)+ retry 10 $ createFileExclusive path+ where+ retry :: Int -> IO a -> IO a+ retry 0 act = act+ retry n act = catchJust (\e -> if isAlreadyExistsError e then Just () else Nothing)+ act (\_ -> threadDelay (100 * 1000) >> retry (n - 1) act)++writeFileOnce :: FilePath -> BL.ByteString -> IO ()+writeFileOnce file content = bracket (openLockFile locked)+ hClose $ \h -> do+ doesFileExist file >>= \case+ True -> removeFile locked+ False -> do BL.hPut h content+ hClose h+ renameFile locked file+ where locked = file ++ ".lock"++writeFileChecked :: FilePath -> Maybe ByteString -> ByteString -> IO (Either (Maybe ByteString) ())+writeFileChecked file prev content = bracket (openLockFile locked)+ hClose $ \h -> do+ (prev,) <$> doesFileExist file >>= \case+ (Nothing, True) -> do+ current <- B.readFile file+ removeFile locked+ return $ Left $ Just current+ (Nothing, False) -> do B.hPut h content+ hClose h+ renameFile locked file+ return $ Right ()+ (Just expected, True) -> do+ current <- B.readFile file+ if current == expected then do B.hPut h content+ hClose h+ renameFile locked file+ return $ return ()+ else do removeFile locked+ return $ Left $ Just current+ (Just _, False) -> do+ removeFile locked+ return $ Left Nothing+ where locked = file ++ ".lock"
+ src/Erebos/Storage/Head.hs view
@@ -0,0 +1,258 @@+{-|+Description: Define, use and watch heads++Provides data types and functions for reading, writing or watching `Head's.+Type class `HeadType' is used to define custom new `Head' types.+-}++module Erebos.Storage.Head (+ -- * Head type and accessors+ Head, HeadType(..),+ HeadID, HeadTypeID, mkHeadTypeID,+ headId, headStorage, headRef, headObject, headStoredObject,++ -- * Loading and storing heads+ loadHeads, loadHead, reloadHead,+ storeHead, replaceHead, updateHead, updateHead_,+ loadHeadRaw, storeHeadRaw, replaceHeadRaw,++ -- * Watching heads+ WatchedHead,+ watchHead, watchHeadWith, unwatchHead,+ watchHeadRaw,+) where++import Control.Concurrent+import Control.Monad+import Control.Monad.Reader++import Data.Bifunctor+import Data.Typeable++import Erebos.Object+import Erebos.Storable+import Erebos.Storage.Backend+import Erebos.Storage.Internal+import Erebos.UUID qualified as U+++-- | Represents loaded Erebos storage head, along with the object it pointed to+-- at the time it was loaded.+--+-- Each possible head type has associated unique ID, represented as+-- `HeadTypeID'. For each type, there can be multiple individual heads in given+-- storage, each also identified by unique ID (`HeadID').+data Head a = Head HeadID (Stored a)+ deriving (Eq, Show)++-- | Instances of this class can be used as objects pointed to by heads in+-- Erebos storage. Each such type must be `Storable' and have a unique ID.+--+-- To create a custom head type, generate a new UUID and assign it to the type using+-- `mkHeadTypeID':+--+-- > instance HeadType MyType where+-- > headTypeID _ = mkHeadTypeID "86e8033d-c476-4f81-9b7c-fd36b9144475"+class Storable a => HeadType a where+ headTypeID :: proxy a -> HeadTypeID+ -- ^ Get the ID of the given head type; must be unique for each `HeadType' instance.++instance MonadIO m => MonadStorage (ReaderT (Head a) m) where+ getStorage = asks $ headStorage+++-- | Get `HeadID' associated with given `Head'.+headId :: Head a -> HeadID+headId (Head uuid _) = uuid++-- | Get storage from which the `Head' was loaded.+headStorage :: Head a -> Storage+headStorage = refStorage . headRef++-- | Get `Ref' of the `Head'\'s associated object.+headRef :: Head a -> Ref+headRef (Head _ sx) = storedRef sx++-- | Get the object the `Head' pointed to when it was loaded.+headObject :: Head a -> a+headObject (Head _ sx) = fromStored sx++-- | Get the object the `Head' pointed to when it was loaded as a `Stored' value.+headStoredObject :: Head a -> Stored a+headStoredObject (Head _ sx) = sx++-- | Create `HeadTypeID' from string representation of UUID.+mkHeadTypeID :: String -> HeadTypeID+mkHeadTypeID = maybe (error "Invalid head type ID") HeadTypeID . U.fromString+++-- | Load all `Head's of type @a@ from storage.+loadHeads :: forall a m. MonadIO m => HeadType a => Storage -> m [Head a]+loadHeads st@Storage {..} =+ map (uncurry Head . fmap (wrappedLoad . Ref st))+ <$> liftIO (backendLoadHeads stBackend (headTypeID @a Proxy))++-- | Try to load a `Head' of type @a@ from storage.+loadHead+ :: forall a m. (HeadType a, MonadIO m)+ => Storage -- ^ Storage from which to load the head+ -> HeadID -- ^ ID of the particular head+ -> m (Maybe (Head a)) -- ^ Head object, or `Nothing' if not found+loadHead st hid = fmap (Head hid . wrappedLoad) <$> loadHeadRaw st (headTypeID @a Proxy) hid++-- | Try to load `Head' using a raw head and type IDs, getting `Ref' if found.+loadHeadRaw+ :: forall m. MonadIO m+ => Storage -- ^ Storage from which to load the head+ -> HeadTypeID -- ^ ID of the head type+ -> HeadID -- ^ ID of the particular head+ -> m (Maybe Ref) -- ^ `Ref' pointing to the head object, or `Nothing' if not found+loadHeadRaw st@Storage {..} tid hid = do+ fmap (Ref st) <$> liftIO (backendLoadHead stBackend tid hid)++-- | Reload the given head from storage, returning `Head' with updated object,+-- or `Nothing' if there is no longer head with the particular ID in storage.+reloadHead :: (HeadType a, MonadIO m) => Head a -> m (Maybe (Head a))+reloadHead (Head hid val) = loadHead (storedStorage val) hid++-- | Store a new `Head' of type 'a' in the storage.+storeHead :: forall a m. MonadIO m => HeadType a => Storage -> a -> m (Head a)+storeHead st obj = do+ let tid = headTypeID @a Proxy+ stored <- wrappedStore st obj+ hid <- storeHeadRaw st tid (storedRef stored)+ return $ Head hid stored++-- | Store a new `Head' in the storage, using the raw `HeadTypeID' and `Ref',+-- the function returns the assigned `HeadID' of the new head.+storeHeadRaw :: forall m. MonadIO m => Storage -> HeadTypeID -> Ref -> m HeadID+storeHeadRaw Storage {..} tid ref = liftIO $ do+ hid <- HeadID <$> U.nextRandom+ backendStoreHead stBackend tid hid (refDigest ref)+ return hid++-- | Try to replace existing `Head' of type @a@ in the storage. Function fails+-- if the head value in storage changed after being loaded here; for automatic+-- retry see `updateHead'.+replaceHead+ :: forall a m. (HeadType a, MonadIO m)+ => Head a -- ^ Existing head, associated object is supposed to match the one in storage+ -> Stored a -- ^ Intended new value+ -> m (Either (Maybe (Head a)) (Head a))+ -- ^+ -- [@`Left' `Nothing'@]:+ -- Nothing was stored – the head no longer exists in storage.+ -- [@`Left' (`Just' h)@]:+ -- Nothing was stored – the head value in storage does not match+ -- the first parameter, but is @h@ instead.+ -- [@`Right' h@]:+ -- Head value was updated in storage, the new head is @h@ (which is+ -- the same as first parameter with associated object replaced by+ -- the second parameter).+replaceHead prev@(Head hid pobj) stored' = liftIO $ do+ let st = headStorage prev+ tid = headTypeID @a Proxy+ stored <- copyStored st stored'+ bimap (fmap $ Head hid . wrappedLoad) (const $ Head hid stored) <$>+ replaceHeadRaw st tid hid (storedRef pobj) (storedRef stored)++-- | Try to replace existing head using raw IDs and `Ref's.+replaceHeadRaw+ :: forall m. MonadIO m+ => Storage -- ^ Storage to use+ -> HeadTypeID -- ^ ID of the head type+ -> HeadID -- ^ ID of the particular head+ -> Ref -- ^ Expected value in storage+ -> Ref -- ^ Intended new value+ -> m (Either (Maybe Ref) Ref)+ -- ^+ -- [@`Left' `Nothing'@]:+ -- Nothing was stored – the head no longer exists in storage.+ -- [@`Left' (`Just' r)@]:+ -- Nothing was stored – the head value in storage does not match+ -- the expected value, but is @r@ instead.+ -- [@`Right' r@]:+ -- Head value was updated in storage, the new head value is @r@+ -- (which is the same as the indended value).+replaceHeadRaw st@Storage {..} tid hid prev new = liftIO $ do+ _ <- copyRef st new+ bimap (fmap $ Ref st) (Ref st) <$> backendReplaceHead stBackend tid hid (refDigest prev) (refDigest new)++-- | Update existing existing `Head' of type @a@ in the storage, using a given+-- function. The update function may be called multiple times in case the head+-- content changes concurrently during evaluation.+updateHead+ :: (HeadType a, MonadIO m)+ => Head a -- ^ Existing head to be updated+ -> (Stored a -> m ( Stored a, b ))+ -- ^ Function that gets current value of the head and returns updated+ -- value, along with a custom extra value to be returned from+ -- `updateHead' call. The function may be called multiple times.+ -> m ( Maybe (Head a), b )+ -- ^ First element contains either the new head as @`Just' h@, or+ -- `Nothing' in case the head no longer exists in storage. Second+ -- element is the value from last call to the update function.+updateHead h f = do+ (o, x) <- f $ headStoredObject h+ replaceHead h o >>= \case+ Right h' -> return (Just h', x)+ Left Nothing -> return (Nothing, x)+ Left (Just h') -> updateHead h' f++-- | Update existing existing `Head' of type @a@ in the storage, using a given+-- function. The update function may be called multiple times in case the head+-- content changes concurrently during evaluation.+updateHead_+ :: (HeadType a, MonadIO m)+ => Head a -- ^ Existing head to be updated+ -> (Stored a -> m (Stored a))+ -- ^ Function that gets current value of the head and returns updated+ -- value; may be called multiple times.+ -> m (Maybe (Head a))+ -- ^ The new head as @`Just' h@, or `Nothing' in case the head no+ -- longer exists in storage.+updateHead_ h = fmap fst . updateHead h . (fmap (,()) .)+++-- | Represents a handle of a watched head, which can be used to cancel the+-- watching.+data WatchedHead = forall a. WatchedHead Storage WatchID (MVar a)++-- | Watch the given head. The callback will be called with the current head+-- value, and then again each time the head changes.+watchHead :: forall a. HeadType a => Head a -> (Head a -> IO ()) -> IO WatchedHead+watchHead h = watchHeadWith h id++-- | Watch the given head using custom selector function. The callback will be+-- called with the value derived from current head state, and then again each+-- time the selected value changes according to its `Eq' instance.+watchHeadWith+ :: forall a b. (HeadType a, Eq b)+ => Head a -- ^ Head to watch+ -> (Head a -> b) -- ^ Selector function+ -> (b -> IO ()) -- ^ Callback+ -> IO WatchedHead -- ^ Watched head handle+watchHeadWith (Head hid val) sel cb = do+ watchHeadRaw (storedStorage val) (headTypeID @a Proxy) hid (sel . Head hid . wrappedLoad) cb++-- | Watch the given head using raw IDs and a selector from `Ref'.+watchHeadRaw :: forall b. Eq b => Storage -> HeadTypeID -> HeadID -> (Ref -> b) -> (b -> IO ()) -> IO WatchedHead+watchHeadRaw st@Storage {..} tid hid sel cb = do+ memo <- newEmptyMVar+ let cb' dgst = do+ let x = sel (Ref st dgst)+ modifyMVar_ memo $ \prev -> do+ when (Just x /= prev) $ cb x+ return $ Just x+ wid <- backendWatchHead stBackend tid hid cb'++ cur <- fmap sel <$> loadHeadRaw st tid hid+ maybe (return ()) cb cur+ putMVar memo cur++ return $ WatchedHead st wid memo++-- | Stop watching previously watched head.+unwatchHead :: WatchedHead -> IO ()+unwatchHead (WatchedHead Storage {..} wid _) = do+ backendUnwatchHead stBackend wid
src/Erebos/Storage/Internal.hs view
@@ -1,89 +1,179 @@-module Erebos.Storage.Internal where+module Erebos.Storage.Internal (+ Storage'(..), Storage, PartialStorage,+ Ref'(..), Ref, PartialRef,+ RefDigest(..),+ WatchID, startWatchID, nextWatchID,+ WatchList(..), WatchListItem(..), watchListAdd, watchListDel, -import Codec.Compression.Zlib+ refStorage,+ refDigest, refDigestFromByteString,+ showRef, showRefDigest, showRefDigestParts,+ readRefDigest,+ hashToRefDigest, + StorageCompleteness(..),+ StorageBackend(..),+ Complete, Partial,++ unsafeStoreRawBytes,+ ioLoadBytesFromStorage,++ Generation(..),+ HeadID(..), HeadTypeID(..),+ Stored(..), storedStorage,+) where+ import Control.Arrow import Control.Concurrent import Control.DeepSeq import Control.Exception-import Control.Monad import Control.Monad.Identity import Crypto.Hash import Data.Bits-import Data.ByteArray (ByteArray, ByteArrayAccess, ScrubbedBytes)-import qualified Data.ByteArray as BA+import Data.ByteArray (ByteArrayAccess, ScrubbedBytes)+import Data.ByteArray qualified as BA import Data.ByteString (ByteString)-import qualified Data.ByteString as B-import qualified Data.ByteString.Char8 as BC-import qualified Data.ByteString.Lazy as BL-import Data.Char+import Data.ByteString.Char8 qualified as BC+import Data.ByteString.Lazy qualified as BL import Data.Function+import Data.HashTable.IO qualified as HT import Data.Hashable-import qualified Data.HashTable.IO as HT import Data.Kind-import Data.List-import Data.Map (Map)-import qualified Data.Map as M-import Data.UUID (UUID)+import Data.Typeable import Foreign.Storable (peek) -import System.Directory-import System.FSNotify (WatchManager)-import System.FilePath-import System.IO-import System.IO.Error import System.IO.Unsafe (unsafePerformIO) -import Erebos.Storage.Platform+import Erebos.UUID (UUID)+import Erebos.Util -data Storage' c = Storage- { stBacking :: StorageBacking c- , stParent :: Maybe (Storage' Identity)+data Storage' c = forall bck. (StorageBackend bck, BackendCompleteness bck ~ c) => Storage+ { stBackend :: bck , stRefGeneration :: MVar (HT.BasicHashTable RefDigest Generation) , stRefRoots :: MVar (HT.BasicHashTable RefDigest [RefDigest]) } +type Storage = Storage' Complete+type PartialStorage = Storage' Partial+ instance Eq (Storage' c) where- (==) = (==) `on` (stBacking &&& stParent)+ Storage { stBackend = b } == Storage { stBackend = b' }+ | Just b'' <- cast b' = b == b''+ | otherwise = False instance Show (Storage' c) where- show st@(Storage { stBacking = StorageDir { dirPath = path }}) = "dir" ++ showParentStorage st ++ ":" ++ path- show st@(Storage { stBacking = StorageMemory {} }) = "mem" ++ showParentStorage st+ show Storage { stBackend = b } = show b ++ showParentStorage b -showParentStorage :: Storage' c -> String-showParentStorage Storage { stParent = Nothing } = ""-showParentStorage Storage { stParent = Just st } = "@" ++ show st+showParentStorage :: StorageBackend bck => bck -> String+showParentStorage bck+ | Just (st :: Storage) <- cast (backendParent bck) = "@" ++ show st+ | Just (st :: PartialStorage) <- cast (backendParent bck) = "@" ++ show st+ | otherwise = "" -data StorageBacking c- = StorageDir { dirPath :: FilePath- , dirWatchers :: MVar ( Maybe WatchManager, [ HeadTypeID ], WatchList c )- }- | StorageMemory { memHeads :: MVar [((HeadTypeID, HeadID), Ref' c)]- , memObjs :: MVar (Map RefDigest BL.ByteString)- , memKeys :: MVar (Map RefDigest ScrubbedBytes)- , memWatchers :: MVar (WatchList c)- }- deriving (Eq) +class (Eq bck, Show bck, Typeable bck, Typeable (BackendParent bck)) => StorageBackend bck where+ type BackendCompleteness bck :: Type -> Type+ type BackendCompleteness bck = Complete++ type BackendParent bck :: Type+ type BackendParent bck = ()+ backendParent :: bck -> BackendParent bck+ default backendParent :: BackendParent bck ~ () => bck -> BackendParent bck+ backendParent _ = ()+++ backendLoadBytes :: bck -> RefDigest -> IO (Maybe BL.ByteString)+ default backendLoadBytes :: BackendParent bck ~ Storage => bck -> RefDigest -> IO (Maybe BL.ByteString)+ backendLoadBytes bck = case backendParent bck of Storage { stBackend = bck' } -> backendLoadBytes bck'++ backendStoreBytes :: bck -> RefDigest -> BL.ByteString -> IO ()+ default backendStoreBytes :: BackendParent bck ~ Storage => bck -> RefDigest -> BL.ByteString -> IO ()+ backendStoreBytes bck = case backendParent bck of Storage { stBackend = bck' } -> backendStoreBytes bck'+++ backendLoadHeads :: bck -> HeadTypeID -> IO [ ( HeadID, RefDigest ) ]+ default backendLoadHeads :: BackendParent bck ~ Storage => bck -> HeadTypeID -> IO [ ( HeadID, RefDigest ) ]+ backendLoadHeads bck = case backendParent bck of Storage { stBackend = bck' } -> backendLoadHeads bck'++ backendLoadHead :: bck -> HeadTypeID -> HeadID -> IO (Maybe RefDigest)+ default backendLoadHead :: BackendParent bck ~ Storage => bck -> HeadTypeID -> HeadID -> IO (Maybe RefDigest)+ backendLoadHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendLoadHead bck'++ backendStoreHead :: bck -> HeadTypeID -> HeadID -> RefDigest -> IO ()+ default backendStoreHead :: BackendParent bck ~ Storage => bck -> HeadTypeID -> HeadID -> RefDigest -> IO ()+ backendStoreHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendStoreHead bck'++ backendReplaceHead :: bck -> HeadTypeID -> HeadID -> RefDigest -> RefDigest -> IO (Either (Maybe RefDigest) RefDigest)+ default backendReplaceHead :: BackendParent bck ~ Storage => bck -> HeadTypeID -> HeadID -> RefDigest -> RefDigest -> IO (Either (Maybe RefDigest) RefDigest)+ backendReplaceHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendReplaceHead bck'++ backendWatchHead :: bck -> HeadTypeID -> HeadID -> (RefDigest -> IO ()) -> IO WatchID+ default backendWatchHead :: BackendParent bck ~ Storage => bck -> HeadTypeID -> HeadID -> (RefDigest -> IO ()) -> IO WatchID+ backendWatchHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendWatchHead bck'++ backendUnwatchHead :: bck -> WatchID -> IO ()+ default backendUnwatchHead :: BackendParent bck ~ Storage => bck -> WatchID -> IO ()+ backendUnwatchHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendUnwatchHead bck'+++ backendListKeys :: bck -> IO [ RefDigest ]+ default backendListKeys :: BackendParent bck ~ Storage => bck -> IO [ RefDigest ]+ backendListKeys bck = case backendParent bck of Storage { stBackend = bck' } -> backendListKeys bck'++ backendLoadKey :: bck -> RefDigest -> IO (Maybe ScrubbedBytes)+ default backendLoadKey :: BackendParent bck ~ Storage => bck -> RefDigest -> IO (Maybe ScrubbedBytes)+ backendLoadKey bck = case backendParent bck of Storage { stBackend = bck' } -> backendLoadKey bck'++ backendStoreKey :: bck -> RefDigest -> ScrubbedBytes -> IO ()+ default backendStoreKey :: BackendParent bck ~ Storage => bck -> RefDigest -> ScrubbedBytes -> IO ()+ backendStoreKey bck = case backendParent bck of Storage { stBackend = bck' } -> backendStoreKey bck'++ backendRemoveKey :: bck -> RefDigest -> IO ()+ default backendRemoveKey :: BackendParent bck ~ Storage => bck -> RefDigest -> IO ()+ backendRemoveKey bck = case backendParent bck of Storage { stBackend = bck' } -> backendRemoveKey bck'+++ newtype WatchID = WatchID Int- deriving (Eq, Ord, Num)+ deriving (Eq, Ord) -data WatchList c = WatchList+startWatchID :: WatchID+startWatchID = WatchID 1++nextWatchID :: WatchID -> WatchID+nextWatchID (WatchID n) = WatchID (n + 1)++data WatchList = WatchList { wlNext :: WatchID- , wlList :: [WatchListItem c]+ , wlList :: [ WatchListItem ] } -data WatchListItem c = WatchListItem+data WatchListItem = WatchListItem { wlID :: WatchID- , wlHead :: (HeadTypeID, HeadID)- , wlFun :: Ref' c -> IO ()+ , wlHead :: ( HeadTypeID, HeadID )+ , wlFun :: RefDigest -> IO () } +watchListAdd :: HeadTypeID -> HeadID -> (RefDigest -> IO ()) -> WatchList -> ( WatchList, WatchID )+watchListAdd tid hid cb wl = ( wl', wlNext wl )+ where+ wl' = wl+ { wlNext = nextWatchID (wlNext wl)+ , wlList = WatchListItem+ { wlID = wlNext wl+ , wlHead = (tid, hid)+ , wlFun = cb+ } : wlList wl+ } +watchListDel :: WatchID -> WatchList -> WatchList+watchListDel wid wl = wl { wlList = filter ((/= wid) . wlID) $ wlList wl }++ newtype RefDigest = RefDigest (Digest Blake2b_256) deriving (Eq, Ord, NFData, ByteArrayAccess) @@ -92,6 +182,9 @@ data Ref' c = Ref (Storage' c) RefDigest +type Ref = Ref' Complete+type PartialRef = Ref' Partial+ instance Eq (Ref' c) where Ref _ d1 == Ref _ d2 = d1 == d2 @@ -126,65 +219,47 @@ readRefDigest :: ByteString -> Maybe RefDigest readRefDigest x = case BC.split '#' x of [alg, dgst] | BA.convert alg == BC.pack "blake2" ->- refDigestFromByteString =<< readHex @ByteString dgst+ refDigestFromByteString =<< readHex dgst _ -> Nothing -refDigestFromByteString :: ByteArrayAccess ba => ba -> Maybe RefDigest+refDigestFromByteString :: ByteString -> Maybe RefDigest refDigestFromByteString = fmap RefDigest . digestFromByteString hashToRefDigest :: BL.ByteString -> RefDigest hashToRefDigest = RefDigest . hashFinalize . hashUpdates hashInit . BL.toChunks -showHex :: ByteArrayAccess ba => ba -> ByteString-showHex = B.concat . map showHexByte . BA.unpack- where showHexChar x | x < 10 = x + o '0'- | otherwise = x + o 'a' - 10- showHexByte x = B.pack [ showHexChar (x `div` 16), showHexChar (x `mod` 16) ]- o = fromIntegral . ord -readHex :: ByteArray ba => ByteString -> Maybe ba-readHex = return . BA.concat <=< readHex'- where readHex' bs | B.null bs = Just []- readHex' bs = do (bx, bs') <- B.uncons bs- (by, bs'') <- B.uncons bs'- x <- hexDigit bx- y <- hexDigit by- (B.singleton (x * 16 + y) :) <$> readHex' bs''- hexDigit x | x >= o '0' && x <= o '9' = Just $ x - o '0'- | x >= o 'a' && x <= o 'z' = Just $ x - o 'a' + 10- | otherwise = Nothing- o = fromIntegral . ord-- newtype Generation = Generation Int deriving (Eq, Show) -data Head' c a = Head HeadID (Stored' c a)- deriving (Eq, Show)-+-- | UUID of individual Erebos storage head. newtype HeadID = HeadID UUID deriving (Eq, Ord, Show) +-- | UUID of Erebos storage head type. newtype HeadTypeID = HeadTypeID UUID deriving (Eq, Ord) -data Stored' c a = Stored (Ref' c) a+data Stored a = Stored+ { storedRef' :: Ref+ , storedObject' :: a+ } deriving (Show) -instance Eq (Stored' c a) where- Stored r1 _ == Stored r2 _ = refDigest r1 == refDigest r2+instance Eq (Stored a) where+ (==) = (==) `on` (refDigest . storedRef') -instance Ord (Stored' c a) where- compare (Stored r1 _) (Stored r2 _) = compare (refDigest r1) (refDigest r2)+instance Ord (Stored a) where+ compare = compare `on` (refDigest . storedRef') -storedStorage :: Stored' c a -> Storage' c-storedStorage (Stored (Ref st _) _) = st+storedStorage :: Stored a -> Storage+storedStorage = refStorage . storedRef' type Complete = Identity type Partial = Either RefDigest -class (Traversable compl, Monad compl) => StorageCompleteness compl where+class (Traversable compl, Monad compl, Typeable compl) => StorageCompleteness compl where type LoadResult compl a :: Type returnLoadResult :: compl a -> LoadResult compl a ioLoadBytes :: Ref' compl -> IO (compl BL.ByteString)@@ -201,71 +276,16 @@ ioLoadBytes (Ref st dgst) = maybe (Left dgst) Right <$> ioLoadBytesFromStorage st dgst unsafeStoreRawBytes :: Storage' c -> BL.ByteString -> IO (Ref' c)-unsafeStoreRawBytes st raw = do- let dgst = hashToRefDigest raw- case stBacking st of- StorageDir { dirPath = sdir } -> writeFileOnce (refPath sdir dgst) $ compress raw- StorageMemory { memObjs = tobjs } ->- dgst `deepseq` -- the TVar may be accessed when evaluating the data to be written- modifyMVar_ tobjs (return . M.insert dgst raw)+unsafeStoreRawBytes st@Storage {..} raw = do+ dgst <- evaluate $ force $ hashToRefDigest raw+ backendStoreBytes stBackend dgst raw return $ Ref st dgst ioLoadBytesFromStorage :: Storage' c -> RefDigest -> IO (Maybe BL.ByteString)-ioLoadBytesFromStorage st dgst = loadCurrent st >>=- \case Just bytes -> return $ Just bytes- Nothing | Just parent <- stParent st -> ioLoadBytesFromStorage parent dgst- | otherwise -> return Nothing- where loadCurrent Storage { stBacking = StorageDir { dirPath = spath } } = handleJust (guard . isDoesNotExistError) (const $ return Nothing) $- Just . decompress . BL.fromChunks . (:[]) <$> (B.readFile $ refPath spath dgst)- loadCurrent Storage { stBacking = StorageMemory { memObjs = tobjs } } = M.lookup dgst <$> readMVar tobjs--refPath :: FilePath -> RefDigest -> FilePath-refPath spath rdgst = intercalate "/" [spath, "objects", BC.unpack alg, pref, rest]- where (alg, dgst) = showRefDigestParts rdgst- (pref, rest) = splitAt 2 $ BC.unpack dgst---openLockFile :: FilePath -> IO Handle-openLockFile path = do- createDirectoryIfMissing True (takeDirectory path)- retry 10 $ createFileExclusive path- where- retry :: Int -> IO a -> IO a- retry 0 act = act- retry n act = catchJust (\e -> if isAlreadyExistsError e then Just () else Nothing)- act (\_ -> threadDelay (100 * 1000) >> retry (n - 1) act)--writeFileOnce :: FilePath -> BL.ByteString -> IO ()-writeFileOnce file content = bracket (openLockFile locked)- hClose $ \h -> do- doesFileExist file >>= \case- True -> removeFile locked- False -> do BL.hPut h content- hClose h- renameFile locked file- where locked = file ++ ".lock"--writeFileChecked :: FilePath -> Maybe ByteString -> ByteString -> IO (Either (Maybe ByteString) ())-writeFileChecked file prev content = bracket (openLockFile locked)- hClose $ \h -> do- (prev,) <$> doesFileExist file >>= \case- (Nothing, True) -> do- current <- B.readFile file- removeFile locked- return $ Left $ Just current- (Nothing, False) -> do B.hPut h content- hClose h- renameFile locked file- return $ Right ()- (Just expected, True) -> do- current <- B.readFile file- if current == expected then do B.hPut h content- hClose h- renameFile locked file- return $ return ()- else do removeFile locked- return $ Left $ Just current- (Just _, False) -> do- removeFile locked- return $ Left Nothing- where locked = file ++ ".lock"+ioLoadBytesFromStorage Storage {..} dgst =+ backendLoadBytes stBackend dgst >>= \case+ Just bytes -> return $ Just bytes+ Nothing+ | Just (parent :: Storage) <- cast (backendParent stBackend) -> ioLoadBytesFromStorage parent dgst+ | Just (parent :: PartialStorage) <- cast (backendParent stBackend) -> ioLoadBytesFromStorage parent dgst+ | otherwise -> return Nothing
src/Erebos/Storage/Key.hs view
@@ -4,21 +4,14 @@ moveKeys, ) where -import Control.Concurrent.MVar import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Data.ByteArray-import qualified Data.ByteString.Char8 as BC-import qualified Data.ByteString.Lazy as BL-import qualified Data.Map as M--import System.Directory-import System.FilePath-import System.IO.Error+import Data.Typeable -import Erebos.Storage+import Erebos.Storable import Erebos.Storage.Internal class Storable pub => KeyPair sec pub | sec -> pub, pub -> sec where@@ -28,59 +21,32 @@ keyFromData :: ScrubbedBytes -> Stored pub -> Maybe sec -keyFilePath :: KeyPair sec pub => FilePath -> Stored pub -> FilePath-keyFilePath sdir pkey = sdir </> "keys" </> (BC.unpack $ showRef $ storedRef pkey)- storeKey :: KeyPair sec pub => sec -> IO () storeKey key = do let spub = keyGetPublic key- case stBacking $ storedStorage spub of- StorageDir { dirPath = dir } -> writeFileOnce (keyFilePath dir spub) (BL.fromStrict $ convert $ keyGetData key)- StorageMemory { memKeys = kstore } -> modifyMVar_ kstore $ return . M.insert (refDigest $ storedRef spub) (keyGetData key)+ case storedStorage spub of+ Storage {..} -> backendStoreKey stBackend (refDigest $ storedRef spub) (keyGetData key) -loadKey :: (KeyPair sec pub, MonadIO m, MonadError String m) => Stored pub -> m sec-loadKey pub = maybe (throwError $ "secret key not found for " <> show (storedRef pub)) return =<< loadKeyMb pub+loadKey :: (KeyPair sec pub, MonadIO m, MonadError e m, FromErebosError e) => Stored pub -> m sec+loadKey pub = maybe (throwOtherError $ "secret key not found for " <> show (storedRef pub)) return =<< loadKeyMb pub -loadKeyMb :: (KeyPair sec pub, MonadIO m) => Stored pub -> m (Maybe sec)+loadKeyMb :: forall sec pub m. (KeyPair sec pub, MonadIO m) => Stored pub -> m (Maybe sec) loadKeyMb spub = liftIO $ run $ storedStorage spub where- run st = tryOneLevel (stBacking st) >>= \case- key@Just {} -> return key- Nothing | Just parent <- stParent st -> run parent- | otherwise -> return Nothing- tryOneLevel = \case- StorageDir { dirPath = dir } -> tryIOError (BC.readFile (keyFilePath dir spub)) >>= \case- Right kdata -> return $ keyFromData (convert kdata) spub- Left _ -> return Nothing- StorageMemory { memKeys = kstore } -> (flip keyFromData spub <=< M.lookup (refDigest $ storedRef spub)) <$> readMVar kstore+ run :: Storage' c -> IO (Maybe sec)+ run Storage {..} = backendLoadKey stBackend (refDigest $ storedRef spub) >>= \case+ Just bytes -> return $ keyFromData bytes spub+ Nothing+ | Just (parent :: Storage) <- cast (backendParent stBackend) -> run parent+ | Just (parent :: PartialStorage) <- cast (backendParent stBackend) -> run parent+ | otherwise -> return Nothing moveKeys :: MonadIO m => Storage -> Storage -> m ()-moveKeys from to = liftIO $ do- case (stBacking from, stBacking to) of- (StorageDir { dirPath = fromPath }, StorageDir { dirPath = toPath }) -> do- files <- listDirectory (fromPath </> "keys")- forM_ files $ \file -> do- renameFile (fromPath </> "keys" </> file) (toPath </> "keys" </> file)-- (StorageDir { dirPath = fromPath }, StorageMemory { memKeys = toKeys }) -> do- let move m file- | Just dgst <- readRefDigest (BC.pack file) = do- let path = fromPath </> "keys" </> file- key <- convert <$> BC.readFile path- removeFile path- return $ M.insert dgst key m- | otherwise = return m- files <- listDirectory (fromPath </> "keys")- modifyMVar_ toKeys $ \keys -> foldM move keys files-- (StorageMemory { memKeys = fromKeys }, StorageDir { dirPath = toPath }) -> do- modifyMVar_ fromKeys $ \keys -> do- forM_ (M.assocs keys) $ \(dgst, key) ->- writeFileOnce (toPath </> "keys" </> (BC.unpack $ showRefDigest dgst)) (BL.fromStrict $ convert key)- return M.empty-- (StorageMemory { memKeys = fromKeys }, StorageMemory { memKeys = toKeys }) -> do- when (fromKeys /= toKeys) $ do- modifyMVar_ fromKeys $ \fkeys -> do- modifyMVar_ toKeys $ return . M.union fkeys- return M.empty+moveKeys Storage { stBackend = from } Storage { stBackend = to } = liftIO $ do+ keys <- backendListKeys from+ forM_ keys $ \key -> do+ backendLoadKey from key >>= \case+ Just sec -> do+ backendStoreKey to key sec+ backendRemoveKey from key+ Nothing -> return ()
+ src/Erebos/Storage/Memory.hs view
@@ -0,0 +1,107 @@+module Erebos.Storage.Memory (+ memoryStorage,+ deriveEphemeralStorage,+ derivePartialStorage,+) where++import Control.Concurrent+import Control.Monad++import Data.ByteArray (ScrubbedBytes)+import Data.ByteString.Lazy qualified as BL+import Data.Function+import Data.Kind+import Data.List+import Data.Map (Map)+import Data.Map qualified as M+import Data.Maybe+import Data.Typeable++import Erebos.Object+import Erebos.Storage.Backend+import Erebos.Storage.Head+import Erebos.Storage.Internal+++data MemoryStorage p (c :: Type -> Type) = StorageMemory+ { memParent :: p+ , memHeads :: MVar [ (( HeadTypeID, HeadID ), RefDigest ) ]+ , memObjs :: MVar (Map RefDigest BL.ByteString)+ , memKeys :: MVar (Map RefDigest ScrubbedBytes)+ , memWatchers :: MVar WatchList+ }++instance Eq (MemoryStorage p c) where+ (==) = (==) `on` memObjs++instance Show (MemoryStorage p c) where+ show StorageMemory {} = "mem"++instance (StorageCompleteness c, Typeable p) => StorageBackend (MemoryStorage p c) where+ type BackendCompleteness (MemoryStorage p c) = c+ type BackendParent (MemoryStorage p c) = p+ backendParent = memParent++ backendLoadBytes StorageMemory {..} dgst =+ M.lookup dgst <$> readMVar memObjs++ backendStoreBytes StorageMemory {..} dgst raw =+ modifyMVar_ memObjs (return . M.insert dgst raw)+++ backendLoadHeads StorageMemory {..} tid = do+ let toRes ( ( tid', hid ), dgst )+ | tid' == tid = Just ( hid, dgst )+ | otherwise = Nothing+ catMaybes . map toRes <$> readMVar memHeads++ backendLoadHead StorageMemory {..} tid hid =+ lookup (tid, hid) <$> readMVar memHeads++ backendStoreHead StorageMemory {..} tid hid dgst =+ modifyMVar_ memHeads $ return . (( ( tid, hid ), dgst ) :)++ backendReplaceHead StorageMemory {..} tid hid expected new = do+ res <- modifyMVar memHeads $ \hs -> do+ case partition ((==(tid, hid)) . fst) hs of+ ( [] , _ ) -> return ( hs, Left Nothing )+ (( _, dgst ) : _, hs' )+ | dgst == expected -> do+ ws <- map wlFun . filter ((==(tid, hid)) . wlHead) . wlList <$> readMVar memWatchers+ return ((( tid, hid ), new ) : hs', Right ( new, ws ))+ | otherwise -> do+ return ( hs, Left $ Just dgst )+ case res of+ Right ( dgst, ws ) -> do+ void $ forkIO $ do+ mapM_ ($ dgst) ws+ return (Right dgst)+ Left x -> return $ Left x++ backendWatchHead StorageMemory {..} tid hid cb = modifyMVar memWatchers $ return . watchListAdd tid hid cb++ backendUnwatchHead StorageMemory {..} wid = modifyMVar_ memWatchers $ return . watchListDel wid+++ backendListKeys StorageMemory {..} = M.keys <$> readMVar memKeys+ backendLoadKey StorageMemory {..} dgst = M.lookup dgst <$> readMVar memKeys+ backendStoreKey StorageMemory {..} dgst key = modifyMVar_ memKeys $ return . M.insert dgst key+ backendRemoveKey StorageMemory {..} dgst = modifyMVar_ memKeys $ return . M.delete dgst+++memoryStorage' :: (StorageCompleteness c, Typeable p) => p -> IO (Storage' c)+memoryStorage' memParent = do+ memHeads <- newMVar []+ memObjs <- newMVar M.empty+ memKeys <- newMVar M.empty+ memWatchers <- newMVar (WatchList startWatchID [])+ newStorage $ StorageMemory {..}++memoryStorage :: IO Storage+memoryStorage = memoryStorage' ()++deriveEphemeralStorage :: Storage -> IO Storage+deriveEphemeralStorage parent = memoryStorage' parent++derivePartialStorage :: Storage -> IO PartialStorage+derivePartialStorage parent = memoryStorage' parent
src/Erebos/Storage/Merge.hs view
@@ -7,7 +7,7 @@ compareGeneration, generationMax, storedGeneration, - generations,+ generations, generationsBy, ancestors, precedes, precedesOrEquals,@@ -17,6 +17,8 @@ findProperty, findPropertyFirst,++ storedDifference, ) where import Control.Concurrent.MVar@@ -25,13 +27,16 @@ import Data.HashTable.IO qualified as HT import Data.Kind import Data.List+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NE import Data.Maybe import Data.Set (Set) import Data.Set qualified as S import System.IO.Unsafe (unsafePerformIO) -import Erebos.Storage+import Erebos.Object+import Erebos.Storable import Erebos.Storage.Internal import Erebos.Util @@ -51,7 +56,7 @@ storeMerge :: (Mergeable a, Storable a) => [Stored (Component a)] -> IO (Stored a) storeMerge [] = error "merge: empty list"-storeMerge xs@(Stored ref _ : _) = wrappedStore (refStorage ref) $ mergeSorted $ filterAncestors xs+storeMerge xs@(x : _) = wrappedStore (storedStorage x) $ mergeSorted $ filterAncestors xs previous :: Storable a => Stored a -> [Stored a] previous (Stored ref _) = case load ref of@@ -99,16 +104,24 @@ -- |Returns list of sets starting with the set of given objects and -- intcrementally adding parents.-generations :: Storable a => [Stored a] -> [Set (Stored a)]-generations = unfoldr gen . (,S.empty)- where gen (hs, cur) = case filter (`S.notMember` cur) hs of- [] -> Nothing- added -> let next = foldr S.insert cur added- in Just (next, (previous =<< added, next))+generations :: Storable a => [ Stored a ] -> NonEmpty (Set (Stored a))+generations = generationsBy previous +-- |Returns list of sets starting with the set of given objects and+-- intcrementally adding parents, with the first parameter being+-- a function to get all the parents of given object.+generationsBy :: Ord a => (a -> [ a ]) -> [ a ] -> NonEmpty (Set a)+generationsBy parents xs = NE.unfoldr gen ( xs, S.fromList xs )+ where+ gen ( hs, cur ) = ( cur, ) $+ case filter (`S.notMember` cur) (parents =<< hs) of+ [] -> Nothing+ added -> let next = foldr S.insert cur added+ in Just ( added, next )+ -- |Returns set containing all given objects and their ancestors ancestors :: Storable a => [Stored a] -> Set (Stored a)-ancestors = last . (S.empty:) . generations+ancestors = NE.last . generations precedes :: Storable a => Stored a -> Stored a -> Bool precedes x y = not $ x `elem` filterAncestors [x, y]@@ -161,3 +174,18 @@ findPropHeads :: forall a b. Storable a => (a -> Maybe b) -> Stored a -> [Stored a] findPropHeads sel sobj | Just _ <- sel $ fromStored sobj = [sobj] | otherwise = findPropHeads sel =<< previous sobj+++-- | Compute symmetrict difference between two stored histories. In other+-- words, return all 'Stored a' objects reachable (via 'previous') from first+-- given set, but not from the second; and vice versa.+storedDifference :: Storable a => [ Stored a ] -> [ Stored a ] -> [ Stored a ]+storedDifference xs' ys' =+ let xs = filterAncestors xs'+ ys = filterAncestors ys'++ filteredPrevious blocked zs = filterAncestors (previous zs ++ blocked) `diffSorted` blocked+ xg = S.toAscList $ NE.last $ generationsBy (filteredPrevious ys) $ filterAncestors (xs ++ ys) `diffSorted` ys+ yg = S.toAscList $ NE.last $ generationsBy (filteredPrevious xs) $ filterAncestors (ys ++ xs) `diffSorted` xs++ in xg `mergeUniq` yg
src/Erebos/Sync.hs view
@@ -10,7 +10,7 @@ import Erebos.Identity import Erebos.Service import Erebos.State-import Erebos.Storage+import Erebos.Storable import Erebos.Storage.Merge data SyncService = SyncPacket (Stored SharedState)@@ -23,7 +23,7 @@ pid <- asks svcPeerIdentity self <- svcSelf when (finalOwner pid `sameIdentity` finalOwner self) $ do- updateLocalHead_ $ \ls -> do+ updateLocalState_ $ \ls -> do let current = sort $ lsShared $ fromStored ls updated = filterAncestors (added : current) if current /= updated@@ -31,6 +31,7 @@ else return ls serviceNewPeer = notifyPeer . lsShared . fromStored =<< svcGetLocal+ serviceUpdatedPeer = serviceNewPeer serviceStorageWatchers _ = (:[]) $ SomeStorageWatcher (lsShared . fromStored) notifyPeer instance Storable SyncService where
+ src/Erebos/UUID.hs view
@@ -0,0 +1,24 @@+module Erebos.UUID (+ UUID,+ toString, fromString,+ toText, fromText,+ toASCIIBytes, fromASCIIBytes,+ nextRandom,+) where++import Crypto.Random.Entropy++import Data.Bits+import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as BSL+import Data.Maybe+import Data.UUID.Types++nextRandom :: IO UUID+nextRandom = do+ [ b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf ]+ <- BS.unpack <$> getEntropy 16+ let version = 4+ b6' = b6 .&. 0x0f .|. (version `shiftL` 4)+ b8' = b8 .&. 0x3f .|. 0x80+ return $ fromJust $ fromByteString $ BSL.pack [ b0, b1, b2, b3, b4, b5, b6', b7, b8', b9, ba, bb, bc, bd, be, bf ]
src/Erebos/Util.hs view
@@ -1,5 +1,14 @@ module Erebos.Util where +import Control.Monad++import Data.ByteArray (ByteArray, ByteArrayAccess)+import Data.ByteArray qualified as BA+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.Char++ uniq :: Eq a => [a] -> [a] uniq (x:y:xs) | x == y = uniq (x:xs) | otherwise = x : uniq (y:xs)@@ -13,15 +22,16 @@ mergeBy _ xs [] = xs mergeBy _ [] ys = ys -mergeUniqBy :: (a -> a -> Ordering) -> [a] -> [a] -> [a]-mergeUniqBy cmp (x : xs) (y : ys) = case cmp x y of- LT -> x : mergeBy cmp xs (y : ys)- EQ -> x : mergeBy cmp xs ys- GT -> y : mergeBy cmp (x : xs) ys+mergeUniqBy :: (a -> a -> Ordering) -> [ a ] -> [ a ] -> [ a ]+mergeUniqBy cmp (x : xs) (y : ys) =+ case cmp x y of+ LT -> x : mergeUniqBy cmp xs (y : ys)+ EQ -> x : mergeUniqBy cmp xs ys+ GT -> y : mergeUniqBy cmp (x : xs) ys mergeUniqBy _ xs [] = xs mergeUniqBy _ [] ys = ys -mergeUniq :: Ord a => [a] -> [a] -> [a]+mergeUniq :: Ord a => [ a ] -> [ a ] -> [ a ] mergeUniq = mergeUniqBy compare diffSorted :: Ord a => [a] -> [a] -> [a]@@ -35,3 +45,24 @@ | x > y = intersectsSorted (x:xs) ys | otherwise = True intersectsSorted _ _ = False+++showHex :: ByteArrayAccess ba => ba -> ByteString+showHex = B.concat . map showHexByte . BA.unpack+ where showHexChar x | x < 10 = x + o '0'+ | otherwise = x + o 'a' - 10+ showHexByte x = B.pack [ showHexChar (x `div` 16), showHexChar (x `mod` 16) ]+ o = fromIntegral . ord++readHex :: ByteArray ba => ByteString -> Maybe ba+readHex = return . BA.concat <=< readHex'+ where readHex' bs | B.null bs = Just []+ readHex' bs = do (bx, bs') <- B.uncons bs+ (by, bs'') <- B.uncons bs'+ x <- hexDigit bx+ y <- hexDigit by+ (B.singleton (x * 16 + y) :) <$> readHex' bs''+ hexDigit x | x >= o '0' && x <= o '9' = Just $ x - o '0'+ | x >= o 'a' && x <= o 'z' = Just $ x - o 'a' + 10+ | otherwise = Nothing+ o = fromIntegral . ord