[Implementation of personal aggregator (del.icio.us, twwitter, google reader). Removal of some old files. prb@mult.ifario.us**20080216103248] { adddir ./src/Blog/Widgets/StreamOfConsciousness hunk ./perpubplat.cabal 19 - fastcgi >= 3001.0.0, bytestring >= 0.9, pureMD5 >= 0.1.1, base64-string + fastcgi >= 3001.0.0, bytestring >= 0.9, pureMD5 >= 0.1.1, base64-string, + hxt >= 7.4 hunk ./src/Blog/Constants.hs 17 -blog_root = "http://mult.ifario.us" +blog_root = "http://localhost:7007" hunk ./src/Blog/Constants.hs 35 -generator_version = "0.9" +generator_version = "0.9.1" hunk ./src/Blog/Constants.hs 53 -base_url = "http://mult.ifario.us" +base_url = "http://localhost:7007" hunk ./src/Blog/Constants.hs 84 -twitter_pass = "" +twitter_pass = "hnadner" hunk ./src/Blog/Constants.hs 93 -flickr_api_key = "" +flickr_api_key = "233b3c334c93b6f999f9b4b810f9d603" hunk ./src/Blog/FrontEnd/Presentation.hs 15 -import qualified Blog.Widgets.Twitter as TwC +import qualified Blog.Widgets.StreamOfConsciousness as SoC hunk ./src/Blog/FrontEnd/Presentation.hs 109 - ; twc <- TwC.get_tweets $ CB.twitter cb + ; soc <- SoC.get_content $ CB.soc cb hunk ./src/Blog/FrontEnd/Presentation.hs 115 - , h3 $ stringToHtml "Twitter" - , primHtml twc + , h3 $ stringToHtml "Stream Of Consciousness" + , primHtml soc hunk ./src/Blog/Widgets/ChromeBackEnd.hs 7 -import Blog.Widgets.Twitter ( TwitterController, boot_twitter ) +import Blog.Widgets.StreamOfConsciousness hunk ./src/Blog/Widgets/ChromeBackEnd.hs 18 - , twitter :: TwitterController} + , soc :: SoCController } hunk ./src/Blog/Widgets/ChromeBackEnd.hs 29 - ; tc <- boot_twitter C.twitter_user C.twitter_pass C.twitter_count - ; return $ ChromeBackEnd f p t dc s ffd tc } + ; socc <- start_soc 20 + ; start_twitter socc "paulrbrown" "" 15 + ; start_delicious socc "prb" + ; start_google_reader socc google_user + ; return $ ChromeBackEnd f p t dc s ffd socc } hunk ./src/Blog/Widgets/FlickrBadge.hs 1 -module Blog.Widgets.FlickrBadge ( flickr_badge ) where - -import Text.XHtml.Strict ( Html, script, (!), (<<), - thetype, src, concatHtml, noHtml, identifier, primHtml, thediv ) - -flickr_badge :: Html -flickr_badge = thediv ! [ identifier "filckr_badge_uber_wrapper"] - << ( thediv ! [ identifier "flickr_badge_wrapper" ] - << ( concatHtml [ script ! [ thetype "text/javascript", - src flickr_script ] << noHtml - , flickr_link ] ) ) - -flickr_link :: Html -flickr_link = primHtml $ "" - ++ "www.flick" - ++ "r.com" - -flickr_count :: String -flickr_count = show 10 - -flickr_script :: String -flickr_script = "http://www.flickr.com/badge_code_v2.gne?count=" - ++ flickr_count - ++ "&display=random&size=t&layout=x&source=user&user=92922008%40N00" + rmfile ./src/Blog/Widgets/FlickrBadge.hs addfile ./src/Blog/Widgets/StreamOfConsciousness.hs hunk ./src/Blog/Widgets/StreamOfConsciousness.hs 1 - +module Blog.Widgets.StreamOfConsciousness ( module Blog.Widgets.StreamOfConsciousness.DeliciousPosts + , module Blog.Widgets.StreamOfConsciousness.GoogleReader + , module Blog.Widgets.StreamOfConsciousness.Twitter + , module Blog.Widgets.StreamOfConsciousness.Thought + , module Blog.Widgets.StreamOfConsciousness.Controller ) where + +import Blog.Widgets.StreamOfConsciousness.DeliciousPosts +import Blog.Widgets.StreamOfConsciousness.Twitter +import Blog.Widgets.StreamOfConsciousness.GoogleReader +import Blog.Widgets.StreamOfConsciousness.Thought +import Blog.Widgets.StreamOfConsciousness.Controller addfile ./src/Blog/Widgets/StreamOfConsciousness/Controller.hs hunk ./src/Blog/Widgets/StreamOfConsciousness/Controller.hs 1 +module Blog.Widgets.StreamOfConsciousness.Controller where + +import Blog.Widgets.StreamOfConsciousness.Thought +import Blog.BackEnd.HttpPoller + +import Control.Concurrent +import Control.Concurrent.MVar +import Control.Concurrent.Chan + +data Worker = Worker { soc_controller :: SoCController + , poller :: HttpPoller } + +data Snapshot = Snapshot { items :: [Thought] + , max_size :: Int + , version :: Int + , rendered :: String } + deriving ( Show, Read, Eq ) + +data SoCRequest = GetData { snapshot_handback :: MVar Snapshot } + | GetHtmlFragment { content :: MVar String } + | Update { retry :: MVar Bool + , snapshot :: Snapshot } + + +data SoCController = SoCController { tid :: ThreadId + , request_channel :: Chan SoCRequest } + +start_soc :: Int -> IO SoCController +start_soc mx_sz = do { rc <- newChan + ; let snap = Snapshot [] mx_sz 0 "" + ; _tid <- forkIO $ loop rc snap + ; return $ SoCController _tid rc } + +stop_soc :: SoCController -> IO () +stop_soc = killThread . tid + +stop_worker :: Worker -> IO () +stop_worker = stop_poller . poller + +get_content :: SoCController -> IO String +get_content c = do { x <- newEmptyMVar + ; writeChan (request_channel c) $ GetHtmlFragment x + ; takeMVar x } + +get_data :: SoCController -> IO Snapshot +get_data c = do { x <- newEmptyMVar + ; writeChan (request_channel c) $ GetData x + ; takeMVar x } + +update :: SoCController -> Snapshot -> IO Bool +update c snap = do { ok <- newEmptyMVar + ; writeChan (request_channel c) $ Update ok snap + ; takeMVar ok } + +collision_delay :: Int +collision_delay = 1000 + +commit :: SoCController -> [Thought] -> IO () +commit socc new_items = do { snap <- get_data socc + ; let items' = take (max_size snap) $ merge new_items $ items snap + ; let rendered' = thoughts_to_xhtml items' + ; let snap' = snap { items = items' + , rendered = rendered' } + ; ok <- update socc snap' + ; if ok then + return () + else + do { threadDelay collision_delay + ; commit socc new_items } + } + +loop :: Chan SoCRequest -> Snapshot -> IO () +loop ch snap = + do { req <- readChan ch + ; snap' <- case req of + GetHtmlFragment c -> + do { putMVar c $ rendered snap + ; return snap } + GetData h -> + do { putMVar h snap + ; return snap } + Update ok snap'' -> + if (version snap) == (version snap'') then + do { putMVar ok True + ; let snap' = snap'' { version = (version snap) + 1 } + ; return snap' } + else + do { putMVar ok False + ; return snap } + ; loop ch snap' } + addfile ./src/Blog/Widgets/StreamOfConsciousness/DeliciousPosts.hs hunk ./src/Blog/Widgets/StreamOfConsciousness/DeliciousPosts.hs 1 +{-# LANGUAGE Arrows, NoMonomorphismRestriction #-} +module Blog.Widgets.StreamOfConsciousness.DeliciousPosts ( start_delicious ) where + +import Blog.Widgets.StreamOfConsciousness.Thought +import Blog.Widgets.StreamOfConsciousness.Controller +import Blog.BackEnd.HttpPoller + +import Text.XML.HXT.Arrow +import Network.HTTP +import Network.URI ( parseURI ) +import Data.Maybe ( fromJust ) + + +delicious_period :: Int +delicious_period = 360 * 10^6 + +start_delicious :: SoCController -> String -> IO Worker +start_delicious socc user = do { let req = Request ( fromJust . parseURI $ "http://feeds.delicious.com/rss/" ++ user ) GET [] "" + ; p <- start_poller "DeliciousPosts" req (handle_posts socc) delicious_period + ; return $ Worker socc p } + +handle_posts :: SoCController -> String -> IO () +handle_posts socc body = do { posts <- runX ( readString parse_opts body >>> getItems ) + ; commit socc posts } + +parse_opts = [(a_validate, v_0), (a_check_namespaces,v_1)] + +atElemQName qn = deep (isElem >>> hasQName qn) +text = getChildren >>> getText +textOf qn = atElemQName qn >>> text + +rdf_uri :: String +rdf_uri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + +rdf_RDF :: QName +rdf_RDF = QN "rdf" "RDF" rdf_uri + +rss_uri :: String +rss_uri = "http://purl.org/rss/1.0/" + +rss_item :: QName +rss_item = QN "rss" "item" rss_uri + +rss_title :: QName +rss_title = QN "rss" "title" rss_uri + +rss_link :: QName +rss_link = QN "rss" "link" rss_uri + +dc_uri :: String +dc_uri = "http://purl.org/dc/elements/1.1/" + +dc_date :: QName +dc_date = QN "dc" "date" dc_uri + +getItem = atElemQName rss_item >>> + proc i -> do + t <- textOf rss_title -< i + u <- textOf rss_link -< i + d <- textOf dc_date -< i + returnA -< Thought Delicious d u t + +getItems = atElemQName rdf_RDF >>> + proc r -> do + items <- getItem -< r + returnA -< items addfile ./src/Blog/Widgets/StreamOfConsciousness/GoogleReader.hs hunk ./src/Blog/Widgets/StreamOfConsciousness/GoogleReader.hs 1 +{-# LANGUAGE Arrows, NoMonomorphismRestriction #-} +module Blog.Widgets.StreamOfConsciousness.GoogleReader ( start_google_reader, google_user ) where + +import Blog.Widgets.StreamOfConsciousness.Thought +import Blog.Widgets.StreamOfConsciousness.Controller +import Blog.BackEnd.HttpPoller + +import Text.XML.HXT.Arrow +import Network.HTTP +import Network.URI ( parseURI ) +import Data.Maybe ( fromJust ) + + +google_reader_period :: Int +google_reader_period = 240 * 10^6 + +google_user :: String +google_user = "14627107182419169041" + +start_google_reader :: SoCController -> String -> IO Worker +start_google_reader socc user = do { let req = Request ( fromJust . parseURI $ "http://www.google.com/reader/public/atom/user/" ++ user ++ "/state/com.google/broadcast" ) GET [] "" + ; p <- start_poller "GoogleReaderSharedItems" req (handle_posts socc) google_reader_period + ; return $ Worker socc p } + +handle_posts :: SoCController -> String -> IO () +handle_posts socc body = do { posts <- runX ( readString parse_opts body >>> getEntries ) + ; commit socc posts } + +parse_opts = [(a_validate, v_0), (a_check_namespaces,v_1)] + +atElemQName qn = deep (isElem >>> hasQName qn) +childElemQName qn = getChildren >>> isElem >>> hasQName qn +text = getChildren >>> getText +textOf qn = childElemQName qn >>> text + +atom_uri :: String +atom_uri = "http://www.w3.org/2005/Atom" + +atom_entry :: QName +atom_entry = QN "atom" "entry" atom_uri + +atom_title :: QName +atom_title = QN "atom" "title" atom_uri + +atom_updated :: QName +atom_updated = QN "atom" "updated" atom_uri + +atom_link :: QName +atom_link = QN "atom" "link" atom_uri + +atom_feed :: QName +atom_feed = QN "atom" "feed" atom_uri + +getEntry = atElemQName atom_entry >>> + proc i -> do + t <- textOf atom_title -< i + l <- childElemQName atom_link >>> hasAttrValue "rel" ((==) "alternate") -< i + u <- getAttrValue "href" -< l + d <- textOf atom_updated -< i + returnA -< Thought GoogleReader d u t + +getEntries = atElemQName atom_feed >>> + proc r -> do + entries <- getEntry -< r + returnA -< entries addfile ./src/Blog/Widgets/StreamOfConsciousness/Thought.hs hunk ./src/Blog/Widgets/StreamOfConsciousness/Thought.hs 1 - +module Blog.Widgets.StreamOfConsciousness.Thought where + +import Text.XHtml.Strict +import Data.List + +import Blog.FrontEnd.ContentAtoms +import qualified Blog.Constants as C + +data Channel = Delicious | Twitter | GoogleReader + deriving ( Show, Read, Eq, Ord, Enum ) + +icon :: Channel -> Html +icon c = image ! [ src $ C.blog_root ++ "/files/" ++ (show c) ++ "-icon.png" + , alt $ show c ] + +data Thought = Thought { channel :: Channel + , date :: String + , url :: String + , txt :: String } + + deriving ( Show, Read ) + +instance Eq Thought where + t1 == t2 = (channel t1 == channel t2) + && (date t1 == date t2) + +instance Ord Thought where + t1 <= t2 = ((date t1) <= (date t2)) + || ( ((date t1) == (date t2)) + && ((channel t1) <= (channel t2)) ) + +thoughts_to_xhtml :: [Thought] -> String +thoughts_to_xhtml = showHtmlFragment . (divid "thoughts") + . (traverse_thoughts "1970-01-01") + +traverse_thoughts :: String -> [Thought] -> Html +traverse_thoughts _ [] = noHtml +traverse_thoughts l_d (t:ts) + = (concatHtml [ if l_d `isPrefixOf` d then + noHtml + else + (p ! [theclass "thought_group" ]) . stringToHtml $ d + , p ! [theclass "thought"] $ concatHtml [ icon $ channel t + , stringToHtml " " + , to_html t + ] + ]) +++ (traverse_thoughts d ts) + where + d = take 10 $ date t + +to_html :: Thought -> Html +to_html (Thought Twitter d u t) = concatHtml [ _a u time + , stringToHtml " " + , text ] + where + wrap st = (thespan ! [ theclass st ]) + time_hunk = (take 9) . (drop 11) + time = (wrap "tweet_stamp") . stringToHtml $ time_hunk d + text = wrap "tweet_text" $ primHtml t + +to_html (Thought _ _ u t) = _a u (primHtml t) + +-- "zip" two lists into an ordered list, eliminating duplicates +merge :: (Eq a, Ord a) => [a] -> [a] -> [a] +merge as bs = merge_ as [] bs + +merge_ :: (Eq a, Ord a) => [a] -> [a] -> [a] -> [a] +merge_ [] y z = (reverse y) ++ z +merge_ x y [] = (reverse y) ++ x +merge_ x@(x0:xs) y z@(z0:zs) | x0 == z0 = merge_ xs (x0:y) zs + | x0 >= z0 = merge_ xs (x0:y) z + | otherwise = merge_ x (z0:y) zs + +dedup :: (Eq a) => [a] -> [a] +dedup = (map head) . group addfile ./src/Blog/Widgets/StreamOfConsciousness/Twitter.hs hunk ./src/Blog/Widgets/StreamOfConsciousness/Twitter.hs 1 +module Blog.Widgets.StreamOfConsciousness.Twitter ( start_twitter ) where + +import Blog.Widgets.StreamOfConsciousness.Thought +import Blog.Widgets.StreamOfConsciousness.Controller + +import Text.ParserCombinators.Parsec + +import Network.HTTP +import Network.HTTP.Headers +import Network.URI ( parseURI ) +import Data.Maybe ( fromJust ) +import Data.List ( elemIndex, intersperse ) +import Text.XHtml.Strict + +import qualified Codec.Binary.Base64.String as B64 + +import Blog.Widgets.JsonUtilities + +import Blog.BackEnd.HttpPoller + +twitter_period :: Int +twitter_period = 300 * 10^6 + +start_twitter :: SoCController -> String -> String -> Int -> IO Worker +start_twitter socc user password count + = do { let req = build_tweet_request user password count + ; p <- start_poller "Tweets" req (handle_tweets socc user) + twitter_period + ; return $ Worker socc p } + +build_tweet_request :: String -> String -> Int -> Request +build_tweet_request user password count = Request uri GET heads "" + where + uri = fromJust $ parseURI $ "http://twitter.com/statuses/user_timeline/" + ++ user ++ ".json?count=" ++ (show count) + heads = [ Header HdrAuthorization $ (++) "Basic " $ B64.encode $ user ++ ":" ++ password ] + +handle_tweets :: SoCController -> String -> String -> IO () +handle_tweets socc user body + = do { let v = parse_json body + ; let texts = map (tweet_body_to_xhtml . uns) $ una $ v "text" + ; let times = map (convert_twitter_tstmp . uns) $ una $ v "created_at" + ; let urls = map ((tweet_id_to_link user) . show . unn) $ una $ v "id" + ; let tweets = map (\ (d,u,t) -> Thought Twitter d u t) $ zip3 times urls texts + ; commit socc tweets } + +tweet_id_to_link :: String -> String -> String +tweet_id_to_link user t_id = "http://twitter.com/" ++ user ++ "/statuses/" ++ t_id + +tweet_body_to_xhtml :: String -> String +tweet_body_to_xhtml = showHtmlFragment . (thespan ! [ theclass "tweet_text" ]) + . primHtml . pre_process + +convert_twitter_tstmp :: String -> String +convert_twitter_tstmp ts = concat [ y, "-", mo', "-", d, "T", tm, "Z" ] + where + mo = take 3 $ drop 4 ts + mo' = pad $ 1 + ( fromJust $ elemIndex mo [ "Jan", "Feb", "Mar", "Apr", "May", "Jun" + , "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] ) + pad = \n -> if n <10 then ('0':show n) else show n + y = take 4 $ drop 26 ts + d = take 2 $ drop 8 ts + tm = take 8 $ drop 11 ts + + +pre_process :: String -> String +pre_process s = case parse pre_process_parser "" s of + Left err -> error . show $ err + Right v -> v + + +pre_process_parser :: Parser String +pre_process_parser = do { ts <- tok `sepBy` (many1 space) + ; return $ concat . (intersperse " ") $ ts } + +tok :: Parser String +tok = try http_link <|> try at_someone <|> word + +word :: Parser String +word = many1 $ noneOf " " + +at_someone :: Parser String +at_someone = do { char '@' + ; s <- many1 $ noneOf " " + ; return $ "@" ++ s ++ "" } + +http_link :: Parser String +http_link = do { string "http://" + ; s <- many1 $ noneOf " " + ; return $ "" ++ s ++ "" } + hunk ./src/Blog/Widgets/Twitter.hs 1 -module Blog.Widgets.Twitter (boot_twitter, get_tweets, TwitterController ( poller, twitter_tid )) where - -import Text.ParserCombinators.Parsec - -import Network.HTTP -import Network.HTTP.Headers -import Network.URI ( parseURI ) -import Data.Maybe ( fromJust ) -import Data.List ( elemIndex, intersperse ) -import Text.XHtml.Strict -import Control.Concurrent -import Control.Concurrent.Chan -import Control.Concurrent.MVar - -import qualified Codec.Binary.Base64.String as B64 - -import Blog.Widgets.JsonUtilities - -import Blog.BackEnd.HttpPoller - -data TRequest = GetTweets ( MVar String ) - | UpdateTweets String - -data TwitterController = TwitterController { request_channel :: Chan TRequest - , poller :: HttpPoller - , twitter_tid :: ThreadId } - -boot_twitter :: String -> String -> Int -> IO TwitterController -boot_twitter user password count = do { let req = build_tweet_request user password count - ; rc <- newChan - ; p <- start_poller "Tweets" req (handle_body rc) (60 * 10^6) - ; tid <- forkIO $ loop rc "" - ; return $ TwitterController rc p tid } - -loop :: Chan TRequest -> String -> IO () -loop rc xh = do { req <- readChan rc - ; case req of - GetTweets hb -> - putMVar hb xh >> loop rc xh - UpdateTweets xh' -> - loop rc xh' } - -build_tweet_request :: String -> String -> Int -> Request -build_tweet_request user password count = Request uri GET heads "" - where - uri = fromJust $ parseURI $ "http://twitter.com/statuses/user_timeline/" - ++ user ++ ".json?count=" ++ (show count) - heads = [ Header HdrAuthorization $ (++) "Basic " $ B64.encode $ user ++ ":" ++ password ] - -handle_body :: Chan TRequest -> String -> IO () -handle_body tc body = do { let v = parse_json body - ; let texts = map uns $ una $ v "text" - ; let times = map (convert_twitter_tstmp . uns) $ una $ v "created_at" - ; update_tweets tc $ tweets_to_xhtml $ zip texts times } - -send :: TwitterController -> TRequest -> IO () -send tc = writeChan (request_channel tc) - -update_tweets :: Chan TRequest -> String -> IO () -update_tweets tc tweets = writeChan tc $ UpdateTweets tweets - -get_tweets :: TwitterController -> IO String -get_tweets tc = do { hb <- newEmptyMVar - ; send tc $ GetTweets hb - ; takeMVar hb } - -convert_twitter_tstmp :: String -> String -convert_twitter_tstmp ts = concat [ y, "-", mo', "-", d, "T", tm, "Z" ] - where - mo = take 3 $ drop 4 ts - mo' = pad $ 1 + ( fromJust $ elemIndex mo [ "Jan", "Feb", "Mar", "Apr", "May", "Jun" - , "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] ) - pad = \n -> if n <10 then ('0':show n) else show n - y = take 4 $ drop 26 ts - d = take 2 $ drop 8 ts - tm = take 8 $ drop 11 ts - -tweets_to_xhtml :: [(String,String)] -> String -tweets_to_xhtml = showHtmlFragment . (ulist ! [ identifier "tweets"]) . concatHtml . (map tweet_to_xhtml) - -tweet_to_xhtml :: (String, String) -> Html -tweet_to_xhtml (txt,tstmp) = (li ! [ theclass "tweet" ]) $ (wrap "tweet_stamp" $ stringToHtml tstmp) - +++ (stringToHtml " ") +++ (wrap "tweet_text" $ primHtml . pre_process $ txt) - where - wrap st = (thespan ! [ theclass st ]) - -pre_process :: String -> String -pre_process s = case parse pre_process_parser "" s of - Left err -> error . show $ err - Right v -> v - - -pre_process_parser :: Parser String -pre_process_parser = do { ts <- tok `sepBy` (many1 space) - ; return $ concat . (intersperse " ") $ ts } - -tok :: Parser String -tok = try http_link <|> try at_someone <|> word - -word :: Parser String -word = many1 $ noneOf " " - -at_someone :: Parser String -at_someone = do { char '@' - ; s <- many1 $ noneOf " " - ; return $ "@" ++ s ++ "" } - -http_link :: Parser String -http_link = do { string "http://" - ; s <- many1 $ noneOf " " - ; return $ "" ++ s ++ "" } + rmfile ./src/Blog/Widgets/Twitter.hs }