hpaste-1.2.0: src/Hpaste/Model/Paste.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS -Wall -fno-warn-name-shadowing #-}
-- | Paste model.
module Hpaste.Model.Paste
(getLatestPastes
,getPasteById
,getPrivatePasteById
,createOrUpdate
,deletePaste
,createPaste
,getAnnotations
,getRevisions
,getPaginatedPastes
,countPublicPastes
,generateHints
,getHints
,validNick)
where
import Hpaste.Types
import Hpaste.Model.Announcer
import Hpaste.Model.Spam
import Data.Pagination
import Control.Applicative ((<$>),(<|>))
import Control.Exception as E
import Control.Monad
import Control.Monad.Env
import Control.Monad.IO
import Data.Char
import Data.List (find,intercalate)
import Data.Maybe
import Data.Monoid.Operator ((++))
import Data.Text (Text,unpack,pack)
import qualified Data.Text as T
import Data.Text.IO as T (writeFile)
import Data.Text.Lazy (fromStrict)
import Language.Haskell.HLint
import Prelude hiding ((++))
import Snap.App
import System.Directory
import System.FilePath
deletePaste :: Integer -> HPModel ()
deletePaste pid = void (exec ["DELETE FROM paste WHERE id = ?"] (Only pid))
-- | Count public pastes.
countPublicPastes :: Maybe String -> HPModel Integer
countPublicPastes mauthor = do
rows <- single ["SELECT COUNT(*)"
,"FROM public_toplevel_paste"
,"WHERE (? IS NULL) OR (author = ?) AND spamrating < ?"]
(mauthor,mauthor,spamMinLevel)
return $ fromMaybe 0 rows
-- | Get the latest pastes.
getLatestPastes :: HPModel [Paste]
getLatestPastes =
query ["SELECT ",pasteFields
,"FROM public_toplevel_paste"
,"WHERE spamrating < ?"
,"ORDER BY created DESC"
,"LIMIT 20"]
(Only spamMinLevel)
-- | Get some paginated pastes.
getPaginatedPastes :: Maybe String -> Pagination -> HPModel (Pagination,[Paste])
getPaginatedPastes mauthor pn@Pagination{..} = do
total <- countPublicPastes mauthor
rows <- query ["SELECT",pasteFields
,"FROM public_toplevel_paste"
,"WHERE (? IS NULL) OR (author = ?) AND spamrating < ?"
,"ORDER BY created DESC"
,"OFFSET " ++ show (max 0 (pnCurrentPage - 1) * pnPerPage)
,"LIMIT " ++ show pnPerPage]
(mauthor,mauthor,spamMinLevel)
return (pn { pnTotal = total },rows)
-- | Get a paste by its id.
getPasteById :: PasteId -> HPModel (Maybe Paste)
getPasteById pid =
listToMaybe <$> query ["SELECT ",pasteFields
,"FROM public_paste"
,"WHERE id = ?"]
(Only pid)
-- | Get a private paste by its id, regardless of any status.
getPrivatePasteById :: PasteId -> HPModel (Maybe Paste)
getPrivatePasteById pid =
listToMaybe <$> query ["SELECT",pasteFields
,"FROM private_paste"
,"WHERE id = ?"]
(Only pid)
-- | Get annotations of a paste.
getAnnotations :: PasteId -> HPModel [Paste]
getAnnotations pid =
query ["SELECT",pasteFields
,"FROM public_paste"
,"WHERE annotation_of = ?"
,"ORDER BY created ASC"]
(Only pid)
-- | Get revisions of a paste.
getRevisions :: PasteId -> HPModel [Paste]
getRevisions pid = do
query ["SELECT",pasteFields
,"FROM public_paste"
,"WHERE revision_of = ? or id = ?"
,"ORDER BY created DESC"]
(pid,pid)
-- | Create a paste, or update an existing one.
createOrUpdate :: [Language] -> [Channel] -> PasteSubmit -> Integer -> Bool -> HPModel (Maybe PasteId)
createOrUpdate langs chans paste@PasteSubmit{..} spamrating public = do
case pasteSubmitId of
Nothing -> createPaste langs chans paste spamrating public
Just pid -> do updatePaste pid paste
return $ Just pid
-- | Create a new paste (possibly annotating an existing one).
createPaste :: [Language] -> [Channel] -> PasteSubmit -> Integer -> Bool -> HPModel (Maybe PasteId)
createPaste langs chans ps@PasteSubmit{..} spamrating public = do
pid <- generatePasteId public
res <- single ["INSERT INTO paste"
,"(id,title,author,content,channel,language,annotation_of,revision_of,spamrating,public)"
,"VALUES"
,"(?,?,?,?,?,?,?,?,?,?)"
,"returning id"]
(pid,pasteSubmitTitle,pasteSubmitAuthor,pasteSubmitPaste
,pasteSubmitChannel,pasteSubmitLanguage,ann_pid,rev_pid,spamrating,public)
when (lang == Just "haskell") $ just res $ createHints ps
just (pasteSubmitChannel >>= lookupChan) $ \chan ->
just res $ \pid -> do
when (spamrating < spamMinLevel) $
announcePaste pasteSubmitType (channelName chan) ps pid
return (pasteSubmitId <|> res)
where lookupChan cid = find ((==cid).channelId) chans
lookupLang lid = find ((==lid).languageId) langs
lang = pasteSubmitLanguage >>= (fmap languageName . lookupLang)
just j m = maybe (return ()) m j
ann_pid = case pasteSubmitType of AnnotationOf pid -> Just pid; _ -> Nothing
rev_pid = case pasteSubmitType of RevisionOf pid -> Just pid; _ -> Nothing
-- | Generate a fresh unique paste id.
generatePasteId :: Bool -> HPModel PasteId
generatePasteId public = do
result <- if public
then single ["SELECT NEXTVAL('paste_id_seq')"] ()
else single ["SELECT (RANDOM()*9223372036854775807) :: BIGINT"] ()
case result of
Just pid@(PasteId i) -> do
result <- single ["SELECT TRUE FROM paste WHERE id = ?"] (Only pid)
case result :: Maybe PasteId of
Just pid -> generatePasteId public
_ -> return pid
-- | Create the hints for a paste.
createHints :: PasteSubmit -> PasteId -> HPModel ()
createHints ps pid = do
hints <- generateHintsForPaste ps pid
forM_ hints $ \hint ->
exec ["INSERT INTO hint"
,"(paste,type,content)"
,"VALUES"
,"(?,?,?)"]
(pid
,suggestionSeverity hint
,show hint)
-- | Announce the paste.
announcePaste :: PasteType -> Text -> PasteSubmit -> PasteId -> HPModel ()
announcePaste ptype channel PasteSubmit{..} pid = do
conf <- env modelStateConfig
verb <- getVerb
unless (seemsLikeSpam pasteSubmitTitle || seemsLikeSpam pasteSubmitAuthor) $ do
announcer <- env modelStateAnns
io $ announce announcer pasteSubmitAuthor channel $ do
nick ++ " " ++ verb ++ " “" ++ pasteSubmitTitle ++ "” at " ++ link conf
where nick | validNick (unpack pasteSubmitAuthor) = pasteSubmitAuthor
| otherwise = "“" ++ pasteSubmitAuthor ++ "”"
link Config{..} = "http://" ++ pack configDomain ++ "/" ++ pid'
pid' = case ptype of
NormalPaste -> showPid pid
AnnotationOf apid -> showPid apid ++ "#a" ++ showPid pid
RevisionOf apid -> showPid apid
getVerb = case ptype of
NormalPaste -> return $ "pasted"
AnnotationOf pid -> do
paste <- getPasteById pid
return $ case paste of
Just Paste{..} -> "annotated “" ++ pasteTitle ++ "” with"
Nothing -> "annotated a paste with"
RevisionOf pid -> do
paste <- getPasteById pid
return $ case paste of
Just Paste{..} -> "revised “" ++ pasteTitle ++ "”:"
Nothing -> "revised a paste:"
showPid (PasteId p) = pack $ show $ (p :: Integer)
seemsLikeSpam = T.isInfixOf "http://"
-- | Is a nickname valid? Digit/letter or one of these: -_/\\;()[]{}?`'
validNick :: String -> Bool
validNick s = first && all ok s && length s > 0 where
ok c = isDigit c || isLetter c || elem c "-_/\\;()[]{}?`'"
first = all (\c -> isDigit c || isLetter c) $ take 1 s
-- | Get hints for a Haskell paste from hlint.
generateHintsForPaste :: PasteSubmit -> PasteId -> HPModel [Suggestion]
generateHintsForPaste PasteSubmit{..} (PasteId pid) = io $
E.catch (generateHints (show pid) pasteSubmitPaste)
(\SomeException{} -> return [])
-- | Get hints for a Haskell paste from hlint.
generateHints :: FilePath -> Text -> IO [Suggestion]
generateHints pid contents = io $ do
tmpdir <- getTemporaryDirectory
let tmp = tmpdir </> pid ++ ".hs"
exists <- doesFileExist tmp
unless exists $ T.writeFile tmp $ contents
!hints <- hlint [tmp,"--quiet","--ignore=Parse error"]
removeFile tmp
return hints
getHints :: PasteId -> HPModel [Hint]
getHints pid =
query ["SELECT type,content"
,"FROM hint"
,"WHERE paste = ?"]
(Only pid)
-- | Update an existing paste.
updatePaste :: PasteId -> PasteSubmit -> HPModel ()
updatePaste pid PasteSubmit{..} = do
_ <- exec (["UPDATE paste"
,"SET"]
++
[intercalate ", " (map set (words fields))]
++
["WHERE id = ?"])
(pasteSubmitTitle
,pasteSubmitAuthor
,pasteSubmitPaste
,pasteSubmitLanguage
,pasteSubmitChannel
,pid)
return ()
where fields = "title author content language channel"
set key = unwords [key,"=","?"]
pasteFields = "id,title,content,author,created,views,language,channel,annotation_of,revision_of"