-- | This package allows to use @bit.ly@ and @j.mp@ URL shortening service -- from Haskell programs. See also "Network.TinyURL" module. module Network.Bitly (Account(..), bitlyAccount, jmpAccount, shorten, expand, Result) where import Network.HTTP import Text.XML.HaXml import Text.XML.HaXml.Util (docContent) import Text.XML.HaXml.Posn (noPos, Posn) import Text.XML.HaXml.Pretty (element, content) -- | Service credentials. data Account = Account { login :: String, -- ^ bit.ly login name apikey :: String, -- ^ API key as found at server :: String -- ^ Server to use, e.g. @http:\/\/api.j.mp@ } deriving (Read, Show) -- | Account to use with bit.ly bitlyAccount :: Account bitlyAccount = Account { login = "", apikey = "", server = "http://api.bit.ly" } -- | Account to use with j.mp jmpAccount :: Account jmpAccount = Account { login = "", apikey = "", server = "http://api.j.mp" } -- | Either an error message or a modified URL type Result = Either String String -- | Given a long URL, @shorten@ encodes it as a shorter one. shorten :: Account -- ^ Account to use -> String -- ^ Long URL -> IO Result -- ^ Either error or short bit.ly URL shorten acc url = request acc "shorten" [("longUrl", url)] [ "bitly", "results", "nodeKeyVal", "shortUrl" ] -- | Given a short bit.ly URL, @expand@ decodes it back into a long source URL. expand :: Account -- ^ Account to use -> String -- ^ Short bit.ly URL -> IO Result -- ^ Either error or long source URL expand acc url = request acc "expand" [("shortUrl", url)] [ "bitly", "results", code, "longUrl" ] where ending d = foldr (\x xs -> if d `elem` xs then xs else x:xs) "" code = dropHeadIf (== '/') $ ending '/' url dropHeadIf :: (a -> Bool) -> [a] -> [a] dropHeadIf _ [] = [] dropHeadIf p all@(x:xs) | p x = xs | otherwise = all -- | Internal function to accomodate all types of requests request :: Account -- ^ Account to use -> String -- ^ Name of the API request (e.g. @shorten@ or @expand@) -> [(String,String)] -- ^ Alist of the parameters specific to the request -> [String] -- ^ Path to the node with the result in the XML response -> IO Result request acc path params xmlpath = do let baseURL = server acc ++ "/" ++ path let params' = loginParams ++ params :: [ (String, String) ] let reqURL = baseURL ++ "?" ++ urlEncodeVars params' let req = getRequest reqURL :: Request String resp <- simpleHTTP req case resp of Left _ -> return $ Left "Network error" Right resp' -> do let doc = xmlParse "" . rspBody $ resp' return $ errorOrResult doc xmlpath where loginParams = [ ("login", login acc) , ("apiKey", apikey acc) , ("format", "xml") , ("version", "2.0.1") ] -- | Analyze XML response errorOrResult :: Document Posn -- ^ Parsed XML document -> [String] -- ^ Path to the node with the result -> Result errorOrResult rootelement xmlpath = do let root = docContent noPos rootelement let cs = tag "bitly" /> tag "statusCode" /> txt $ root case cs of [] -> Left "No statusCode in response" ((CString _ code _):_) -> if code /= "OK" then let err = concatMap (render . content) $ tag "bitly" /> tag "errorMessage" /> txt $ root in Left $ "Bit.ly error: " ++ err else let url' = concatMap (render . content) $ foldr (/>) txt (map tag xmlpath) root in if null url' then Left "Result not found" else Right url' _ -> Left "Unexpected statusCode in response"