-- khjk.org website generator script
-- copyright 2008-2009 Sven M. Hallberg <pesco@khjk.org>
-- all rights reserved (aka "just ask")

import Text.Format.Cleartext
import Text.Parsers.Frisby
import Text.Parsers.Frisby.Char
import Text.Regex
import System.Directory
import System.Cmd
import System
import Control.Monad
import Control.OldException
import Data.Char (ord, toUpper, isSpace)
import Data.List
import Data.Time
import Data.Maybe
import Data.HashTable (hashInt)
import Numeric (showHex)
import Network.HTTP
import Network.URI
import qualified Base64
import Email


sprueche =
    [ "Heute die Welt, morgen das Sonnensystem!"
    , "Mad science supplies at insane prices!"
    , "\"Raumflug!\""
    , "\"GERMANIA 'Fabrikat'\""
    , "Stand back! I'm going to try MAD SCIENCE!"
    , "Institute of applied mad sciences"
    , "Code without the crap"
    , "Inventing wheels since 2005"
    ]


data Eintrag = Eintrag String Zeitangabe String Txt Doc [Comment]  -- id, datum, autor, titel, inhalt, kommentare
    deriving Show

entryid     (Eintrag x _ _ _ _ _) = x
entryauthor (Eintrag _ _ x _ _ _) = x
entrytitle  (Eintrag _ _ _ x _ _) = x
entrytext   (Eintrag _ _ _ _ x _) = x


data Comment = Comment Zeitangabe String Doc
    deriving Show

type Project = String

decl    = "<?xml version='1.0' encoding='UTF-8' ?>\n"
doctype = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n\
          \          \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
xmlns   = "http://www.w3.org/1999/xhtml"
license = "http://creativecommons.org/licenses/by-nc-sa/3.0/"

type Template = String -> String -> String -> String    -- Title -> Meta -> Content -> HTML

type UIDs = [(String,String)]   -- mapping author names to user ids

mkpage :: String -> [Project] -> [Eintrag] -> Template
mkpage spruch projects log title hdmt content = decl ++ doctype ++ html
    where
    htitle
        | null title = "KHJK"
        | otherwise  = "KHJK - " ++ xml_escape title

    html = "<html xmlns='" ++ xmlns ++ "'>\n" ++ head ++ body ++ "</html>\n"
    head = "<head>\n\
           \<title>" ++ htitle ++ "</title>\n\
           \<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>\n\
           \<meta name='keywords' content='" ++ keyw ++ "' />\n\
           \<base href='http://www.khjk.org/' />\n\
           \<link rel='stylesheet' type='text/css' href='css/khjk.css' />\n"
           ++ hdmt ++
           "</head>\n"
    body = "<body>\n" ++ top ++ side ++ ctnt ++ foot ++ "</body>\n"
    top  = "<div id='top'>\n\
           \<div id='logo'><a href='http://www.khjk.org/'>-/\\-</a></div>\n\
           \<div id='titel'><a href='http://www.khjk.org/'>Karl Hans Janke Kollaborativ</a></div>\n\
           \<div id='spruch'>" ++ spruch ++ "</div>\n\
           \</div>\n"
    ctnt = "<div id='content'>\n" ++ content ++ "</div>\n"
    side = "<div id='sidebar'>\n" ++ main ++ proj ++ arch ++ meta ++ link ++ "</div>\n"
    main = "<h2>main</h2>\n\
           \<a href='http://www.khjk.org/'>front page</a><br />\n"
    proj = "<h2>projects</h2>\n" ++ projlinks projects
    arch = "<h2>archive</h2>\n" ++ archlinks log
    foot = "<div id='footer'>" ++ "" ++ "</div>\n"
    meta = "<h2>meta</h2>\n\
           \<a rel='license' href='" ++ license ++"'>license</a><br />\n\
           \<a type='application/atom+xml' href='atom.xml'>atom feed</a><br />\n\
           \<a href='mailto:sm@khjk.org'>contact</a><br />\n\
           \<a type='text/x-haskell' href='generate.hs'>source code</a><br />\n\
           \<a href='http://de.wikipedia.org/wiki/Karl_Hans_Janke'>k. h. janke</a><br />\n\
           \<a href='impressum.html'>impressum</a><br />\n"
    link = "<h2>sympathies</h2>\n\
           \<a href='http://www.tinkerlog.com/'>alex</a><br />\n\
           \<a href='http://blog.hep-cat.de/'>atari</a><br />\n\
           \<a href='http://www.attraktor.org/'>attraktor</a><br />\n\
           \<a href='http://www.ccc.de/'>ccc</a><br />\n\
           \<a href='http://www.hamburg.ccc.de/'>ccc hamburg</a><br />\n\
           \<a href='http://www.ursus-maritimus.org/'>eisbaer</a><br />\n\
           \<a href='http://www.hanez.org/'>hannes</a><br />\n\
           \<a href='http://blog.kapsobor.de/'>iso</a><br />\n\
           \<a href='http://www.phrozen.org/'>john</a><br />\n\
           \<a href='http://www.ladyada.net/'>ladyada</a><br />\n\
           \<a href='http://www.block4.com/'>malte</a><br />\n\
           \<a href='http://www.shixi.de/'>shixi</a><br />\n"
    keyw = "KHJK, Karl Hans Janke, Programming, Coding, Hacking, Tinkering, Electronics, \
           \Mad Science, Haskell, CCC, Chaos Computer Club, Hamburg, Attraktor, Weblog, \
           \Photography"

