packages feed

flickr 0.2 → 0.2.1

raw patch · 2 files changed

+437/−1 lines, 2 files

Files

+ Flickr/Monad.hs view
@@ -0,0 +1,435 @@+{-# OPTIONS_GHC -XExistentialQuantification -XDeriveDataTypeable #-}
+--------------------------------------------------------------------
+-- |
+-- Module      : Flickr.Monad
+-- Description : Monadic layer for supporting flickr.com interactions.
+-- Copyright   : (c) Sigbjorn Finne, 2008
+-- License     : BSD3
+--
+-- Maintainer: Sigbjorn Finne <sof@forkIO.com>
+-- Stability : provisional
+-- Portability: portable
+--
+-- Monadic layer for supporting flickr.com interactions.
+--------------------------------------------------------------------
+module Flickr.Monad where
+
+import Util.Fetch
+import Util.Keys ( hsflickrAPIKey, APIKey(..) )
+import Util.MD5  ( md5sumStr )
+import Data.List ( intercalate, sortBy )
+import Util.Post
+
+import Text.XML.Light as XML
+import Text.XML.Light.Proc as XML
+import Flickr.Utils
+import Control.Exception as CE
+import System.FilePath as FilePath ( takeExtension )
+
+import System.IO as IO
+import Data.Typeable
+
+data FM a = FM (FMEnv -> IO a)
+
+data FMEnv
+ = FMEnv
+    { fm_api_key         :: APIKey
+    , fm_is_signed       :: Bool
+    , fm_post_method     :: Bool
+    , fm_per_page        :: Maybe Int
+    , fm_page            :: Maybe Int
+    , fm_is_paged        :: Bool
+    , fm_perm_level      :: Maybe String
+    , fm_auth_token      :: Maybe String
+    , fm_auth_mini_token :: Maybe String
+    , fm_api_base        :: Maybe String
+    }
+
+instance Monad FM where
+  return x = FM (\ _ -> return x)
+  (FM a) >>= k = FM $ \ env -> do
+      v <- a env
+      case k v of
+        (FM b) ->  b env
+    
+liftIO :: IO a -> FM a
+liftIO x = FM (\ _ -> x)
+
+flick :: FM a -> IO a
+flick a = flickAPI hsflickrAPIKey a
+
+flickAPI :: APIKey -> FM a -> IO a
+flickAPI ak fm = 
+ case (handleFlickr (\ err -> liftIO (print err) >> liftIO (throwIO (toException err)))
+                    fm) of
+   FM flickr_action_and_stuff -> flickr_action_and_stuff initEnv
+ where
+  initEnv = 
+    FMEnv
+      { fm_api_key         = ak
+      , fm_is_signed       = False
+      , fm_post_method     = False
+      , fm_per_page        = Nothing
+      , fm_page            = Nothing
+      , fm_perm_level      = Nothing
+      , fm_is_paged        = False
+      , fm_auth_token      = Nothing
+      , fm_auth_mini_token = Nothing
+      , fm_api_base        = Nothing
+      }
+
+withAPIKey :: APIKey -> FM a -> FM a
+withAPIKey ak (FM x) = FM (\ env -> x env{fm_api_key=ak})
+
+getAPIKey :: FM APIKey
+getAPIKey = FM (\ env -> return (fm_api_key env))
+
+withSharedSecret :: String -> FM a -> FM a
+withSharedSecret s (FM x) = FM ( \ env -> x env{fm_api_key=(fm_api_key env){apiSecret=s}})
+
+withAuthToken :: String -> FM a -> FM a
+withAuthToken s (FM x) = FM ( \ env -> x env{fm_auth_token=Just s})
+
+withAuthMiniToken :: String -> FM a -> FM a
+withAuthMiniToken s (FM x) = FM ( \ env -> x env{fm_auth_mini_token=Just s})
+
+getAuthToken :: FM String
+getAuthToken = FM (\env -> return (fromMaybe "" (fm_auth_token env)))
+
+getAuthMiniToken :: FM String
+getAuthMiniToken = FM (\env -> return (fromMaybe "" (fm_auth_mini_token env)))
+
+withPageSize :: Int -> FM a -> FM a
+withPageSize sz (FM x) = FM (\ env -> x env{fm_is_paged=True,fm_per_page=Just sz})
+
+pagedCall :: Maybe Int -> FM a -> FM a
+pagedCall mbPg (FM x) = FM (\ env -> x env{fm_is_paged=True,fm_page=mbPg})
+
+signedMethod :: FM a -> FM a
+signedMethod (FM x) = FM (\ env -> x env{fm_is_signed=True})
+
+withReadPerm  :: FM a -> FM a
+withReadPerm (FM x) = FM (\ env -> x env{fm_is_signed=True, fm_perm_level=Just "read"})
+
+withWritePerm :: FM a -> FM a
+withWritePerm (FM x) = FM (\ env -> x env{fm_is_signed=True, fm_perm_level=Just "write"})
+
+withDeletePerm :: FM a -> FM a
+withDeletePerm (FM x) = FM (\ env -> x env{fm_is_signed=True, fm_perm_level=Just "delete"})
+
+postMethod :: FM a -> FM a
+postMethod (FM x) = FM (\ env -> x env{fm_post_method=True})
+
+withBase :: URLString -> FM a -> FM a
+withBase b (FM x) = FM (\ env -> x env{fm_api_base=Just b})
+
+getBaseURL :: FM URLString
+getBaseURL = FM ( \ env -> return (fromMaybe api_base (fm_api_base env)) )
+
+getMobileAuthURL :: FM URLString
+getMobileAuthURL = FM (\ env -> return (fromMaybe "" (fm_api_base env)))
+
+api_base :: URLString
+api_base = "http://api.flickr.com/services/rest/"
+
+auth_base :: URLString
+auth_base = "http://api.flickr.com/services/auth/?"
+
+upload_base :: URLString
+upload_base = "http://api.flickr.com/services/upload/"
+
+replace_base :: URLString
+replace_base = "http://api.flickr.com/services/replace/"
+
+flickTranslate :: (String -> ErrM a)
+               -> FM String
+	       -> FM a
+flickTranslate f a = do
+  x <- a
+  case f x of
+    Left  e -> {-liftIO (print e) >> -}liftIO (throwIO (toException e))
+    Right r -> return r
+
+flickCall_ :: String -> [(String,String)] -> FM ()
+flickCall_ m args = flickTranslate (checkResponse) (flickCall m args) >> return ()
+
+mbArg :: String -> Maybe String -> [(String,String)] -> [(String,String)]
+mbArg _ Nothing xs = xs
+mbArg t (Just a) xs = (t,a):xs
+
+lsArg :: String -> [String] -> [(String,String)] -> [(String,String)]
+lsArg _ [] xs = xs
+lsArg t ls xs = (t,intercalate "," ls):xs
+
+flickrCall :: String -> [(String,String)] -> FM String
+flickrCall m args = flickCall m args
+
+flickCall :: String -> [(String,String)] -> FM String
+flickCall m args = FM $ \ env -> do
+  let
+     mb _ Nothing = []
+     mb x (Just v) = [(x,show v)]
+     
+     pgContext = fm_is_paged env
+
+     pageContext ls
+      | not pgContext = ls
+      | otherwise = ls ++ concat [ mb "per_page" (fm_per_page env)
+                                 , mb "page"     (fm_page env)
+			         ]
+     isSigned xs
+      | fm_is_signed env = xs ++ [("api_sig",api_sig)]
+      | otherwise = ("format", "rest") : xs
+    
+     withAToken xs 
+      | fm_is_signed env =
+        case fm_auth_token env of
+          Nothing -> xs
+	  Just x  -> 
+	     -- if auth_token is already an arg (cf. flickr.auth.checkToken),
+	     -- don't add a second one. ToDo: issue a warning/heads-up about
+	     -- this ?
+	    case lookup "auth_token" xs of
+	      Nothing -> ("auth_token",x):xs
+	      _ -> xs
+      | otherwise = xs
+
+     withPerms xs = 
+       case fm_perm_level env of
+         Nothing -> xs
+	 Just x  -> 
+	   case fm_auth_token env of
+	     Just{} -> xs
+	     _     -> ("perms",x):xs
+     
+     withMethod xs = 
+       case m of
+         "" -> xs
+	 _  -> ("method",m):xs
+
+     withNoFiles xs = 
+       filter (\ (_,y) -> case y of { '@':_ -> False; _ -> True }) xs
+
+     api_sig_inp = apiSecret (fm_api_key env) ++ 
+                   concatMap (\ (x,y) -> x++y) (
+                   sortBy (\ a b -> compare (fst a) (fst b))
+		         (("api_key", apiKey $ fm_api_key env) : 
+			  withMethod (withAToken (withPerms $ withNoFiles args))))
+
+     api_sig = md5sumStr api_sig_inp
+               
+     restMeth
+      | fm_post_method env = restPost
+      | otherwise          = restGet
+
+--  print ("XX",args,fm_is_signed env, fm_auth_token env, api_sig_inp)
+  restMeth (fromMaybe api_base (fm_api_base env))
+           (pageContext $
+            (withMethod 
+              (("api_key", apiKey $ fm_api_key env) : 
+	          isSigned (withAToken $ withPerms args))))
+			  
+mkLoginURL :: String -> String -> FM String
+mkLoginURL fr p = FM $ \ env -> do
+  return (genLoginURL (apiKey $ fm_api_key env) (apiSecret $ fm_api_key env)
+                      fr p)        
+
+genLoginURL :: String
+            -> String
+	    -> String
+	    -> String
+	    -> String
+genLoginURL api_key secret frob perm = 
+  auth_base ++ "api_key="++api_key ++ "&frob="++frob ++
+              "&perms="++perm++"&api_sig="++api_sig
+ where
+  api_sig = md5sumStr $ secret ++ 
+                        "api_key" ++ api_key ++ 
+                        "frob" ++ frob ++ 
+                        "perms" ++ perm
+
+restGet :: {-URL-}String -> [(String,String)] -> IO String
+restGet a kv = do
+--debug:  print (a ++ wArgs kv)
+  readContentsURL (a ++ wArgs kv)
+ where
+   wArgs [] = ""
+   wArgs xs = '?':intercalate "&" (map (\ (k,v) -> k ++ '=':v) xs)
+  
+restPost :: {-URL-}String -> [(String,String)] -> IO String
+restPost a kv = do
+--debug:  print (a ++ wArgs kv)
+   -- ToDo: fix, the encoding of arguments that 'toRequest' performs
+   -- interacts badly with signature computation. Signatures need to be
+   -- done over the encoded strings, it seems.
+  (vs2,hs0,body0) <- toRequest r (Just PostQuery)
+  let body = case body0 of { "" -> ""; _ -> '\r':'\n':body0}
+  let hs = ("Content-Length",show (length body)):hs0
+  let vs  = wArgs vs1 ++ (if null vs2 then "" else if null vs1 then '?':vs2 else '&':vs2)
+-- debug:  print (a++vs,hs,body)
+  postContentsURL (a++vs) hs body
+ where
+   r0 = newPostRequest "flickr"
+   (r,vs1)  = foldr (\ (x,y) (acc,hs) -> 
+                  case y of
+		    '@':xs -> (addNameFile x xs (extToTy xs) acc,hs)
+		    _      -> (addNameValue x y acc, hs))
+	      (r0,[])
+	      kv
+
+   wArgs [] = ""
+   wArgs xs = '?':intercalate "&" (map (\ (k,v) -> k ++ '=':v) xs)
+   
+    -- ToDo: write/plug into general mime.types-like package here.
+   extToTy fp = 
+     case FilePath.takeExtension fp of
+       ""     -> Just "image/jpeg"
+       ".gif" -> Just "image/gif"
+       ".jpg" -> Just "image/jpeg"
+       ".png" -> Just "image/png"
+       _      -> Just "image/jpeg"
+  
+type ErrM a = Either FlickErr a
+
+data FlickErr
+ = FlickErr
+     { flickErrorCode   :: Int
+     , flickErrorType   :: FlickErrorType
+     , flickErrorMsg    :: String
+     , flickErrorLoc    :: Maybe String
+     , flickErrorSource :: String
+     } deriving Typeable
+
+data SomeFlickException = forall e . Exception e => SomeFlickException e 
+    deriving Typeable 
+
+instance Show SomeFlickException where 
+    show (SomeFlickException e) = show e 
+
+instance Exception SomeFlickException 
+
+flickToException :: Exception e => e -> SomeException 
+flickToException = toException . SomeFlickException 
+
+flickFromException :: Exception e => SomeException -> Maybe e 
+flickFromException x = do 
+    SomeFlickException a <- fromException x 
+    cast a 
+
+instance Exception FlickErr where
+  toException = flickToException
+  fromException = flickFromException
+
+handleFlickr :: (FlickErr -> FM a) -> FM a -> FM a
+handleFlickr h e = catchFlickr e h
+
+tryFlick :: FM a -> FM (Either FlickErr a)
+tryFlick f = handleFlickr (\ x -> return (Left x)) (f >>= return.Right)
+
+throwFlickErr :: FlickErr -> FM a
+throwFlickErr e = FM (\ _ -> throwIO e)
+
+catchFlickr :: FM a -> (FlickErr -> FM a) -> FM a
+catchFlickr (FM f) hdlr = FM $ \ env -> 
+  CE.catch (f env) 
+           (\ e1 -> case hdlr e1 of { (FM act) -> act env })
+
+instance Show FlickErr where
+  show x = unlines (
+   [ "Flickr error:"
+   , ""
+   , " Code: " ++ show (flickErrorCode x)
+   , " Type: " ++ show (flickErrorType x)
+   , " Details: " ++ flickErrorMsg x
+   ] ++ (if flickErrorType x == IllformedError  || flickErrorType x == FlickParseError
+          then [" Source: " ++ flickErrorSource x]
+	  else []))
+
+flickError :: FlickErr
+flickError 
+ = FlickErr
+     { flickErrorCode   = (-1)
+     , flickErrorType   = UnexpectedResponse
+     , flickErrorMsg    = ""
+     , flickErrorLoc    = Nothing
+     , flickErrorSource = ""
+     }
+
+data FlickErrorType
+    -- fatal errors while processing response payloads:
+ = UnexpectedResponse -- not a 'rsp'
+ | MissingStatus      -- no 'stat' on 'rsp' element
+ | EmptyResponse
+ | IllformedError
+ | FlickParseError
+ | FlickrAPIError     -- see 'code' + 'msg' field for details
+   deriving ( Eq )
+
+instance Show FlickErrorType where
+  show x = 
+   case x of
+      UnexpectedResponse -> "unexpected XML Flickr response"
+      MissingStatus      -> "unexpected response; missing 'rsp' top element"
+      EmptyResponse      -> "empty 'rsp' content"
+      IllformedError     -> "ill-formed 'rsp' content"
+      FlickParseError    -> "ok response returned, but ill-formed"
+      FlickrAPIError     -> "Flickr API error"
+
+parseDoc :: (Element -> Maybe a)
+         -> String
+	 -> ErrM a
+parseDoc f s = 
+  case checkResponse s of
+    Left err -> Left err
+    Right x  ->
+     case f x of
+        Nothing -> 
+         Left flickError{ flickErrorType   = FlickParseError
+	                , flickErrorSource = show (length s) ++ '\n':s
+	   	        }
+        Just res -> Right res
+
+checkResponse :: String -> ErrM Element
+checkResponse s = 
+  case parseXMLDoc s of
+    Nothing -> Left flickError { flickErrorSource = s
+                               , flickErrorType   = UnexpectedResponse
+			       }
+    Just e  
+     | elName e /= nsName "rsp" -> Left flickError{flickErrorSource=s}
+     | otherwise -> 
+	  case pAttr "stat" e of
+	    Nothing -> Left flickError{ flickErrorType   = MissingStatus
+	                              , flickErrorSource = s
+				      }
+	    Just "ok" -> 
+	      case elChildren e of
+	        [] -> Right blank_element
+		(x:_) -> Right x
+	    Just "fail" -> 
+	      case findChild (nsName "err") e of
+	        Nothing -> 
+		  Left flickError{ flickErrorType   = IllformedError
+		                 , flickErrorSource = s
+				 }
+		Just e1 -> 
+		  case pAttr "code" e1 of
+		    Nothing -> 
+		      Left flickError{ flickErrorSource = s
+		                     , flickErrorType   = IllformedError
+				     }
+		    Just v_str  -> 
+		      case reads v_str of
+		       ((v,_):_) -> Left 
+		          flickError { flickErrorType = FlickrAPIError
+			             , flickErrorCode = v
+				     , flickErrorSource = s
+				     , flickErrorMsg  = 
+				         fromMaybe "" (pAttr "msg" e1)
+			             }
+                       _ -> Left flickError{ flickErrorSource = s
+	                                   , flickErrorType   = IllformedError
+				           }
+            _ -> Left flickError{ flickErrorSource = s
+	                        , flickErrorType   = IllformedError
+				}
flickr.cabal view
@@ -1,5 +1,5 @@ name: flickr
-version: 0.2
+version: 0.2.1
 Synopsis: Haskell binding to the Flickr API
 Description:
    The flickr API binding lets you access flickr.com's
@@ -25,6 +25,7 @@ 
 library
  Exposed-modules: Flickr.API,
+                  Flickr.Monad,
                   Flickr.Types,
                   Flickr.Types.Import,
                   Flickr.Activity,