From 90d227f8d08f8fd5b260a2f3d3965d98cf8a52bf Mon Sep 17 00:00:00 2001 From: prolic Date: Tue, 7 Jan 2025 19:09:51 -0300 Subject: [PATCH] remove followRelay from Follow type The followRelay field in the Follow type has been removed as it's redundant with NIP-65 (RelayListMetadata). Instead of storing relay information with each follow, we now rely on RelayListMetadata events to discover which relays to use for each contact. - Reduces complexity in follow list handling - Removes duplicate relay information storage event ID handling and post notifications - Fix event ID serialization to avoid double quotes in QML - Add missing notifications for post changes - Store events locally before publishing - Fix repost handling in MainContent.qml --- resources/qml/content/MainContent.ui.qml | 4 +- src/Futr.hs | 44 +++++++---- src/Store/Lmdb.hs | 98 +++++++++++------------- src/Types.hs | 19 +---- src/UI.hs | 31 +++----- 5 files changed, 88 insertions(+), 108 deletions(-) diff --git a/resources/qml/content/MainContent.ui.qml b/resources/qml/content/MainContent.ui.qml index 48b6db0..f1d3e47 100644 --- a/resources/qml/content/MainContent.ui.qml +++ b/resources/qml/content/MainContent.ui.qml @@ -26,7 +26,9 @@ Rectangle { onMessageSubmitted: function(text) { if (targetPost.postType == "repost") { - quoteRepost(targetPost.referencedEventId, text) + if (targetPost.referencedPosts.length > 0) { + quoteRepost(targetPost.referencedPosts[0].id, text) + } } else { quoteRepost(targetPost.id, text) } diff --git a/src/Futr.hs b/src/Futr.hs index deb7982..b6a4058 100644 --- a/src/Futr.hs +++ b/src/Futr.hs @@ -9,7 +9,7 @@ import Control.Monad (forM, forM_, unless, void, when) import Data.Aeson (ToJSON, pairs, toEncoding, (.=)) import Data.List (nub) import Data.Map.Strict qualified as Map -import Data.Maybe (catMaybes, fromMaybe, listToMaybe) +import Data.Maybe (catMaybes, listToMaybe) import Data.Proxy (Proxy(..)) import Data.Set qualified as Set import Data.Text (Text, isPrefixOf, pack, unpack) @@ -28,17 +28,12 @@ import Effectful.FileSystem ) import Effectful.State.Static.Shared (State, get, gets, modify, put) import Effectful.TH -import Lmdb.Connection (closeEnvironment, withCursor) -import Lmdb.Connection qualified as Connection -import Lmdb.Map qualified as LMap -import Lmdb.Types (KeyValue(..)) +import Lmdb.Connection (closeEnvironment) import QtQuick import GHC.Generics (Generic) import Graphics.QML hiding (fireSignal, runEngineLoop) import Graphics.QML qualified as QML -import Pipes.Prelude qualified as Pipes import System.FilePath (()) -import Control.Concurrent.MVar (MVar, newMVar) import Logging import KeyMgmt (Account(..), AccountId(..), KeyMgmt, KeyMgmtState(..)) @@ -52,7 +47,7 @@ import Nostr.Publisher import Nostr.RelayConnection (RelayConnection) import Nostr.RelayPool import Nostr.Subscription -import Nostr.Types ( Event(..), EventId, Profile(..), Relay(..), RelayURI, Tag(..) +import Nostr.Types ( Event(..), EventId, Relay(..), RelayURI, Tag(..) , getUri, metadataFilter ) import Nostr.Util import Presentation.KeyMgmtUI (KeyMgmtUI) @@ -60,7 +55,7 @@ import Presentation.RelayMgmtUI (RelayMgmtUI) import RelayMgmt (RelayMgmt) import Store.Lmdb ( LmdbState(..), LmdbStore, initialLmdbState, initializeLmdbState , getEvent, getFollows, putEvent ) -import Types hiding (Comment, QuoteRepost) +import Types -- | Signal key class for LoginStatusChanged. data LoginStatusChanged deriving Typeable @@ -138,6 +133,9 @@ runFutr = interpret $ \_ -> \case Login obj input -> do kst <- get @KeyMgmtState case Map.lookup (AccountId input) (accountMap kst) of + Nothing -> do + logError $ "Account not found: " <> input + return () Just a -> do logInfo $ "Starting login for account: " <> pack (show $ accountPubKeyXO a) let pk = accountPubKeyXO a @@ -191,7 +189,7 @@ runFutr = interpret $ \_ -> \case Just userPK -> do currentFollows <- getFollows userPK unless (targetPK `elem` map pubkey currentFollows) $ do - let newFollow = Follow targetPK Nothing Nothing + let newFollow = Follow targetPK Nothing newFollows = newFollow : currentFollows sendFollowListEvent newFollows notify $ emptyUpdates { followsChanged = True } @@ -238,7 +236,10 @@ runFutr = interpret $ \_ -> \case Nothing -> logError "Failed to create seal" >> return Nothing let validGiftWraps = catMaybes giftWraps - forM_ validGiftWraps $ \gw -> publishGiftWrap gw senderPubKeyXO + forM_ validGiftWraps $ \gw -> do + putEvent $ EventWithRelays gw Set.empty + publishGiftWrap gw senderPubKeyXO + notify $ emptyUpdates { privateMessagesChanged = True } (Nothing, _) -> logError "No key pair found" (_, (Nothing, _)) -> logError "No current chat recipient" @@ -249,7 +250,10 @@ runFutr = interpret $ \_ -> \case let u = createShortTextNote input (keyPairToPubKeyXO kp) now signed <- signEvent u kp case signed of - Just s -> publishToOutbox s + Just s -> do + putEvent $ EventWithRelays s Set.empty + publishToOutbox s + notify $ emptyUpdates { postsChanged = True } Nothing -> logError "Failed to sign short text note" Logout obj -> do @@ -306,8 +310,10 @@ runFutr = interpret $ \_ -> \case let authorInboxUris = Set.fromList $ map getUri authorRelays targetUris = eventRelayUris `Set.union` authorInboxUris `Set.union` relaySet + putEvent $ EventWithRelays s targetUris forM_ (Set.toList targetUris) $ \relay -> publishToRelay s relay + notify $ emptyUpdates { postsChanged = True } Nothing -> return () Nothing -> logError "Failed to sign repost" @@ -320,13 +326,14 @@ runFutr = interpret $ \_ -> \case mEvent <- getEvent eid case mEvent of Nothing -> logError $ "Failed to fetch event " <> pack (show eid) - Just EventWithRelays{relays} | Set.null relays -> do - logError "Failed to fetch event: no relays" Just EventWithRelays{event, relays} -> do let q = createQuoteRepost event (Set.findMin relays) quote (keyPairToPubKeyXO kp) now signed <- signEvent q kp case signed of - Just s -> publishToOutbox s + Just s -> do + putEvent $ EventWithRelays s Set.empty + publishToOutbox s + notify $ emptyUpdates { postsChanged = True } Nothing -> logError "Failed to sign quote repost" Comment eid comment' -> do @@ -357,8 +364,10 @@ runFutr = interpret $ \_ -> \case let authorInboxUris = Set.fromList $ map getUri authorRelays targetUris = eventRelayUris `Set.union` authorInboxUris `Set.union` relaySet + putEvent $ EventWithRelays s targetUris forM_ (Set.toList targetUris) $ \relay -> publishToRelay s relay + notify $ emptyUpdates { postsChanged = True } Nothing -> return () Nothing -> logError "Failed to sign comment" @@ -371,7 +380,10 @@ runFutr = interpret $ \_ -> \case let deletion = createEventDeletion [eid] reason (keyPairToPubKeyXO kp) now signed <- signEvent deletion kp case signed of - Just s -> publishToOutbox s + Just s -> do + putEvent $ EventWithRelays s Set.empty + publishToOutbox s + notify $ emptyUpdates { postsChanged = True, privateMessagesChanged = True } Nothing -> logError "Failed to sign event deletion" diff --git a/src/Store/Lmdb.hs b/src/Store/Lmdb.hs index d93ef7f..c5f8665 100644 --- a/src/Store/Lmdb.hs +++ b/src/Store/Lmdb.hs @@ -23,41 +23,33 @@ module Store.Lmdb ) where import Control.Concurrent.MVar (MVar, newMVar, withMVar) -import Control.Monad (forM_,void) +import Control.Monad (forM_) import Data.Aeson (ToJSON, FromJSON, encode, decode, eitherDecode) import Data.ByteString.Lazy (fromStrict, toStrict) import Data.List (sort) import Data.Maybe (mapMaybe) import Data.Set qualified as Set -import Data.Text (pack) -import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.Text.Encoding (encodeUtf8) import Effectful -import Effectful.Exception (throwIO) import Effectful.Dispatch.Dynamic import Effectful.State.Static.Shared (State, get, modify, put) -import Effectful.State.Static.Shared qualified as State -import Effectful.FileSystem import Effectful.TH (makeEffect) import Lmdb.Codec qualified as Codec import Lmdb.Connection import Lmdb.Map qualified as Map import Lmdb.Types import Pipes.Prelude qualified as Pipes -import System.FilePath (()) import Pipes ((>->)) -import qualified Data.ByteString.Lazy as BL -import qualified Data.ByteString.Base16 as B16 -import qualified Data.Text.Encoding as TE import GHC.Generics (Generic) import qualified Data.Cache.LRU as LRU import Logging import Nostr.Event (validateEvent, unwrapGiftWrap, unwrapSeal) import Nostr.Keys (PubKeyXO, keyPairToPubKeyXO) -import Nostr.Types ( Event(..), EventId(..), Kind(..), Profile, Relay(..), Tag(..) +import Nostr.Types ( Event(..), EventId(..), Kind(..), Profile, Tag(..) , Rumor(..), rumorPubKey, rumorTags, rumorCreatedAt, emptyProfile ) import Nostr.Util -import Types (AppState(..), EventWithRelays(..), Follow(..)) +import Types (EventWithRelays(..), Follow(..)) -- | Timeline types @@ -104,96 +96,94 @@ makeEffect ''LmdbStore runLmdbStore :: (Util :> es, IOE :> es, State LmdbState :> es, Logging :> es) => Eff (LmdbStore : es) a -> Eff es a -runLmdbStore = interpret $ \env -> \case +runLmdbStore = interpret $ \_ -> \case -- Event operations (main storage operation) PutEvent ev -> do - LmdbState{..} <- get + -- Bind the current state to avoid shadowing + currentState <- get @LmdbState kp <- getKeyPair - liftIO $ withMVar lmdbLock $ \_ -> withTransaction lmdbEnv $ \txn -> do - Map.repsert' txn eventDb (eventId $ event ev) ev + liftIO $ withMVar (lmdbLock currentState) $ \_ -> withTransaction (lmdbEnv currentState) $ \txn -> do + Map.repsert' txn (eventDb currentState) (eventId $ event ev) ev case kind (event ev) of GiftWrap -> do - mSealedEvent <- liftIO $ unwrapGiftWrap (event ev) kp + mSealedEvent <- unwrapGiftWrap (event ev) kp case mSealedEvent of Just sealedEvent | validateEvent sealedEvent -> case kind sealedEvent of Seal -> do - mDecryptedRumor <- liftIO $ unwrapSeal sealedEvent kp + mDecryptedRumor <- unwrapSeal sealedEvent kp case mDecryptedRumor of - Just decryptedRumor | pubKey sealedEvent == rumorPubKey decryptedRumor -> do - let participants = if rumorPubKey decryptedRumor == keyPairToPubKeyXO kp - then sort $ getAllPTags (rumorTags decryptedRumor) - else filter (/= keyPairToPubKeyXO kp) $ rumorPubKey decryptedRumor : sort (getAllPTags (rumorTags decryptedRumor)) - addTimelineEntryTx txn chatTimelineDb ev participants (rumorCreatedAt decryptedRumor) + Just decryptedRumor + | pubKey sealedEvent == rumorPubKey decryptedRumor -> do + let participants = if rumorPubKey decryptedRumor == keyPairToPubKeyXO kp + then sort $ getAllPTags (rumorTags decryptedRumor) + else filter (/= keyPairToPubKeyXO kp) + (rumorPubKey decryptedRumor : sort (getAllPTags (rumorTags decryptedRumor))) + addTimelineEntryTx txn (chatTimelineDb currentState) ev participants (rumorCreatedAt decryptedRumor) _ -> pure () _ -> pure () _ -> pure () ShortTextNote -> - addTimelineEntryTx txn postTimelineDb ev [pubKey $ event ev] (createdAt $ event ev) + addTimelineEntryTx txn (postTimelineDb currentState) ev [pubKey $ event ev] (createdAt $ event ev) Repost -> do let etags = [t | t@(ETag _ _ _) <- tags (event ev)] let mOriginalEvent = eitherDecode (fromStrict $ encodeUtf8 $ content $ event ev) case (etags, mOriginalEvent) of - (ETag _ _ _:_, Right originalEvent) | validateEvent originalEvent -> do - Map.repsert' txn eventDb (eventId originalEvent) (EventWithRelays originalEvent Set.empty) - addTimelineEntryTx txn postTimelineDb ev [pubKey $ event ev] (createdAt $ event ev) + (ETag _ _ _ : _, Right originalEvent) + | validateEvent originalEvent -> do + Map.repsert' txn (eventDb currentState) (eventId originalEvent) + (EventWithRelays originalEvent Set.empty) + addTimelineEntryTx txn (postTimelineDb currentState) ev [pubKey $ event ev] + (createdAt $ event ev) _ -> pure () EventDeletion -> do let eventIdsToDelete = [eid | ETag eid _ _ <- tags (event ev)] forM_ eventIdsToDelete $ \eid -> do - mEvent <- Map.lookup' (readonly txn) eventDb eid + mEvent <- Map.lookup' (readonly txn) (eventDb currentState) eid case mEvent of Just deletedEv -> do let key = (pubKey $ event deletedEv, createdAt $ event deletedEv) db = case kind (event deletedEv) of - ShortTextNote -> postTimelineDb - Repost -> postTimelineDb - _ -> chatTimelineDb + ShortTextNote -> (postTimelineDb currentState) + Repost -> (postTimelineDb currentState) + _ -> (chatTimelineDb currentState) Map.delete' txn db key - Map.delete' txn eventDb eid + Map.delete' txn (eventDb currentState) eid Nothing -> pure () Metadata -> case eitherDecode (fromStrict $ encodeUtf8 $ content $ event ev) of Right profile -> - Map.repsert' txn profileDb (pubKey $ event ev) (profile, createdAt $ event ev) + Map.repsert' txn (profileDb currentState) (pubKey $ event ev) (profile, createdAt $ event ev) Left _ -> pure () FollowList -> do - let followList' = [Follow pk (fmap InboxRelay relay') petName' | PTag pk relay' petName' <- tags (event ev)] + let followList' = [Follow pk petName' | PTag pk _ petName' <- tags (event ev)] authorPk = pubKey $ event ev - Map.repsert' txn followsDb authorPk followList' + Map.repsert' txn (followsDb currentState) authorPk followList' _ -> pure () -- Update caches after transaction - LmdbState{..} <- get @LmdbState - let newEventCache = LRU.insert (eventId $ event ev) ev eventCache + let newEventCache = LRU.insert (eventId $ event ev) ev (eventCache currentState) newProfileCache = case kind (event ev) of - Metadata -> case eitherDecode (fromStrict $ encodeUtf8 $ content $ event ev) of - Right profile -> LRU.insert (pubKey $ event ev) (profile, createdAt $ event ev) profileCache - Left _ -> profileCache - _ -> profileCache + Metadata -> case eitherDecode (fromStrict $ encodeUtf8 $ content (event ev)) of + Right profile -> LRU.insert (pubKey $ event ev) (profile, createdAt $ event ev) (profileCache currentState) + Left _ -> profileCache currentState + _ -> profileCache currentState newFollowsCache = case kind (event ev) of - FollowList -> let followList' = [Follow pk (fmap InboxRelay relay') petName' | PTag pk relay' petName' <- tags (event ev)] - in LRU.insert (pubKey $ event ev) followList' followsCache - _ -> followsCache - put @LmdbState $ LmdbState - { lmdbLock = lmdbLock - , lmdbEnv = lmdbEnv - , eventDb = eventDb - , profileDb = profileDb - , postTimelineDb = postTimelineDb - , chatTimelineDb = chatTimelineDb - , followsDb = followsDb - , eventCache = newEventCache + FollowList -> let followList' = [Follow pk petName' | PTag pk _ petName' <- tags (event ev)] + in LRU.insert (pubKey $ event ev) followList' (followsCache currentState) + _ -> followsCache currentState + + put @LmdbState currentState + { eventCache = newEventCache , profileCache = newProfileCache , followsCache = newFollowsCache - , timelineCache = timelineCache } GetEvent eid -> do diff --git a/src/Types.hs b/src/Types.hs index 0371e37..de7c347 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -4,17 +4,15 @@ module Types where -import Control.Concurrent.MVar (MVar) -import Data.Aeson (FromJSON, ToJSON, Value(..), toJSON, parseJSON, (.:), (.=), withObject, object) +import Data.Aeson (FromJSON, ToJSON, toJSON, parseJSON, (.:), (.=), withObject, object) import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Set (Set) import Data.Text (Text) import Effectful.Concurrent.STM (TChan, TQueue) -import Lmdb.Types (Database, Environment, Mode(..)) import GHC.Generics (Generic) import Nostr.Keys (KeyPair, PubKeyXO) -import Nostr.Types (Event, EventId, Filter, Profile, Relay(..), RelayURI, Request, SubscriptionId) +import Nostr.Types (Event, EventId, Filter, Relay(..), RelayURI, Request, SubscriptionId) -- | Status of a publish operation @@ -129,7 +127,6 @@ data AppState = AppState -- | Follow. data Follow = Follow { pubkey :: PubKeyXO - , followRelay :: Maybe Relay , petName :: Maybe Text } deriving (Eq, Show, Generic) @@ -137,9 +134,6 @@ data Follow = Follow instance ToJSON Follow where toJSON Follow{..} = object [ "pubkey" .= pubkey - , "followRelay" .= (case followRelay of - Just (InboxRelay uri) -> object ["contents" .= uri, "tag" .= ("InboxRelay" :: Text)] - Nothing -> Null) , "petName" .= petName ] @@ -148,16 +142,7 @@ instance ToJSON Follow where instance FromJSON Follow where parseJSON = withObject "Follow" $ \v -> Follow <$> v .: "pubkey" - <*> (v .: "followRelay" >>= parseRelay) <*> v .: "petName" - where - parseRelay Null = pure Nothing - parseRelay (Object o) = do - tag <- o .: "tag" - if tag == ("InboxRelay" :: Text) - then Just . InboxRelay <$> o .: "contents" - else fail $ "Unknown relay tag: " ++ show tag - parseRelay _ = fail "Invalid relay format" -- | Initial application state. diff --git a/src/UI.hs b/src/UI.hs index 8207d16..62b6d3a 100644 --- a/src/UI.hs +++ b/src/UI.hs @@ -6,6 +6,7 @@ module UI where import Control.Monad.Fix (mfix) import Data.Aeson (decode, eitherDecode, encode) +import Data.ByteString.Base16 qualified as B16 import Data.ByteString.Lazy qualified as BSL import Data.List (find, nub) import Data.Maybe (fromMaybe, mapMaybe) @@ -22,13 +23,14 @@ import Prelude hiding (drop) import Text.Read (readMaybe) import Text.Regex.TDFA + import Logging import Nostr import Nostr.Bech32 import Nostr.Event (createMetadata) import Nostr.Publisher import Nostr.Keys (PubKeyXO, keyPairToPubKeyXO) -import Nostr.Types (Event(..), EventId(..), Kind(..), Profile(..), Relationship(..), Rumor(..), Tag(..), getUri) +import Nostr.Types (Event(..), EventId(..), Kind(..), Profile(..), Relationship(..), Rumor(..), Tag(..)) import Nostr.Util import Presentation.KeyMgmtUI qualified as KeyMgmtUI import Presentation.RelayMgmtUI qualified as RelayMgmtUI @@ -109,18 +111,17 @@ runUI = interpret $ \_ -> \case st <- runE $ get @AppState followData <- case keyPairToPubKeyXO <$> keyPair st of Just upk | upk == pubKeyXO -> - return $ Just Follow { pubkey = pubKeyXO, followRelay = Nothing, petName = Nothing } + return $ Just Follow { pubkey = pubKeyXO, petName = Nothing } Just upk -> do follows <- runE $ getFollows upk if pubKeyXO `elem` map pubkey follows - then return $ Just Follow { pubkey = pubKeyXO, followRelay = Nothing, petName = Nothing } + then return $ Just Follow { pubkey = pubKeyXO, petName = Nothing } else return Nothing Nothing -> return Nothing accessor st followData followClass <- newClass [ followProp "pubkey" $ \_ -> return . maybe "" (pubKeyXOToBech32 . pubkey), - followProp "relay" $ \_ -> return . maybe "" (\f -> maybe "" getUri (followRelay f)), followProp "petname" $ \_ -> return . maybe "" (fromMaybe "" . petName), followProp "displayName" $ \_ -> maybe (return "") (\follow -> do (profile, _) <- runE $ getProfile (pubkey follow) @@ -148,7 +149,7 @@ runUI = interpret $ \_ -> \case postClass <- mfix $ \postClass' -> newClass [ defPropertySigRO' "id" changeKey' $ \obj -> do let eid = fromObjRef obj :: EventId - let value = pack $ show eid + let value = TE.decodeUtf8 $ B16.encode $ getEventId eid return value, defPropertySigRO' "postType" changeKey' $ \obj -> do @@ -351,30 +352,20 @@ runUI = interpret $ \_ -> \case defMethod' "sendShortTextNote" $ \_ input -> runE $ sendShortTextNote input, -- NIP-01 short text note defMethod' "repost" $ \_ eid -> runE $ do -- NIP-18 repost - let unquoted = read (unpack eid) :: String - let eid' = read unquoted :: EventId + let eid' = read (unpack eid) :: EventId repost eid', defMethod' "quoteRepost" $ \_ eid quote -> runE $ do -- NIP-18 quote repost - let unquoted = read (unpack eid) :: String - let eid' = read unquoted :: EventId + let eid' = read (unpack eid) :: EventId quoteRepost eid' quote, defMethod' "comment" $ \_ eid input -> runE $ do -- NIP-22 comment - let unquoted = read (unpack eid) :: String - let eid' = read unquoted :: EventId + let eid' = read (unpack eid) :: EventId comment eid' input, defMethod' "deleteEvent" $ \_ eid input -> runE $ do -- NIP-09 delete post - let unquoted = read (unpack eid) :: String - let eid' = read unquoted :: EventId - deleteEvent eid' input, - - defMethod' "getPost" $ \_ eid -> do - let unquoted = read (unpack eid) :: String - let eid' = read unquoted :: EventId - postObj <- newObject postClass eid' - return postObj + let eid' = read (unpack eid) :: EventId + deleteEvent eid' input ] rootObj <- newObject rootClass ()