packages feed

advent-of-code-api 0.2.11.0 → 0.2.12.0

raw patch · 9 files changed

+1171/−990 lines, 9 filessetup-changedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Advent: instance GHC.Exception.Type.Exception Advent.AoCError
- Advent: instance GHC.Generics.Generic Advent.AoCError
- Advent: instance GHC.Generics.Generic Advent.AoCOpts
- Advent: instance GHC.Show.Show (Advent.AoC a)
- Advent: instance GHC.Show.Show Advent.AoCError
- Advent: instance GHC.Show.Show Advent.AoCOpts
- Advent.API: instance (Advent.API.FromTags tag a, GHC.TypeLits.KnownSymbol tag) => Servant.API.ContentTypes.MimeUnrender (Advent.API.HTMLTags tag) a
- Advent.API: instance GHC.Show.Show Advent.API.AoCUserAgent
- Advent.API: instance forall k a (cls :: k). (GHC.Classes.Ord a, GHC.Enum.Enum a, GHC.Enum.Bounded a) => Advent.API.FromTags cls (Data.Map.Internal.Map a Data.Text.Internal.Text)
- Advent.Types: instance GHC.Enum.Bounded Advent.Types.Day
- Advent.Types: instance GHC.Enum.Bounded Advent.Types.Part
- Advent.Types: instance GHC.Enum.Enum Advent.Types.Day
- Advent.Types: instance GHC.Enum.Enum Advent.Types.Part
- Advent.Types: instance GHC.Generics.Generic Advent.Types.DailyLeaderboard
- Advent.Types: instance GHC.Generics.Generic Advent.Types.DailyLeaderboardMember
- Advent.Types: instance GHC.Generics.Generic Advent.Types.Day
- Advent.Types: instance GHC.Generics.Generic Advent.Types.DayStats
- Advent.Types: instance GHC.Generics.Generic Advent.Types.GlobalLeaderboard
- Advent.Types: instance GHC.Generics.Generic Advent.Types.GlobalLeaderboardMember
- Advent.Types: instance GHC.Generics.Generic Advent.Types.Leaderboard
- Advent.Types: instance GHC.Generics.Generic Advent.Types.LeaderboardMember
- Advent.Types: instance GHC.Generics.Generic Advent.Types.NextDayTime
- Advent.Types: instance GHC.Generics.Generic Advent.Types.Part
- Advent.Types: instance GHC.Generics.Generic Advent.Types.PublicCode
- Advent.Types: instance GHC.Generics.Generic Advent.Types.Rank
- Advent.Types: instance GHC.Generics.Generic Advent.Types.SubmitInfo
- Advent.Types: instance GHC.Generics.Generic Advent.Types.SubmitRes
- Advent.Types: instance GHC.Read.Read Advent.Types.DayStats
- Advent.Types: instance GHC.Read.Read Advent.Types.Part
- Advent.Types: instance GHC.Read.Read Advent.Types.PublicCode
- Advent.Types: instance GHC.Read.Read Advent.Types.SubmitInfo
- Advent.Types: instance GHC.Read.Read Advent.Types.SubmitRes
- Advent.Types: instance GHC.Show.Show Advent.Types.DailyLeaderboard
- Advent.Types: instance GHC.Show.Show Advent.Types.DailyLeaderboardMember
- Advent.Types: instance GHC.Show.Show Advent.Types.Day
- Advent.Types: instance GHC.Show.Show Advent.Types.DayStats
- Advent.Types: instance GHC.Show.Show Advent.Types.GlobalLeaderboard
- Advent.Types: instance GHC.Show.Show Advent.Types.GlobalLeaderboardMember
- Advent.Types: instance GHC.Show.Show Advent.Types.Leaderboard
- Advent.Types: instance GHC.Show.Show Advent.Types.LeaderboardMember
- Advent.Types: instance GHC.Show.Show Advent.Types.NextDayTime
- Advent.Types: instance GHC.Show.Show Advent.Types.Part
- Advent.Types: instance GHC.Show.Show Advent.Types.PublicCode
- Advent.Types: instance GHC.Show.Show Advent.Types.Rank
- Advent.Types: instance GHC.Show.Show Advent.Types.SubmitInfo
- Advent.Types: instance GHC.Show.Show Advent.Types.SubmitRes
+ Advent: SessionKey :: Text -> SessionKey
+ Advent: [getSessionKey] :: SessionKey -> Text
+ Advent: instance GHC.Internal.Exception.Type.Exception Advent.AoCError
+ Advent: instance GHC.Internal.Generics.Generic Advent.AoCError
+ Advent: instance GHC.Internal.Generics.Generic Advent.AoCOpts
+ Advent: instance GHC.Internal.Show.Show (Advent.AoC a)
+ Advent: instance GHC.Internal.Show.Show Advent.AoCError
+ Advent: instance GHC.Internal.Show.Show Advent.AoCOpts
+ Advent: newtype SessionKey
+ Advent.API: SessionKey :: Text -> SessionKey
+ Advent.API: [getSessionKey] :: SessionKey -> Text
+ Advent.API: dummyToken :: SessionKey
+ Advent.API: instance (Advent.API.FromTags tag a, GHC.Internal.TypeLits.KnownSymbol tag) => Servant.API.ContentTypes.MimeUnrender (Advent.API.HTMLTags tag) a
+ Advent.API: instance GHC.Classes.Eq Advent.API.SessionKey
+ Advent.API: instance GHC.Classes.Ord Advent.API.SessionKey
+ Advent.API: instance GHC.Internal.Show.Show Advent.API.AoCUserAgent
+ Advent.API: instance GHC.Internal.Show.Show Advent.API.SessionKey
+ Advent.API: instance Web.Internal.HttpApiData.ToHttpApiData Advent.API.SessionKey
+ Advent.API: instance forall k a (cls :: k). (GHC.Classes.Ord a, GHC.Internal.Enum.Enum a, GHC.Internal.Enum.Bounded a) => Advent.API.FromTags cls (Data.Map.Internal.Map a Data.Text.Internal.Text)
+ Advent.API: newtype SessionKey
+ Advent.Types: [lbDay1Ts] :: Leaderboard -> UTCTime
+ Advent.Types: [lbNumDays] :: Leaderboard -> Int
+ Advent.Types: [lbmStarIndex] :: LeaderboardMember -> Map Day (Map Part Int)
+ Advent.Types: instance GHC.Internal.Enum.Bounded Advent.Types.Day
+ Advent.Types: instance GHC.Internal.Enum.Bounded Advent.Types.Part
+ Advent.Types: instance GHC.Internal.Enum.Enum Advent.Types.Day
+ Advent.Types: instance GHC.Internal.Enum.Enum Advent.Types.Part
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.DailyLeaderboard
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.DailyLeaderboardMember
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.Day
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.DayStats
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.GlobalLeaderboard
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.GlobalLeaderboardMember
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.Leaderboard
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.LeaderboardMember
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.NextDayTime
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.Part
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.PublicCode
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.Rank
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.SubmitInfo
+ Advent.Types: instance GHC.Internal.Generics.Generic Advent.Types.SubmitRes
+ Advent.Types: instance GHC.Internal.Read.Read Advent.Types.DayStats
+ Advent.Types: instance GHC.Internal.Read.Read Advent.Types.Part
+ Advent.Types: instance GHC.Internal.Read.Read Advent.Types.PublicCode
+ Advent.Types: instance GHC.Internal.Read.Read Advent.Types.SubmitInfo
+ Advent.Types: instance GHC.Internal.Read.Read Advent.Types.SubmitRes
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.DailyLeaderboard
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.DailyLeaderboardMember
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.Day
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.DayStats
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.GlobalLeaderboard
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.GlobalLeaderboardMember
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.Leaderboard
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.LeaderboardMember
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.NextDayTime
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.Part
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.PublicCode
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.Rank
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.SubmitInfo
+ Advent.Types: instance GHC.Internal.Show.Show Advent.Types.SubmitRes
- Advent: aocReq :: Maybe AoCUserAgent -> Integer -> AoC a -> ClientM a
+ Advent: aocReq :: Maybe AoCUserAgent -> Maybe SessionKey -> Integer -> AoC a -> ClientM a
- 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: adventAPIClient :: Maybe AoCUserAgent -> Integer -> ClientM NextDayTime :<|> (ClientM Stats :<|> ((Day -> (Maybe SessionKey -> ClientM (Map Part Text)) :<|> ((SessionKey -> ClientM Text) :<|> (SessionKey -> SubmitInfo -> ClientM (Text :<|> SubmitRes)))) :<|> (ClientM GlobalLeaderboard :<|> ((Day -> ClientM DailyLeaderboard) :<|> (SessionKey -> PublicCode -> ClientM Leaderboard)))))
- Advent.API: adventAPIPuzzleClient :: Maybe AoCUserAgent -> Integer -> Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))
+ Advent.API: adventAPIPuzzleClient :: Maybe AoCUserAgent -> Integer -> Day -> (Maybe SessionKey -> ClientM (Map Part Text)) :<|> ((SessionKey -> ClientM Text) :<|> (SessionKey -> SubmitInfo -> ClientM (Text :<|> SubmitRes)))
- 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
+ Advent.API: type AdventAPI = Header "User-Agent" AoCUserAgent :> Capture "year" Integer :> Get '[Scripts] NextDayTime :<|> "stats" :> Get '[Pres] Stats :<|> "day" :> Capture "day" Day :> Header "Cookie" SessionKey :> Get '[Articles] Map Part Text :<|> "input" :> Header' '[Required, Strict] "Cookie" SessionKey :> Get '[RawText] Text :<|> "answer" :> Header' '[Required, Strict] "Cookie" SessionKey :> ReqBody '[FormUrlEncoded] SubmitInfo :> Post '[Articles] Text :<|> SubmitRes :<|> "leaderboard" :> Get '[Divs] GlobalLeaderboard :<|> "day" :> Capture "day" Day :> Get '[Divs] DailyLeaderboard :<|> "private" :> "view" :> Header' '[Required, Strict] "Cookie" SessionKey :> Capture "code" PublicCode :> Get '[JSON] Leaderboard
- Advent.Types: LB :: Integer -> Integer -> Map Integer LeaderboardMember -> Leaderboard
+ Advent.Types: LB :: Integer -> Integer -> Map Integer LeaderboardMember -> Int -> UTCTime -> Leaderboard
- Advent.Types: LBM :: Maybe Integer -> Maybe Text -> Integer -> Integer -> Maybe UTCTime -> Int -> Map Day (Map Part UTCTime) -> LeaderboardMember
+ Advent.Types: LBM :: Maybe Integer -> Maybe Text -> Integer -> Integer -> Maybe UTCTime -> Int -> Map Day (Map Part UTCTime) -> Map Day (Map Part Int) -> LeaderboardMember

Files