frontpage :: UIDs -> Template -> Int -> [Eintrag] -> String
frontpage fu tmpl monat log = tmpl "" meta (concatMap (eintrag fu) frontlog)
    where
    meta = "<link rel='alternate' type='application/atom+xml' title='Atom feed'\
                \ href='atom.xml' />\n\
           \<link rel='alternate' type='application/rss+xml' title='RSS feed'\
                \ href='rss.xml' />\n"

    frontlog = let (a,b) = splitAt 5 log in a ++ takeWhile thismon b
    thismon e = snd (yearmon e) == monat

projlinks :: [Project] -> String
projlinks = concatMap projlink . sort
    where
    projlink name = "<a href='prj/" ++ att_escape name ++ "'>"
                    ++ xml_escape name ++ "</a><br />\n"

projurl :: Project -> String
projurl name = "http://www.khjk.org/prj/" ++ name

archlinks :: [Eintrag] -> String
archlinks = concatMap archlink . reverse . nub . sort . map yearmon
    where
    archlink (y,m) = "<a href='" ++ fnam ++ "'>"
                      ++ mnam ++ " " ++ show y
                      ++ "</a><br />\n"
       where
       mnam = monthnames !! (m-1)
       fnam = "log/" ++ show y ++ "/" ++ mnam ++ ".html"

yearmon (Eintrag _ (ZA (y:m:_)) _ _ _ _) = (y,m)

monthnames = ["jan","feb","mar","apr","may","jun","jul",
              "aug","sep","oct","nov","dec"]


archpage :: UIDs -> Template -> (Int,Int) -> [Eintrag] -> String
archpage fu tmpl (y,m) log = tmpl titl "" ctnt
    where
    titl = mnam ++ " " ++ show y
    ctnt = "<h1>" ++ titl ++ "</h1>\n" ++ concatMap (eintrag fu) log

    mnam = monthnames !! (m-1)


projpage :: UIDs -> Template -> Project -> Doc -> String
projpage fu tmpl prj doc = tmpl prj "" (doctohtml doc) -- ++ proj_flattrbtn fu prj doc)

proj_flattrbtn :: UIDs -> Project -> Doc -> String
proj_flattrbtn uids prj doc = case lookup "flattr" meta of
    Just uid -> flattrbtn "default"
                    (maybe uid id (lookup uid uids))
                    (if null title then [St prj] else title)
                    (docdesc doc)
                    (maybe "software" id (lookup "category" meta))
                    (doclang doc)
                    ("khjk" : doctags doc)
                    (projurl prj)
    Nothing  -> ""
    where
    meta  = metadata doc
    title = doctitle' doc


entpage :: UIDs -> Template -> (Maybe Eintrag, Eintrag, Maybe Eintrag) -> String
entpage fu tmpl (ep,ent,en) = tmpl (txttoascii titel) "" content
    where
    Eintrag _ _ _ titel _ cs = ent
    content  = prevnext ++ eintrag fu ent ++ comments
    prevnext = "<div class='prevnext'>\n" ++ prev ++ next ++ "</div>\n"
    prev     = "<span class='prevlink'>" ++ link ep tp ++ "</span>\n"
    next     = "<span class='nextlink'>" ++ link en tn ++ "</span>\n"
    comments
        | null cs   = ""
        | otherwise = "<div id='comments'>\n"
                      ++ cmthead
                      ++ concatMap comment cs
                      ++ replylink ent
                      ++ "</div>\n"
    cmthead  = "<hr/><h2>Comments:</h2>\n"
    tp = "&lt;&lt; prev"
    tn = "next &gt;&gt;"

    link Nothing  _ = " "
    link (Just e) t = "<a href='" ++ entrypath e ++ "'>" ++ t ++ "</a>"

