packages feed

swarm-0.6.0.0: src/swarm-tournament/Swarm/Web/Tournament/Database/Query.hs

{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-unrecognised-pragmas #-}

{-# HLINT ignore "Functor law" #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- SQL Queries for Swarm tournaments.
module Swarm.Web.Tournament.Database.Query where

import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
import Control.Monad.Trans.Reader (ReaderT, ask)
import Data.ByteString.Lazy qualified as LBS
import Data.Maybe (listToMaybe)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Time.Clock
import Database.SQLite.Simple
import Database.SQLite.Simple.ToField
import Swarm.Game.Scenario.Scoring.CodeSize
import Swarm.Game.State (Sha1 (..))
import Swarm.Game.Tick (TickNumber (..))
import Swarm.Web.Auth
import Swarm.Web.Tournament.Type

type ConnectInfo = String

databaseFilename :: ConnectInfo
databaseFilename = "swarm-games.db"

newtype UserId = UserId Int

instance ToField UserId where
  toField (UserId x) = toField x

data AuthenticationStorage m = AuthenticationStorage
  { usernameFromCookie :: TL.Text -> m (Maybe UserAlias)
  , cookieFromUsername :: UserAlias -> m TL.Text
  }

data PersistenceLayer m = PersistenceLayer
  { scenarioStorage :: ScenarioPersistence m ScenarioUploadResponsePayload
  , solutionStorage :: ScenarioPersistence m SolutionUploadResponsePayload
  , authenticationStorage :: AuthenticationStorage m
  }

data ScenarioPersistence m a = ScenarioPersistence
  { lookupCache :: Sha1 -> m (Maybe AssociatedSolutionCharacterization)
  -- ^ Looks up by key
  , storeCache :: CharacterizationResponse a -> m Sha1
  -- ^ Stores and returns key
  , getContent :: Sha1 -> m (Maybe LBS.ByteString)
  -- ^ Dump file contents
  }

data UserAttributedUpload = UserAttributedUpload
  { uploader :: UserAlias
  , fileUpload :: FileUpload
  }

data CharacterizationResponse a = CharacterizationResponse
  { upload :: UserAttributedUpload
  , associatedCharacterization :: AssociatedSolutionCharacterization
  , payload :: a
  }

data ScenarioUploadResponsePayload = ScenarioUploadResponsePayload
  { swarmGameVersion :: Sha1
  , sTitle :: T.Text
  }

newtype SolutionUploadResponsePayload = SolutionUploadResponsePayload
  { scenariohash :: Sha1
  }

instance FromRow AssociatedSolutionCharacterization where
  fromRow =
    AssociatedSolutionCharacterization
      <$> (Sha1 <$> field)
      <*> fromRow

instance FromRow SolutionCharacterization where
  fromRow =
    SolutionCharacterization
      <$> field
      <*> (TickNumber <$> field)
      <*> field
      <*> (ScenarioCodeMetrics <$> field <*> field)

instance FromRow TournamentGame where
  fromRow =
    TournamentGame
      <$> field
      <*> field
      <*> (Sha1 <$> field)
      <*> field
      <*> (Sha1 <$> field)
      <*> field

instance FromRow TournamentSolution where
  fromRow =
    TournamentSolution
      <$> field
      <*> field
      <*> fromRow

instance FromRow SolutionFileCharacterization where
  fromRow =
    SolutionFileCharacterization
      <$> (Sha1 <$> field)
      <*> fromRow

-- * Authentication

-- | If the username already exists, overwrite the row.
insertCookie ::
  UserAlias ->
  ReaderT Connection IO TL.Text
insertCookie gitHubUsername = do
  conn <- ask
  liftIO $ do
    [Only cookieString] <-
      query
        conn
        "REPLACE INTO users (alias) VALUES (?) RETURNING cookie;"
        (Only gitHubUsername)
    return cookieString

-- | If the username already exists, overwrite the row.
insertGitHubTokens ::
  UserAlias ->
  ReceivedTokens ->
  ReaderT Connection IO ()
insertGitHubTokens gitHubUsername gitHubTokens = do
  conn <- ask
  currentTime <- liftIO getCurrentTime
  let expirationOf = mkExpirationTime currentTime
  liftIO $ do
    execute
      conn
      "REPLACE INTO github_tokens (alias, github_access_token, github_access_token_expires_at, github_refresh_token, github_refresh_token_expires_at) VALUES (?, ?, ?, ?, ?);"
      ( gitHubUsername
      , token $ accessToken gitHubTokens
      , expirationOf accessToken
      , token $ refreshToken gitHubTokens
      , expirationOf refreshToken
      )
    return ()
 where
  mkExpirationTime currTime accessor =
    addUTCTime (fromIntegral $ expirationSeconds $ accessor gitHubTokens) currTime

getUsernameFromCookie ::
  TL.Text ->
  ReaderT Connection IO (Maybe UserAlias)
getUsernameFromCookie cookieText = do
  conn <- ask
  liftIO . fmap (fmap (UserAlias . fromOnly) . listToMaybe) $
    query conn "SELECT alias FROM users WHERE cookie = ?;" (Only cookieText)

-- * Retrieval

lookupScenarioContent :: Sha1 -> ReaderT Connection IO (Maybe LBS.ByteString)
lookupScenarioContent sha1 = do
  conn <- ask
  liftIO . fmap (fmap fromOnly . listToMaybe) $
    query conn "SELECT content FROM scenarios WHERE content_sha1 = ?;" (Only sha1)

lookupSolutionContent :: Sha1 -> ReaderT Connection IO (Maybe LBS.ByteString)
lookupSolutionContent sha1 = do
  conn <- ask
  liftIO . fmap (fmap fromOnly . listToMaybe) $
    query conn "SELECT content FROM solution_submission WHERE content_sha1 = ?;" (Only sha1)

lookupSolutionSubmission :: Sha1 -> ReaderT Connection IO (Maybe AssociatedSolutionCharacterization)
lookupSolutionSubmission contentSha1 = do
  conn <- ask
  liftIO $ runMaybeT $ do
    evaluationId :: Int <-
      MaybeT $
        fmap fromOnly . listToMaybe
          <$> query conn "SELECT solution_evaluation FROM solution_submission WHERE content_sha1 = ?;" (Only contentSha1)

    MaybeT $
      listToMaybe
        <$> query conn "SELECT scenario, wall_time_seconds, ticks, seed, char_count, ast_size FROM evaluated_solution WHERE id = ?;" (Only evaluationId)

-- | There should only be one builtin solution for the scenario.
lookupScenarioSolution :: Sha1 -> ReaderT Connection IO (Maybe AssociatedSolutionCharacterization)
lookupScenarioSolution scenarioSha1 = do
  conn <- ask
  solnChar <-
    liftIO . fmap listToMaybe $
      query conn "SELECT wall_time_seconds, ticks, seed, char_count, ast_size FROM evaluated_solution WHERE builtin AND scenario = ? LIMIT 1;" (Only scenarioSha1)
  return $ AssociatedSolutionCharacterization scenarioSha1 <$> solnChar

listGames :: ReaderT Connection IO [TournamentGame]
listGames = do
  conn <- ask
  liftIO $
    query_ conn "SELECT original_filename, scenario_uploader, scenario, submission_count, swarm_git_sha1, title FROM agg_scenario_submissions;"

listSubmissions :: Sha1 -> ReaderT Connection IO GameWithSolutions
listSubmissions scenarioSha1 = do
  conn <- ask
  liftIO $ do
    [game] <- query conn "SELECT original_filename, scenario_uploader, scenario, submission_count, swarm_git_sha1, title FROM agg_scenario_submissions WHERE scenario = ?;" (Only scenarioSha1)
    solns <- query conn "SELECT uploaded_at, solution_submitter, solution_sha1, wall_time_seconds, ticks, seed, char_count, ast_size FROM all_solution_submissions WHERE scenario = ?;" (Only scenarioSha1)
    return $ GameWithSolutions game solns

-- * Insertion

insertScenario ::
  CharacterizationResponse ScenarioUploadResponsePayload ->
  ReaderT Connection IO Sha1
insertScenario s = do
  conn <- ask
  h <- liftIO $ do
    [Only resultList] <-
      query
        conn
        "INSERT INTO scenarios (content_sha1, content, original_filename, title, uploader, swarm_git_sha1) VALUES (?, ?, ?, ?, ?, ?) RETURNING content_sha1;"
        ( scenarioSha
        , fileContent $ fileUpload $ upload s
        , filename . fileMetadata . fileUpload $ upload s
        , sTitle $ payload s
        , uploader $ upload s
        , swarmGameVersion $ payload s
        )
    _ <- insertSolution conn True scenarioSha $ characterization $ associatedCharacterization s

    return resultList
  return $ Sha1 h
 where
  scenarioSha = fileHash . fileMetadata . fileUpload $ upload s

insertSolutionSubmission ::
  CharacterizationResponse SolutionUploadResponsePayload ->
  ReaderT Connection IO Sha1
insertSolutionSubmission (CharacterizationResponse solutionUpload s (SolutionUploadResponsePayload scenarioSha)) = do
  conn <- ask
  liftIO $ do
    solutionEvalId <- insertSolution conn False scenarioSha $ characterization s
    [Only echoedSha1] <-
      query
        conn
        "INSERT INTO solution_submission (uploader, content_sha1, solution_evaluation, content) VALUES (?, ?, ?, ?) RETURNING content_sha1;"
        ( uploader solutionUpload
        , fileHash $ fileMetadata $ fileUpload solutionUpload
        , solutionEvalId
        , fileContent $ fileUpload solutionUpload
        )
    return $ Sha1 echoedSha1

insertSolution ::
  Connection ->
  Bool ->
  Sha1 ->
  SolutionCharacterization ->
  IO Int
insertSolution conn isBuiltin scenarioSha s = do
  [Only evaluationId] <-
    query
      conn
      "INSERT INTO evaluated_solution (scenario, builtin, wall_time_seconds, ticks, seed, char_count, ast_size) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id;"
      insertion_items
  return evaluationId
 where
  insertion_items =
    ( scenarioSha
    , isBuiltin
    , solutionWallTime s
    , getTickNumber $ solutionTicks s
    , scenarioSeed s
    , sourceTextLength $ solutionCodeMetrics s
    , astSize $ solutionCodeMetrics s
    )