packages feed

advent-of-code-api 0.2.2.1 → 0.2.3.0

raw patch · 7 files changed

+658/−310 lines, 7 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Advent: LB :: Integer -> Integer -> Map Integer LeaderboardMember -> Leaderboard
- Advent: LBM :: Integer -> Maybe Text -> Integer -> Integer -> Maybe UTCTime -> Int -> Map Day (Map Part UTCTime) -> LeaderboardMember
- Advent: [lbEvent] :: Leaderboard -> Integer
- Advent: [lbMembers] :: Leaderboard -> Map Integer LeaderboardMember
- Advent: [lbOwnerId] :: Leaderboard -> Integer
- Advent: [lbmCompletion] :: LeaderboardMember -> Map Day (Map Part UTCTime)
- Advent: [lbmGlobalScore] :: LeaderboardMember -> Integer
- Advent: [lbmId] :: LeaderboardMember -> Integer
- Advent: [lbmLastStarTS] :: LeaderboardMember -> Maybe UTCTime
- Advent: [lbmLocalScore] :: LeaderboardMember -> Integer
- Advent: [lbmName] :: LeaderboardMember -> Maybe Text
- Advent: [lbmStars] :: LeaderboardMember -> Int
- Advent: data Leaderboard
- Advent: data LeaderboardMember
- Advent.API: Day :: Finite 25 -> Day
- Advent.API: LB :: Integer -> Integer -> Map Integer LeaderboardMember -> Leaderboard
- Advent.API: LBM :: Integer -> Maybe Text -> Integer -> Integer -> Maybe UTCTime -> Int -> Map Day (Map Part UTCTime) -> LeaderboardMember
- Advent.API: Part1 :: Part
- Advent.API: Part2 :: Part
- Advent.API: PublicCode :: Integer -> PublicCode
- Advent.API: SubCorrect :: Maybe Integer -> SubmitRes
- Advent.API: SubIncorrect :: Int -> Maybe String -> SubmitRes
- Advent.API: SubInvalid :: SubmitRes
- Advent.API: SubUnknown :: String -> SubmitRes
- Advent.API: SubWait :: Int -> SubmitRes
- Advent.API: SubmitInfo :: Part -> String -> SubmitInfo
- Advent.API: [dayFinite] :: Day -> Finite 25
- Advent.API: [getPublicCode] :: PublicCode -> Integer
- Advent.API: [lbEvent] :: Leaderboard -> Integer
- Advent.API: [lbMembers] :: Leaderboard -> Map Integer LeaderboardMember
- Advent.API: [lbOwnerId] :: Leaderboard -> Integer
- Advent.API: [lbmCompletion] :: LeaderboardMember -> Map Day (Map Part UTCTime)
- Advent.API: [lbmGlobalScore] :: LeaderboardMember -> Integer
- Advent.API: [lbmId] :: LeaderboardMember -> Integer
- Advent.API: [lbmLastStarTS] :: LeaderboardMember -> Maybe UTCTime
- Advent.API: [lbmLocalScore] :: LeaderboardMember -> Integer
- Advent.API: [lbmName] :: LeaderboardMember -> Maybe Text
- Advent.API: [lbmStars] :: LeaderboardMember -> Int
- Advent.API: [siAnswer] :: SubmitInfo -> String
- Advent.API: [siLevel] :: SubmitInfo -> Part
- Advent.API: class FromArticles a
- Advent.API: data Articles
- Advent.API: data Leaderboard
- Advent.API: data LeaderboardMember
- Advent.API: data Part
- Advent.API: data SubmitInfo
- Advent.API: data SubmitRes
- Advent.API: dayInt :: Day -> Integer
- Advent.API: fromArticles :: FromArticles a => [Text] -> a
- Advent.API: instance (Advent.API.FromArticles a, Advent.API.FromArticles b) => Advent.API.FromArticles (a Servant.API.Alternative.:<|> b)
- Advent.API: instance (GHC.Classes.Ord a, GHC.Enum.Enum a, GHC.Enum.Bounded a) => Advent.API.FromArticles (Data.Map.Internal.Map a Data.Text.Internal.Text)
- Advent.API: instance Advent.API.FromArticles Advent.API.SubmitRes
- Advent.API: instance Advent.API.FromArticles Data.Text.Internal.Text
- Advent.API: instance Advent.API.FromArticles [Data.Text.Internal.Text]
- Advent.API: instance Advent.API.FromArticles a => Servant.API.ContentTypes.MimeUnrender Advent.API.Articles a
- Advent.API: instance Data.Aeson.Types.FromJSON.FromJSON Advent.API.Day
- Advent.API: instance Data.Aeson.Types.FromJSON.FromJSON Advent.API.Leaderboard
- Advent.API: instance Data.Aeson.Types.FromJSON.FromJSON Advent.API.LeaderboardMember
- Advent.API: instance Data.Aeson.Types.FromJSON.FromJSON Advent.API.Part
- Advent.API: instance Data.Aeson.Types.FromJSON.FromJSONKey Advent.API.Day
- Advent.API: instance Data.Aeson.Types.FromJSON.FromJSONKey Advent.API.Part
- Advent.API: instance GHC.Classes.Eq Advent.API.Day
- Advent.API: instance GHC.Classes.Eq Advent.API.Leaderboard
- Advent.API: instance GHC.Classes.Eq Advent.API.LeaderboardMember
- Advent.API: instance GHC.Classes.Eq Advent.API.Part
- Advent.API: instance GHC.Classes.Eq Advent.API.PublicCode
- Advent.API: instance GHC.Classes.Eq Advent.API.SubmitInfo
- Advent.API: instance GHC.Classes.Eq Advent.API.SubmitRes
- Advent.API: instance GHC.Classes.Ord Advent.API.Day
- Advent.API: instance GHC.Classes.Ord Advent.API.Leaderboard
- Advent.API: instance GHC.Classes.Ord Advent.API.LeaderboardMember
- Advent.API: instance GHC.Classes.Ord Advent.API.Part
- Advent.API: instance GHC.Classes.Ord Advent.API.PublicCode
- Advent.API: instance GHC.Classes.Ord Advent.API.SubmitInfo
- Advent.API: instance GHC.Classes.Ord Advent.API.SubmitRes
- Advent.API: instance GHC.Enum.Bounded Advent.API.Day
- Advent.API: instance GHC.Enum.Bounded Advent.API.Part
- Advent.API: instance GHC.Enum.Enum Advent.API.Day
- Advent.API: instance GHC.Enum.Enum Advent.API.Part
- Advent.API: instance GHC.Generics.Generic Advent.API.Day
- Advent.API: instance GHC.Generics.Generic Advent.API.Leaderboard
- Advent.API: instance GHC.Generics.Generic Advent.API.LeaderboardMember
- Advent.API: instance GHC.Generics.Generic Advent.API.Part
- Advent.API: instance GHC.Generics.Generic Advent.API.PublicCode
- Advent.API: instance GHC.Generics.Generic Advent.API.SubmitInfo
- Advent.API: instance GHC.Generics.Generic Advent.API.SubmitRes
- Advent.API: instance GHC.Read.Read Advent.API.Part
- Advent.API: instance GHC.Read.Read Advent.API.PublicCode
- Advent.API: instance GHC.Read.Read Advent.API.SubmitInfo
- Advent.API: instance GHC.Read.Read Advent.API.SubmitRes
- Advent.API: instance GHC.Show.Show Advent.API.Day
- Advent.API: instance GHC.Show.Show Advent.API.Leaderboard
- Advent.API: instance GHC.Show.Show Advent.API.LeaderboardMember
- Advent.API: instance GHC.Show.Show Advent.API.Part
- Advent.API: instance GHC.Show.Show Advent.API.PublicCode
- Advent.API: instance GHC.Show.Show Advent.API.SubmitInfo
- Advent.API: instance GHC.Show.Show Advent.API.SubmitRes
- Advent.API: instance Servant.API.ContentTypes.Accept Advent.API.Articles
- Advent.API: instance Web.Internal.FormUrlEncoded.ToForm Advent.API.SubmitInfo
- Advent.API: instance Web.Internal.HttpApiData.ToHttpApiData Advent.API.Day
- Advent.API: instance Web.Internal.HttpApiData.ToHttpApiData Advent.API.Part
- Advent.API: instance Web.Internal.HttpApiData.ToHttpApiData Advent.API.PublicCode
- Advent.API: mkDay :: Integer -> Maybe Day
- Advent.API: mkDay_ :: Integer -> Day
- Advent.API: newtype Day
- Advent.API: newtype PublicCode
- Advent.API: parseSubmitRes :: Text -> SubmitRes
- Advent.API: partChar :: Part -> Char
- Advent.API: partInt :: Part -> Int
- Advent.API: showSubmitRes :: SubmitRes -> String
+ Advent: [AoCDailyLeaderboard] :: Day -> AoC DailyLeaderboard
+ Advent: [AoCGlobalLeaderboard] :: AoC GlobalLeaderboard
+ Advent.API: class FromTags tag a
+ Advent.API: data HTMLTags (tag :: Symbol)
+ Advent.API: fromTags :: FromTags tag a => p tag -> [Text] -> Maybe a
+ Advent.API: instance (Advent.API.FromTags tag a, GHC.TypeLits.KnownSymbol tag) => Servant.API.ContentTypes.MimeUnrender (Advent.API.HTMLTags tag) a
+ Advent.API: instance Advent.API.FromTags "article" Advent.Types.SubmitRes
+ Advent.API: instance Advent.API.FromTags "div" Advent.Types.DailyLeaderboard
+ Advent.API: instance Advent.API.FromTags "div" Advent.Types.GlobalLeaderboard
+ Advent.API: instance Servant.API.ContentTypes.Accept (Advent.API.HTMLTags cls)
+ Advent.API: instance forall k (cls :: k) a b. (Advent.API.FromTags cls a, Advent.API.FromTags cls b) => Advent.API.FromTags cls (a Servant.API.Alternative.:<|> b)
+ Advent.API: instance forall k (cls :: k). Advent.API.FromTags cls Data.Text.Internal.Text
+ Advent.API: instance forall k (cls :: k). Advent.API.FromTags cls [Data.Text.Internal.Text]
+ 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.API: type Articles = HTMLTags "article"
+ Advent.API: type Divs = HTMLTags "div"
+ Advent.Types: DLB :: Map Rank DailyLeaderboardMember -> Map Rank DailyLeaderboardMember -> DailyLeaderboard
+ Advent.Types: DLBM :: Rank -> UTCTime -> Either Integer Text -> Maybe Text -> Maybe Text -> Bool -> DailyLeaderboardMember
+ Advent.Types: Day :: Finite 25 -> Day
+ Advent.Types: GLB :: Map Rank (Integer, NonEmpty GlobalLeaderboardMember) -> GlobalLeaderboard
+ Advent.Types: GLBM :: Rank -> Integer -> Either Integer Text -> Maybe Text -> Maybe Text -> Bool -> GlobalLeaderboardMember
+ Advent.Types: LB :: Integer -> Integer -> Map Integer LeaderboardMember -> Leaderboard
+ Advent.Types: LBM :: Integer -> Maybe Text -> Integer -> Integer -> Maybe UTCTime -> Int -> Map Day (Map Part UTCTime) -> LeaderboardMember
+ Advent.Types: Part1 :: Part
+ Advent.Types: Part2 :: Part
+ Advent.Types: PublicCode :: Integer -> PublicCode
+ Advent.Types: Rank :: Finite 100 -> Rank
+ Advent.Types: SubCorrect :: Maybe Integer -> SubmitRes
+ Advent.Types: SubIncorrect :: Int -> Maybe String -> SubmitRes
+ Advent.Types: SubInvalid :: SubmitRes
+ Advent.Types: SubUnknown :: String -> SubmitRes
+ Advent.Types: SubWait :: Int -> SubmitRes
+ Advent.Types: SubmitInfo :: Part -> String -> SubmitInfo
+ Advent.Types: [dayFinite] :: Day -> Finite 25
+ Advent.Types: [dlbStar1] :: DailyLeaderboard -> Map Rank DailyLeaderboardMember
+ Advent.Types: [dlbStar2] :: DailyLeaderboard -> Map Rank DailyLeaderboardMember
+ Advent.Types: [dlbmImage] :: DailyLeaderboardMember -> Maybe Text
+ Advent.Types: [dlbmLink] :: DailyLeaderboardMember -> Maybe Text
+ Advent.Types: [dlbmRank] :: DailyLeaderboardMember -> Rank
+ Advent.Types: [dlbmSupporter] :: DailyLeaderboardMember -> Bool
+ Advent.Types: [dlbmTime] :: DailyLeaderboardMember -> UTCTime
+ Advent.Types: [dlbmUser] :: DailyLeaderboardMember -> Either Integer Text
+ Advent.Types: [getPublicCode] :: PublicCode -> Integer
+ Advent.Types: [getRank] :: Rank -> Finite 100
+ Advent.Types: [glbMap] :: GlobalLeaderboard -> Map Rank (Integer, NonEmpty GlobalLeaderboardMember)
+ Advent.Types: [glbmImage] :: GlobalLeaderboardMember -> Maybe Text
+ Advent.Types: [glbmLink] :: GlobalLeaderboardMember -> Maybe Text
+ Advent.Types: [glbmRank] :: GlobalLeaderboardMember -> Rank
+ Advent.Types: [glbmScore] :: GlobalLeaderboardMember -> Integer
+ Advent.Types: [glbmSupporter] :: GlobalLeaderboardMember -> Bool
+ Advent.Types: [glbmUser] :: GlobalLeaderboardMember -> Either Integer Text
+ Advent.Types: [lbEvent] :: Leaderboard -> Integer
+ Advent.Types: [lbMembers] :: Leaderboard -> Map Integer LeaderboardMember
+ Advent.Types: [lbOwnerId] :: Leaderboard -> Integer
+ Advent.Types: [lbmCompletion] :: LeaderboardMember -> Map Day (Map Part UTCTime)
+ Advent.Types: [lbmGlobalScore] :: LeaderboardMember -> Integer
+ Advent.Types: [lbmId] :: LeaderboardMember -> Integer
+ Advent.Types: [lbmLastStarTS] :: LeaderboardMember -> Maybe UTCTime
+ Advent.Types: [lbmLocalScore] :: LeaderboardMember -> Integer
+ Advent.Types: [lbmName] :: LeaderboardMember -> Maybe Text
+ Advent.Types: [lbmStars] :: LeaderboardMember -> Int
+ Advent.Types: [siAnswer] :: SubmitInfo -> String
+ Advent.Types: [siLevel] :: SubmitInfo -> Part
+ Advent.Types: data DailyLeaderboard
+ Advent.Types: data DailyLeaderboardMember
+ Advent.Types: data GlobalLeaderboardMember
+ Advent.Types: data Leaderboard
+ Advent.Types: data LeaderboardMember
+ Advent.Types: data Part
+ Advent.Types: data SubmitInfo
+ Advent.Types: data SubmitRes
+ Advent.Types: dayInt :: Day -> Integer
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.DailyLeaderboard
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.DailyLeaderboardMember
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.Day
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.GlobalLeaderboard
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.GlobalLeaderboardMember
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.Leaderboard
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.LeaderboardMember
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.Part
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSON Advent.Types.Rank
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSONKey Advent.Types.Day
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSONKey Advent.Types.Part
+ Advent.Types: instance Data.Aeson.Types.FromJSON.FromJSONKey Advent.Types.Rank
+ Advent.Types: instance Data.Aeson.Types.ToJSON.ToJSON Advent.Types.DailyLeaderboard
+ Advent.Types: instance Data.Aeson.Types.ToJSON.ToJSON Advent.Types.DailyLeaderboardMember
+ Advent.Types: instance Data.Aeson.Types.ToJSON.ToJSON Advent.Types.GlobalLeaderboard
+ Advent.Types: instance Data.Aeson.Types.ToJSON.ToJSON Advent.Types.GlobalLeaderboardMember
+ Advent.Types: instance Data.Aeson.Types.ToJSON.ToJSON Advent.Types.Rank
+ Advent.Types: instance Data.Aeson.Types.ToJSON.ToJSONKey Advent.Types.Rank
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.DailyLeaderboard
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.DailyLeaderboardMember
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.Day
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.GlobalLeaderboard
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.GlobalLeaderboardMember
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.Leaderboard
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.LeaderboardMember
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.Part
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.PublicCode
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.Rank
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.SubmitInfo
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.SubmitRes
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.DailyLeaderboard
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.DailyLeaderboardMember
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.Day
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.GlobalLeaderboard
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.GlobalLeaderboardMember
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.Leaderboard
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.LeaderboardMember
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.Part
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.PublicCode
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.Rank
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.SubmitInfo
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.SubmitRes
+ 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.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.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.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.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.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.Types: instance Web.Internal.FormUrlEncoded.ToForm Advent.Types.SubmitInfo
+ Advent.Types: instance Web.Internal.HttpApiData.ToHttpApiData Advent.Types.Day
+ Advent.Types: instance Web.Internal.HttpApiData.ToHttpApiData Advent.Types.Part
+ Advent.Types: instance Web.Internal.HttpApiData.ToHttpApiData Advent.Types.PublicCode
+ Advent.Types: mkDay :: Integer -> Maybe Day
+ Advent.Types: mkDay_ :: Integer -> Day
+ Advent.Types: newtype Day
+ Advent.Types: newtype GlobalLeaderboard
+ Advent.Types: newtype PublicCode
+ Advent.Types: newtype Rank
+ Advent.Types: parseSubmitRes :: Text -> SubmitRes
+ Advent.Types: partChar :: Part -> Char
+ Advent.Types: partInt :: Part -> Int
+ Advent.Types: showSubmitRes :: SubmitRes -> String
- Advent.API: adventAPIClient :: Integer -> (Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))) :<|> (PublicCode -> ClientM Leaderboard)
+ Advent.API: adventAPIClient :: Integer -> (Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))) :<|> (ClientM GlobalLeaderboard :<|> ((Day -> ClientM DailyLeaderboard) :<|> (PublicCode -> ClientM Leaderboard)))
- Advent.API: processHTML :: Text -> [Text]
+ Advent.API: processHTML :: String -> Text -> [Text]
- Advent.API: type AdventAPI = Capture "year" Integer :> ("day" :> Capture "day" Day :> (Get '[Articles] (Map Part Text) :<|> "input" :> Get '[RawText] Text :<|> "answer" :> ReqBody '[FormUrlEncoded] SubmitInfo :> Post '[Articles] (Text :<|> SubmitRes)) :<|> "leaderboard" :> "private" :> "view" :> Capture "code" PublicCode :> Get '[JSON] Leaderboard)
+ Advent.API: type AdventAPI = Capture "year" Integer :> ("day" :> Capture "day" Day :> (Get '[Articles] (Map Part Text) :<|> "input" :> Get '[RawText] Text :<|> "answer" :> ReqBody '[FormUrlEncoded] SubmitInfo :> Post '[Articles] (Text :<|> SubmitRes)) :<|> "leaderboard" :> (Get '[Divs] GlobalLeaderboard :<|> "day" :> Capture "day" Day :> Get '[Divs] DailyLeaderboard :<|> "private" :> "view" :> Capture "code" PublicCode :> Get '[JSON] Leaderboard))

Files

CHANGELOG.md view
@@ -1,6 +1,19 @@ Changelog ========= +Version 0.2.3.0+---------------++*November 21, 2019*++<https://github.com/mstksg/advent-of-code-api/releases/tag/v0.2.3.0>++*   Add API commands for daily and global leaderboards.+*   In the process, the Servant API is reshuffled a bit: `Articles` has been+    generalized to `HTMLTags "article"`, to also support `HTMLTags "div"`.+    `FromArticle` is now `FromTags "article"`.+*   Move some of the data types to be in their own module, *Advent.Types*.+ Version 0.2.2.1 --------------- 
README.md view
@@ -35,8 +35,9 @@ Session Keys ------------ -Session keys are required for all commands, but if you enter a bogus key-you should be able to get at least Part 1 from `AoCPrompt`.+Session keys are required for most commands, but if you enter a bogus key+you should be able to get at least Part 1 from `AoCPrompt`.  Session keys are+also not needed for daily and global leaderboards.  The session key can be found by logging in on a web client and checking the cookies.  You can usually check these with in-browser developer
advent-of-code-api.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: bc9fa15aa4120fc135cccf4203d4b3e26d035a13f3ade163809aee30e14ebc42+-- hash: d17a4d7274081d52248b30e3b0ad9dec6be3dc449140e6d4cfd85f442966d604  name:           advent-of-code-api-version:        0.2.2.1+version:        0.2.3.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@@ -43,6 +43,7 @@   exposed-modules:       Advent       Advent.API+      Advent.Types   other-modules:       Advent.Throttle       Advent.Cache
src/Advent.hs view
@@ -50,7 +50,6 @@   , Day(..)   , AoCOpts(..)   , SubmitRes(..), showSubmitRes-  , Leaderboard(..), LeaderboardMember(..)   , runAoC   , defaultAoCOpts   , AoCError(..)@@ -74,6 +73,7 @@ import           Advent.API import           Advent.Cache import           Advent.Throttle+import           Advent.Types import           Control.Concurrent.STM import           Control.Exception import           Control.Monad.Except@@ -92,10 +92,13 @@ 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 qualified Data.Text.Lazy.Encoding as TL import qualified System.IO.Unsafe        as Unsafe  #if MIN_VERSION_base(4,11,0)@@ -172,6 +175,33 @@         :: 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.+    --+    -- Calls to this 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.+    --+    -- Calls to this will be cached if fetched after each event ends.+    --+    -- @since 0.2.3.0+    AoCGlobalLeaderboard+        :: AoC GlobalLeaderboard+ deriving instance Show (AoC a) deriving instance Typeable (AoC a) @@ -181,6 +211,8 @@ aocDay (AoCInput  d     ) = Just d aocDay (AoCSubmit d _ _ ) = Just d aocDay (AoCLeaderboard _) = Nothing+aocDay (AoCDailyLeaderboard d) = Just d+aocDay AoCGlobalLeaderboard = Nothing  -- | A possible (syncronous, logical, pure) error returnable from 'runAoC'. -- Does not cover any asynchronous or IO errors.@@ -259,8 +291,12 @@     AoCInput  i       -> let _ :<|> r :<|> _ = adventAPIPuzzleClient yr i in r     AoCSubmit i p ans -> let _ :<|> _ :<|> r = adventAPIPuzzleClient yr i                          in  r (SubmitInfo p ans) <&> \(x :<|> y) -> (x, y)-    AoCLeaderboard c  -> let _ :<|> r = adventAPIClient yr+    AoCLeaderboard c  -> let _ :<|> _ :<|> _ :<|> r = adventAPIClient yr                          in  r (PublicCode c)+    AoCDailyLeaderboard d -> let _ :<|> _ :<|> r :<|> _ = adventAPIClient yr+                             in  r d+    AoCGlobalLeaderboard  -> let _ :<|> r :<|> _ :<|> _ = adventAPIClient yr+                             in  r   -- | Cache file for a given 'AoC' command@@ -274,6 +310,8 @@     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   where     keyDir = case sess of       Nothing -> ""@@ -291,12 +329,18 @@       Just c  -> pure (Nothing, c)       Nothing -> (Just _aSessionKey,) . (</> "advent-of-code-api") <$> getTemporaryDirectory -    let cacher = case apiCache keyMayb _aYear a of+    (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 a+                         else saverLoader (not eventOver) a      cacher . runExceptT $ do       forM_ (aocDay a) $ \d -> do@@ -333,8 +377,11 @@     oneYear = 60 * 60 * 24 * 356.25  -saverLoader :: AoC a -> SaverLoader (Either AoCError a)-saverLoader = \case+saverLoader+    :: Bool             -- ^ is the event ongoing (True) or over (False)?+    -> AoC a+    -> SaverLoader (Either AoCError a)+saverLoader evt = \case     AoCPrompt d -> SL { _slSave = either (const Nothing) (Just . encodeMap)                       , _slLoad = \str ->                           let mp     = decodeMap str@@ -346,11 +393,29 @@                       }     AoCSubmit{} -> noCache     AoCLeaderboard{} -> noCache+    AoCDailyLeaderboard d -> SL+        { _slSave = either (const Nothing) (Just . TL.toStrict . TL.decodeUtf8 . A.encode)+        , _slLoad = \str -> do+            r <- A.decode . TL.encodeUtf8 . TL.fromStrict $ str+            let l = M.size (dlbStar1 r) + M.size (dlbStar2 r)+            guard $ l >= fullLength d+            pure $ Right r+        }+    AoCGlobalLeaderboard{}+      | evt       -> noCache+      | otherwise -> SL+          { _slSave = either (const Nothing) (Just . TL.toStrict . TL.decodeUtf8 . A.encode)+          , _slLoad = fmap Right . A.decode . TL.encodeUtf8 . TL.fromStrict+          }   where     expectedParts :: Day -> Set Part     expectedParts d       | d == maxBound = S.singleton Part1       | otherwise     = S.fromDistinctAscList [Part1 ..]+    fullLength :: Day -> Int+    fullLength d+      | d == maxBound = 100+      | otherwise     = 200     sep = ">>>>>>>>>"     encodeMap mp = T.intercalate "\n" . concat $                             [ maybeToList $ M.lookup Part1 mp
src/Advent/API.hs view
@@ -1,12 +1,19 @@ {-# LANGUAGE CPP                        #-} {-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE FlexibleContexts           #-} {-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE FunctionalDependencies     #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase                 #-} {-# LANGUAGE MultiParamTypeClasses      #-} {-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE RecordWildCards            #-}+{-# LANGUAGE ScopedTypeVariables        #-}+{-# LANGUAGE TupleSections              #-}+{-# LANGUAGE TypeApplications           #-} {-# LANGUAGE TypeInType                 #-} {-# LANGUAGE TypeOperators              #-}+{-# LANGUAGE ViewPatterns               #-}  -- | -- Module      : Advent.API@@ -29,163 +36,54 @@ --  module Advent.API (-  -- * Types-    Day(..)-  , Part(..)-  , SubmitInfo(..)-  , SubmitRes(..), showSubmitRes-  , PublicCode(..)-  , Leaderboard(..)-  , LeaderboardMember(..)   -- * Servant API-  , AdventAPI+    AdventAPI   , adventAPI   , adventAPIClient   , adventAPIPuzzleClient-  -- * Util-  , mkDay, mkDay_, dayInt-  , partInt-  , partChar-  -- * Internal-  , processHTML-  , parseSubmitRes+  -- * Types+  , HTMLTags+  , FromTags(..)   , Articles-  , FromArticles(..)+  , Divs   , RawText+  -- * Internal+  , processHTML   ) where --- import qualified Data.Attoparsec.Text    as P-import           Control.Applicative+import           Advent.Types import           Control.Monad-import           Data.Aeson+import           Control.Monad.State import           Data.Bifunctor import           Data.Char import           Data.Finite import           Data.Foldable-import           Data.Functor.Classes-import           Data.Map                   (Map)+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.Clock-import           Data.Time.Clock.POSIX-import           Data.Typeable-import           Data.Void-import           GHC.Generics+import           Data.Text              (Text)+import           Data.Time.Format+import           Data.Time.LocalTime+import           GHC.TypeLits import           Servant.API import           Servant.Client-import           Text.HTML.TagSoup.Tree     (TagTree(..))-import           Text.Printf-import           Text.Read                  (readMaybe)-import qualified Data.ByteString.Lazy       as BSL-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 qualified Text.HTML.TagSoup.Tree     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.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 qualified Text.HTML.TagSoup.Tree as H  #if !MIN_VERSION_base(4,11,0) import           Data.Semigroup ((<>)) #endif --- | Describes the day: a number between 1 and 25 inclusive.------ 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 }-  deriving (Eq, Ord, Enum, Bounded, Typeable, Generic)--instance Show Day where-    showsPrec = showsUnaryWith (\d -> showsPrec d . dayInt) "mkDay"---- | A given part of a problem.  All Advent of Code challenges are--- two-parts.------ You can usually get 'Part1' (if it is already released) with a nonsense--- session key, but 'Part2' always requires a valid session key.------ Note also that Challenge #25 typically only has a single part.-data Part = Part1 | Part2-  deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable, Generic)---- | Info required to submit an answer for a part.-data SubmitInfo = SubmitInfo-    { 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-    -- usually happens if rank is under 1000)-    = 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-    -- 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-    -- 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-  deriving (Show, Read, Eq, Ord, Typeable, Generic)---- | Member ID of public leaderboard (the first part of the registration--- code, before the hyphen).  It can be found as the number in the URL:------ > https://adventofcode.com/2019/leaderboard/private/view/12345------ (the @12345@ above)-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-    }-  deriving (Show, Eq, Ord, Typeable, Generic)---- | Leaderboard position for a given member.-data LeaderboardMember = LBM-    { lbmGlobalScore :: 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-    }-  deriving (Show, Eq, Ord, Typeable, Generic)--instance ToHttpApiData Part where-    toUrlPiece = T.pack . show . partInt-    toQueryParam = toUrlPiece--instance ToHttpApiData Day where-    toUrlPiece = T.pack . show . dayInt-    toQueryParam = toUrlPiece--instance ToHttpApiData PublicCode where-    toUrlPiece   = (<> ".json") . T.pack . show . getPublicCode-    toQueryParam = toUrlPiece--instance WF.ToForm SubmitInfo where-    toForm = WF.genericToForm WF.FormOptions-      { WF.fieldLabelModifier = camelTo2 '-' . drop 2 }- -- | Raw "text/plain" MIME type data RawText @@ -195,84 +93,116 @@ instance MimeUnrender RawText Text where     mimeUnrender _ = first show . T.decodeUtf8' . BSL.toStrict --- | Interpret repsonse as a list of HTML 'Text' in @<article>@ tags.-data Articles+-- | Interpret repsonse as a list of HTML 'Text' found in the given type of+-- tag+--+-- @since 0.2.3.0+data HTMLTags (tag :: Symbol) --- | Class for interpreting a list of 'Text' in article tags to some--- desired output.-class FromArticles a where-    fromArticles :: [Text] -> a+-- | Interpret a response as a list of HTML 'Text' found in @<article>@ tags.+type Articles = HTMLTags "article" -instance Accept Articles where+-- | Interpret a response as a list of HTML 'Text' found in @<div>@ tags.+--+-- @since 0.2.3.0+type Divs     = HTMLTags "div"++-- | Class for interpreting a list of 'Text' in tags to some desired+-- output.+--+-- @since 0.2.3.0+class FromTags tag a where+    fromTags :: p tag -> [Text] -> Maybe a++instance Accept (HTMLTags cls) where     contentType _ = "text" M.// "html" -instance FromArticles a => MimeUnrender Articles a where-    mimeUnrender _ = fmap fromArticles-                   . bimap show processHTML-                   . T.decodeUtf8'-                   . BSL.toStrict+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 -instance FromArticles [Text] where-    fromArticles = id+instance FromTags cls [Text] where+    fromTags _ = Just -instance FromArticles Text where-    fromArticles = T.unlines+instance FromTags cls Text where+    fromTags _ = Just . T.unlines -instance (Ord a, Enum a, Bounded a) => FromArticles (Map a Text) where-    fromArticles = M.fromList . zip [minBound ..]+instance (Ord a, Enum a, Bounded a) => FromTags cls (Map a Text) where+    fromTags _ = Just . M.fromList . zip [minBound ..] -instance (FromArticles a, FromArticles b) => FromArticles (a :<|> b) where-    fromArticles xs = fromArticles xs :<|> fromArticles xs+instance (FromTags cls a, FromTags cls b) => FromTags cls (a :<|> b) where+    fromTags p xs = (:<|>) <$> fromTags p xs <*> fromTags p xs -instance FromArticles SubmitRes where-    fromArticles = parseSubmitRes . fold  . listToMaybe+instance FromTags "article" SubmitRes where+    fromTags _ = Just . parseSubmitRes . fold  . listToMaybe -instance FromJSON Leaderboard where-    parseJSON = withObject "Leaderboard" $ \o ->-        LB <$> (strInt =<< (o .: "event"))-           <*> (strInt =<< (o .: "owner_id"))-           <*> o .: "members"+instance FromTags "div" DailyLeaderboard where+    fromTags _ = Just . assembleDLB . mapMaybe parseMember       where-        strInt t = case readMaybe t of-          Nothing -> fail "bad int"-          Just i  -> pure i+        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")+            dlbmTime <- fmap (localTimeToUTC (read "EST"))+                      . 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) }) -instance FromJSON LeaderboardMember where-    parseJSON = withObject "LeaderboardMember" $ \o ->-        LBM <$> o .: "global_score"-            <*> optional (o .: "name")-            <*> o .: "local_score"-            <*> (strInt =<< (o .: "id"))-            <*> optional (fromEpoch =<< (o .: "last_star_ts"))-            <*> o .: "stars"-            <*> (do cdl <- o .: "completion_day_level"-                    (traverse . traverse) ((fromEpoch =<<) . (.: "get_star_ts")) cdl-                )+instance FromTags "div" GlobalLeaderboard where+    fromTags _ = Just . GLB . reScore . M.fromListWith (<>)+               . map (\x -> (Down (glbmScore x), x :| []))+               . mapMaybe parseMember       where-        strInt t = case readMaybe t of-          Nothing -> fail "bad int"-          Just i  -> pure i-        fromEpoch t = case readMaybe t of-          Nothing -> fail "bad stamp"-          Just i  -> pure . posixSecondsToUTCTime $ fromInteger i--instance FromJSONKey Day where-    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)-instance FromJSONKey Part where-    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)--instance FromJSON Part where-    parseJSON = withText "Part" $ \case-      "1" -> pure Part1-      "2" -> pure Part2-      _   -> fail "Bad part"-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+        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')   @@ -288,9 +218,14 @@                      :> ReqBody '[FormUrlEncoded] SubmitInfo                      :> Post    '[Articles] (Text :<|> SubmitRes)                 )-  :<|> "leaderboard" :> "private" :> "view"-                     :> Capture "code" PublicCode-                     :> Get '[JSON] Leaderboard+  :<|> "leaderboard"+    :> (Get '[Divs] GlobalLeaderboard+   :<|> "day"     :> Capture "day" Day :> Get '[Divs] DailyLeaderboard+   :<|> "private" :> "view"+                  :> Capture "code" PublicCode+                  :> Get '[JSON] Leaderboard++       )       )  -- | 'Proxy' used for /servant/ functions.@@ -301,6 +236,8 @@ adventAPIClient     :: Integer     -> (Day -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)) )+  :<|> ClientM GlobalLeaderboard+  :<|> (Day -> ClientM DailyLeaderboard)   :<|> (PublicCode -> ClientM Leaderboard) adventAPIClient = client adventAPI @@ -314,17 +251,42 @@   where     pis :<|> _ = adventAPIClient y --- | Process an HTML webpage into a list of all contents in <article>s-processHTML :: Text -> [Text]-processHTML = map H.renderTree-            . mapMaybe isArticle-            . H.universeTree-            . H.parseTree-            . cleanDoubleTitle+userNameNaked :: [TagTree Text] -> Maybe Text+userNameNaked = (listToMaybe .) . mapMaybe $ \x -> do+  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])+findTag uni tag cls = listToMaybe . flip mapMaybe uni $ \x -> do+  TagBranch tag' attr cld <- Just x+  guard $ tag' == tag+  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")+    ]   where-    isArticle :: TagTree Text -> Maybe [TagTree Text]-    isArticle (TagBranch n _ ts) = ts <$ guard (n == "article")-    isArticle _                  = Nothing+    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.parseTree+               . cleanDoubleTitle+  where+    getTag :: TagTree Text -> Maybe Text+    getTag (TagBranch n _ ts) = H.renderTree ts <$ guard (n == T.pack tag)+    getTag _                  = Nothing     -- 2016 Day 2 Part 2 has a malformed `<span>...</title>` tag that     -- causes tagsoup to choke.  this converts all </title> except for the     -- first one to be <span>.@@ -332,90 +294,4 @@     cleanDoubleTitle t = case T.splitOn "</title>" t of         x:xs -> x <> "</title>" <> T.intercalate "</span>" xs         []   -> ""      -- this shouldn't ever happen because splitOn is always non-empty---- | Parse 'Text' into a 'SubmitRes'.-parseSubmitRes :: Text -> SubmitRes-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"-                               ]-    parseCorrect :: P.Parsec Void Text SubmitRes-    parseCorrect = do-      _ <- P.manyTill P.anySingle (P.string' "that's the right answer") P.<?> "Right answer"-      r <- optional . (P.<?> "Rank") . P.try $ do-        P.manyTill P.anySingle (P.string' "rank")-          *> P.skipMany (P.satisfy (not . isDigit))-        P.decimal-      pure $ SubCorrect r-    parseIncorrect = do-      _ <- P.manyTill P.anySingle (P.string' "that's not the right answer") P.<?> "Not the right answer"-      hint <- optional . (P.<?> "Hint") . P.try $ do-        P.manyTill P.anySingle "your answer is" *> P.space1-        P.takeWhile1P (Just "dot") (/= '.')-      P.manyTill P.anySingle (P.string' "wait") *> P.space1-      waitAmt <- (1 <$ P.string' "one") <|> P.decimal-      pure $ SubIncorrect (waitAmt * 60) (T.unpack <$> hint)-    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-      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")---- | 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---- | Convert a @'Finite' 25@ day into a day integer (1 - 25).  Inverse of--- 'mkDay'.-dayInt :: Day -> Integer-dayInt = (+ 1) . getFinite . dayFinite---- | Convert a 'Part' to an 'Int'.-partInt :: Part -> Int-partInt Part1 = 1-partInt Part2 = 2---- | Construct a 'Day' from a day integer (1 - 25).  If input is out of--- range, 'Nothing' is returned.  See 'mkDay_' for an unsafe version useful--- for literals.------ Inverse of 'dayInt'.-mkDay :: Integer -> Maybe Day-mkDay = fmap Day . packFinite . subtract 1---- | Construct a @'Finite' 25@ (the type of a Day) from a day--- integer (1 - 25).  Is undefined if input is out of range.  Can be useful--- for compile-time literals, like @'mkDay_' 4@------ Inverse of 'dayInt'.-mkDay_ :: Integer -> Day-mkDay_ = fromMaybe e . mkDay-  where-    e = errorWithoutStackTrace "Advent.mkDay_: Date out of range (1 - 25)"---- | A character associated with a given part.  'Part1' is associated with--- @\'a\'@, and 'Part2' is associated with @\'b\'@-partChar :: Part -> Char-partChar Part1 = 'a'-partChar Part2 = 'b' 
+ src/Advent/Types.hs view
@@ -0,0 +1,392 @@+{-# LANGUAGE CPP                        #-}+{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE FunctionalDependencies     #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase                 #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE RecordWildCards            #-}+{-# LANGUAGE ScopedTypeVariables        #-}+{-# LANGUAGE TupleSections              #-}+{-# LANGUAGE TypeApplications           #-}+{-# LANGUAGE TypeInType                 #-}+{-# LANGUAGE TypeOperators              #-}+{-# LANGUAGE ViewPatterns               #-}++-- |+-- Module      : Advent.Types+-- Copyright   : (c) Justin Le 2019+-- License     : BSD3+--+-- Maintainer  : justin@jle.im+-- Stability   : experimental+-- Portability : non-portable+--+-- 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(..)+  -- * Util+  , mkDay, mkDay_, dayInt+  , partInt+  , partChar+  -- * Internal+  , parseSubmitRes+  ) where++import           Control.Applicative+import           Data.Aeson+import           Data.Aeson.Types+import           Data.Char+import           Data.Finite+import           Data.Foldable+import           Data.Functor.Classes+import           Data.List.NonEmpty         (NonEmpty(..))+import           Data.Map                   (Map)+import           Data.Maybe+import           Data.Text                  (Text)+import           Data.Time.Clock+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.Text                  as T+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++#if !MIN_VERSION_base(4,11,0)+import           Data.Semigroup ((<>))+#endif++-- | Describes the day: a number between 1 and 25 inclusive.+--+-- 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 }+  deriving (Eq, Ord, Enum, Bounded, Typeable, Generic)++instance Show Day where+    showsPrec = showsUnaryWith (\d -> showsPrec d . dayInt) "mkDay"++-- | A given part of a problem.  All Advent of Code challenges are+-- two-parts.+--+-- You can usually get 'Part1' (if it is already released) with a nonsense+-- session key, but 'Part2' always requires a valid session key.+--+-- Note also that Challenge #25 typically only has a single part.+data Part = Part1 | Part2+  deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable, Generic)++-- | Info required to submit an answer for a part.+data SubmitInfo = SubmitInfo+    { 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+    -- usually happens if rank is under 1000)+    = 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+    -- 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+    -- 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+  deriving (Show, Read, Eq, Ord, Typeable, Generic)++-- | Member ID of public leaderboard (the first part of the registration+-- code, before the hyphen).  It can be found as the number in the URL:+--+-- > https://adventofcode.com/2019/leaderboard/private/view/12345+--+-- (the @12345@ above)+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+    }+  deriving (Show, Eq, Ord, Typeable, Generic)++-- | Leaderboard position for a given member.+data LeaderboardMember = LBM+    { lbmGlobalScore :: 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+    }+  deriving (Show, Eq, Ord, Typeable, Generic)++-- | Ranking between 1 to 100, for daily and global leaderboards+newtype Rank = Rank { getRank :: Finite 100 }+  deriving (Show, Eq, Ord, Typeable, Generic)++-- | Single daily leaderboard position+data DailyLeaderboardMember = DLBM+    { dlbmRank      :: Rank+    , dlbmTime      :: UTCTime+    , dlbmUser      :: Either Integer Text+    , dlbmLink      :: Maybe Text+    , dlbmImage     :: Maybe Text+    , dlbmSupporter :: Bool+    }+  deriving (Show, Eq, Ord, Typeable, Generic)++-- | Daily leaderboard, containing Star 1 and Star 2 completions+data DailyLeaderboard = DLB {+    dlbStar1 :: Map Rank DailyLeaderboardMember+  , dlbStar2 :: Map Rank DailyLeaderboardMember+  }+  deriving (Show, Eq, Ord, Typeable, Generic)++-- | Single global leaderboard position+data GlobalLeaderboardMember = GLBM+    { 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+newtype GlobalLeaderboard = GLB {+    glbMap :: Map Rank (Integer, NonEmpty GlobalLeaderboardMember)+  }+  deriving (Show, Eq, Ord, Typeable, Generic)+++instance ToHttpApiData Part where+    toUrlPiece = T.pack . show . partInt+    toQueryParam = toUrlPiece++instance ToHttpApiData Day where+    toUrlPiece = T.pack . show . dayInt+    toQueryParam = toUrlPiece++instance ToHttpApiData PublicCode where+    toUrlPiece   = (<> ".json") . T.pack . show . getPublicCode+    toQueryParam = toUrlPiece++instance WF.ToForm SubmitInfo where+    toForm = WF.genericToForm WF.FormOptions+      { WF.fieldLabelModifier = camelTo2 '-' . drop 2 }++instance FromJSON Leaderboard where+    parseJSON = withObject "Leaderboard" $ \o ->+        LB <$> (strInt =<< (o .: "event"))+           <*> (strInt =<< (o .: "owner_id"))+           <*> o .: "members"+      where+        strInt t = case readMaybe t of+          Nothing -> fail "bad int"+          Just i  -> pure i++instance FromJSON LeaderboardMember where+    parseJSON = withObject "LeaderboardMember" $ \o ->+        LBM <$> o .: "global_score"+            <*> optional (o .: "name")+            <*> o .: "local_score"+            <*> (strInt =<< (o .: "id"))+            <*> optional (fromEpoch =<< (o .: "last_star_ts"))+            <*> o .: "stars"+            <*> (do cdl <- o .: "completion_day_level"+                    (traverse . traverse) ((fromEpoch =<<) . (.: "get_star_ts")) cdl+                )+      where+        strInt t = case readMaybe t of+          Nothing -> fail "bad int"+          Just i  -> pure i+        fromEpoch t = case readMaybe t of+          Nothing -> fail "bad stamp"+          Just i  -> pure . posixSecondsToUTCTime $ fromInteger i++instance FromJSONKey Day where+    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)+instance FromJSONKey Part where+    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)++instance FromJSON Part where+    parseJSON = withText "Part" $ \case+      "1" -> pure Part1+      "2" -> pure Part2+      _   -> fail "Bad part"+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++instance ToJSONKey Rank where+    toJSONKey = toJSONKeyText $ T.pack . show . (+ 1) . getFinite . getRank+instance FromJSONKey Rank where+    fromJSONKey = FromJSONKeyTextParser (parseJSON . String)++instance ToJSON Rank where+    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++instance ToJSON DailyLeaderboard where+    toJSON = genericToJSON defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 3 }+instance FromJSON DailyLeaderboard where+    parseJSON = genericParseJSON defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 3 }++instance ToJSON GlobalLeaderboard where+    toJSON = genericToJSON defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 3 }+instance FromJSON GlobalLeaderboard where+    parseJSON = genericParseJSON defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 3 }++instance ToJSON DailyLeaderboardMember where+    toJSON = genericToJSON defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 4 }+instance FromJSON DailyLeaderboardMember where+    parseJSON = genericParseJSON defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 4 }++instance ToJSON GlobalLeaderboardMember where+    toJSON = genericToJSON defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 4 }+instance FromJSON GlobalLeaderboardMember where+    parseJSON = genericParseJSON defaultOptions+        { fieldLabelModifier = camelTo2 '-' . drop 4 }++-- | Parse 'Text' into a 'SubmitRes'.+parseSubmitRes :: Text -> SubmitRes+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"+                               ]+    parseCorrect :: P.Parsec Void Text SubmitRes+    parseCorrect = do+      _ <- P.manyTill P.anySingle (P.string' "that's the right answer") P.<?> "Right answer"+      r <- optional . (P.<?> "Rank") . P.try $ do+        P.manyTill P.anySingle (P.string' "rank")+          *> P.skipMany (P.satisfy (not . isDigit))+        P.decimal+      pure $ SubCorrect r+    parseIncorrect = do+      _ <- P.manyTill P.anySingle (P.string' "that's not the right answer") P.<?> "Not the right answer"+      hint <- optional . (P.<?> "Hint") . P.try $ do+        P.manyTill P.anySingle "your answer is" *> P.space1+        P.takeWhile1P (Just "dot") (/= '.')+      P.manyTill P.anySingle (P.string' "wait") *> P.space1+      waitAmt <- (1 <$ P.string' "one") <|> P.decimal+      pure $ SubIncorrect (waitAmt * 60) (T.unpack <$> hint)+    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+      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")++-- | 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++-- | Convert a @'Finite' 25@ day into a day integer (1 - 25).  Inverse of+-- 'mkDay'.+dayInt :: Day -> Integer+dayInt = (+ 1) . getFinite . dayFinite++-- | Convert a 'Part' to an 'Int'.+partInt :: Part -> Int+partInt Part1 = 1+partInt Part2 = 2++-- | Construct a 'Day' from a day integer (1 - 25).  If input is out of+-- range, 'Nothing' is returned.  See 'mkDay_' for an unsafe version useful+-- for literals.+--+-- Inverse of 'dayInt'.+mkDay :: Integer -> Maybe Day+mkDay = fmap Day . packFinite . subtract 1++-- | Construct a @'Finite' 25@ (the type of a Day) from a day+-- integer (1 - 25).  Is undefined if input is out of range.  Can be useful+-- for compile-time literals, like @'mkDay_' 4@+--+-- Inverse of 'dayInt'.+mkDay_ :: Integer -> Day+mkDay_ = fromMaybe e . mkDay+  where+    e = errorWithoutStackTrace "Advent.mkDay_: Date out of range (1 - 25)"++-- | A character associated with a given part.  'Part1' is associated with+-- @\'a\'@, and 'Part2' is associated with @\'b\'@+partChar :: Part -> Char+partChar Part1 = 'a'+partChar Part2 = 'b'+
test/Spec.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE ViewPatterns #-} -import           Advent.API+import           Advent.Types import           Control.Monad import           Data.List import           System.Directory