replylink ent = "<div class='reply-link'><a href='mailto:"
                ++ replyaddress ent
                ++ "'>&gt;&gt; reply</a></div>\n"

replyaddress (Eintrag id _ _ title _ _)
        = "comment-" ++ deslash id ++ "@khjk.org?" ++ urlEncodeVars vs
    where
    deslash = map (\x -> if x=='/' then '.' else x)
    vs = [("subject", "Re: " ++ txttoascii title)]


atom :: [Eintrag] -> String
atom log = decl ++ feed
    where
    feed = "<feed xmlns='http://www.w3.org/2005/Atom'>\n"
           ++ info ++ date ++ ents ++ "</feed>\n"
    info = "<title type='text'>KHJK</title>\n\
           \<subtitle type='text'>Karl Hans Janke Kollaborativ</subtitle>\n\
           \<id>http://www.khjk.org/</id>\n\
           \<link href='http://www.khjk.org/' rel='alternate' type='text/html'/>\n\
           \<link href='http://www.khjk.org/atom.xml' rel='self' type='application/atom+xml'/>\n\
           \<author>\n\
           \    <name>Sven Moritz Hallberg</name>\n\
           \    <email>sm@khjk.org</email>\n\
           \</author>\n"
    ents = concatMap atomeintrag (take 10 log)

    date = case log of
        [] ->
            ""
        (Eintrag _ d _ _ _ _:_) ->
            "<updated>" ++ rfc3339date d ++ "</updated>\n"

atomeintrag :: Eintrag -> String
atomeintrag ent@(Eintrag id datum autor titel inhalt _)
        = concat ["<entry>\n", titletag, linktag, idtag, authortag, datetag,
                  conttag, "</entry>\n"]
    where
    titletag  = "<title type='html'>" ++ xml_escape (txttohtml titel) ++ "</title>\n"
    authortag = "<author>\n\
                \    <name>" ++ xml_escape autor ++ "</name>\n\
                \    <email>" ++ xml_escape autor ++ "@khjk.org</email>\n\
                \</author>\n"
    linktag   = "<link href='" ++ link ++ "'/>\n"
    datetag   = "<updated>" ++ rfc3339date datum ++ "</updated>\n"
    conttag   = "<content type='html'>\n" ++ xml_escape (doctohtml inhalt) ++ "</content>\n"
    idtag     = "<id>" ++ link ++ "</id>\n"
    link      = entrylink ent


rss :: [Eintrag] -> String
rss log = decl ++ "<rss version='2.0' " ++ atom ++ ">\n" ++ chan ++ "</rss>\n"
    where
    atom = "xmlns:atom='http://www.w3.org/2005/Atom'"
    chan = "<channel>\n" ++ info ++ date ++ ents ++ "</channel>\n"
    info = "<title>KHJK</title>\n\
           \<link>http://www.khjk.org/</link>\n\
           \<description>Karl Hans Janke Kollaborativ</description>\n\
           \<language>de</language>\n\
           \<webMaster>sm@khjk.org (Sven Moritz Hallberg)</webMaster>\n\
           \<atom:link href='http://www.khjk.org/rss.xml'\
           \ rel='self' type='application/rss+xml' />\n"
    ents = concatMap rsseintrag (take 10 log)

    date = case log of
        [] ->
            ""
        (Eintrag _ d _ _ _ _:_) ->
            "<pubDate>" ++ rfc822date d ++ "</pubDate>\n\
            \<lastBuildDate>" ++ rfc822date d ++ "</lastBuildDate>\n"

