packages feed

bluesky-tools 0.4.0.0 → 0.6.0.0

raw patch · 7 files changed

+125/−17 lines, 7 filesdep ~http-api-dataPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: http-api-data

API changes (from Hackage documentation)

- Bluesky.Did: Did :: Text -> Did
- Bluesky.Did: newtype Did
+ Bluesky.Did: BadIdentifierCharacters :: DidError
+ Bluesky.Did: BadMethod :: DidError
+ Bluesky.Did: BadPercentEncoding :: DidError
+ Bluesky.Did: EndsWithColon :: DidError
+ Bluesky.Did: NoDidPrefix :: DidError
+ Bluesky.Did: NoMethodSeparator :: DidError
+ Bluesky.Did: data Did
+ Bluesky.Did: data DidError
+ Bluesky.Did: instance Data.Aeson.Types.ToJSON.ToJSON Bluesky.Did.Did
+ Bluesky.Did: instance GHC.Classes.Eq Bluesky.Did.DidError
+ Bluesky.Did: instance GHC.Classes.Ord Bluesky.Did.DidError
+ Bluesky.Did: instance GHC.Show.Show Bluesky.Did.DidError
+ Bluesky.Did: makeDid :: Text -> Either DidError Did
+ Bluesky.Handle: instance Data.Aeson.Types.FromJSON.FromJSON Bluesky.Handle.Handle
+ Bluesky.Handle: instance Data.Aeson.Types.ToJSON.ToJSON Bluesky.Handle.Handle
+ Bluesky.Handle: instance GHC.Generics.Generic Bluesky.Handle.Handle

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ # Revision history for bluesky-tools +## 0.6.0.0 -- 2024-01-11++* Support older versions of `http-api-data`.+* Add `ToJSON Did`, and both `FromJSON`, `ToJSON` for `Handle`.+* Implement DID syntax validating `makeHandle`, and enforce it by removing the+  export of the `Did` constructor.+ ## 0.4.0.0 -- 2024-01-06  * Stop exporting Document constructor so I can continue to change it.
bluesky-tools.cabal view
@@ -1,7 +1,7 @@ cabal-version:      3.4  name:               bluesky-tools-version:            0.4.0.0+version:            0.6.0.0 synopsis:           Tools for interacting with Bluesky / AT Protocol description:   bluesky-tools provides tools that I've found useful while trying to integrate@@ -48,7 +48,7 @@     base ^>=4.18,     containers ^>=0.6,     dns ^>=4.2,-    http-api-data ^>=0.6,+    http-api-data >= 0.5 && < 0.6,     http-client ^>=0.7,     http-types ^>=0.12,     network-uri ^>=2.6,@@ -62,6 +62,7 @@   hs-source-dirs: test   main-is: Main.hs   other-modules:+    Did     Handle   build-depends:     base ^>=4.18,
src/Bluesky/Did.hs view
@@ -1,5 +1,5 @@ module Bluesky.Did-  ( Did(Did), rawDid+  ( Did, rawDid, makeDid, DidError(..)   , Document(alsoKnownAs), getPds   , getDocument   ) where@@ -7,6 +7,7 @@ import qualified Data.Aeson as Aeson import Data.Aeson ((.:)) import qualified Data.Aeson.Types as Aeson+import qualified Data.Char as Char import qualified Data.Map as Map import Data.Maybe import Data.Monoid (First(First, getFirst))@@ -23,12 +24,49 @@ -- -- A DID is a Decentralized Identifier. They're codified by various W3C -- standards. This type only aims to capture how they are used in atproto.------ Currently we do not implement DID validation. newtype Did = Did { rawDid :: Text }   deriving stock (Eq, Ord, Show)-  deriving newtype (Aeson.FromJSON)+  deriving newtype (Aeson.ToJSON) +data DidError+  = NoDidPrefix+  | NoMethodSeparator+    -- ^ after "did:", there must be another colon to delimit method from+    -- identifier+  | BadMethod+    -- ^ method must match the regex @[a-z]+@; this does not check if the method+    -- is supported by atproto+  | BadIdentifierCharacters+    -- ^ identifier section must match @[a-zA-Z0-9._:%-]@+    -- (nb. that general DID URIs may also have query and fragment components,+    -- i.e. @?@ and @#@ characters, but atproto DID identifiers may not)+  | EndsWithColon+  | BadPercentEncoding+    -- ^ the spec says implementations don't need to validate percent encoding,+    -- but we validate that every % is followed by two hex digits+  deriving stock (Show, Eq, Ord)++makeDid :: Text -> Either DidError Did+makeDid raw+  | not ("did:" `Text.isPrefixOf` raw) = Left NoDidPrefix+  | Text.null method+    || Text.any (not . isMethodChar) method = Left BadMethod+  | Text.any (not . isIdentifierChar) colonBody = Left BadIdentifierCharacters+  | Text.takeEnd 1 raw == ":" = Left EndsWithColon+  | any (not . Text.all Char.isHexDigit) percentEncodes = Left BadPercentEncoding+  | otherwise = Right (Did raw)+  where+    afterPrefix = Text.drop 4 raw+    (method, colonBody) = Text.breakOn ":" afterPrefix+    isMethodChar c = Char.isAscii c && Char.isLower c+    isIdentifierChar c =+      Char.isAscii c+      && (Char.isAlphaNum c || elem c ("._:%-" :: [Char]))+    percentEncodes = map (Text.take 2) . drop 1 $ Text.splitOn "%" colonBody++instance Aeson.FromJSON Did where+  parseJSON = Aeson.withText "Bluesky.Did.Did" $ either (fail . show) pure . makeDid+ -- | The DID methods supported by atproto. Note that DIDs are used outside of -- atproto and there are many more methods in those contexts, but we don't -- support them here.@@ -37,8 +75,8 @@   | Plc -- ^ https://github.com/did-method-plc/did-method-plc   deriving stock (Eq, Ord, Show) -method :: Did -> Maybe Method-method Did{ rawDid }+getMethod :: Did -> Maybe Method+getMethod Did{ rawDid }   | "did:web:" `Text.isPrefixOf` rawDid = Just Web   | "did:plc:" `Text.isPrefixOf` rawDid = Just Plc   | otherwise = Nothing@@ -50,7 +88,7 @@   } deriving stock (Eq, Ord, Show, Generic)  instance Aeson.FromJSON Service where-  parseJSON = Aeson.withObject "Service" $ \o -> do+  parseJSON = Aeson.withObject "Bluesky.Did.Service" $ \o -> do     serviceId <- o .: "id"     serviceType <- o .: "type"     serviceEndpointString <- o .: "serviceEndpoint" -- [sic]@@ -96,7 +134,7 @@ -- | This is currently only implemented for did:plc: DIDs. getDocument :: HasCallStack => HTTP.Manager -> Did -> IO (Maybe Document) getDocument httpManager did@(Did rawDid) =-  case method did of+  case getMethod did of     Nothing -> error "Unknown DID method"     Just Web -> error "Support for did:web: is not yet implemented"     Just Plc -> do
src/Bluesky/Handle.hs view
@@ -16,6 +16,7 @@ import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Data.Text (Text)+import GHC.Generics import GHC.Stack (HasCallStack)  import qualified Network.DNS as DNS@@ -27,7 +28,8 @@  -- | https://atproto.com/specs/handle newtype Handle = Handle { rawHandle :: Text }-  deriving stock (Eq, Ord, Show)+  deriving stock (Eq, Ord, Show, Generic)+  deriving newtype (Aeson.ToJSON)  data HandleError   = TooLong@@ -78,10 +80,17 @@ instance FromHttpApiData Handle where   parseUrlPiece = Bifunctor.first (Text.pack . show) . makeHandle +instance Aeson.FromJSON Handle where+  parseJSON =+    Aeson.withText "Bluesky.Handle.Handle" $ either (fail . show) pure . makeHandle+ -- | Returns 'Nothing' in ordinary cases where this handle can't be resolved by--- DNS. May raise an exception if either the handle has an invalid TLD or--- something goes wrong with DNS resolution.+-- DNS. May raise an exception if: --+-- * the handle has an invalid TLD,+-- * something goes wrong with DNS resolution,+-- * the DID returned is syntactically invalid.+-- -- Note that this handle shouldn't be considered valid for this DID until you've -- looked up the associated DID document and checked it appears there. resolveViaDns :: HasCallStack => Handle -> IO (Maybe Did)@@ -94,14 +103,17 @@         DNS.lookupTXT resolver ("_atproto." <> Text.encodeUtf8 rawHandle)       case results of         Right [Text.decodeASCII -> Text.stripPrefix "did=" -> Just rawDid] ->-          pure (Just (Did rawDid))+          either (error . show) (pure . Just) $ makeDid rawDid         Right [] -> pure Nothing         Left DNS.NameError -> pure Nothing         other -> error (show other)  -- | Returns 'Nothing' when the expected hostname reports 404 for the HTTP -- resolution endpoint. May raise an exception if either the handle has an--- invalid TLD or there's no HTTP server at the expected domain at all.+-- invalid TLD, the HTTP server doesn't return 200 or 404, or there's no HTTP+-- server at the expected domain at all. (This is probably a bit too strict, and+-- should ignore more HTTP errors, but I'll see based on my real-world+-- experience.) -- -- Note that this handle shouldn't be considered valid for this DID until you've -- looked up the associated DID document and checked it appears there.@@ -155,8 +167,8 @@     fromE (Right (Just dnsException, Just httpException)) =       Except.throwIO BothFailed{ dnsException, httpException } --- | 'Just True' if this 'Handle' appears in the DID 'Document' for the 'Did'.--- 'Just False' if the document is available and doesn't affirm the handle.+-- | @Just True@ if this 'Handle' appears in the DID 'Document' for the 'Did'.+-- @Just False@ if the document is available and doesn't affirm the handle. -- 'Nothing' if the document can't be fetched. verifyHandle :: HTTP.Manager -> Handle -> Did -> IO (Maybe Bool) verifyHandle httpManager (Handle rawHandle) did = runMaybeT $ do
+ test/Did.hs view
@@ -0,0 +1,47 @@+module Did where++import Data.Text (Text)++import Bluesky.Did++testAccept :: Text -> IO ()+testAccept t = case makeDid t of+  Left err -> error $ "Did not accept " <> show t <> ": " <> show err+  Right _ -> pure ()++testReject :: DidError -> Text -> IO ()+testReject expectedErr t = case makeDid t of+  Left actualErr+    | expectedErr == actualErr -> pure ()+    | otherwise -> error $ "Wrong error for " <> show t <> ": " <> show actualErr+  Right _ -> error $ "Incorrectly accepted " <> show t++main :: IO ()+main = do+  -- examples from the docs+  mapM_ testAccept+    [ -- valid and usable+      "did:plc:z72i7hdynmk6r22z27h6tvur"+    , "did:web:blueskyweb.xyz"+    , -- syntactically valid but invalid method+      "did:method:val:two"+    , "did:m:v"+    , "did:method::::val"+    , "did:method:-:_:."+    , "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"+    ]+  -- invalid syntax+  testReject BadMethod "did:METHOD:val"+  testReject BadMethod "did:m123:val"+  testReject NoDidPrefix "DID:method:val"+  testReject EndsWithColon "did:method:"+  testReject BadIdentifierCharacters "did:method:val/two"+  testReject BadIdentifierCharacters "did:method:val?two"+  testReject BadIdentifierCharacters "did:method:val#two"++  -- my own examples+  testAccept "did:method:A:b"+  -- percent encoding verification+  testAccept "did:plc:%ff"+  testAccept "did:plc:%F0"+  testReject BadPercentEncoding "did:plc:%FG"
test/Handle.hs view
@@ -18,6 +18,7 @@  main :: IO () main = do+  -- examples from the docs   mapM_ testAccept     [ "jay.bsky.social"     , "8.cn"
test/Main.hs view
@@ -1,7 +1,9 @@ module Main (main) where +import qualified Did import qualified Handle  main :: IO () main = do+  Did.main   Handle.main