diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/SlackBuilder/LatestVersionCheck.hs | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/lib/SlackBuilder/LatestVersionCheck.hs b/lib/SlackBuilder/LatestVersionCheck.hs new file mode 100644 index 0000000..5dae251 --- /dev/null +++ b/lib/SlackBuilder/LatestVersionCheck.hs @@ -0,0 +1,192 @@ +{- This Source Code Form is subject to the terms of the Mozilla Public License, + v. 2.0. If a copy of the MPL was not distributed with this file, You can + obtain one at https://mozilla.org/MPL/2.0/. -} + +-- | This module contains implementations to check the latest version of a +-- package hosted by a specific service. +module SlackBuilder.LatestVersionCheck + ( PackageOwner(..) + , TextArguments(..) + , latestGitHub + , latestPackagist + , latestText + , stableTagTransform + ) where + +import SlackBuilder.Config +import qualified Data.Aeson as Aeson +import Data.Aeson ((.:)) +import Data.Aeson.TH (defaultOptions, deriveJSON) +import Data.HashMap.Strict (HashMap) +import qualified Data.HashMap.Strict as HashMap +import Data.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text.Encoding +import Data.Vector (Vector, (!?)) +import qualified Data.Vector as Vector +import Network.HTTP.Req + ( header + , runReq + , defaultHttpConfig + , req + , GET(..) + , https + , jsonResponse + , NoReqBody(..) + , (/:) + , responseBody + , useHttpsURI + , bsResponse + , POST(..) + , ReqBodyJson(..) + ) +import Text.URI (mkURI) +import SlackBuilder.Trans +import qualified Data.Aeson.KeyMap as KeyMap +import GHC.Records (HasField(..)) +import Control.Monad.Trans.Reader (asks) +import Control.Monad.IO.Class (MonadIO(..)) +import Control.Monad ((>=>)) + +data PackageOwner = PackageOwner + { owner :: Text + , name :: Text + } deriving (Eq, Show) + +-- | Removes the leading "v" from the version string and returns the result if +-- it looks like a version. +stableTagTransform :: Text -> Maybe Text +stableTagTransform = Text.stripPrefix "v" >=> checkForStable + where + checkForStable tag + | Text.any (`elem` ['-', '+']) tag = Nothing + | otherwise = Just tag + +-- * Packagist + +newtype PackagistPackage = PackagistPackage + { version :: Text + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''PackagistPackage) + +newtype PackagistResponse = PackagistResponse + { packages :: HashMap Text (Vector PackagistPackage) + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''PackagistResponse) + +latestPackagist :: PackageOwner -> SlackBuilderT (Maybe Text) +latestPackagist PackageOwner{..} = do + packagistResponse <- runReq defaultHttpConfig $ + let uri = https "repo.packagist.org" /: "p2" + /: owner + /: name <> ".json" + in req GET uri NoReqBody jsonResponse mempty + let packagistPackages = packages $ responseBody packagistResponse + fullName = Text.intercalate "/" [owner, name] + + pure $ HashMap.lookup fullName packagistPackages + >>= fmap (version . fst) . Vector.uncons + +-- * Remote text file + +data TextArguments = TextArguments + { versionPicker :: Text -> Text + , textURL :: Text + } + +latestText :: TextArguments -> SlackBuilderT (Maybe Text) +latestText TextArguments{..} = do + uri <- liftIO $ useHttpsURI <$> mkURI textURL + packagistResponse <- traverse (runReq defaultHttpConfig . go . fst) uri + + pure $ versionPicker . Text.Encoding.decodeUtf8 . responseBody + <$> packagistResponse + where + go uri = req GET uri NoReqBody bsResponse mempty + +-- * GitHub + +newtype GhRefNode = GhRefNode + { name :: Text + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhRefNode) + +newtype GhRef = GhRef + { nodes :: Vector GhRefNode + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhRef) + +newtype GhRepository = GhRepository + { refs :: GhRef + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhRepository) + +newtype GhData = GhData + { repository :: GhRepository + } deriving (Eq, Show) + +instance Aeson.FromJSON GhData where + parseJSON (Aeson.Object keyMap) + | Just data' <- KeyMap.lookup "data" keyMap = + GhData <$> Aeson.withObject "GhData" (.: "repository") data' + parseJSON _ = fail "data key not found in the response" + +data GhVariables = GhVariables + { name :: Text + , owner :: Text + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhVariables) + +data GhQuery = GhQuery + { query :: Text + , variables :: GhVariables + } deriving (Eq, Show) + +$(deriveJSON defaultOptions ''GhQuery) + +latestGitHub + :: PackageOwner + -> (Text -> Maybe Text) + -> SlackBuilderT (Maybe Text) +latestGitHub PackageOwner{..} versionTransform = do + ghToken' <- SlackBuilderT $ asks ghToken + ghResponse <- runReq defaultHttpConfig $ + let uri = https "api.github.com" /: "graphql" + query = GhQuery + { query = githubQuery + , variables = GhVariables + { owner = owner + , name = name + } + } + authorizationHeader = header "authorization" + $ Text.Encoding.encodeUtf8 + $ "Bearer " <> ghToken' + in req POST uri (ReqBodyJson query) jsonResponse + $ authorizationHeader <> header "User-Agent" "SlackBuilder" + let ghNodes = nodes + $ refs + $ (getField @"repository" :: GhData -> GhRepository) + $ responseBody ghResponse + refs' = Vector.reverse + $ Vector.catMaybes + $ versionTransform . getField @"name" <$> ghNodes + pure $ refs' !? 0 + where + githubQuery = + "query ($name: String!, $owner: String!) {\n\ + \ repository(name: $name, owner: $owner) {\n\ + \ refs(last: 10, refPrefix: \"refs/tags/\", orderBy: { field: TAG_COMMIT_DATE, direction: ASC }) {\n\ + \ nodes {\n\ + \ id,\n\ + \ name\n\ + \ }\n\ + \ }\n\ + \ }\n\ + \}" |