rsseintrag :: Eintrag -> String
rsseintrag ent@(Eintrag id datum autor titel inhalt _)
        = concat ["<item>\n", titletag, linktag, desctag, authortag, guidtag,
                  datetag, "</item>\n"]
    where
    titletag  = "<title>" ++ xml_escape (txttohtml titel) ++ "</title>\n"
    authortag = "<author>"
                ++ xml_escape (autor ++ "@khjk.org (" ++ autor ++ ")")
                ++ "</author>\n"
    linktag   = "<link>" ++ link ++ "</link>\n"
    datetag   = "<pubDate>" ++ rfc822date datum ++ "</pubDate>\n"
    desctag   = "<description>\n" ++ xml_escape (doctohtml inhalt) ++ "</description>\n"
    guidtag   = "<guid>" ++ link ++ "</guid>\n"
    link      = entrylink ent

eintrag :: UIDs -> Eintrag -> String
eintrag uids ent@(Eintrag id datum autor titel inhalt cs)
        = "<div class='entry' id='" ++ att_escape id ++ "'>\n"
          ++ uebschr ++ meta ++ text
         -- ++ entry_flattrbtn uids ent
          ++ cmtlink
         -- ++ replylink ent
          ++ "</div>\n"
    where
    uebschr = "<h2><a href='" ++ link ++ "'>"
              ++ txttohtml titel ++ "</a></h2>\n"
    meta    = "<div class='entrymeta'>" 
              ++ xml_escape (show datum)
              ++ ", " ++ autor
              ++ (if null tags then "" else "<br/>tags: " ++ tags)
              ++ "</div>\n"
    text    = doctohtml inhalt
    link    = entrypath ent
    tags    = unwords (entrytags ent)
    cmtlink
        | null cs   = ""
        | otherwise = "<div class='comment-link'><a href='"
                      ++ link ++ "#comments'>("
                      ++ show n ++ " comment"
                      ++ (if n>1 then "s" else "")
                      ++ xml_escape [ellipsis] ++ ")</a></div>\n"
        where
        n = length cs

ellipsis = '\x2026'

readflattruids = liftM read (readFile "localstate/flattruids")

entry_flattrbtn :: [(String,String)] -> Eintrag -> String
entry_flattrbtn uids e = case lookup author uids of
    Just uid -> flattrbtn "compact"
                    uid
                    (entrytitle e)
                    (docdesc doc)
                    "text"
                    (doclang doc)
                    ("khjk" : "blog" : author : doctags doc)
                    (entrylink e)
    Nothing  -> ""
    where
    doc    = entrytext e
    author = entryauthor e

flattrbtn :: String -> String -> Txt      -> String -> 
             String -> String -> [String] -> String -> String
flattrbtn style uid title desc cat lang tags url =
        "<div class='flattrbtn'>\n\
        \<script type=\"text/javascript\">\n\
        \var flattr_btn = " ++ esc style ++ ";\n\
        \var flattr_uid = " ++ esc uid ++ ";\n\
        \var flattr_tle = " ++ esc (txttoascii title) ++ ";\n\
        \var flattr_dsc = " ++ esc desc ++ ";\n\
        \var flattr_cat = " ++ esc cat ++ ";\n\
        \var flattr_lng = " ++ esc lang ++ ";\n\
        \var flattr_tag = " ++ esc taglist ++ ";\n\
        \var flattr_url = " ++ esc url ++ ";\n\
        \var flattr_hide = 'false';\n\
        \</script>\n\
        \<script src=\"http://api.flattr.com/button/load.js\"\
        \ type=\"text/javascript\"></script>\n\
        \</div>\n"
    where
    esc = xml_escape . jsstring
    taglist = concat (intersperse "," tags)

entrypath e   = xml_escape (entryid e) ++ ".html"
entrylink e   = "http://www.khjk.org/" ++ entrypath e
entrytags     = doctags . entrytext

docabstract = liftM parsetxt . lookup "abstract" . metadata
doctags = maybe [] words . lookup "tags" . metadata
doclang = maybe "en_GB" lang . lookup "lang" . metadata
    where
    lang "en" = "en_GB"
    lang "de" = "de_DE"
    lang x    = x

doctitle' d = if null t then topheading d else t
    where t = doctitle d

topheading d
    | null hs   = []  -- no headings in document
    | n>m       = []  -- top-level heading is not the first in document
    | otherwise = h
    where
    hs = [(h,n) | SECT n h <- d]
    (h,n) = head hs
    m = minimum (map snd hs)

docdesc d = maybe (docpreview d) id (lookup "abstract" (metadata d))

