[Added support for Javascript-free Flickr collage. Started on generic approach to having background sidebars. prb@mult.ifario.us**20080129054741] { hunk ./perpubplat/perpubplat.cabal 19 - old-time >= 1.0, containers, directory + old-time >= 1.0, containers, directory, utf8-string >= 0.2, HTTP >= 3001, + old-locale >= 1.0, json >= 0.1, network >= 2.1 hunk ./perpubplat/perpubplat.cabal 31 - Blog.BackEnd.ModelTransformations, - Blog.Widgets.TagCloud, Blog.Widgets.FlickrBadge, + Blog.BackEnd.ModelTransformations, Blog.BackEnd.HttpPoller, + Blog.Widgets.TagCloud, Blog.Widgets.FlickrCollage, + Blog.Widgets.ChromeBackEnd addfile ./perpubplat/src/Blog/BackEnd/HttpPoller.hs hunk ./perpubplat/src/Blog/BackEnd/HttpPoller.hs 1 +module Blog.BackEnd.HttpPoller where + +import Network.HTTP +import qualified Control.Exception as E +import System.Time +import Control.Concurrent ( ThreadId, threadDelay, killThread ) + +data HttpPoller = HttpPoller { request :: Request + , handle_response :: Result Response -> IO () + , polling_period_millis :: Int + , p_tid :: ThreadId } + +stop_poller :: HttpPoller -> IO () +stop_poller = killThread . p_tid + +poller_loop :: HttpPoller -> IO () +poller_loop p = do { cont <- + do { ct_start <- getClockTime + ; resp <- simpleHTTP $ request p + ; ct_stop <- getClockTime + ; logtime (request p) ct_stop ct_start + ; (handle_response p) resp + ; return True } + `E.catch` handle_exception + ; if cont then + do { print $ "Sleeping for " + ++ (show . polling_period_millis $ p) ++ " microseconds." + ; loop <- + do { threadDelay $ polling_period_millis p + ; return True } + `E.catch` handle_exception + ; if loop then poller_loop p else return () } + else return () + } + +logtime :: Request -> ClockTime -> ClockTime -> IO () +logtime req ct_stop ct_start = + print $ "Took " ++ td ++ " seconds to perform " + ++ (show . rqMethod $ req ) + ++ " " + ++ (show . rqURI $ req ) + where + td = tenths_of_a_second $ diffClockTimes ct_stop ct_start + +handle_exception :: E.Exception -> IO Bool +handle_exception (E.ErrorCall msg) = + do { print $ "Exception during HTTP operation: " ++ msg + ; return True } +handle_exception (E.AsyncException E.ThreadKilled) = + do { print $ "Kill received; exiting gracefully." + ; return False } +handle_exception (E.IOException ex) = + do { print $ "IOException encountered: " ++ show ex + ; return True } +handle_exception e = + do { print $ "Unexpected exception encountered; stopping poller. Exception was: " ++ (show e) + ; return False } + +tenths_of_a_second :: TimeDiff -> String +tenths_of_a_second (TimeDiff 0 0 0 0 m s p) = fmt $ show hundreths + where + hundreths = 6000*m + 100*s + fromInteger ((p `div` (10^10))) +tenths_of_a_second t = timeDiffToString t + +fmt :: String -> String +fmt s = (take l ps) ++ "." ++ ((drop l) ps) + where + ps = pad 3 '0' s + l = (length ps) - 2 + +pad :: Int -> Char -> String -> String +pad i c s | length s >= i = s + | otherwise = pad (i-1) c (c:s) hunk ./perpubplat/src/Blog/FrontEnd/Presentation.hs 13 -import qualified Blog.Widgets.FlickrBadge as FB +import qualified Blog.Widgets.FlickrCollage as FC +import qualified Blog.Widgets.ChromeBackEnd as CB hunk ./perpubplat/src/Blog/FrontEnd/Presentation.hs 21 + -> CB.ChromeBackEnd -- ^ a backend for chrome data hunk ./perpubplat/src/Blog/FrontEnd/Presentation.hs 23 - -> String -assemble_page v m | paged_posts == [] = "" - | otherwise = showHtml . concatHtml $ - [ topmatter v - , body . (divid "container") . concatHtml $ - [ heading - , divid "posts" $ phunk +++ bottom_nav v m - , sidebar v m paged_posts - , footer ] ] + -> IO String +assemble_page v cb m = + case paged_posts of + [] -> + return "" + _ -> + do { sb <- sidebar v cb m paged_posts + ; return . showHtml . concatHtml $ + [ topmatter v + , body . (divid "container") . concatHtml $ + [ heading + , divid "posts" $ phunk +++ bottom_nav v m + , sb + , footer ] ] } hunk ./perpubplat/src/Blog/FrontEnd/Presentation.hs 104 -sidebar :: (V.Viewable v) => v -> B.Model -> [B.Item] -> Html -sidebar w b p = (divid "sidebar") . concatHtml $ - [ sidebar_nav w b - , contents_block "sidebarcontents" w b p - , h3 $ stringToHtml "Tags" - , TC.tag_cloud (B.all_posts b) C.tags_to_show - , h3 $ stringToHtml "Pictures" - , FB.flickr_badge ] +sidebar :: (V.Viewable v) => v -> CB.ChromeBackEnd -> B.Model -> [B.Item] -> IO Html +sidebar w cb b p = + do { fc <- FC.build_collage $ CB.flickr_controller cb + ; return $ (divid "sidebar") . concatHtml $ + [ sidebar_nav w b + , contents_block "sidebarcontents" w b p + , h3 $ stringToHtml "Tags" + , TC.tag_cloud (B.all_posts b) C.tags_to_show + , h3 $ stringToHtml "Pictures" + , fc ] } hunk ./perpubplat/src/Blog/FrontEnd/Syndication.hs 7 +import Data.Maybe (fromJust) hunk ./perpubplat/src/Blog/FrontEnd/Syndication.hs 59 - | otherwise = [ A.Summary $ A.AtomContent A.XHTML s ] - where - s = unwrap $ B.summary i + | otherwise = [ A.Summary . (A.AtomContent A.XHTML) . fromJust $ B.summary i ] hunk ./perpubplat/src/Blog/FrontEnd/Syndication.hs 61 -unwrap :: Maybe a -> a -unwrap (Just s) = s addfile ./perpubplat/src/Blog/Widgets/ChromeBackEnd.hs hunk ./perpubplat/src/Blog/Widgets/ChromeBackEnd.hs 1 - +module Blog.Widgets.ChromeBackEnd where + +import Blog.Widgets.FlickrCollage (FlickrController, boot_flickr_collage) +import Blog.BackEnd.HttpPoller + +data ChromeBackEnd = ChromeBackEnd { flickr_controller :: FlickrController + , flickr_poller :: HttpPoller } + +boot :: IO ChromeBackEnd +boot = do { (f,p) <- boot_flickr_collage + ; return $ ChromeBackEnd f p } addfile ./perpubplat/src/Blog/Widgets/FlickrCollage.hs hunk ./perpubplat/src/Blog/Widgets/FlickrCollage.hs 1 +module Blog.Widgets.FlickrCollage where + +import Blog.FrontEnd.ContentAtoms +import Blog.BackEnd.HttpPoller + +import Text.ParserCombinators.Parsec; +import Random + +import Network.HTTP +import Network.HTTP.Headers +import Network.URI +import qualified Text.Json as J +import Data.Maybe +import qualified Data.Map as M +import Control.Concurrent +import Control.Concurrent.MVar +import Control.Concurrent.Chan +import Data.Char +import Data.List +import qualified Codec.Binary.UTF8.String as UTF8 + +import Text.XHtml.Strict + +build_collage :: FlickrController -> IO Html +build_collage fc = do { images <- get_photos fc + ; return $ ( divid "flickr_badge_uber_wrapper") + . ( divid "flickr_badge_wrapper" ) + . concatHtml $ [ concatHtml . (map to_xhtml) $ images + , flickr_link ] } + +flickr_link :: Html +flickr_link = primHtml $ "" + ++ "www.flick" + ++ "r.com" + +flickr_api_key :: String +flickr_api_key = "233b3c334c93b6f999f9b4b810f9d603" + +flickr_service_url :: String +flickr_service_url = "http://api.flickr.com/services/rest/" + +flickr_view_url :: String +flickr_view_url = "http://www.flickr.com/photos/" ++ user_id ++ "/" + +user_id :: String +user_id = "92922008@N00" + +display_count :: Int +display_count = 10 + +photo_count :: Int +photo_count = 500 + +polling_frequency :: Int +polling_frequency = 15 * 60 * 10^6 + +data FRequest = Put { photo_urls :: [FlickrPhoto] } | Get { callback :: MVar [FlickrPhoto]} + +data FlickrController = FlickrController { request_channel :: Chan FRequest + , fc_tid :: ThreadId} + +data FlickrPhoto = FlickrPhoto { photo_id :: String + , owner :: String + , secret :: String + , server :: String + , farm :: Int + , photo_title :: String } + deriving ( Show, Ord, Eq ) + +boot_flickr_collage :: IO (FlickrController, HttpPoller) +boot_flickr_collage = + do { temp_tid <- myThreadId + ; rc <- newChan + ; let fc = FlickrController rc temp_tid + ; ftid <- forkIO (run_loop fc []) + ; let p = HttpPoller (flickr_people_getPublicPhotos_req user_id) + (handle_flickr_response fc) polling_frequency temp_tid + ; ptid <- forkIO (poller_loop p) + ; return (fc { fc_tid = ftid}, p { p_tid = ptid }) } + +handle_flickr_response :: FlickrController -> Result Response -> IO () +handle_flickr_response fc resp = put_photos fc $ response_to_photo_urls resp + +response_to_photo_urls :: Result Response -> [FlickrPhoto] +response_to_photo_urls resp = + case resp of + Right ( Response (2,0,0) _ _ body ) -> + map (to_photo . uno) + ( una . (flip (M.!) $ "photo") . uno + . (flip (M.!) $ "photos") . uno + . parse_json . UTF8.decodeString $ body ) + Right rsp -> + error $ "Unexpected HTTP response " ++ (show_reason rsp) + Left ce -> + error $ show ce + +run_loop :: FlickrController -> [FlickrPhoto] -> IO () +run_loop fc photos = do { req <- readChan $ request_channel fc + ; case req of + Put photos' -> + run_loop fc photos' + Get cb -> + do { putMVar cb photos + ; run_loop fc photos } + } + +get_photos :: FlickrController -> IO [FlickrPhoto] +get_photos fc = do { cb <- newEmptyMVar + ; writeChan (request_channel fc) $ Get cb + ; photos <- takeMVar cb + ; case photos of + [] -> + return [] + _ -> + do { let c = min (length photos) display_count + ; idxs <- n_different_random (0,(length photos)-1) c [] + ; return $ map ((!!) photos) idxs } } + +put_photos :: FlickrController -> [FlickrPhoto] -> IO () +put_photos fc photos = writeChan (request_channel fc) $ Put photos + +compose_uri :: String -> [(String,String)] -> URI +compose_uri b a = fromJust . parseURI $ b ++ "?" ++ (conc a) + where + to_nvp = \(x,y) -> x ++ "=" ++ (urlEncode y) + conc = concat . (intersperse "&") . (map to_nvp) + +flickr_people_getPublicPhotos_req :: String -> Request +flickr_people_getPublicPhotos_req user = Request url GET [ Header HdrAcceptCharset "utf-8" ] "" + where + url = compose_uri flickr_service_url [ ("api_key", flickr_api_key) + , ("format","json") + , ("nojsoncallback","1") + , ("method","flickr.people.getPublicPhotos") + , ("per_page",show photo_count) + , ("user_id",user) ] + +parse_json :: String -> J.Value +parse_json s = case parse J.json "" s of + Left e -> error $ "Unable to parse JSON response body; error was:\n" ++ (show e) + Right v -> v + +show_reason :: Response -> String +show_reason (Response (c1,c2,c3) r _ _) = (show (c1*100 + c2*10 + c3)) + ++ ": " ++ r +to_xhtml :: FlickrPhoto -> Html +to_xhtml fp = _a (photo_page_url fp) ( image ! [ src (image_url fp) + , theclass "flickr_badge_image" + , alt (photo_title fp) ] ) + +to_photo :: M.Map String J.Value -> FlickrPhoto +to_photo m = FlickrPhoto { photo_id = uns $ m M.! "id" + , owner = uns $ m M.! "owner" + , secret = uns $ m M.! "secret" + , server = uns $ m M.! "server" + , photo_title = uns $ m M.! "title" + , farm = unn $ m M.! "farm" } + +photo_page_url :: FlickrPhoto -> String +photo_page_url fp = flickr_view_url ++ (photo_id fp) + +image_url :: FlickrPhoto -> String +image_url fp = "http://farm" ++ (show $ farm fp) ++ ".static.flickr.com/" ++ (server fp) + ++ "/" ++ (photo_id fp) ++ "_" ++ (secret fp) ++ "_t.jpg" + +n_different_random :: (Int,Int) -> Int -> [Int] -> IO [Int] +n_different_random (l,h) n ns = do { x <- getStdRandom $ randomR (l,h) + ; if (x `elem` ns) then + n_different_random (l,h) n ns + else + if (n == 1) then + return (x:ns) + else + n_different_random (l,h) (n-1) (x:ns) } + +blank :: J.Value +blank = J.String "" + +zero :: J.Value +zero = J.Number 0 + +empty :: J.Value +empty = J.Object M.empty + +empty_array :: J.Value +empty_array = J.Array [] + +unn :: J.Value -> Int +unn (J.Number n) = fromInteger . round $ n + +uno :: J.Value -> M.Map String J.Value +uno (J.Object o) = o + +una :: J.Value -> [J.Value] +una (J.Array a) = a + +uns :: J.Value -> String +uns (J.String s) = s hunk ./perpubplat_servlet/src/blog.hs 17 +import qualified Blog.Widgets.ChromeBackEnd as ChromeB hunk ./perpubplat_servlet/src/blog.hs 28 - , comment_c :: CommentQ.CommentController } + , comment_c :: CommentQ.CommentController + , chrome_b :: ChromeB.ChromeBackEnd } hunk ./perpubplat_servlet/src/blog.hs 54 - ; output $ P.assemble_page v m } + ; h <- liftIO $ P.assemble_page v (chrome_b con) m + ; output h } hunk ./perpubplat_servlet/src/blog.hs 143 - ; let con = Controllers dc cc + ; cb <- ChromeB.boot + ; let con = Controllers dc cc cb }