279 lines
No EOL
8.5 KiB
Haskell
279 lines
No EOL
8.5 KiB
Haskell
{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, RecordWildCards,
|
||
TemplateHaskell, TypeFamilies, OverloadedStrings, ScopedTypeVariables, BangPatterns #-}
|
||
|
||
module Main where
|
||
import Control.Applicative ((<$>), optional)
|
||
import Control.Exception (bracket)
|
||
import Control.Monad (msum, mzero)
|
||
import Control.Monad.IO.Class (MonadIO)
|
||
import Control.Monad.Reader (ask)
|
||
import Control.Monad.State (get, put)
|
||
import Control.Monad.Trans (liftIO)
|
||
import Data.Acid
|
||
import Data.Acid.Advanced
|
||
import Data.Acid.Local
|
||
import Data.ByteString (ByteString)
|
||
import Data.Data (Data, Typeable)
|
||
import Data.IxSet (Indexable(..), IxSet(..), (@=), Proxy(..), getOne, ixFun, ixSet)
|
||
import Data.SafeCopy (SafeCopy, base, deriveSafeCopy)
|
||
import Data.Text (Text, pack)
|
||
import Data.Text.Lazy (toStrict)
|
||
import Data.Time
|
||
import System.Environment(getEnv)
|
||
|
||
|
||
import qualified Crypto.Hash.SHA512 as SHA (hash)
|
||
import qualified Data.ByteString.Char8 as B
|
||
import qualified Data.ByteString.Base64 as B64 (encode)
|
||
import qualified Data.IxSet as IxSet
|
||
import qualified Data.Text as Text
|
||
|
||
|
||
{-CouchDB imports-}
|
||
|
||
import Database.CouchDB hiding (runCouchDB')
|
||
import Database.CouchDB.JSON
|
||
import Text.JSON
|
||
import Data.List (intersperse, (\\))
|
||
import System.Locale (defaultTimeLocale)
|
||
|
||
-- data types and acid-state setup
|
||
|
||
newtype EntryId = EntryId { unEntryId :: Integer }
|
||
deriving (Eq, Ord, Data, Enum, Typeable, SafeCopy)
|
||
|
||
instance Show EntryId where
|
||
show = show . unEntryId
|
||
|
||
data BlogLang = EN | DE
|
||
deriving (Eq, Ord, Data, Typeable)
|
||
|
||
instance Show BlogLang where
|
||
show DE = "de"
|
||
show EN = "en"
|
||
|
||
$(deriveSafeCopy 0 'base ''BlogLang)
|
||
|
||
data Comment = Comment {
|
||
cauthor :: Text,
|
||
ctext :: Text,
|
||
cdate :: UTCTime
|
||
} deriving (Eq, Ord, Show, Data, Typeable)
|
||
|
||
$(deriveSafeCopy 0 'base ''Comment)
|
||
|
||
data Entry = Entry {
|
||
entryId :: EntryId,
|
||
lang :: BlogLang,
|
||
author :: Text,
|
||
title :: Text,
|
||
btext :: Text,
|
||
mtext :: Text,
|
||
edate :: UTCTime,
|
||
tags :: [Text],
|
||
comments :: [Comment]
|
||
} deriving (Eq, Ord, Show, Data, Typeable)
|
||
|
||
$(deriveSafeCopy 0 'base ''Entry)
|
||
|
||
-- ixSet requires different datatypes for field indexes, so let's define some
|
||
newtype Author = Author Text deriving (Eq, Ord, Data, Typeable, SafeCopy)
|
||
newtype Title = Title Text deriving (Eq, Ord, Data, Typeable, SafeCopy)
|
||
newtype BText = BText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- standard text
|
||
newtype MText = MText Text deriving (Eq, Ord, Data, Typeable, SafeCopy) -- "read more" text
|
||
newtype Tag = Tag Text deriving (Eq, Ord, Data, Typeable, SafeCopy)
|
||
newtype EDate = EDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy)
|
||
newtype SDate = SDate UTCTime deriving (Eq, Ord, Data, Typeable, SafeCopy)
|
||
newtype Username = Username Text deriving (Eq, Ord, Data, Typeable, SafeCopy)
|
||
newtype SessionID = SessionID Text deriving (Eq, Ord, Data, Typeable, SafeCopy)
|
||
|
||
instance Indexable Entry where
|
||
empty = ixSet [ ixFun $ \e -> [ entryId e]
|
||
, ixFun $ (:[]) . lang
|
||
, ixFun $ \e -> [ Author $ author e ]
|
||
, ixFun $ \e -> [ Title $ title e]
|
||
, ixFun $ \e -> [ BText $ btext e]
|
||
, ixFun $ \e -> [ MText $ mtext e]
|
||
, ixFun $ \e -> [ EDate $ edate e]
|
||
, ixFun $ \e -> map Tag (tags e)
|
||
, ixFun $ comments
|
||
]
|
||
|
||
data User = User {
|
||
username :: Text,
|
||
password :: ByteString
|
||
} deriving (Eq, Ord, Data, Typeable)
|
||
|
||
$(deriveSafeCopy 0 'base ''User)
|
||
|
||
data Session = Session {
|
||
sessionID :: Text,
|
||
user :: User,
|
||
sdate :: UTCTime
|
||
} deriving (Eq, Ord, Data, Typeable)
|
||
|
||
$(deriveSafeCopy 0 'base ''Session)
|
||
|
||
instance Indexable User where
|
||
empty = ixSet [ ixFun $ \u -> [Username $ username u]
|
||
, ixFun $ (:[]) . password
|
||
]
|
||
|
||
instance Indexable Session where
|
||
empty = ixSet [ ixFun $ \s -> [SessionID $ sessionID s]
|
||
, ixFun $ (:[]) . user
|
||
, ixFun $ \s -> [SDate $ sdate s]
|
||
]
|
||
|
||
data Blog = Blog {
|
||
blogSessions :: IxSet Session,
|
||
blogUsers :: IxSet User,
|
||
blogEntries :: IxSet Entry
|
||
} deriving (Data, Typeable)
|
||
|
||
$(deriveSafeCopy 0 'base ''Blog)
|
||
|
||
initialBlogState :: Blog
|
||
initialBlogState =
|
||
Blog { blogSessions = empty
|
||
, blogUsers = empty
|
||
, blogEntries = empty }
|
||
|
||
-- acid-state database functions (purity is necessary!)
|
||
|
||
insertEntry :: Entry -> Update Blog Entry
|
||
insertEntry e =
|
||
do b@Blog{..} <- get
|
||
put $ b { blogEntries = IxSet.insert e blogEntries }
|
||
return e
|
||
|
||
updateEntry :: Entry -> Update Blog Entry
|
||
updateEntry e =
|
||
do b@Blog{..} <- get
|
||
put $ b { blogEntries = IxSet.updateIx (entryId e) e blogEntries}
|
||
return e
|
||
|
||
getPost :: EntryId -> Query Blog (Maybe Entry)
|
||
getPost eid =
|
||
do b@Blog{..} <- ask
|
||
return $ getOne $ blogEntries @= eid
|
||
|
||
latestPosts :: Query Blog [Entry]
|
||
latestPosts =
|
||
do b@Blog{..} <- ask
|
||
return $ IxSet.toDescList (Proxy :: Proxy EDate) $ blogEntries
|
||
|
||
addSession :: Text -> User -> UTCTime -> Update Blog Session
|
||
addSession sId u t =
|
||
do b@Blog{..} <- get
|
||
let s = Session sId u t
|
||
put $ b { blogSessions = IxSet.insert s blogSessions}
|
||
return s
|
||
|
||
addUser :: Text -> String -> Update Blog User
|
||
addUser un pw =
|
||
do b@Blog{..} <- get
|
||
let u = User un $ hashString pw
|
||
put $ b { blogUsers = IxSet.insert u blogUsers}
|
||
return u
|
||
|
||
-- various functions
|
||
hashString :: String -> ByteString
|
||
hashString = B64.encode . SHA.hash . B.pack
|
||
|
||
$(makeAcidic ''Blog
|
||
[ 'insertEntry
|
||
, 'updateEntry
|
||
, 'getPost
|
||
, 'latestPosts
|
||
, 'addSession
|
||
, 'addUser
|
||
])
|
||
|
||
-- CouchDB database functions
|
||
|
||
runCouchDB' :: CouchMonad a -> IO a
|
||
runCouchDB' = runCouchDB "127.0.0.1" 5984
|
||
|
||
instance JSON Comment where
|
||
showJSON = undefined
|
||
readJSON val = do
|
||
obj <- jsonObject val
|
||
scauthor <- jsonField "cauthor" obj
|
||
jsscdate <- jsonField "cdate" obj :: Result JSValue
|
||
let rcdate = stripResult $ jsonInt jsscdate
|
||
sctext <- jsonField "ctext" obj
|
||
return $ Comment (pack scauthor) (pack sctext) (parseSeconds rcdate)
|
||
|
||
instance JSON Entry where
|
||
showJSON = undefined
|
||
readJSON val = do
|
||
obj <- jsonObject val
|
||
sauthor <- jsonField "author" obj
|
||
stitle <- jsonField "title" obj
|
||
day <- jsonField "day" obj
|
||
month <- jsonField "month" obj
|
||
year <- jsonField "year" obj
|
||
stext <- jsonField "text" obj
|
||
comments <- jsonField "comments" obj
|
||
oldid <- jsonField "_id" obj
|
||
let leTime = parseShittyTime year month day oldid
|
||
return $ Entry (EntryId $ getUnixTime leTime) DE (pack sauthor) (pack $ stitle \\ "\n") (pack stext) (Text.empty)
|
||
leTime [] comments
|
||
|
||
|
||
getUnixTime :: UTCTime -> Integer
|
||
getUnixTime t = read $ formatTime defaultTimeLocale "%s" t
|
||
|
||
parseSeconds :: Integer -> UTCTime
|
||
parseSeconds t = readTime defaultTimeLocale "%s" $ show t
|
||
|
||
parseShittyTime :: Int -> Int -> Int -> String -> UTCTime
|
||
parseShittyTime y m d i = readTime defaultTimeLocale "%Y %m %e %k:%M:%S" newPartTime
|
||
where
|
||
firstPart = take 2 i
|
||
secondPart = take 2 $ drop 2 i
|
||
thirdPart = drop 4 i
|
||
newPartTime = concat $ intersperse " " [show y, showMonth m, show d, " "] ++
|
||
intersperse ":" [firstPart, secondPart, thirdPart]
|
||
showMonth mn
|
||
| mn < 10 = "0" ++ show mn
|
||
| otherwise = show mn
|
||
|
||
getOldEntries = runCouchDB' $ queryView (db "tazblog") (doc "entries") (doc "latestDE") []
|
||
|
||
parseOldEntries :: IO [Entry]
|
||
parseOldEntries = do
|
||
queryResult <- getOldEntries
|
||
let entries = map (stripResult . readJSON . snd) queryResult
|
||
return entries
|
||
|
||
stripResult :: Result a -> a
|
||
stripResult (Ok z) = z
|
||
stripResult (Error s) = error $ "JSON error: " ++ s
|
||
|
||
pasteToDB :: AcidState Blog -> Entry -> IO (EventResult InsertEntry)
|
||
pasteToDB acid !e = update' acid (InsertEntry e)
|
||
|
||
main :: IO()
|
||
main = do
|
||
tbDir <- getEnv "TAZBLOG"
|
||
bracket (openLocalStateFrom (tbDir ++ "/BlogState") initialBlogState)
|
||
(createCheckpointAndClose)
|
||
(\acid -> convertEntries acid)
|
||
|
||
convertEntries acid = do
|
||
entries <- parseOldEntries
|
||
let r = map forceHack entries
|
||
rs <- sequence r
|
||
putStrLn $ show rs
|
||
where
|
||
forceHack !x = do
|
||
xy <- pasteToDB acid x
|
||
return $ show xy
|
||
|
||
testThis :: IO ()
|
||
testThis = do
|
||
acid <- openLocalState initialBlogState
|
||
allE <- query' acid LatestPosts
|
||
putStrLn $ show allE |