docpreview = cut 400 . txttoascii . rep
    where
    -- preview representation of block tokens
    rep xs = concat (intersperse [St ell] [t | TEXT t <- xs])
    
    -- cut off at first word boundary after at least n characters
    cut n xs = a ++ takeWhile (not.isSpace) b ++ ell
        where (a,b) = splitAt n xs

    ell = " [" ++ ellipsis : "] "

jsstring xs = "'" ++ concatMap esc xs ++ "'"
    where
    esc c
        | c `elem` "\\'"  = ['\\',c]
        | c == '\b'       = "\\b"
        | c == '\f'       = "\\f"
        | c == '\n'       = "\\n"
        | c == '\r'       = "\\r"
        | c == '\t'       = "\\t"
        | otherwise       = [c]

comment :: Comment -> String
comment (Comment timestamp author content)
        = "<div class='comment'>\n" ++ meta ++ body ++ "</div>\n"
    where
    meta = "<div class='meta'>"
           ++ "<span class='author'>" ++ xml_escape author ++ "</span>"
           ++ time ++ "</div>\n"
    time = case timestamp of
              ZA [] -> ""
              _     -> "<span class='timestamp'>"
                       ++ ", " ++ xml_escape (show timestamp)
                       ++ "</span>"
    body = "<div class='body'>\n" ++ doctohtml content ++ "</div>\n"


-- taken from XmlOut.hs
xml_escape, att_escape :: String -> String
xml_escape = xml_escape' "<>&"
att_escape = xml_escape' "<>&\""

xml_escape' escme = concatMap (\c -> if c `elem` escme || ord c > 127 then unirep c else [c])
    where
    unirep c = "&#" ++ show (ord c) ++ ";"


-- datentyp fuer verschieden exakte zeitangeben
newtype Zeitangabe = ZA [Int]  -- felder: jahr, monat, tag, stunde, minute, ...
instance Ord Zeitangabe where
    compare (ZA a) (ZA b) = uncurry compare (snip a b)
        where
        snip a b = unzip (zip a b)   -- cuts lists down to be the same length
instance Eq Zeitangabe where
    a == b = compare a b == EQ
instance Show Zeitangabe where
    show (ZA [j,m,t]) = showdate j m t
    show (ZA [j,m,t,h,min]) = showdate j m t ++ " " ++ showhm h min
    show (ZA [j,m,t,h,min,s]) = showdate j m t ++ " " ++ showhms h min s

showdate = showdate_ger
showdate_iso j m t = show2 j ++ "-" ++ show2 m ++ "-" ++ show2 t
showdate_ger j m t = show t ++ "." ++ show m ++ "." ++ show j
show2 x = if x<10 then '0' : show x else show x
showhm h m = show h ++ ":" ++ show2 m
showhms h m s = showhm h m ++ ":" ++ show2 s
show2hm h m = show2 h ++ ":" ++ show2 m
show2hms h m s = show2hm h m ++ ":" ++ show2 s

showdate_rfc j m t
  | m<1 || m>12 = error ("showdate_rfc: month (" ++ show m ++ ") out of bounds")
  | otherwise = show t ++ " " ++ month ++ " " ++ show j
  where
  month = toUpper x : xs
  (x:xs) = monthnames!!(m-1)

-- the good old email timestamp format from 1982
rfc822date :: Zeitangabe -> String
rfc822date (ZA [j,m,t]) = rfc822date (ZA [j,m,t,0,0])
rfc822date (ZA [j,m,t,h,min]) =
    showdate_rfc j m t ++ " " ++ show2hm h min ++ " GMT"
rfc822date (ZA [j,m,t,h,min,s]) =
    showdate_rfc j m t ++ " " ++ show2hms h min s ++ " GMT"

-- the fancy new timestamp format from 2002
rfc3339date :: Zeitangabe -> String
rfc3339date (ZA [j,m,t]) = rfc3339date (ZA [j,m,t,0,0])
rfc3339date (ZA [j,m,t,h,min]) = rfc3339date (ZA [j,m,t,h,min,0])
rfc3339date (ZA [j,m,t,h,min,s]) =
    showdate_iso j m t ++ "T" ++ show2hms h min s ++ "Z"

