advent-of-code-api 0.2.10.0 → 0.2.11.0
raw patch · 5 files changed
+145/−9 lines, 5 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Advent: DayStats :: Integer -> Integer -> DayStats
+ Advent: [AoCStats] :: AoC (Map Day DayStats)
+ Advent: [_aInferRankOnSubmission] :: AoCOpts -> Bool
+ Advent: [dsGold] :: DayStats -> Integer
+ Advent: [dsSilver] :: DayStats -> Integer
+ Advent: data DayStats
+ Advent: inferSubmitRes :: AoCOpts -> Day -> Part -> SubmitRes -> IO (Either AoCError SubmitRes)
+ Advent: inferSubmitRes_ :: AoCOpts -> Day -> Part -> SubmitRes -> IO SubmitRes
+ Advent: statsForDayPart :: Day -> Part -> Stats -> Integer
+ Advent: type Stats = Map Day DayStats
+ Advent.API: instance Advent.API.FromTags "pre" Advent.Types.Stats
+ Advent.Types: DayStats :: Integer -> Integer -> DayStats
+ Advent.Types: [dsGold] :: DayStats -> Integer
+ Advent.Types: [dsSilver] :: DayStats -> Integer
+ Advent.Types: data DayStats
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.DayStats
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.DayStats
+ Advent.Types: instance GHC.Generics.Generic Advent.Types.DayStats
+ Advent.Types: instance GHC.Read.Read Advent.Types.DayStats
+ Advent.Types: instance GHC.Show.Show Advent.Types.DayStats
+ Advent.Types: type Stats = Map Day DayStats
- Advent: AoCOpts :: String -> Integer -> AoCUserAgent -> Maybe FilePath -> Bool -> Int -> AoCOpts
+ Advent: AoCOpts :: String -> Integer -> AoCUserAgent -> Maybe FilePath -> Bool -> Int -> Bool -> AoCOpts
- Advent.API: adventAPIClient :: Maybe AoCUserAgent -> Integer -> ClientM NextDayTime :<|> ((Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))) :<|> (ClientM GlobalLeaderboard :<|> ((Day -> ClientM DailyLeaderboard) :<|> (PublicCode -> ClientM Leaderboard))))
+ Advent.API: adventAPIClient :: Maybe AoCUserAgent -> Integer -> ClientM NextDayTime :<|> (ClientM Stats :<|> ((Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))) :<|> (ClientM GlobalLeaderboard :<|> ((Day -> ClientM DailyLeaderboard) :<|> (PublicCode -> ClientM Leaderboard)))))
- Advent.API: type AdventAPI = Header "User-Agent" AoCUserAgent :> Capture "year" Integer :> Get '[Scripts] NextDayTime :<|> "day" :> Capture "day" Day :> Get '[Articles] Map Part Text :<|> "input" :> Get '[RawText] Text :<|> "answer" :> ReqBody '[FormUrlEncoded] SubmitInfo :> Post '[Articles] Text :<|> SubmitRes :<|> "leaderboard" :> Get '[Divs] GlobalLeaderboard :<|> "day" :> Capture "day" Day :> Get '[Divs] DailyLeaderboard :<|> "private" :> "view" :> Capture "code" PublicCode :> Get '[JSON] Leaderboard
+ Advent.API: type AdventAPI = Header "User-Agent" AoCUserAgent :> Capture "year" Integer :> Get '[Scripts] NextDayTime :<|> "stats" :> Get '[Pres] Stats :<|> "day" :> Capture "day" Day :> Get '[Articles] Map Part Text :<|> "input" :> Get '[RawText] Text :<|> "answer" :> ReqBody '[FormUrlEncoded] SubmitInfo :> Post '[Articles] Text :<|> SubmitRes :<|> "leaderboard" :> Get '[Divs] GlobalLeaderboard :<|> "day" :> Capture "day" Day :> Get '[Divs] DailyLeaderboard :<|> "private" :> "view" :> Capture "code" PublicCode :> Get '[JSON] Leaderboard
Files
- CHANGELOG.md +15/−1
- advent-of-code-api.cabal +1/−1
- src/Advent.hs +77/−6
- src/Advent/API.hs +36/−1
- src/Advent/Types.hs +16/−0
CHANGELOG.md view
@@ -1,8 +1,22 @@ Changelog ========= +Version 0.2.11.0+----------------++*December 6, 2025*++<https://github.com/mstksg/advent-of-code-api/releases/tag/v0.2.11.0>++* Add endpoint for `<year>/stats` to get current star counts. Use with care,+ never cached.+* Add `inferSubmitRes`, `inferSubmitRes_` to add stats-based rank at time of+ execution.+* Add `_aInferRankOnSubmission` (default `False`) to automatically tag your+ `SubmitRes` with an inferred rank based on stats.+ Version 0.2.10.0----------------+---------------- *December 1, 2025*
advent-of-code-api.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: advent-of-code-api-version: 0.2.10.0+version: 0.2.11.0 synopsis: Advent of Code REST API bindings and servant API description: Haskell bindings for Advent of Code REST API and a servant API. Please use responsibly! See README.md or "Advent" module for an introduction and
src/Advent.hs view
@@ -52,9 +52,14 @@ , Part(..) , Day(..) , NextDayTime(..)+ , DayStats(..)+ , Stats , AoCOpts(..) , AoCUserAgent(..) , SubmitRes(..), showSubmitRes+ , statsForDayPart+ , inferSubmitRes+ , inferSubmitRes_ , runAoC , runAoC_ , defaultAoCOpts@@ -243,6 +248,15 @@ AoCNextDayTime :: AoC NextDayTime + -- | Fetch the per-day completion stats for the current year. Does not+ -- require a session key.+ --+ -- /Cacheing rules/: Not cached.+ --+ -- @since 0.2.11.0+ AoCStats+ :: AoC Stats+ deriving instance Show (AoC a) deriving instance Typeable (AoC a) @@ -255,6 +269,7 @@ aocDay (AoCDailyLeaderboard d) = Just d aocDay AoCGlobalLeaderboard = Nothing aocDay AoCNextDayTime = Nothing+aocDay AoCStats = Nothing -- | A possible (syncronous, logical, pure) error returnable from 'runAoC'. -- Does not cover any asynchronous or IO errors.@@ -306,6 +321,10 @@ -- | Throttle delay, in milliseconds. Minimum is 1000000. Default -- is 3000000 (3 seconds). , _aThrottle :: Int+ -- | Attempt to infer submission rank by fetching stats after a+ -- correct submission that doesn't include rank information.+ -- Default is False.+ , _aInferRankOnSubmission :: Bool } deriving (Show, Typeable, Generic) @@ -326,6 +345,7 @@ , _aCache = Nothing , _aForce = False , _aThrottle = 3000000+ , _aInferRankOnSubmission = False } -- | HTTPS base of Advent of Code API.@@ -339,14 +359,16 @@ AoCInput i -> let _ :<|> r :<|> _ = adventAPIPuzzleClient aua yr i in r AoCSubmit i p ans -> let _ :<|> _ :<|> r = adventAPIPuzzleClient aua yr i in r (SubmitInfo p ans) <&> \(x :<|> y) -> (x, y)- AoCLeaderboard c -> let _ :<|> _ :<|> _ :<|> _ :<|> r = adventAPIClient aua yr+ AoCLeaderboard c -> let _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> r = adventAPIClient aua yr in r (PublicCode c)- AoCDailyLeaderboard d -> let _ :<|> _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr+ AoCDailyLeaderboard d -> let _ :<|> _ :<|> _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr in r d- AoCGlobalLeaderboard -> let _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr+ AoCGlobalLeaderboard -> let _ :<|> _ :<|> _ :<|> r :<|> _ :<|> _ = adventAPIClient aua yr in r- AoCNextDayTime -> let r :<|> _ = adventAPIClient aua yr+ AoCNextDayTime -> let r :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ = adventAPIClient aua yr in r+ AoCStats -> let _ :<|> r :<|> _ :<|> _ :<|> _ :<|> _ = adventAPIClient aua yr+ in r -- | Cache file for a given 'AoC' command@@ -363,6 +385,7 @@ AoCDailyLeaderboard d -> Just $ printf "daily/%04d/%02d.json" yr (dayInt d) AoCGlobalLeaderboard{} -> Just $ printf "global/%04d.json" yr AoCNextDayTime -> Nothing+ AoCStats -> Nothing where keyDir = case sess of Nothing -> ""@@ -375,7 +398,7 @@ -- of leading and trailing whitespace and run through 'URI.encode' -- before submitting. runAoC :: AoCOpts -> AoC a -> IO (Either AoCError a)-runAoC AoCOpts{..} a = do+runAoC opts@AoCOpts{..} a = do (keyMayb, cacheDir) <- case _aCache of Just c -> pure (Nothing, c) Nothing -> (Just _aSessionKey,) . (</> "advent-of-code-api") <$> getTemporaryDirectory@@ -406,7 +429,12 @@ . throttling aocThrottler (max 1000000 _aThrottle) $ runClientM (aocReq (Just _aUserAgent) _aYear a) =<< aocClientEnv _aSessionKey mcr <- maybe (throwError AoCThrottleError) pure mtr- either (throwError . AoCClientError) pure mcr+ res <- either (throwError . AoCClientError) pure mcr+ case (a, _aInferRankOnSubmission, res) of+ (AoCSubmit d p _, True, (txt, sr)) -> do+ sr' <- liftIO $ inferSubmitRes_ opts d p sr+ pure (txt, sr')+ _ -> pure res -- | A version of 'runAoC' that throws an IO exception (of type 'AoCError') -- upon failure, instead of an 'Either'.@@ -415,6 +443,48 @@ runAoC_ :: AoCOpts -> AoC a -> IO a runAoC_ o = either throwIO pure <=< runAoC o +-- | Extract the relevant completion count for a given part on a day's stats.+--+-- @since 0.2.11.0+statsForDayPart :: Day -> Part -> Stats -> Integer+statsForDayPart d Part1 = maybe 0 dsSilver . M.lookup d+statsForDayPart d Part2 = maybe 0 dsGold . M.lookup d++-- | If an Advent of Code submission returns 'SubCorrect' without an attached+-- rank, attempt to infer the rank from 'AoCStats', assuming that it was just+-- submitted and received at this moment.+--+-- If the response is anything other than 'SubCorrect Nothing', it is returned+-- unchanged.+--+-- @since 0.2.11.0+inferSubmitRes+ :: AoCOpts+ -> Day+ -> Part+ -> SubmitRes+ -> IO (Either AoCError SubmitRes)+inferSubmitRes opts d p sr = case sr of+ SubCorrect Nothing -> do+ stats <- runAoC opts AoCStats+ pure $ stats >>= \st ->+ case M.lookup d st of+ Just _ -> Right . SubCorrect . Just $ statsForDayPart d p st+ Nothing -> Right $ SubCorrect Nothing+ _ -> pure $ Right sr++-- | Variant of 'inferSubmitRes' that returns the original response if stats+-- lookup fails.+--+-- @since 0.2.11.0+inferSubmitRes_+ :: AoCOpts+ -> Day+ -> Part+ -> SubmitRes+ -> IO SubmitRes+inferSubmitRes_ opts d p sr = either (const sr) id <$> inferSubmitRes opts d p sr+ aocClientEnv :: String -> IO ClientEnv aocClientEnv s = do t <- getCurrentTime@@ -473,6 +543,7 @@ pure $ Right lb } AoCNextDayTime{} -> noCache+ AoCStats{} -> noCache where expectedParts :: Set Part expectedParts
src/Advent/API.hs view
@@ -113,6 +113,9 @@ -- | Interpret a response as a list of HTML 'T.Text' found in @<script>@ tags. type Scripts = HTMLTags "script" +-- | Interpret a response as a list of HTML 'T.Text' found in @<pre>@ tags.+type Pres = HTMLTags "pre"+ -- | Class for interpreting a list of 'T.Text' in tags to some desired -- output. --@@ -229,6 +232,9 @@ where t' = "var " <> t <> " = " +instance FromTags "pre" Stats where+ fromTags _ = listToMaybe . mapMaybe parseStats+ -- | A structured user agent, based on -- <https://www.reddit.com/r/adventofcode/comments/z9dhtd/please_include_your_contact_info_in_the_useragent/> data AoCUserAgent = AoCUserAgent@@ -247,6 +253,7 @@ Header "User-Agent" AoCUserAgent :> Capture "year" Integer :> (Get '[Scripts] NextDayTime+ :<|> "stats" :> Get '[Pres] Stats :<|> "day" :> Capture "day" Day :> (Get '[Articles] (Map Part Text) :<|> "input" :> Get '[RawText] Text@@ -273,6 +280,7 @@ :: Maybe AoCUserAgent -> Integer -> ClientM NextDayTime+ :<|> ClientM Stats :<|> (Day -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)) ) :<|> ClientM GlobalLeaderboard :<|> (Day -> ClientM DailyLeaderboard)@@ -288,7 +296,34 @@ -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)) adventAPIPuzzleClient aua y = pis where- _ :<|> pis :<|> _ = adventAPIClient aua y+ _ :<|> _ :<|> pis :<|> _ = adventAPIClient aua y++parseStats :: Text -> Maybe Stats+parseStats = fmap M.fromList . traverse parseStatLine . filter asBranch . H.parseTree+ where+ asBranch TagBranch{} = True+ asBranch _ = False++parseStatLine :: TagTree Text -> Maybe (Day, DayStats)+parseStatLine (TagBranch _ _ cs) = do+ dayTxt <- listToMaybe [t | TagLeaf (H.TagText t) <- cs, not (T.null (T.strip t))]+ dy <- mkDay =<< readMaybe (T.unpack (T.strip dayTxt))+ gold <- findNum "stats-both" cs+ silver <- findNum "stats-firstonly" cs+ pure (dy, DayStats gold silver)+ where+ findNum cls = listToMaybe . mapMaybe (go cls)+ go cls (TagBranch "span" attr inner) = do+ guard $ ("class", cls) `elem` attr+ readMaybe . T.unpack . T.strip $ innerText inner+ go _ _ = Nothing+parseStatLine _ = Nothing++innerText :: [TagTree Text] -> Text+innerText = T.concat . mapMaybe go . H.universeTree+ where+ go (TagLeaf (H.TagText t)) = Just t+ go _ = Nothing userNameNaked :: [TagTree Text] -> Maybe Text userNameNaked = (listToMaybe .) . mapMaybe $ \x -> do
src/Advent/Types.hs view
@@ -43,6 +43,8 @@ , GlobalLeaderboard(..) , GlobalLeaderboardMember(..) , NextDayTime(..)+ , DayStats(..)+ , Stats -- * Util , mkDay, mkDay_, dayInt , _DayInt, pattern DayInt@@ -259,6 +261,20 @@ data NextDayTime = NextDayTime Day Int | NoNextDayTime deriving (Show, Eq, Ord, Typeable, Generic)++-- | Stats for a single day on the event stats page.+--+-- @since 0.2.11.0+data DayStats = DayStats+ { dsGold :: Integer -- ^ Users who completed both parts+ , dsSilver :: Integer -- ^ Users who only completed the first part+ }+ deriving (Show, Read, Eq, Ord, Typeable, Generic)++-- | Stats for all days on the event stats page.+--+-- @since 0.2.11.0+type Stats = Map Day DayStats instance ToHttpApiData Part where toUrlPiece = T.pack . show . partInt