CHANGELOG.md view
@@ -1,6 +1,22 @@ Changelog ========= +Version 0.2.12.0+----------------++*September 28, 2026*++<https://github.com/mstksg/advent-of-code-api/releases/tag/v0.2.12.0>++*   Add `lbNumDays` and `lbDay1Ts` to `Leaderboard`, parsed from the `num_days`+    and `day1_ts` fields of the private leaderboard JSON.+*   Add `lbmStarIndex` to `LeaderboardMember`, parsed from the `star_index`+    field alongside each completion's `get_star_ts`.+*   The `session` cookie is now an explicit `Header "Cookie"` in+    `AdventAPI` via servant, scoped only to the endpoints that actually need it+    (optional for `day` prompt, required for `input`/`answer`/private+    leaderboard). Adds `SessionKey`, re-exported from `Advent`.+ Version 0.2.11.0 ---------------- 
Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
advent-of-code-api.cabal view
@@ -1,57 +1,59 @@-cabal-version: 1.12+cabal-version:      1.12  -- This file has been generated from package.yaml by hpack version 0.36.0. -- -- see: https://github.com/sol/hpack -name:           advent-of-code-api-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-                tutorial.-category:       Web-homepage:       https://github.com/mstksg/advent-of-code-api#readme-bug-reports:    https://github.com/mstksg/advent-of-code-api/issues-author:         Justin Le-maintainer:     justin@jle.im-copyright:      (c) Justin Le 2018-license:        BSD3-license-file:   LICENSE-build-type:     Simple-tested-with:-    GHC >= 8.0+name:               advent-of-code-api+version:            0.2.12.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+  tutorial.++category:           Web+homepage:           https://github.com/mstksg/advent-of-code-api#readme+bug-reports:        https://github.com/mstksg/advent-of-code-api/issues+author:             Justin Le+maintainer:         justin@jle.im+copyright:          (c) Justin Le 2018+license:            BSD3+license-file:       LICENSE+build-type:         Simple+tested-with:        GHC >=8.0 extra-source-files:-    README.md-    CHANGELOG.md-    test-data/correct-rank.txt-    test-data/correct.txt-    test-data/incorrect-high.txt-    test-data/incorrect-low.txt-    test-data/incorrect-wait.txt-    test-data/incorrect.txt-    test-data/invalid.txt-    test-data/wait.txt-    test-data/wait2.txt+  CHANGELOG.md+  README.md+  test-data/correct-rank.txt+  test-data/correct.txt+  test-data/incorrect-high.txt+  test-data/incorrect-low.txt+  test-data/incorrect-wait.txt+  test-data/incorrect.txt+  test-data/invalid.txt+  test-data/wait.txt+  test-data/wait2.txt  source-repository head-  type: git+  type:     git   location: https://github.com/mstksg/advent-of-code-api  library   exposed-modules:-      Advent-      Advent.API-      Advent.Types+    Advent+    Advent.API+    Advent.Types+   other-modules:-      Advent.Throttle-      Advent.Cache-  hs-source-dirs:-      src-  ghc-options: -Wall -Wcompat -Werror=incomplete-patterns+    Advent.Cache+    Advent.Throttle++  hs-source-dirs:   src+  ghc-options:      -Wall -Wcompat -Werror=incomplete-patterns   build-depends:       aeson-    , base >=4.9 && <5+    , base                 >=4.9 && <5     , bytestring     , containers     , deepseq@@ -62,7 +64,7 @@     , http-client     , http-client-tls     , http-media-    , megaparsec >=7+    , megaparsec           >=7     , mtl     , profunctors     , servant@@ -72,22 +74,25 @@     , tagsoup     , text     , time-    , time-compat >=1.9+    , time-compat          >=1.9+   default-language: Haskell2010  test-suite advent-of-code-api-test-  type: exitcode-stdio-1.0-  main-is: Spec.hs-  other-modules:-      Paths_advent_of_code_api-  hs-source-dirs:-      test-  ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -threaded -rtsopts -with-rtsopts=-N+  type:             exitcode-stdio-1.0+  main-is:          Spec.hs+  other-modules:    Paths_advent_of_code_api+  hs-source-dirs:   test+  ghc-options:+    -Wall -Wcompat -Werror=incomplete-patterns -threaded -rtsopts+    -with-rtsopts=-N+   build-depends:-      HUnit-    , advent-of-code-api-    , base >=4.9 && <5+      advent-of-code-api+    , base                >=4.9 && <5     , directory     , filepath+    , HUnit     , text+   default-language: Haskell2010
src/Advent.hs view
@@ -1,18 +1,17 @@-{-# LANGUAGE CPP                #-}-{-# LANGUAGE DataKinds          #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric      #-}-{-# LANGUAGE GADTs              #-}-{-# LANGUAGE KindSignatures     #-}-{-# LANGUAGE LambdaCase         #-}-{-# LANGUAGE OverloadedStrings  #-}-{-# LANGUAGE PatternSynonyms    #-}-{-# LANGUAGE PolyKinds          #-}-{-# LANGUAGE RecordWildCards    #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TupleSections      #-}-{-# LANGUAGE TypeApplications   #-}-{-# LANGUAGE ViewPatterns       #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ViewPatterns #-}  -- | -- Module      : Advent@@ -45,77 +44,88 @@ -- Please use responsibly.  All actions are by default rate limited to one -- per three seconds, but this can be adjusted to a hard-limited cap of one -- per second.- module Advent (   -- * API-    AoC(..)-  , Part(..)-  , Day(..)-  , NextDayTime(..)-  , DayStats(..)-  , Stats-  , AoCOpts(..)-  , AoCUserAgent(..)-  , SubmitRes(..), showSubmitRes-  , statsForDayPart-  , inferSubmitRes-  , inferSubmitRes_-  , runAoC-  , runAoC_-  , defaultAoCOpts-  , AoCError(..)+  AoC (..),+  Part (..),+  Day (..),+  NextDayTime (..),+  DayStats (..),+  Stats,+  AoCOpts (..),+  AoCUserAgent (..),+  SessionKey (..),+  SubmitRes (..),+  showSubmitRes,+  statsForDayPart,+  inferSubmitRes,+  inferSubmitRes_,+  runAoC,+  runAoC_,+  defaultAoCOpts,+  AoCError (..),+   -- ** Calendar-  , challengeReleaseTime-  , timeToRelease-  , challengeReleased+  challengeReleaseTime,+  timeToRelease,+  challengeReleased,+   -- * Utility+   -- ** Day-  , mkDay, mkDay_, dayInt, pattern DayInt, _DayInt-  , aocDay-  , aocServerTime+  mkDay,+  mkDay_,+  dayInt,+  pattern DayInt,+  _DayInt,+  aocDay,+  aocServerTime,+   -- ** Part-  , partChar, partInt+  partChar,+  partInt,+   -- ** Leaderboard-  , fullDailyBoard+  fullDailyBoard,+   -- ** Throttler-  , setAoCThrottleLimit, getAoCThrottleLimit+  setAoCThrottleLimit,+  getAoCThrottleLimit,+   -- * Internal-  , aocReq-  , aocBase-  ) where+  aocReq,+  aocBase,+) where -import           Advent.API-import           Advent.Cache-import           Advent.Throttle-import           Advent.Types-import           Control.Concurrent.STM-import           Control.Exception-import           Control.Monad-import           Control.Monad.Except-import           Data.Kind-import           Data.Map                (Map)-import           Data.Maybe-import           Data.Set                (Set)-import           Data.Text               (Text)-import           Data.Time hiding        (Day)-import           Data.Typeable-import           GHC.Generics            (Generic)-import           Network.HTTP.Client-import           Network.HTTP.Client.TLS-import           Servant.API-import           Servant.Client-import           System.Directory-import           System.FilePath-import           Text.Printf-import qualified Data.Aeson              as A-import qualified Data.Map                as M-import qualified Data.Set                as S-import qualified Data.Text               as T-import qualified Data.Text.Encoding      as T-import qualified Data.Text.Lazy          as TL+import Advent.API+import Advent.Cache+import Advent.Throttle+import Advent.Types+import Control.Exception+import Control.Monad+import Control.Monad.Except+import qualified Data.Aeson as A+import Data.Either (fromRight)+import Data.Kind+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe+import Data.Set (Set)+import qualified Data.Set as S+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL-import qualified Servant.Client          as Servant-import qualified System.IO.Unsafe        as Unsafe+import Data.Time hiding (Day)+import Data.Typeable+import GHC.Generics (Generic)+import Network.HTTP.Client.TLS+import Servant.API+import Servant.Client+import System.Directory+import System.FilePath+import qualified System.IO.Unsafe as Unsafe+import Text.Printf  #if MIN_VERSION_mtl(2,3,0) import           Control.Monad.IO.Class (liftIO)@@ -152,138 +162,143 @@ -- up to and including Christmas Day (December 25th).  You can convert an -- integer day (1 - 25) into a 'Day' using 'mkDay' or 'mkDay_'. data AoC :: Type -> Type where-    -- | Fetch prompts for a given day.  Returns a 'Map' of 'Part's and-    -- their associated promps, as HTML.-    ---    -- _Cacheing rules_: Is cached on a per-day basis.  An empty session-    -- key is given, it will be happy with only having Part 1 cached.  If-    -- a non-empty session key is given, it will trigger a cache-    -- invalidation on every request until both Part 1 and Part 2 are-    -- received.-    AoCPrompt-        :: Day-        -> AoC (Map Part Text)--    -- | Fetch input, as plaintext.  Returned verbatim.  Be aware that-    -- input might contain trailing newlines.-    ---    -- /Cacheing rules/: Is cached forever, per day per session key.-    AoCInput :: Day -> AoC Text--    -- | Submit a plaintext answer (the 'String') to a given day and part.-    -- Receive a server reponse (as HTML) and a response code 'SubmitRes'.-    ---    -- __WARNING__: Answers are not length-limited.  Answers are stripped-    -- of leading and trailing whitespace and run through 'URI.encode'-    -- before submitting.-    ---    -- /Cacheing rules/: Is never cached.-    AoCSubmit-        :: Day-        -> Part-        -> String-        -> AoC (Text, SubmitRes)--    -- | Fetch the leaderboard for a given leaderboard public code (owner-    -- member ID).  Requires session key.-    ---    -- The public code can be found in the URL of the leaderboard:-    ---    -- > https://adventofcode.com/2019/leaderboard/private/view/12345-    ---    -- (the @12345@ above)-    ---    -- __NOTE__: This is the most expensive and taxing possible API call,-    -- and makes up the majority of bandwidth to the Advent of Code-    -- servers.  As a courtesy to all who are participating in Advent of-    -- Code, please use this super respectfully, especially in December: if-    -- you set up automation for this, please do not use it more than once-    -- per day.-    ---    -- /Cacheing rules/: Is never cached, so please use responsibly (see-    -- note above).-    ---    -- @since 0.2.0.0-    AoCLeaderboard-        :: Integer-        -> AoC Leaderboard--    -- | Fetch the daily leaderboard for a given day.  Does not require-    -- a session key.-    ---    -- Leaderboard API calls tend to be expensive, so please be respectful-    -- when using this.  If you automate this, please do not fetch any more-    -- often than necessary.-    ---    -- /Cacheing rules/: Will be cached if a full leaderboard is observed.-    ---    -- @since 0.2.3.0-    AoCDailyLeaderboard-        :: Day-        -> AoC DailyLeaderboard--    -- | Fetch the global leaderboard.  Does not require-    -- a session key.-    ---    -- Leaderboard API calls tend to be expensive, so please be respectful-    -- when using this.  If you automate this, please do not fetch any more-    -- often than necessary.-    ---    -- /Cacheing rules/: Will not cache if an event is ongoing, but will be-    -- cached if received after the event is over.-    ---    -- @since 0.2.3.0-    AoCGlobalLeaderboard-        :: AoC GlobalLeaderboard--    -- | From the calendar, fetch the next release's day and the-    -- number of seconds util its release, if there is any at all.-    ---    -- This does an actual request to the AoC servers, and is only accurate-    -- to the second; to infer this information (to the millisecond level)-    -- from the system clock, you should probably use 'timeToRelease' and-    -- 'aocServerTime' instead, which requires no network requests.-    ---    -- @since 0.2.8.0-    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+  -- | Fetch prompts for a given day.  Returns a 'Map' of 'Part's and+  -- their associated promps, as HTML.+  --+  -- _Cacheing rules_: Is cached on a per-day basis.  An empty session+  -- key is given, it will be happy with only having Part 1 cached.  If+  -- a non-empty session key is given, it will trigger a cache+  -- invalidation on every request until both Part 1 and Part 2 are+  -- received.+  AoCPrompt ::+    Day ->+    AoC (Map Part Text)+  -- | Fetch input, as plaintext.  Returned verbatim.  Be aware that+  -- input might contain trailing newlines.+  --+  -- /Cacheing rules/: Is cached forever, per day per session key.+  AoCInput :: Day -> AoC Text+  -- | Submit a plaintext answer (the 'String') to a given day and part.+  -- Receive a server reponse (as HTML) and a response code 'SubmitRes'.+  --+  -- __WARNING__: Answers are not length-limited.  Answers are stripped+  -- of leading and trailing whitespace and run through 'URI.encode'+  -- before submitting.+  --+  -- /Cacheing rules/: Is never cached.+  AoCSubmit ::+    Day ->+    Part ->+    String ->+    AoC (Text, SubmitRes)+  -- | Fetch the leaderboard for a given leaderboard public code (owner+  -- member ID).  Requires session key.+  --+  -- The public code can be found in the URL of the leaderboard:+  --+  -- > https://adventofcode.com/2019/leaderboard/private/view/12345+  --+  -- (the @12345@ above)+  --+  -- __NOTE__: This is the most expensive and taxing possible API call,+  -- and makes up the majority of bandwidth to the Advent of Code+  -- servers.  As a courtesy to all who are participating in Advent of+  -- Code, please use this super respectfully, especially in December: if+  -- you set up automation for this, please do not use it more than once+  -- per day.+  --+  -- /Cacheing rules/: Is never cached, so please use responsibly (see+  -- note above).+  --+  -- @since 0.2.0.0+  AoCLeaderboard ::+    Integer ->+    AoC Leaderboard+  -- | Fetch the daily leaderboard for a given day.  Does not require+  -- a session key.+  --+  -- Leaderboard API calls tend to be expensive, so please be respectful+  -- when using this.  If you automate this, please do not fetch any more+  -- often than necessary.+  --+  -- /Cacheing rules/: Will be cached if a full leaderboard is observed.+  --+  -- @since 0.2.3.0+  AoCDailyLeaderboard ::+    Day ->+    AoC DailyLeaderboard+  -- | Fetch the global leaderboard.  Does not require+  -- a session key.+  --+  -- Leaderboard API calls tend to be expensive, so please be respectful+  -- when using this.  If you automate this, please do not fetch any more+  -- often than necessary.+  --+  -- /Cacheing rules/: Will not cache if an event is ongoing, but will be+  -- cached if received after the event is over.+  --+  -- @since 0.2.3.0+  AoCGlobalLeaderboard ::+    AoC GlobalLeaderboard+  -- | From the calendar, fetch the next release's day and the+  -- number of seconds util its release, if there is any at all.+  --+  -- This does an actual request to the AoC servers, and is only accurate+  -- to the second; to infer this information (to the millisecond level)+  -- from the system clock, you should probably use 'timeToRelease' and+  -- 'aocServerTime' instead, which requires no network requests.+  --+  -- @since 0.2.8.0+  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)  -- | Get the day associated with a given API command, if there is one. aocDay :: AoC a -> Maybe Day-aocDay (AoCPrompt d     ) = Just d-aocDay (AoCInput  d     ) = Just d-aocDay (AoCSubmit d _ _ ) = Just d+aocDay (AoCPrompt d) = Just d+aocDay (AoCInput d) = Just d+aocDay (AoCSubmit d _ _) = Just d aocDay (AoCLeaderboard _) = Nothing aocDay (AoCDailyLeaderboard d) = Just d aocDay AoCGlobalLeaderboard = Nothing-aocDay AoCNextDayTime       = Nothing-aocDay AoCStats             = Nothing+aocDay AoCNextDayTime = Nothing+aocDay AoCStats = Nothing  -- | A possible (syncronous, logical, pure) error returnable from 'runAoC'. -- Does not cover any asynchronous or IO errors.+#if MIN_VERSION_servant_client_core(0,16,0) data AoCError     -- | An error in the http request itself     --     -- Note that if you are building this with servant-client-core <= 0.16,     -- this will contain @ServantError@ instead of @ClientError@, which was     -- the previous name of ths type.-#if MIN_VERSION_servant_client_core(0,16,0)     = AoCClientError ClientError+    -- | Tried to interact with a challenge that has not yet been+    -- released.  Contains the amount of time until release.+    | AoCReleaseError NominalDiffTime+    -- | The throttler limit is full.  Either make less requests, or adjust+    -- it with 'setAoCThrottleLimit'.+    | AoCThrottleError+  deriving (Show, Typeable, Generic) #else+data AoCError+    -- | An error in the http request itself+    --+    -- Note that if you are building this with servant-client-core <= 0.16,+    -- this will contain @ServantError@ instead of @ClientError@, which was+    -- the previous name of ths type.     = AoCClientError ServantError-#endif     -- | Tried to interact with a challenge that has not yet been     -- released.  Contains the amount of time until release.     | AoCReleaseError NominalDiffTime@@ -291,6 +306,7 @@     -- it with 'setAoCThrottleLimit'.     | AoCThrottleError   deriving (Show, Typeable, Generic)+#endif instance Exception AoCError  -- | Setings for running an API request.@@ -305,27 +321,27 @@ -- Throttling is hard-limited to a minimum of 1 second between calls. -- Please be respectful and do not try to bypass this. data AoCOpts = AoCOpts-    { -- | Session key-      _aSessionKey :: String-      -- | Year of challenge-    , _aYear       :: Integer-      -- | Structured user agent to use.  See-      -- <https://www.reddit.com/r/adventofcode/comments/z9dhtd/please_include_your_contact_info_in_the_useragent/>-    , _aUserAgent  :: AoCUserAgent-      -- | Cache directory.  If 'Nothing' is given, one will be allocated-      -- using 'getTemporaryDirectory'.-    , _aCache      :: Maybe FilePath-      -- | Fetch results even if cached.  Still subject to throttling.-      -- Default is False.-    , _aForce      :: Bool-      -- | 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-    }+  { _aSessionKey :: String+  -- ^ Session key+  , _aYear :: Integer+  -- ^ Year of challenge+  , _aUserAgent :: AoCUserAgent+  -- ^ Structured user agent to use.  See+  -- <https://www.reddit.com/r/adventofcode/comments/z9dhtd/please_include_your_contact_info_in_the_useragent/>+  , _aCache :: Maybe FilePath+  -- ^ Cache directory.  If 'Nothing' is given, one will be allocated+  -- using 'getTemporaryDirectory'.+  , _aForce :: Bool+  -- ^ Fetch results even if cached.  Still subject to throttling.+  -- Default is False.+  , _aThrottle :: Int+  -- ^ Throttle delay, in milliseconds.  Minimum is 1000000.  Default+  -- is 3000000 (3 seconds).+  , _aInferRankOnSubmission :: Bool+  -- ^ Attempt to infer submission rank by fetching stats after a+  -- correct submission that doesn't include rank information.+  -- Default is False.+  }   deriving (Show, Typeable, Generic)  -- | Sensible defaults for 'AoCOpts' for a given user agent, year and session@@ -333,18 +349,19 @@ -- -- Use system temporary directory as cache, and throttle requests to one -- request per three seconds.-defaultAoCOpts-    :: AoCUserAgent-    -> Integer-    -> String-    -> AoCOpts-defaultAoCOpts aua y s = AoCOpts+defaultAoCOpts ::+  AoCUserAgent ->+  Integer ->+  String ->+  AoCOpts+defaultAoCOpts aua y s =+  AoCOpts     { _aSessionKey = s-    , _aYear       = y-    , _aUserAgent  = aua-    , _aCache      = Nothing-    , _aForce      = False-    , _aThrottle   = 3000000+    , _aYear = y+    , _aUserAgent = aua+    , _aCache = Nothing+    , _aForce = False+    , _aThrottle = 3000000     , _aInferRankOnSubmission = False     } @@ -353,43 +370,52 @@ aocBase = BaseUrl Https "adventofcode.com" 443 ""  -- | 'ClientM' request for a given 'AoC' API call.-aocReq :: Maybe AoCUserAgent -> Integer -> AoC a -> ClientM a-aocReq aua yr = \case-    AoCPrompt i       -> let r :<|> _        = adventAPIPuzzleClient aua yr i in r-    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-                         in  r (PublicCode c)-    AoCDailyLeaderboard d -> let _ :<|> _ :<|> _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr-                             in  r d-    AoCGlobalLeaderboard  -> let _ :<|> _ :<|> _ :<|> r :<|> _ :<|> _ = adventAPIClient aua yr-                             in  r-    AoCNextDayTime        -> let r :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ = adventAPIClient aua yr-                             in  r-    AoCStats              -> let _ :<|> r :<|> _ :<|> _ :<|> _ :<|> _ = adventAPIClient aua yr-                             in  r-+aocReq :: Maybe AoCUserAgent -> Maybe SessionKey -> Integer -> AoC a -> ClientM a+aocReq aua sess yr = \case+  AoCPrompt i -> let r :<|> _ :<|> _ = adventAPIPuzzleClient aua yr i in r sess+  AoCInput i -> let _ :<|> r :<|> _ = adventAPIPuzzleClient aua yr i in r (reqSess sess)+  AoCSubmit i p ans ->+    let _ :<|> _ :<|> r = adventAPIPuzzleClient aua yr i+     in r (reqSess sess) (SubmitInfo p ans) <&> \(x :<|> y) -> (x, y)+  AoCLeaderboard c ->+    let _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> r = adventAPIClient aua yr+     in r (reqSess sess) (PublicCode c)+  AoCDailyLeaderboard d ->+    let _ :<|> _ :<|> _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr+     in r d+  AoCGlobalLeaderboard ->+    let _ :<|> _ :<|> _ :<|> r :<|> _ :<|> _ = adventAPIClient aua yr+     in r+  AoCNextDayTime ->+    let r :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ = adventAPIClient aua yr+     in r+  AoCStats ->+    let _ :<|> r :<|> _ :<|> _ :<|> _ :<|> _ = adventAPIClient aua yr+     in r+  where+    reqSess = fromMaybe dummyToken  -- | Cache file for a given 'AoC' command-apiCache-    :: Maybe String           -- ^ session key-    -> Integer                -- ^ year-    -> AoC a-    -> Maybe FilePath+apiCache ::+  -- | session key+  Maybe String ->+  -- | year+  Integer ->+  AoC a ->+  Maybe FilePath apiCache sess yr = \case-    AoCPrompt d      -> Just $ printf "prompt/%04d/%02d.html"        yr (dayInt d)-    AoCInput  d      -> Just $ printf "input/%s%04d/%02d.txt" keyDir yr (dayInt d)-    AoCSubmit{}      -> Nothing-    AoCLeaderboard{} -> Nothing-    AoCDailyLeaderboard d  -> Just $ printf "daily/%04d/%02d.json" yr (dayInt d)-    AoCGlobalLeaderboard{} -> Just $ printf "global/%04d.json" yr-    AoCNextDayTime         -> Nothing-    AoCStats               -> Nothing+  AoCPrompt d -> Just $ printf "prompt/%04d/%02d.html" yr (dayInt d)+  AoCInput d -> Just $ printf "input/%s%04d/%02d.txt" keyDir yr (dayInt d)+  AoCSubmit{} -> Nothing+  AoCLeaderboard{} -> Nothing+  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 -> ""-      Just s  -> strip s ++ "/"+      Just s -> strip s ++ "/"  -- | Run an 'AoC' command with a given 'AoCOpts' to produce the result -- or a list of (lines of) errors.@@ -399,42 +425,49 @@ -- before submitting. runAoC :: AoCOpts -> AoC a -> IO (Either AoCError a) runAoC opts@AoCOpts{..} a = do-    (keyMayb, cacheDir) <- case _aCache of-      Just c  -> pure (Nothing, c)-      Nothing -> (Just _aSessionKey,) . (</> "advent-of-code-api") <$> getTemporaryDirectory+  (keyMayb, cacheDir) <- case _aCache of+    Just c -> pure (Nothing, c)+    Nothing -> (Just _aSessionKey,) . (</> "advent-of-code-api") <$> getTemporaryDirectory -    (yy,mm,dd) <- toGregorian-                . localDay-                . utcToLocalTime (read "EST")-              <$> getCurrentTime-    let eventOver = yy > _aYear-                 || (mm == 12 && dd > 25)-        cacher = case apiCache keyMayb _aYear a of-          Nothing -> id-          Just fp -> cacheing (cacheDir </> fp) $-                       if _aForce-                         then noCache-                         else saverLoader-                                (not (null _aSessionKey))-                                (not eventOver)-                                a+  (yy, mm, dd) <-+    toGregorian+      . localDay+      . utcToLocalTime (read "EST")+      <$> getCurrentTime+  let eventOver =+        yy > _aYear+          || (mm == 12 && dd > 25)+      cacher = case apiCache keyMayb _aYear a of+        Nothing -> id+        Just fp ->+          cacheing (cacheDir </> fp) $+            if _aForce+              then noCache+              else+                saverLoader+                  (not (null _aSessionKey))+                  (not eventOver)+                  a -    cacher . runExceptT $ do-      forM_ (aocDay a) $ \d -> do-        rel <- liftIO $ timeToRelease _aYear d-        when (rel > 0) $-          throwError $ AoCReleaseError rel+  cacher . runExceptT $ do+    forM_ (aocDay a) $ \d -> do+      rel <- liftIO $ timeToRelease _aYear d+      when (rel > 0) $+        throwError $+          AoCReleaseError rel -      mtr <- liftIO-           . throttling aocThrottler (max 1000000 _aThrottle)-           $ runClientM (aocReq (Just _aUserAgent) _aYear a) =<< aocClientEnv _aSessionKey-      mcr <- maybe (throwError AoCThrottleError) pure mtr-      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+    let sess = SessionKey (T.pack _aSessionKey)+    mtr <-+      liftIO+        . throttling aocThrottler (max 1000000 _aThrottle)+        $ runClientM (aocReq (Just _aUserAgent) (Just sess) _aYear a) =<< aocClientEnv+    mcr <- maybe (throwError AoCThrottleError) pure mtr+    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'.@@ -448,7 +481,7 @@ -- @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+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@@ -458,122 +491,116 @@ -- unchanged. -- -- @since 0.2.11.0-inferSubmitRes-    :: AoCOpts-    -> Day-    -> Part-    -> SubmitRes-    -> IO (Either AoCError SubmitRes)+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+  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-    v <- atomically . newTVar $ createCookieJar [c t]-    mgr <- newTlsManager-    pure $ (mkClientEnv mgr aocBase)-        { Servant.cookieJar = Just v }-  where-    c t = Cookie-      { cookie_name             = "session"-      , cookie_value            = T.encodeUtf8 . T.pack $ s-      , cookie_expiry_time      = addUTCTime oneYear t-      , cookie_domain           = "adventofcode.com"-      , cookie_path             = "/"-      , cookie_creation_time    = t-      , cookie_last_access_time = t-      , cookie_persistent       = True-      , cookie_host_only        = True-      , cookie_secure_only      = True-      , cookie_http_only        = True-      }-    oneYear = 60 * 60 * 24 * 356.25+inferSubmitRes_ ::+  AoCOpts ->+  Day ->+  Part ->+  SubmitRes ->+  IO SubmitRes+inferSubmitRes_ opts d p sr = fromRight sr <$> inferSubmitRes opts d p sr +aocClientEnv :: IO ClientEnv+aocClientEnv = (`mkClientEnv` aocBase) <$> newTlsManager -saverLoader-    :: Bool             -- ^ is there a non-empty session token?-    -> Bool             -- ^ is the event ongoing (True) or over (False)?-    -> AoC a-    -> SaverLoader (Either AoCError a)+saverLoader ::+  -- | is there a non-empty session token?+  Bool ->+  -- | is the event ongoing (True) or over (False)?+  Bool ->+  AoC a ->+  SaverLoader (Either AoCError a) saverLoader validToken evt = \case-    AoCPrompt{} -> SL { _slSave = either (const Nothing) (Just . encodeMap)-                      , _slLoad = \str ->-                          let mp     = decodeMap str-                              hasAll = S.null (expectedParts `S.difference` M.keysSet mp)-                          in  Right mp <$ guard hasAll-                      }-    AoCInput{}  -> SL { _slSave = either (const Nothing) Just-                      , _slLoad = Just . Right-                      }-    AoCSubmit{} -> noCache-    AoCLeaderboard{} -> noCache-    AoCDailyLeaderboard{} -> SL-        { _slSave = either (const Nothing) (Just . TL.toStrict . TL.decodeUtf8 . A.encode)-        , _slLoad = \str -> do-            r <- A.decode . TL.encodeUtf8 . TL.fromStrict $ str-            guard $ fullDailyBoard r-            pure $ Right r-        }-    AoCGlobalLeaderboard{} -> SL-        { _slSave = either-                        (const Nothing)-                        (Just . TL.toStrict . TL.decodeUtf8 . A.encode @(Bool, GlobalLeaderboard) . (evt,))-        , _slLoad = \str -> do-            (evt', lb) <- A.decode @(Bool, GlobalLeaderboard) . TL.encodeUtf8 . TL.fromStrict $ str-            guard $ not evt'        -- only load cache if evt' is false: it was saved in a non-evt time-            pure $ Right lb-        }-    AoCNextDayTime{} -> noCache-    AoCStats{} -> noCache+  AoCPrompt{} ->+    SL+      { _slSave = either (const Nothing) (Just . encodeMap)+      , _slLoad = \str ->+          let mp = decodeMap str+              hasAll = S.null (expectedParts `S.difference` M.keysSet mp)+           in Right mp <$ guard hasAll+      }+  AoCInput{} ->+    SL+      { _slSave = either (const Nothing) Just+      , _slLoad = Just . Right+      }+  AoCSubmit{} -> noCache+  AoCLeaderboard{} -> noCache+  AoCDailyLeaderboard{} ->+    SL+      { _slSave = either (const Nothing) (Just . TL.toStrict . TL.decodeUtf8 . A.encode)+      , _slLoad = \str -> do+          r <- A.decode . TL.encodeUtf8 . TL.fromStrict $ str+          guard $ fullDailyBoard r+          pure $ Right r+      }+  AoCGlobalLeaderboard{} ->+    SL+      { _slSave =+          either+            (const Nothing)+            (Just . TL.toStrict . TL.decodeUtf8 . A.encode @(Bool, GlobalLeaderboard) . (evt,))+      , _slLoad = \str -> do+          (evt', lb) <- A.decode @(Bool, GlobalLeaderboard) . TL.encodeUtf8 . TL.fromStrict $ str+          guard $ not evt' -- only load cache if evt' is false: it was saved in a non-evt time+          pure $ Right lb+      }+  AoCNextDayTime{} -> noCache+  AoCStats{} -> noCache   where     expectedParts :: Set Part     expectedParts       | validToken = S.fromDistinctAscList [Part1 ..]-      | otherwise  = S.singleton Part1+      | otherwise = S.singleton Part1     sep = ">>>>>>>>>"-    encodeMap mp = T.intercalate "\n" . concat $-                            [ maybeToList $ M.lookup Part1 mp-                            , [sep]-                            , maybeToList $ M.lookup Part2 mp-                            ]+    encodeMap mp =+      T.intercalate "\n" . concat $+        [ maybeToList $ M.lookup Part1 mp+        , [sep]+        , maybeToList $ M.lookup Part2 mp+        ]     decodeMap xs = mkMap Part1 part1 <> mkMap Part2 part2       where         (part1, drop 1 -> part2) = span (/= sep) (T.lines xs)-        mkMap p (T.intercalate "\n"->ln)+        mkMap p (T.intercalate "\n" -> ln)           | T.null (T.strip ln) = M.empty-          | otherwise           = M.singleton p ln+          | otherwise = M.singleton p ln  -- | Get time until release of a given challenge.-timeToRelease-    :: Integer              -- ^ year-    -> Day                  -- ^ day-    -> IO NominalDiffTime+timeToRelease ::+  -- | year+  Integer ->+  -- | day+  Day ->+  IO NominalDiffTime timeToRelease y d = (zonedTimeToUTC (challengeReleaseTime y d) `diffUTCTime`) <$> getCurrentTime  -- | Check if a challenge has been released yet.-challengeReleased-    :: Integer              -- ^ year-    -> Day                  -- ^ day-    -> IO Bool+challengeReleased ::+  -- | year+  Integer ->+  -- | day+  Day ->+  IO Bool challengeReleased y = fmap (<= 0) . timeToRelease y  -- | Utility to get the current time on AoC servers.  Basically just gets the current
src/Advent/API.hs view
@@ -1,16 +1,16 @@-{-# LANGUAGE CPP                   #-}-{-# LANGUAGE DataKinds             #-}-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE FlexibleInstances     #-}-{-# LANGUAGE LambdaCase            #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE PolyKinds             #-}-{-# LANGUAGE RecordWildCards       #-}-{-# LANGUAGE ScopedTypeVariables   #-}-{-# LANGUAGE TypeApplications      #-}-{-# LANGUAGE TypeOperators         #-}-{-# LANGUAGE ViewPatterns          #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ViewPatterns #-}  -- | -- Module      : Advent.API@@ -30,54 +30,56 @@ -- manual requestes.  See notes in "Advent". -- -- @since 0.2.0.0---- module Advent.API (   -- * Servant API-    AdventAPI-  , AoCUserAgent(..)-  , adventAPI-  , adventAPIClient-  , adventAPIPuzzleClient+  AdventAPI,+  AoCUserAgent (..),+  SessionKey (..),+  dummyToken,+  adventAPI,+  adventAPIClient,+  adventAPIPuzzleClient,+   -- * Types-  , HTMLTags-  , FromTags(..)-  , Articles-  , Divs-  , Scripts-  , RawText+  HTMLTags,+  FromTags (..),+  Articles,+  Divs,+  Scripts,+  RawText,+   -- * Internal-  , processHTML-  ) where+  processHTML,+) where -import           Advent.Types-import           Control.Applicative-import           Control.Monad-import           Control.Monad.State-import           Data.Bifunctor-import           Data.Char-import           Data.Finite-import           Data.Foldable-import           Data.List.NonEmpty     (NonEmpty(..))-import           Data.Map               (Map)-import           Data.Maybe-import           Data.Ord-import           Data.Proxy-import           Data.Text              (Text)-import           Data.Time hiding       (Day)-import           GHC.TypeLits-import           Servant.API-import           Servant.Client-import           Text.HTML.TagSoup.Tree (TagTree(..))-import           Text.Read              (readMaybe)-import qualified Data.ByteString.Lazy   as BSL-import qualified Data.List.NonEmpty     as NE-import qualified Data.Map               as M-import qualified Data.Text              as T-import qualified Data.Text.Encoding     as T-import qualified Network.HTTP.Media     as M-import qualified Text.HTML.TagSoup      as H+import Advent.Types+import Control.Applicative+import Control.Monad+import Control.Monad.State+import Data.Bifunctor+import qualified Data.ByteString.Lazy as BSL+import Data.Char+import Data.Finite+import Data.Foldable+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe+import Data.Ord+import Data.Proxy+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Time hiding (Day)+import GHC.TypeLits+import qualified Network.HTTP.Media as M+import Servant.API+import Servant.Client+import qualified Text.HTML.TagSoup as H+import Text.HTML.TagSoup.Tree (TagTree (..)) import qualified Text.HTML.TagSoup.Tree as H+import Text.Read (readMaybe)  #if !MIN_VERSION_base(4,11,0) import           Data.Semigroup ((<>))@@ -91,10 +93,10 @@ data RawText  instance Accept RawText where-    contentType _ = "text" M.// "plain"+  contentType _ = "text" M.// "plain"  instance MimeUnrender RawText Text where-    mimeUnrender _ = first show . T.decodeUtf8' . BSL.toStrict+  mimeUnrender _ = first show . T.decodeUtf8' . BSL.toStrict  -- | Interpret repsonse as a list of HTML 'T.Text' found in the given type of -- tag@@ -108,7 +110,7 @@ -- | Interpret a response as a list of HTML 'T.Text' found in @<div>@ tags. -- -- @since 0.2.3.0-type Divs     = HTMLTags "div"+type Divs = HTMLTags "div"  -- | Interpret a response as a list of HTML 'T.Text' found in @<script>@ tags. type Scripts = HTMLTags "script"@@ -121,179 +123,215 @@ -- -- @since 0.2.3.0 class FromTags tag a where-    fromTags :: p tag -> [Text] -> Maybe a+  fromTags :: p tag -> [Text] -> Maybe a  instance Accept (HTMLTags cls) where-    contentType _ = "text" M.// "html"+  contentType _ = "text" M.// "html"  instance (FromTags tag a, KnownSymbol tag) => MimeUnrender (HTMLTags tag) a where-    mimeUnrender _ str = do-      x <- first show . T.decodeUtf8' . BSL.toStrict $ str-      maybe (Left "No parse") pure-         . fromTags (Proxy @tag)-         . processHTML (symbolVal (Proxy @tag))-         $ x+  mimeUnrender _ str = do+    x <- first show . T.decodeUtf8' . BSL.toStrict $ str+    maybe (Left "No parse") pure+      . fromTags (Proxy @tag)+      . processHTML (symbolVal (Proxy @tag))+      $ x  instance FromTags cls [Text] where-    fromTags _ = Just+  fromTags _ = Just  instance FromTags cls Text where-    fromTags _ = Just . T.unlines+  fromTags _ = Just . T.unlines  instance (Ord a, Enum a, Bounded a) => FromTags cls (Map a Text) where-    fromTags _ = Just . M.fromList . zip [minBound ..]+  fromTags _ = Just . M.fromList . zip [minBound ..]  instance (FromTags cls a, FromTags cls b) => FromTags cls (a :<|> b) where-    fromTags p xs = (:<|>) <$> fromTags p xs <*> fromTags p xs+  fromTags p xs = (:<|>) <$> fromTags p xs <*> fromTags p xs  instance FromTags "article" SubmitRes where-    fromTags _ = Just . parseSubmitRes . fold  . listToMaybe+  fromTags _ = Just . parseSubmitRes . fold . listToMaybe  instance FromTags "div" DailyLeaderboard where-    fromTags _ = Just . assembleDLB . mapMaybe parseMember-      where-        parseMember :: Text -> Maybe DailyLeaderboardMember-        parseMember contents = do-            dlbmRank <- fmap Rank . packFinite . subtract 1-                    =<< readMaybe . filter isDigit . T.unpack . fst-                    =<< findTag uni "span" (Just "leaderboard-position")-            dlbmDecTime <- fmap mkDiff-                         . parseTimeM True defaultTimeLocale "%b %d  %H:%M:%S"-                         . T.unpack . fst-                       =<< findTag uni "span" (Just "leaderboard-time")-            dlbmUser <- eitherUser tr-            pure DLBM{..}-          where-            dlbmLink      = lookup "href" . snd =<< findTag uni "a" Nothing-            dlbmSupporter = "AoC++" `T.isInfixOf` contents-            dlbmImage     = lookup "src" . snd =<< findTag uni "img" Nothing-            tr  = H.parseTree contents-            uni = H.universeTree tr-        assembleDLB = flipper . snd . foldl' (uncurry go) (Nothing, DLB M.empty M.empty)-          where-            flipper dlb@(DLB a b)-              | M.null a  = DLB b a-              | otherwise = dlb-            go counter dlb m@DLBM{..} = case counter of-                Nothing      -> dlb2-                Just Nothing -> dlb1-                Just (Just i)-                  | dlbmRank <= i -> dlb1-                  | otherwise     -> dlb2-              where-                dlb1 = (Just Nothing        , dlb { dlbStar1 = M.insert dlbmRank m (dlbStar1 dlb) })-                dlb2 = (Just (Just dlbmRank), dlb { dlbStar2 = M.insert dlbmRank m (dlbStar2 dlb) })-        mkDiff t = t `diffLocalTime` decemberFirst-        decemberFirst = LocalTime (fromGregorian 1970 12 1) midnight+  fromTags _ = Just . assembleDLB . mapMaybe parseMember+    where+      parseMember :: Text -> Maybe DailyLeaderboardMember+      parseMember contents = do+        dlbmRank <-+          fmap Rank . packFinite . subtract 1+            =<< readMaybe . filter isDigit . T.unpack . fst+            =<< findTag uni "span" (Just "leaderboard-position")+        dlbmDecTime <-+          fmap mkDiff+            . parseTimeM True defaultTimeLocale "%b %d  %H:%M:%S"+            . T.unpack+            . fst+            =<< findTag uni "span" (Just "leaderboard-time")+        dlbmUser <- eitherUser tr+        pure DLBM{..}+        where+          dlbmLink = lookup "href" . snd =<< findTag uni "a" Nothing+          dlbmSupporter = "AoC++" `T.isInfixOf` contents+          dlbmImage = lookup "src" . snd =<< findTag uni "img" Nothing+          tr = H.parseTree contents+          uni = H.universeTree tr+      assembleDLB = flipper . snd . foldl' (uncurry go) (Nothing, DLB M.empty M.empty)+        where+          flipper dlb@(DLB a b)+            | M.null a = DLB b a+            | otherwise = dlb+          go counter dlb m@DLBM{..} = case counter of+            Nothing -> dlb2+            Just Nothing -> dlb1+            Just (Just i)+              | dlbmRank <= i -> dlb1+              | otherwise -> dlb2+            where+              dlb1 = (Just Nothing, dlb{dlbStar1 = M.insert dlbmRank m (dlbStar1 dlb)})+              dlb2 = (Just (Just dlbmRank), dlb{dlbStar2 = M.insert dlbmRank m (dlbStar2 dlb)})+      mkDiff t = t `diffLocalTime` decemberFirst+      decemberFirst = LocalTime (fromGregorian 1970 12 1) midnight  instance FromTags "div" GlobalLeaderboard where-    fromTags _ = Just . GLB . reScore . M.fromListWith (<>)-               . map (\x -> (Down (glbmScore x), x :| []))-               . mapMaybe parseMember-      where-        parseMember :: Text -> Maybe GlobalLeaderboardMember-        parseMember contents = do-            glbmScore <- readMaybe . filter isDigit . T.unpack . fst-                     =<< findTag uni "span" (Just "leaderboard-totalscore")-            glbmUser <- eitherUser tr-            pure GLBM{..}-          where-            glbmRank      = Rank 0-            glbmLink      = lookup "href" . snd =<< findTag uni "a" Nothing-            glbmSupporter = "AoC++" `T.isInfixOf` contents-            glbmImage     = lookup "src" . snd =<< findTag uni "img" Nothing-            tr  = H.parseTree contents-            uni = H.universeTree tr-        reScore = fmap (\xs -> (glbmScore (NE.head xs), xs))-                . M.fromList-                . flip evalState 0-                . traverse go-                . toList-          where-            go xs = do-              currScore <- get-              xs' <- forM xs $ \x -> x { glbmRank = Rank currScore } <$ modify succ-              pure (Rank currScore, xs')+  fromTags _ =+    Just+      . GLB+      . reScore+      . M.fromListWith (<>)+      . map (\x -> (Down (glbmScore x), x :| []))+      . mapMaybe parseMember+    where+      parseMember :: Text -> Maybe GlobalLeaderboardMember+      parseMember contents = do+        glbmScore <-+          readMaybe . filter isDigit . T.unpack . fst+            =<< findTag uni "span" (Just "leaderboard-totalscore")+        glbmUser <- eitherUser tr+        pure GLBM{..}+        where+          glbmRank = Rank 0+          glbmLink = lookup "href" . snd =<< findTag uni "a" Nothing+          glbmSupporter = "AoC++" `T.isInfixOf` contents+          glbmImage = lookup "src" . snd =<< findTag uni "img" Nothing+          tr = H.parseTree contents+          uni = H.universeTree tr+      reScore =+        fmap (\xs -> (glbmScore (NE.head xs), xs))+          . M.fromList+          . flip evalState 0+          . traverse go+          . toList+        where+          go xs = do+            currScore <- get+            xs' <- forM xs $ \x -> x{glbmRank = Rank currScore} <$ modify succ+            pure (Rank currScore, xs')  instance FromTags "script" NextDayTime where-    fromTags _ = (<|> Just NoNextDayTime) . listToMaybe . mapMaybe findNDT-      where-        -- var server_eta = 25112;-        -- var key = "2020-15-"+server_eta;-        findNDT body = do-          eta    <- T.unpack <$> grabKey "server_eta" body-          yd     <- grabKey "key" body-          sec    <- readMaybe eta-          dayStr <- listToMaybe . drop 1 . T.splitOn "-" $ yd-          dy     <- mkDay =<< readMaybe (T.unpack dayStr)-          pure $ NextDayTime dy sec-        grabKey t str =-            fst . T.breakOn ";\n" <$> T.stripPrefix t' (snd (T.breakOn t' str))-          where-            t' = "var " <> t <> " = "+  fromTags _ = (<|> Just NoNextDayTime) . listToMaybe . mapMaybe findNDT+    where+      -- var server_eta = 25112;+      -- var key = "2020-15-"+server_eta;+      findNDT body = do+        eta <- T.unpack <$> grabKey "server_eta" body+        yd <- grabKey "key" body+        sec <- readMaybe eta+        dayStr <- listToMaybe . drop 1 . T.splitOn "-" $ yd+        dy <- mkDay =<< readMaybe (T.unpack dayStr)+        pure $ NextDayTime dy sec+      grabKey t str =+        fst . T.breakOn ";\n" <$> T.stripPrefix t' (snd (T.breakOn t' str))+        where+          t' = "var " <> t <> " = "  instance FromTags "pre" Stats where-    fromTags _ = listToMaybe . mapMaybe parseStats+  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-    { _auaRepo :: Text      -- ^ repository where your code is hosted-    , _auaEmail :: Text     -- ^ email address or contact-    }+  { _auaRepo :: Text+  -- ^ repository where your code is hosted+  , _auaEmail :: Text+  -- ^ email address or contact+  }   deriving (Show)  instance ToHttpApiData AoCUserAgent where   toQueryParam AoCUserAgent{..} = _auaRepo <> " " <> _auaEmail --- | REST API of Advent of Code.+-- | An AoC @session@ cookie value. See README and docs for how to get this. ----- Note that most of these requests assume a "session=" cookie.+-- @since 0.2.12.0+newtype SessionKey = SessionKey {getSessionKey :: Text}+  deriving (Show, Eq, Ord)++instance ToHttpApiData SessionKey where+  toQueryParam (SessionKey s) = "session=" <> s++-- | Placeholder used when a 'Required' 'SessionKey' is needed but none was+-- given.+dummyToken :: SessionKey+dummyToken = SessionKey T.empty++-- | REST API of Advent of Code. 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)+  Header "User-Agent" AoCUserAgent+    :> Capture "year" Integer+    :> ( Get '[Scripts] NextDayTime+           :<|> "stats" :> Get '[Pres] Stats+           :<|> "day"+             :> Capture "day" Day+             :> ( Header "Cookie" SessionKey+                    :> Get '[Articles] (Map Part Text)+                    :<|> "input"+                      :> Header' '[Required, Strict] "Cookie" SessionKey+                      :> Get '[RawText] Text+                    :<|> "answer"+                      :> Header' '[Required, Strict] "Cookie" SessionKey+                      :> 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-       ))-      )-+           :<|> ( "leaderboard"+                    :> ( Get '[Divs] GlobalLeaderboard+                           :<|> "day" :> Capture "day" Day :> Get '[Divs] DailyLeaderboard+                           :<|> "private"+                             :> "view"+                             :> Header' '[Required, Strict] "Cookie" SessionKey+                             :> Capture "code" PublicCode+                             :> Get '[JSON] Leaderboard+                       )+                )+       )  -- | 'Proxy' used for /servant/ functions. adventAPI :: Proxy AdventAPI adventAPI = Proxy  -- | 'ClientM' requests based on 'AdventAPI', generated by servant.-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)+adventAPIClient ::+  Maybe AoCUserAgent ->+  Integer ->+  ClientM NextDayTime+    :<|> ClientM Stats+    :<|> ( Day ->+           (Maybe SessionKey -> ClientM (Map Part Text))+             :<|> (SessionKey -> ClientM Text)+             :<|> (SessionKey -> SubmitInfo -> ClientM (Text :<|> SubmitRes))+         )+    :<|> ClientM GlobalLeaderboard+    :<|> (Day -> ClientM DailyLeaderboard)+    :<|> (SessionKey -> PublicCode -> ClientM Leaderboard) adventAPIClient = client adventAPI  -- | A subset of 'adventAPIClient' for only puzzle-related API routes, not -- leaderboard ones.-adventAPIPuzzleClient-    :: Maybe AoCUserAgent-    -> Integer-    -> Day-    -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes))+adventAPIPuzzleClient ::+  Maybe AoCUserAgent ->+  Integer ->+  Day ->+  (Maybe SessionKey -> ClientM (Map Part Text))+    :<|> (SessionKey -> ClientM Text)+    :<|> (SessionKey -> SubmitInfo -> ClientM (Text :<|> SubmitRes)) adventAPIPuzzleClient aua y = pis   where     _ :<|> _ :<|> pis :<|> _ = adventAPIClient aua y@@ -302,15 +340,15 @@ parseStats = fmap M.fromList . traverse parseStatLine . filter asBranch . H.parseTree   where     asBranch TagBranch{} = True-    asBranch _           = False+    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)+  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@@ -323,11 +361,11 @@ innerText = T.concat . mapMaybe go . H.universeTree   where     go (TagLeaf (H.TagText t)) = Just t-    go _                       = Nothing+    go _ = Nothing  userNameNaked :: [TagTree Text] -> Maybe Text userNameNaked = (listToMaybe .) . mapMaybe $ \x -> do-  TagLeaf (H.TagText (T.strip->u)) <- Just x+  TagLeaf (H.TagText (T.strip -> u)) <- Just x   guard . not $ T.null u   pure u findTag :: [TagTree Text] -> Text -> Maybe Text -> Maybe (Text, [H.Attribute Text])@@ -337,31 +375,37 @@   forM_ cls $ \c -> guard $ ("class", c) `elem` attr   pure (H.renderTree cld, attr) eitherUser :: [TagTree Text] -> Maybe (Either Integer Text)-eitherUser tr = asum [-      Right <$> userNameNaked tr-    , fmap Right $ userNameNaked . H.parseTree . fst-               =<< findTag uni "a" Nothing-    , fmap Left  $ readMaybe . filter isDigit . T.unpack . fst-               =<< findTag uni "span" (Just "leaderboard-anon")+eitherUser tr =+  asum+    [ Right <$> userNameNaked tr+    , fmap Right $+        userNameNaked . H.parseTree . fst+          =<< findTag uni "a" Nothing+    , fmap Left $+        readMaybe . filter isDigit . T.unpack . fst+          =<< findTag uni "span" (Just "leaderboard-anon")     ]   where     uni = H.universeTree tr  -- | Process an HTML webpage into a list of all contents in the given tag -- type-processHTML-    :: String       -- ^ tag type-    -> Text         -- ^ html-    -> [Text]-processHTML tag = mapMaybe getTag-               . H.universeTree-               . H.tagTree-               . cleanTags-               . H.parseTags+processHTML ::+  -- | tag type+  String ->+  -- | html+  Text ->+  [Text]+processHTML tag =+  mapMaybe getTag+    . H.universeTree+    . H.tagTree+    . cleanTags+    . H.parseTags   where     getTag :: TagTree Text -> Maybe Text     getTag (TagBranch n _ ts) = H.renderTree ts <$ guard (n == T.pack tag)-    getTag _                  = Nothing+    getTag _ = Nothing  -- | Some days, including: --@@ -373,14 +417,15 @@ -- tags ignore their actual tag type and instead close the last opened tag -- (if there is any).  If no tag is currently open then it just leaves it -- unchanged.-cleanTags-    :: [H.Tag str]-    -> [H.Tag str]+cleanTags ::+  [H.Tag str] ->+  [H.Tag str] cleanTags = flip evalState [] . mapM go   where     go t = case t of-      H.TagOpen n _ -> t <$ modify (n:)-      H.TagClose _  -> get >>= \case-        []   -> pure t-        m:ms -> H.TagClose m <$ put ms-      _             -> pure t+      H.TagOpen n _ -> t <$ modify (n :)+      H.TagClose _ ->+        get >>= \case+          [] -> pure t+          m : ms -> H.TagClose m <$ put ms+      _ -> pure t
src/Advent/Cache.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE RecordWildCards           #-}+{-# LANGUAGE RecordWildCards #-}  -- | -- Module      : Advent.Throttle@@ -11,50 +11,49 @@ -- Portability : non-portable -- -- (Internal) Implement cacheing of API requests.- module Advent.Cache (-    cacheing-  , SaverLoader(..)-  , noCache-  ) where+  cacheing,+  SaverLoader (..),+  noCache,+) where -import           Control.DeepSeq-import           Control.Exception-import           Control.Monad-import           Control.Monad.IO.Class-import           Data.Text              (Text)-import           System.Directory-import           System.FilePath-import           System.IO.Error-import qualified Data.Text.IO           as T+import Control.DeepSeq+import Control.Exception+import Control.Monad+import Control.Monad.IO.Class+import Data.Text (Text)+import qualified Data.Text.IO as T+import System.Directory+import System.FilePath+import System.IO.Error -data SaverLoader a =-     SL { _slSave :: a -> Maybe Text-        , _slLoad :: Text -> Maybe a-        }+data SaverLoader a+  = SL+  { _slSave :: a -> Maybe Text+  , _slLoad :: Text -> Maybe a+  }  noCache :: SaverLoader a noCache = SL (const Nothing) (const Nothing) -cacheing-    :: MonadIO m-    => FilePath-    -> SaverLoader a-    -> m a-    -> m a+cacheing ::+  MonadIO m =>+  FilePath ->+  SaverLoader a ->+  m a ->+  m a cacheing fp SL{..} act = do-    old <- liftIO $ do-      createDirectoryIfMissing True (takeDirectory fp)-      (_slLoad =<<) <$> readFileMaybe fp-    case old of-      Nothing -> do-        r <- act-        liftIO . mapM_ (T.writeFile fp) $ _slSave r-        pure r-      Just o  -> pure o+  old <- liftIO $ do+    createDirectoryIfMissing True (takeDirectory fp)+    (_slLoad =<<) <$> readFileMaybe fp+  case old of+    Nothing -> do+      r <- act+      liftIO . mapM_ (T.writeFile fp) $ _slSave r+      pure r+    Just o -> pure o  readFileMaybe :: FilePath -> IO (Maybe Text) readFileMaybe =-     (traverse (evaluate . force) . either (const Nothing) Just =<<)-   . tryJust (guard . isDoesNotExistError)-   . T.readFile+  (traverse (evaluate . force) . either (const Nothing) Just)+    <=< (tryJust (guard . isDoesNotExistError) . T.readFile)
src/Advent/Throttle.hs view
@@ -1,6 +1,6 @@-{-# LANGUAGE LambdaCase      #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections   #-}+{-# LANGUAGE TupleSections #-}  -- | -- Module      : Advent.Throttle@@ -12,34 +12,34 @@ -- Portability : non-portable -- -- (Internal) Implement throttling of API requests.- module Advent.Throttle (-    Throttler-  , newThrottler-  , throttling-  , setLimit-  , getLimit-  ) where+  Throttler,+  newThrottler,+  throttling,+  setLimit,+  getLimit,+) where -import           Control.Concurrent-import           Control.Exception-import           Data.IORef+import Control.Concurrent+import Control.Exception+import Data.IORef -data Throttler = Throt { _throtSem     :: QSem-                       , _throtWaiting :: IORef Int-                       , _throtLim     :: IORef Int-                       }+data Throttler = Throt+  { _throtSem :: QSem+  , _throtWaiting :: IORef Int+  , _throtLim :: IORef Int+  }  acquireThrottler :: Throttler -> IO Bool acquireThrottler Throt{..} = do-    currWait <- readIORef _throtWaiting-    throtLim <- readIORef _throtLim-    if currWait >= throtLim-      then pure False-      else do-        atomicModifyIORef' _throtWaiting ((,()) . (+1))-        waitQSem _throtSem `finally` atomicModifyIORef' _throtWaiting ((,()) . subtract 1)-        pure True+  currWait <- readIORef _throtWaiting+  throtLim <- readIORef _throtLim+  if currWait >= throtLim+    then pure False+    else do+      atomicModifyIORef' _throtWaiting ((,()) . (+ 1))+      waitQSem _throtSem `finally` atomicModifyIORef' _throtWaiting ((,()) . subtract 1)+      pure True  releaseThrottler :: Throttler -> IO () releaseThrottler Throt{..} = signalQSem _throtSem@@ -55,30 +55,35 @@ -- | Create a new 'Throttler' with a given maximum capacity. newThrottler :: Int -> IO Throttler newThrottler n = do-    s <- newQSem 1-    w <- newIORef 0-    l <- newIORef n-    pure Throt-      { _throtSem     = s+  s <- newQSem 1+  w <- newIORef 0+  l <- newIORef n+  pure+    Throt+      { _throtSem = s       , _throtWaiting = w-      , _throtLim     = l+      , _throtLim = l       }  -- | Perform an IO action with the given 'Throttler' and delay.  The IO -- action will "wait in line" and be performed when the line is clear.  The -- IO action will delay the next incoming IO action by the delay amount -- given.-throttling-    :: Throttler-    -> Int                  -- ^ delay (in milliseconds)-    -> IO a-    -> IO (Maybe a)-throttling throt delay act = bracketOnError (acquireThrottler throt)-                                            (const (releaseThrottler throt)) $ \case+throttling ::+  Throttler ->+  -- | delay (in milliseconds)+  Int ->+  IO a ->+  IO (Maybe a)+throttling throt delay act = bracketOnError+  (acquireThrottler throt)+  (const (releaseThrottler throt))+  $ \case     False -> pure Nothing-    True  -> Just <$> do-      res <- act-      _ <- forkIO $ do-        threadDelay delay-        releaseThrottler throt-      pure res+    True ->+      Just <$> do+        res <- act+        _ <- forkIO $ do+          threadDelay delay+          releaseThrottler throt+        pure res
src/Advent/Types.hs view
@@ -1,18 +1,17 @@-{-# LANGUAGE CPP                        #-}-{-# LANGUAGE DataKinds                  #-}-{-# LANGUAGE DeriveGeneric              #-}-{-# LANGUAGE FlexibleContexts           #-}-{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE LambdaCase                 #-}-{-# LANGUAGE MultiParamTypeClasses      #-}-{-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE PatternSynonyms            #-}-{-# LANGUAGE PolyKinds                  #-}-{-# LANGUAGE RecordWildCards            #-}-{-# LANGUAGE ScopedTypeVariables        #-}-{-# LANGUAGE TypeOperators              #-}-{-# LANGUAGE ViewPatterns               #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE ViewPatterns #-}  -- | -- Module      : Advent.Types@@ -26,66 +25,69 @@ -- Data types used for the underlying API. -- -- @since 0.2.3.0---- module Advent.Types (   -- * Types-    Day(..)-  , Part(..)-  , SubmitInfo(..)-  , SubmitRes(..), showSubmitRes-  , PublicCode(..)-  , Leaderboard(..)-  , LeaderboardMember(..)-  , Rank(..)-  , DailyLeaderboard(..)-  , DailyLeaderboardMember(..)-  , GlobalLeaderboard(..)-  , GlobalLeaderboardMember(..)-  , NextDayTime(..)-  , DayStats(..)-  , Stats+  Day (..),+  Part (..),+  SubmitInfo (..),+  SubmitRes (..),+  showSubmitRes,+  PublicCode (..),+  Leaderboard (..),+  LeaderboardMember (..),+  Rank (..),+  DailyLeaderboard (..),+  DailyLeaderboardMember (..),+  GlobalLeaderboard (..),+  GlobalLeaderboardMember (..),+  NextDayTime (..),+  DayStats (..),+  Stats,+   -- * Util-  , mkDay, mkDay_, dayInt-  , _DayInt, pattern DayInt-  , partInt-  , partChar-  , fullDailyBoard-  , dlbmCompleteTime-  , dlbmTime-  , challengeReleaseTime+  mkDay,+  mkDay_,+  dayInt,+  _DayInt,+  pattern DayInt,+  partInt,+  partChar,+  fullDailyBoard,+  dlbmCompleteTime,+  dlbmTime,+  challengeReleaseTime,+   -- * Internal-  , parseSubmitRes-  ) where+  parseSubmitRes,+) where -import           Control.Applicative-import           Data.Aeson-import           Data.Aeson.Types-import           Data.Bifunctor-import           Data.Char-import           Data.Finite-import           Data.Functor.Classes-import           Data.List.NonEmpty         (NonEmpty(..))-import           Data.Map                   (Map)-import           Data.Maybe-import           Data.Profunctor-import           Data.Text                  (Text)-import           Data.Time hiding           (Day)-import           Data.Time.Clock.POSIX-import           Data.Typeable-import           Data.Void-import           GHC.Generics-import           Servant.API-import           Text.Printf-import           Text.Read                  (readMaybe)-import qualified Data.Map                   as M-import qualified Data.Text                  as T-import qualified Text.HTML.TagSoup          as H-import qualified Text.Megaparsec            as P-import qualified Text.Megaparsec.Char       as P+import Control.Applicative+import Data.Aeson+import Data.Aeson.Types+import Data.Bifunctor+import Data.Char+import Data.Finite+import Data.Functor.Classes+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe+import Data.Profunctor+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time hiding (Day)+import Data.Time.Clock.POSIX+import Data.Typeable+import Data.Void+import GHC.Generics+import Servant.API+import qualified Text.HTML.TagSoup as H+import qualified Text.Megaparsec as P+import qualified Text.Megaparsec.Char as P import qualified Text.Megaparsec.Char.Lexer as P-import qualified Web.FormUrlEncoded         as WF-+import Text.Printf+import Text.Read (readMaybe)+import qualified Web.FormUrlEncoded as WF  #if !MIN_VERSION_base(4,16,0) import           Data.Foldable              (asum)@@ -103,11 +105,11 @@ -- -- Represented by a 'Finite' ranging from 0 to 24 inclusive; you should -- probably make one using the smart constructor 'mkDay'.-newtype Day = Day { dayFinite :: Finite 25 }+newtype Day = Day {dayFinite :: Finite 25}   deriving (Eq, Ord, Enum, Bounded, Typeable, Generic)  instance Show Day where-    showsPrec = showsUnaryWith (\d -> showsPrec d . dayInt) "mkDay"+  showsPrec = showsUnaryWith (\d -> showsPrec d . dayInt) "mkDay"  -- | A given part of a problem.  All Advent of Code challenges are -- two-parts.@@ -119,30 +121,30 @@  -- | Info required to submit an answer for a part. data SubmitInfo = SubmitInfo-    { siLevel  :: Part-    , siAnswer :: String-    }+  { siLevel :: Part+  , siAnswer :: String+  }   deriving (Show, Read, Eq, Ord, Typeable, Generic)  -- | The result of a submission. data SubmitRes-    -- | Correct submission, including global rank (if reported, which+  = -- | Correct submission, including global rank (if reported, which     -- usually happens if rank is under 1000)-    = SubCorrect (Maybe Integer)-    -- | Incorrect submission.  Contains the number of /seconds/ you must+    SubCorrect (Maybe Integer)+  | -- | Incorrect submission.  Contains the number of /seconds/ you must     -- wait before trying again.  The 'Maybe' contains possible hints given     -- by the server (usually "too low" or "too high").-    | SubIncorrect Int (Maybe String)-    -- | Submission was rejected because an incorrect submission was+    SubIncorrect Int (Maybe String)+  | -- | Submission was rejected because an incorrect submission was     -- recently submitted.  Contains the number of /seconds/ you must wait     -- before trying again.-    | SubWait Int-    -- | Submission was rejected because it was sent to an invalid question+    SubWait Int+  | -- | Submission was rejected because it was sent to an invalid question     -- or part.  Usually happens if you submit to a part you have already     -- answered or have not yet unlocked.-    | SubInvalid-    -- | Could not parse server response.  Contains parse error.-    | SubUnknown String+    SubInvalid+  | -- | Could not parse server response.  Contains parse error.+    SubUnknown String   deriving (Show, Read, Eq, Ord, Typeable, Generic)  -- | Member ID of public leaderboard (the first part of the registration@@ -151,27 +153,50 @@ -- > https://adventofcode.com/2019/leaderboard/private/view/12345 -- -- (the @12345@ above)-newtype PublicCode = PublicCode { getPublicCode :: Integer }+newtype PublicCode = PublicCode {getPublicCode :: Integer}   deriving (Show, Read, Eq, Ord, Typeable, Generic)  -- | Leaderboard type, representing private leaderboard information. data Leaderboard = LB-    { lbEvent   :: Integer                        -- ^ The year of the event-    , lbOwnerId :: Integer                        -- ^ The Member ID of the owner, or the public code-    , lbMembers :: Map Integer LeaderboardMember  -- ^ A map from member IDs to their leaderboard info-    }+  { lbEvent :: Integer+  -- ^ The year of the event+  , lbOwnerId :: Integer+  -- ^ The Member ID of the owner, or the public code+  , lbMembers :: Map Integer LeaderboardMember+  -- ^ A map from member IDs to their leaderboard info+  , lbNumDays :: Int+  -- ^ The number of days in this event: 25 for+  --   years before 2025, 12 from 2025 onward.+  --+  --   @since 0.2.12.0+  , lbDay1Ts :: UTCTime+  -- ^ Release time of day 1 for this event.+  --+  --   @since 0.2.12.0+  }   deriving (Show, Eq, Ord, Typeable, Generic)  -- | Leaderboard position for a given member. data LeaderboardMember = LBM-    { lbmGlobalScore :: Maybe Integer               -- ^ Global leaderboard score-    , lbmName        :: Maybe Text                  -- ^ Username, if user specifies one-    , lbmLocalScore  :: Integer                     -- ^ Score for this leaderboard-    , lbmId          :: Integer                     -- ^ Member ID-    , lbmLastStarTS  :: Maybe UTCTime               -- ^ Time of last puzzle solved, if any-    , lbmStars       :: Int                         -- ^ Number of stars (puzzle parts) solved-    , lbmCompletion  :: Map Day (Map Part UTCTime)  -- ^ Completion times of each day and puzzle part-    }+  { lbmGlobalScore :: Maybe Integer+  -- ^ Global leaderboard score+  , lbmName :: Maybe Text+  -- ^ Username, if user specifies one+  , lbmLocalScore :: Integer+  -- ^ Score for this leaderboard+  , lbmId :: Integer+  -- ^ Member ID+  , lbmLastStarTS :: Maybe UTCTime+  -- ^ Time of last puzzle solved, if any+  , lbmStars :: Int+  -- ^ Number of stars (puzzle parts) solved+  , lbmCompletion :: Map Day (Map Part UTCTime)+  -- ^ Completion times of each day and puzzle part+  , lbmStarIndex :: Map Day (Map Part Int)+  -- ^ Board-wide chronological sequence number of+  --   each completion, in the same shape as+  --   'lbmCompletion'.+  }   deriving (Show, Eq, Ord, Typeable, Generic)  -- | Ranking between 1 to 100, for daily and global leaderboards@@ -180,25 +205,25 @@ -- to add or subtract accordingly if you want to display or parse it. -- -- @since 0.2.3.0-newtype Rank = Rank { getRank :: Finite 100 }+newtype Rank = Rank {getRank :: Finite 100}   deriving (Show, Eq, Ord, Typeable, Generic)  -- | Single daily leaderboard position -- -- @since 0.2.3.0 data DailyLeaderboardMember = DLBM-    { dlbmRank      :: Rank-    -- | Time from midnight EST of December 1st for that event.  Use-    -- 'dlbmCompleteTime' to convert to an actual time for event-    -- completion, and 'dlbmTime' to get the time it took to solve.-    ---    -- @since 0.2.7.0-    , dlbmDecTime   :: NominalDiffTime      -- ^ time from midnight EST.-    , dlbmUser      :: Either Integer Text-    , dlbmLink      :: Maybe Text-    , dlbmImage     :: Maybe Text-    , dlbmSupporter :: Bool-    }+  { dlbmRank :: Rank+  , dlbmDecTime :: NominalDiffTime+  -- ^ Time from midnight EST of December 1st for that event.  Use+  -- 'dlbmCompleteTime' to convert to an actual time for event+  -- completion, and 'dlbmTime' to get the time it took to solve.+  --+  -- @since 0.2.7.0+  , dlbmUser :: Either Integer Text+  , dlbmLink :: Maybe Text+  , dlbmImage :: Maybe Text+  , dlbmSupporter :: Bool+  }   deriving (Show, Eq, Ord, Typeable, Generic)  -- | Turn a 'dlbmDecTime' field into a 'ZonedTime' for the actual@@ -206,7 +231,8 @@ -- -- @since 0.2.7.0 dlbmCompleteTime :: Integer -> Day -> NominalDiffTime -> ZonedTime-dlbmCompleteTime y d t = r+dlbmCompleteTime y d t =+  r     { zonedTimeToLocalTime = dlbmTime d t `addLocalTime` zonedTimeToLocalTime r     }   where@@ -217,15 +243,16 @@ -- -- @since 0.2.7.0 dlbmTime :: Day -> NominalDiffTime -> NominalDiffTime-dlbmTime d = uncurry daysAndTimeOfDayToTime-           . first (subtract (dayInt d - 1))-           . timeToDaysAndTimeOfDay+dlbmTime d =+  uncurry daysAndTimeOfDayToTime+    . first (subtract (dayInt d - 1))+    . timeToDaysAndTimeOfDay  -- | Daily leaderboard, containing Star 1 and Star 2 completions -- -- @since 0.2.3.0-data DailyLeaderboard = DLB {-    dlbStar1 :: Map Rank DailyLeaderboardMember+data DailyLeaderboard = DLB+  { dlbStar1 :: Map Rank DailyLeaderboardMember   , dlbStar2 :: Map Rank DailyLeaderboardMember   }   deriving (Show, Eq, Ord, Typeable, Generic)@@ -234,13 +261,13 @@ -- -- @since 0.2.3.0 data GlobalLeaderboardMember = GLBM-    { glbmRank      :: Rank-    , glbmScore     :: Integer-    , glbmUser      :: Either Integer Text-    , glbmLink      :: Maybe Text-    , glbmImage     :: Maybe Text-    , glbmSupporter :: Bool-    }+  { glbmRank :: Rank+  , glbmScore :: Integer+  , glbmUser :: Either Integer Text+  , glbmLink :: Maybe Text+  , glbmImage :: Maybe Text+  , glbmSupporter :: Bool+  }   deriving (Show, Eq, Ord, Typeable, Generic)  -- | Global leaderboard for the entire event@@ -249,8 +276,8 @@ -- a non-empty list of all members who achieved that rank and score. -- -- @since 0.2.3.0-newtype GlobalLeaderboard = GLB {-    glbMap :: Map Rank (Integer, NonEmpty GlobalLeaderboardMember)+newtype GlobalLeaderboard = GLB+  { glbMap :: Map Rank (Integer, NonEmpty GlobalLeaderboardMember)   }   deriving (Show, Eq, Ord, Typeable, Generic) @@ -258,17 +285,20 @@ -- seconds until the challenge is released. -- -- @since 0.2.8.0-data NextDayTime = NextDayTime Day Int-                 | NoNextDayTime+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-    }+  { 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.@@ -277,145 +307,191 @@ type Stats = Map Day DayStats  instance ToHttpApiData Part where-    toUrlPiece = T.pack . show . partInt-    toQueryParam = toUrlPiece+  toUrlPiece = T.pack . show . partInt+  toQueryParam = toUrlPiece  instance ToHttpApiData Day where-    toUrlPiece = T.pack . show . dayInt-    toQueryParam = toUrlPiece+  toUrlPiece = T.pack . show . dayInt+  toQueryParam = toUrlPiece  instance ToHttpApiData PublicCode where-    toUrlPiece   = (<> ".json") . T.pack . show . getPublicCode-    toQueryParam = toUrlPiece+  toUrlPiece = (<> ".json") . T.pack . show . getPublicCode+  toQueryParam = toUrlPiece  instance WF.ToForm SubmitInfo where-    toForm = WF.genericToForm WF.FormOptions-      { WF.fieldLabelModifier = camelTo2 '-' . drop 2 }+  toForm =+    WF.genericToForm+      WF.FormOptions+        { WF.fieldLabelModifier = camelTo2 '-' . drop 2+        }  instance FromJSON Leaderboard where-    parseJSON = withObject "Leaderboard" $ \o ->-        LB <$> (strInt =<< (o .: "event"))-           <*> o .: "owner_id"-           <*> o .: "members"-      where-        strInt t = case readMaybe t of-          Nothing -> fail "bad int"-          Just i  -> pure i+  parseJSON = withObject "Leaderboard" $ \o ->+    LB+      <$> (strInt =<< (o .: "event"))+      <*> o .: "owner_id"+      <*> o .: "members"+      <*> o .: "num_days"+      <*> ( (fromEpochText =<< (o .: "day1_ts"))+              <|> (fromEpochNumber <$> (o .: "day1_ts"))+          )+    where+      strInt t = case readMaybe t of+        Nothing -> fail "bad int"+        Just i -> pure i  instance FromJSON LeaderboardMember where-    parseJSON = withObject "LeaderboardMember" $ \o ->-        LBM <$> optional (o .: "global_score")-            <*> optional (o .: "name")-            <*> o .: "local_score"-            <*> o .: "id"-            <*> optional (-                    (fromEpochText   =<< (o .: "last_star_ts"))-                <|> (fromEpochNumber <$> (o .: "last_star_ts"))-                )-            <*> o .: "stars"-            <*> (do cdl <- o .: "completion_day_level"-                    (traverse . traverse) (\c ->-                          (fromEpochText   =<< (c .: "get_star_ts"))-                      <|> (fromEpochNumber <$> (c .: "get_star_ts"))-                      ) cdl-                )-      where-        fromEpochText t = case readMaybe t of-          Nothing -> fail "bad stamp"-          Just i  -> pure . posixSecondsToUTCTime $ fromInteger i-        fromEpochNumber = posixSecondsToUTCTime+  parseJSON = withObject "LeaderboardMember" $ \o -> do+    cdl <- o .: "completion_day_level"+    LBM+      <$> optional (o .: "global_score")+      <*> optional (o .: "name")+      <*> o .: "local_score"+      <*> o .: "id"+      <*> optional+        ( (fromEpochText =<< (o .: "last_star_ts"))+            <|> (fromEpochNumber <$> (o .: "last_star_ts"))+        )+      <*> o .: "stars"+      <*> (traverse . traverse)+        ( \c ->+            (fromEpochText =<< (c .: "get_star_ts"))+              <|> (fromEpochNumber <$> (c .: "get_star_ts"))+        )+        cdl+      <*> (traverse . traverse) (.: "star_index") cdl +fromEpochText :: String -> Parser UTCTime+fromEpochText t = case readMaybe t of+  Nothing -> fail "bad stamp"+  Just i -> pure . posixSecondsToUTCTime $ fromInteger i++fromEpochNumber :: NominalDiffTime -> UTCTime+fromEpochNumber = posixSecondsToUTCTime+ -- | @since 0.2.4.2 instance ToJSONKey Day where-    toJSONKey = toJSONKeyText $ T.pack . show . dayInt+  toJSONKey = toJSONKeyText $ T.pack . show . dayInt+ instance FromJSONKey Day where-    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)+  fromJSONKey = FromJSONKeyTextParser (parseJSON . String)+ -- | @since 0.2.4.2 instance ToJSONKey Part where-    toJSONKey = toJSONKeyText $ \case-      Part1 -> "1"-      Part2 -> "2"+  toJSONKey = toJSONKeyText $ \case+    Part1 -> "1"+    Part2 -> "2"+ instance FromJSONKey Part where-    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)+  fromJSONKey = FromJSONKeyTextParser (parseJSON . String)  -- | @since 0.2.4.2 instance ToJSON Part where-    toJSON = String . (\case Part1 -> "1"; Part2 -> "2")+  toJSON = String . (\case Part1 -> "1"; Part2 -> "2")+ instance FromJSON Part where-    parseJSON = withText "Part" $ \case-      "1" -> pure Part1-      "2" -> pure Part2-      _   -> fail "Bad part"+  parseJSON = withText "Part" $ \case+    "1" -> pure Part1+    "2" -> pure Part2+    _ -> fail "Bad part"  -- | @since 0.2.4.2 instance ToJSON Day where-    toJSON = String . T.pack . show . dayInt+  toJSON = String . T.pack . show . dayInt+ instance FromJSON Day where-    parseJSON = withText "Day" $ \t ->-      case readMaybe (T.unpack t) of-        Nothing -> fail "No read day"-        Just i  -> case mkDay i of-          Nothing -> fail "Day out of range"-          Just d  -> pure d+  parseJSON = withText "Day" $ \t ->+    case readMaybe (T.unpack t) of+      Nothing -> fail "No read day"+      Just i -> case mkDay i of+        Nothing -> fail "Day out of range"+        Just d -> pure d  instance ToJSONKey Rank where-    toJSONKey = toJSONKeyText $ T.pack . show . (+ 1) . getFinite . getRank+  toJSONKey = toJSONKeyText $ T.pack . show . (+ 1) . getFinite . getRank instance FromJSONKey Rank where-    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)+  fromJSONKey = FromJSONKeyTextParser (parseJSON . String)  instance ToJSON Rank where-    toJSON = String . T.pack . show . (+ 1) . getFinite . getRank+  toJSON = String . T.pack . show . (+ 1) . getFinite . getRank instance FromJSON Rank where-    parseJSON = withText "Rank" $ \t ->-      case readMaybe (T.unpack t) of-        Nothing -> fail "No read rank"-        Just i  -> case packFinite (i - 1) of-          Nothing -> fail "Rank out of range"-          Just d  -> pure $ Rank d+  parseJSON = withText "Rank" $ \t ->+    case readMaybe (T.unpack t) of+      Nothing -> fail "No read rank"+      Just i -> case packFinite (i - 1) of+        Nothing -> fail "Rank out of range"+        Just d -> pure $ Rank d  instance ToJSON DailyLeaderboard where-    toJSON = genericToJSON defaultOptions-        { fieldLabelModifier = camelTo2 '-' . drop 3 }+  toJSON =+    genericToJSON+      defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 3+        } instance FromJSON DailyLeaderboard where-    parseJSON = genericParseJSON defaultOptions-        { fieldLabelModifier = camelTo2 '-' . drop 3 }+  parseJSON =+    genericParseJSON+      defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 3+        }  instance ToJSON GlobalLeaderboard where-    toJSON = genericToJSON defaultOptions-        { fieldLabelModifier = camelTo2 '-' . drop 3 }+  toJSON =+    genericToJSON+      defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 3+        } instance FromJSON GlobalLeaderboard where-    parseJSON = genericParseJSON defaultOptions-        { fieldLabelModifier = camelTo2 '-' . drop 3 }+  parseJSON =+    genericParseJSON+      defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 3+        }  instance ToJSON DailyLeaderboardMember where-    toJSON = genericToJSON defaultOptions-        { fieldLabelModifier = camelTo2 '-' . drop 4 }+  toJSON =+    genericToJSON+      defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 4+        } instance FromJSON DailyLeaderboardMember where-    parseJSON = genericParseJSON defaultOptions-        { fieldLabelModifier = camelTo2 '-' . drop 4 }+  parseJSON =+    genericParseJSON+      defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 4+        }  instance ToJSON GlobalLeaderboardMember where-    toJSON = genericToJSON defaultOptions-        { fieldLabelModifier = camelTo2 '-' . drop 4 }+  toJSON =+    genericToJSON+      defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 4+        } instance FromJSON GlobalLeaderboardMember where-    parseJSON = genericParseJSON defaultOptions-        { fieldLabelModifier = camelTo2 '-' . drop 4 }+  parseJSON =+    genericParseJSON+      defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 4+        }  -- | Parse 'T.Text' into a 'SubmitRes'. parseSubmitRes :: Text -> SubmitRes-parseSubmitRes = either (SubUnknown . P.errorBundlePretty) id-               . P.runParser choices "Submission Response"-               . mconcat-               . mapMaybe deTag-               . H.parseTags+parseSubmitRes =+  either (SubUnknown . P.errorBundlePretty) id+    . P.runParser choices "Submission Response"+    . mconcat+    . mapMaybe deTag+    . H.parseTags   where     deTag (H.TagText t) = Just t-    deTag _             = Nothing-    choices             = asum [ P.try parseCorrect   P.<?> "Correct"-                               , P.try parseIncorrect P.<?> "Incorrect"-                               , P.try parseWait      P.<?> "Wait"-                               ,       parseInvalid   P.<?> "Invalid"-                               ]+    deTag _ = Nothing+    choices =+      asum+        [ P.try parseCorrect P.<?> "Correct"+        , P.try parseIncorrect P.<?> "Incorrect"+        , P.try parseWait P.<?> "Wait"+        , parseInvalid P.<?> "Invalid"+        ]     parseCorrect :: P.Parsec Void Text SubmitRes     parseCorrect = do       _ <- P.manyTill P.anySingle (P.string' "that's the right answer") P.<?> "Right answer"@@ -435,8 +511,9 @@     parseWait = do       _ <- P.manyTill P.anySingle (P.string' "an answer too recently") P.<?> "An answer too recently"       P.skipMany (P.satisfy (not . isDigit))-      m <- optional . (P.<?> "Delay minutes") . P.try $-              P.decimal <* P.char 'm' <* P.space1+      m <-+        optional . (P.<?> "Delay minutes") . P.try $+          P.decimal <* P.char 'm' <* P.space1       s <- P.decimal <* P.char 's' P.<?> "Delay seconds"       pure . SubWait $ maybe 0 (* 60) m + s     parseInvalid = SubInvalid <$ P.manyTill P.anySingle (P.string' "solving the right level")@@ -444,14 +521,15 @@ -- | Pretty-print a 'SubmitRes' showSubmitRes :: SubmitRes -> String showSubmitRes = \case-    SubCorrect Nothing    -> "Correct"-    SubCorrect (Just r)   -> printf "Correct (Rank %d)" r-    SubIncorrect i Nothing  -> printf "Incorrect (%d minute wait)" (i `div` 60)-    SubIncorrect i (Just h) -> printf "Incorrect (%s) (%d minute wait)" h (i `div` 60)-    SubWait i             -> let (m,s) = i `divMod` 60-                             in   printf "Wait (%d min %d sec wait)"  m s-    SubInvalid            -> "Invalid"-    SubUnknown r          -> printf "Unknown (%s)" r+  SubCorrect Nothing -> "Correct"+  SubCorrect (Just r) -> printf "Correct (Rank %d)" r+  SubIncorrect i Nothing -> printf "Incorrect (%d minute wait)" (i `div` 60)+  SubIncorrect i (Just h) -> printf "Incorrect (%s) (%d minute wait)" h (i `div` 60)+  SubWait i ->+    let (m, s) = i `divMod` 60+     in printf "Wait (%d min %d sec wait)" m s+  SubInvalid -> "Invalid"+  SubUnknown r -> printf "Unknown (%s)" r  -- | Convert a @'Finite' 25@ day into a day integer (1 - 25).  Inverse of -- 'mkDay'.@@ -489,7 +567,7 @@ _DayInt = dimap a b . right'   where     a i = maybe (Left i) Right . mkDay $ i-    b   = either pure (fmap dayInt)+    b = either pure (fmap dayInt)  -- | Pattern synonym allowing you to match on an 'Integer' as if it were -- a 'Day':@@ -504,7 +582,7 @@ -- -- @since 0.2.4.0 pattern DayInt :: Day -> Integer-pattern DayInt d <- (mkDay->Just d)+pattern DayInt d <- (mkDay -> Just d)   where     DayInt d = dayInt d @@ -517,24 +595,27 @@ -- | Check if a 'DailyLeaderboard' is filled up or not. -- -- @since 0.2.4.0-fullDailyBoard-    :: DailyLeaderboard-    -> Bool+fullDailyBoard ::+  DailyLeaderboard ->+  Bool fullDailyBoard DLB{..} = (M.size dlbStar1 + M.size dlbStar2) >= 200  -- | Prompt release time. -- -- Changed from 'UTCTime' to 'ZonedTime' in v0.2.7.0.  To use as -- a 'UTCTime', use 'zonedTimeToUTC'.-challengeReleaseTime-    :: Integer              -- ^ year-    -> Day                  -- ^ day-    -> ZonedTime-challengeReleaseTime y d = ZonedTime-    { zonedTimeToLocalTime = LocalTime-        { localDay       = fromGregorian y 12 (fromIntegral (dayInt d))-        , localTimeOfDay = midnight-        }+challengeReleaseTime ::+  -- | year+  Integer ->+  -- | day+  Day ->+  ZonedTime+challengeReleaseTime y d =+  ZonedTime+    { zonedTimeToLocalTime =+        LocalTime+          { localDay = fromGregorian y 12 (fromIntegral (dayInt d))+          , localTimeOfDay = midnight+          }     , zonedTimeZone = read "EST"     }-
test/Spec.hs view
@@ -1,32 +1,34 @@ {-# LANGUAGE ViewPatterns #-} -import           Advent.Types-import           Control.Monad-import           Data.List-import           System.Directory-import           System.Exit-import           System.FilePath-import           Test.HUnit-import           Text.Read        (readMaybe)-import qualified Data.Text        as T-import qualified Data.Text.IO     as T+import Advent.Types+import Control.Monad+import Data.List+import qualified Data.Text as T+import qualified Data.Text.IO as T+import System.Directory+import System.Exit+import System.FilePath+import Test.HUnit+import Text.Read (readMaybe)  fileTest :: FilePath -> IO Test fileTest fp = do-    ls <- T.lines <$> T.readFile ("test-data" </> fp)-    (T.strip->x, T.unlines->xs) <- maybe (fail "Empty test file") pure $-                                      uncons ls+  ls <- T.lines <$> T.readFile ("test-data" </> fp)+  (T.strip -> x, T.unlines -> xs) <-+    maybe (fail "Empty test file") pure $+      uncons ls -    r <- maybe (fail "No parse expected result") pure $-            readMaybe (T.unpack x)+  r <-+    maybe (fail "No parse expected result") pure $+      readMaybe (T.unpack x) -    pure . TestLabel fp $ r ~=? parseSubmitRes xs+  pure . TestLabel fp $ r ~=? parseSubmitRes xs  main :: IO () main = do-    tests <- fmap TestList-           . mapM fileTest-         =<< listDirectory "test-data"-    c <- runTestTT tests-    unless (failures c == 0) $-      exitFailure+  tests <-+    fmap TestList+      . mapM fileTest+      =<< listDirectory "test-data"+  c <- runTestTT tests+  unless (failures c == 0) exitFailure