p_zeitangabe = do
    jahr    <- newRule $ many1 digit ## read
    monat   <- newRule $ max2 digit ## read
    tag     <- newRule $ max2 digit ## read
    isodate <- newRule $ jahr <> char '-' ->> monat <<- char '-' <> tag ##
                         \((j,m),t) -> [j,m,t]
    gerdate <- newRule $ tag <> char '.' ->> monat <<- char '.' <> jahr ##
                         \((t,m),j) -> [j,m,t]
    amidate <- newRule $ monat <> char '/' ->> tag <<- char '/' <> jahr ##
                         \((m,t),j) -> [j,m,t]
    datum   <- newRule $ choice $ map (`onlyIf` validdate)
                         [isodate, gerdate, amidate]
    stunden <- newRule $ (max2 digit ## read) `onlyIf` (<24)
    minuten <- newRule $ (max2 digit ## read) `onlyIf` (<60)
    sekunden <- newRule $ (max2 digit ## read) `onlyIf` (<60)
    uhrzeit <- newRule $ stunden ++> (char ':' ->> minuten)
                         +/+ (char ':' ->> sekunden)
                         +/+ unit []
    za <- newRule $ many space ->> datum <++> option [] (many1 space ->> uhrzeit)
                    <<- many space <<- eof
    return (za ## ZA)
    where
    infixr 3 +/+
    infixr 3 ++>
    (+/+) :: P s a -> P s [a] -> P s [a]
    (++>) :: P s a -> P s [a] -> P s [a]
    a +/+ b = a <> option [] b ## uncurry (:)
    a ++> b = a <> b ## uncurry (:)
    max2 p = (p <> p ## \(x,y) -> [x,y]) // (p ## \x -> [x])
    validdate [y,m,d]
      | y<1  = False
      | m<1  = False
      | m>12 = False
      | d<1  = False
      | d>monlen y m = False
      | otherwise = True

monlen y m
    | m==2 = if leapyear then 29 else 28
    | m`elem`[4,6,9,11] = 30
    | otherwise = 31
    where
    leapyear = y `mod` 4 == 0 && y `mod` 100 /= 0

parse_zeitangabe = runPeg peg
  where
  peg = do za <- p_zeitangabe
           return (za ## Just // unit Nothing)


readEntry :: FilePath -> IO (Maybe Eintrag)
readEntry f = do
    doc <- readctxfile f
    let metas = metadata doc
        datum = lookup "date" metas
        zeit  = lookup "time" metas
        autor = lookup "author" metas
        titel = lookup "title" metas
        text  = doc
        zad   = datum >>= parse_zeitangabe :: Maybe Zeitangabe
        zaz   = zeit >>= parse_zeitangabe :: Maybe Zeitangabe
        za    = maybe zad Just zaz
        id    = dropext f

    when (isNothing zeit && isNothing datum) $
        putStrLn (f ++ ": missing time or date metatag")
    when (isNothing zeit && isJust datum && isNothing zad) $
        putStrLn (f ++ ": unable to parse date metatag")
    when (isJust zeit && isNothing zaz) $
        putStrLn (f ++ ": unable to parse time metatag")
    when (isNothing autor) $
        putStrLn (f ++ ": missing author metatag")
    when (isNothing titel) $
        putStrLn (f ++ ": missing title metatag")

    let mbox = dropext f ++ ".mbox"
    cs <- readcommentfile mbox

    let eintrag = do z <- za :: Maybe Zeitangabe
                     a <- autor
                     t <- titel
                     return (Eintrag id z a (parsetxt t) text cs)

    when (isNothing eintrag) $ do
        putStrLn (f ++ ": ignored")
    return eintrag

dropext path
    | null path'        = path                  -- no extension (no slashes)
    | head path' == '/' = path                  -- no extension (with slash)
    | null (tail path') = path                  -- dotfile (no slashes)
    | path'!!1 == '/'   = path                  -- dotfile (with slash)
    | otherwise         = reverse (tail path')
    where
    path' = dropWhile (\x -> x/='.' && x/='/') (reverse path)

readcommentfile :: FilePath -> IO [Comment]
readcommentfile f = do
    ex <- doesFileExist f
    mbox <- if ex then liftM mbox (readFile f) else return []
    return (map email_to_comment mbox)

email_to_comment :: Email -> Comment
email_to_comment (hdrs,body) = Comment timestamp author doc
    where
    timestamp = maybe (ZA []) rfcdate_to_za (lookup "Date" hdrs)
    author    = case liftM addressname (lookup "From" hdrs) of
                    Nothing -> "anonymous"
                    Just "" -> "anonymous"
                    Just x  -> x
    doc       = parsedoc body

addressname :: String -> String
addressname addr = name
    where
    Emailaddress name _user _arg _domain = emailaddress addr

rfcdate_to_za :: String -> Zeitangabe
rfcdate_to_za str = ZA [y,m,d,h',min',s]
    where
    (cy, m')   = divMod (m + cm) 12
    (cm, d')   = divMod (d + cd) (monlen y m)
    (cd, h')   = divMod (h + zh + ch) 24
    (ch, min') = divMod (min + zm) 60
    (y,m,d,h,min,s,zh,zm) = read_rfcdate str

twitterlink e = "http://khjk.org/" ++ entrypath e
    

-- replace Im tokens with thumbnails linked to the original
autothumb :: Eintrag -> IO Eintrag
autothumb (Eintrag id d a t doc cs) = do
    doc' <- mapM atbtok doc
    return $ Eintrag id d a t doc' cs
    where
    atbtok (TEXT txt) = do
        txt' <- mapM atltok txt
        return (TEXT txt')
    atbtok (FLOA mod doc cap) = do
        doc' <- mapM atbtok doc
        return (FLOA mod doc' cap)
    atbtok x = return x

    atltok (Im u)
        | "http://" `isPrefixOf` u = return (Im u)
        | ".klein.jpg" `isSuffixOf` u = return (Im u)
        | ".klein.png" `isSuffixOf` u = return (Im u)
        | otherwise = do ex <- doesFileExist uk
                         unless ex $ do
                            putStrLn ("> " ++ convcmd_k)
                            system convcmd_k
                            return ()
                         ex <- doesFileExist um
                         unless ex $ do
                            putStrLn ("> " ++ convcmd_m)
                            system convcmd_m
                            return ()
                         return (Hy [Im uk] um)
        where
        uk = subRegex (mkRegex "\\.[^.]+$") u ".klein\\0"
        um = subRegex (mkRegex "\\.[^.]+$") u ".medium\\0"
        convcmd_k = "convert " ++ u ++ " -scale 400 " ++ uk
        convcmd_m = "convert " ++ u ++ " -scale 1024 " ++ um
    atltok x = return x

-- automatically post new log entries to twitter
autotweet :: [Eintrag] -> IO ()
autotweet log = handle (\e -> print e) $ do
    lasttweet <- liftM (fromJust . parse_zeitangabe) (readFile "localstate/lasttweet")
    tweeters  <- liftM read (readFile "localstate/tweeters")
    ms <- mapM (at1 tweeters) (takeWhile (after lasttweet) log)
    let xs = catMaybes ms
    when (not (null xs)) $
        writeFile "localstate/lasttweet" (show (entdate (head xs)))
    where
    entdate (Eintrag _ d _ _ _ _) = d
    after za ent = entdate ent > za
    at1 tweeters ent@(Eintrag id _ autor title _ _) =
        case lookup autor tweeters of
            Nothing -> return Nothing
            Just u -> handle (\e -> print e >> return Nothing) $ do
                p <- readFile ("../authdata/twitter/" ++ u)
                putStrLn ("tweeting: " ++ u ++ ": " ++ tweettext)
                ok <- tweet u p tweettext
                return (if ok then Just ent else Nothing)
        where
        tweettext  = tweettitle ++ ": " ++ link
        tweettitle
            | length asctitle <= titleroom = asctitle
            | otherwise = take (titleroom - 3) asctitle ++ [ellipsis]
        asctitle = txttoascii title
        titleroom = 138 - length link
        link = twitterlink ent

tweet :: String -> String -> String -> IO Bool
tweet user pass text = do
    res <- simpleHTTP req
    case res of
        Right rsp -> case rspCode rsp of
            (2,0,0) -> return True
            (a,b,c) -> putStrLn (show a++show b++show c++" "++rspReason rsp)
                       >> return False
        Left err -> print err >> return False
    where
    req = Request { rqURI=uri, rqMethod=mth, rqHeaders=[aut], rqBody=bdy }
    mth = POST
    hst = URIAuth { uriUserInfo = ""
                  , uriRegName  = "twitter.com"
                  , uriPort     = ""
                  }
    uri = URI { uriScheme    = "http:"
              , uriAuthority = Just hst
              , uriPath      = "/statuses/update.json"
              , uriQuery     = "?" ++ urlEncodeVars [("status",text)]
              , uriFragment  = ""
              }
    bdy = ""
    aut = Header HdrAuthorization ("Basic " ++ base64 (user ++ ":" ++ pass))

base64 = Base64.encode . map (fromIntegral . ord)   -- no unicode!

main = do
    args <- getArgs
    let publish = (args == ["publish"])
    if publish
        then (putStrLn "publish mode - will interact with web services.")
        else (putStrLn "review mode - will not interact with web services.")

    filenames <- findlogentries
    eintraege <- liftM catMaybes $ mapM readEntry filenames
    eintraege' <- mapM autothumb eintraege
    projects <- findprojects

    fu <- readflattruids

    t <- getCurrentTime
    let log = sortBy neuster eintraege'
        (jahr,monat,tag) = toGregorian (utctDay t)
        toint x = fromIntegral (x `mod` fromIntegral (maxBound :: Int)) :: Int
        woche = toint $ (utctDay t `diffDays` fromGregorian 2009 1 1) `div` 7
        spruchnum = toint (hashInt woche) `mod` length sprueche
        spruch = sprueche!!spruchnum

        template = mkpage spruch projects log

    writeentries fu template log
    writearchive fu template log
    writeprojectpages fu template projects

    putStrLn "writing atom.xml"
    writeFile "atom.xml" (atom log)
    putStrLn "writing rss.xml"
    writeFile "rss.xml" (rss log)
    putStrLn ("writing index.html -- " ++ spruch)
    writeFile "index.html" (frontpage fu template monat log)

    --when publish (autotweet log)  -- needs oauth now or something.

neuster :: Eintrag -> Eintrag -> Ordering
neuster (Eintrag _ d1 _ _ _ _) (Eintrag _ d2 _ _ _ _) = compare d2 d1

writeprojectpages fu tmpl = mapM_ write1
    where
    write1 prj = do
        let ft = "prj/" ++ prj ++ "/index.txt"
            fh = "prj/" ++ prj ++ "/index.html"
        ex <- doesFileExist ft
        when ex $ do
            doc <- readctxfile ft
            putStrLn ("writing " ++ fh)
            writeFile fh (projpage fu tmpl prj doc)

writearchive fu tmpl = mapM_ write1 . groupBy cmpyearmon
    where
    cmpyearmon a b = yearmon a == yearmon b
    write1 log = do
        putStrLn ("writing " ++ fnam)
        writeFile fnam (archpage fu tmpl (y,m) log)
        where
        (y,m) = yearmon (head log)
        fnam = "log/" ++ show y ++ "/" ++ mnam ++ ".html"
        mnam = monthnames !! (m-1)

writeentries fu tmpl log = mapM_ (writeentpage fu tmpl) log_pcn
    where
    log_pcn  = zip3 prevs log nexts
    nexts    = Nothing : jlog
    prevs    = tail jlog ++ [Nothing]
    jlog     = map Just log

writeentpage fu tmpl (ep,ent,en) = do
    putStrLn ("writing " ++ fnam ++ cmts)
    writeFile fnam (entpage fu tmpl (ep,ent,en))
    where
    fnam = entrypath ent
    Eintrag _ _ _ _ _ cs = ent
    cmts
        | null cs   = ""
        | otherwise = " (" ++ show (length cs) ++ " comments)"


findprojects = search "prj"
    where
    search dir = do
        putStr ("scanning " ++ dir ++ "... ")
        xs <- getDirectoryContents dir
        let xs' = [ x | x<-xs, head x /= '.' ]
        dirs <- filterM isdir xs'
        putStrLn (show (length dirs) ++ " entries")
        return dirs
        where
        isdir x = doesDirectoryExist (dir ++ "/" ++ x)

findlogentries = search "log"
    where
    search dir = do
        putStr ("scanning " ++ dir ++ "... ")
        xs <- getDirectoryContents dir
        let xs' = [ dir ++ "/" ++ x | x<-xs, head x /= '.' ]
        (dirs,files) <- partitionM doesDirectoryExist xs'
        let files' = [ f | f<-files, ".txt" `isSuffixOf` f ]
        putStrLn (show (length files') ++ " entries")
        yss <- mapM search dirs
        return (concat yss ++ files')

partitionM p [] = return ([],[])
partitionM p (x:xs) = do
    t <- p x
    (ys,zs) <- partitionM p xs
    return (if t then (x:ys,zs) else (ys,x:zs))
