googleplus 0.2.2 → 0.3.0
raw patch · 4 files changed
+342/−57 lines, 4 files
Files
- Web/GooglePlus.hs +255/−41
- Web/GooglePlus/Monad.hs +5/−1
- Web/GooglePlus/Types.hs +75/−10
- googleplus.cabal +7/−5
Web/GooglePlus.hs view
@@ -37,9 +37,23 @@ {-# LANGUAGE OverloadedStrings, FlexibleContexts, TypeSynonymInstances #-} module Web.GooglePlus (getPerson, getActivity,+ getComment, getLatestActivityFeed, enumActivityFeed,- enumActivities) where+ getActivityFeed,+ enumActivities,+ getActivities,+ enumPersonSearch,+ getPersonSearch,+ enumPeopleByActivity,+ getPeopleByActivity,+ enumActivitySearch,+ getActivitySearch,+ enumComments,+ getComments,+ SearchOrderBy(..),+ ActivityCollection(..),+ ListByActivityCollection(..)) where import Web.GooglePlus.Types import Web.GooglePlus.Monad@@ -53,6 +67,7 @@ fromJSON, parseJSON, Result(..),+ (.:), (.:?), Value(Object)) import Data.Aeson.Types (typeMismatch)@@ -62,12 +77,11 @@ import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as LBS import Data.Enumerator (Enumerator,- joinI, checkContinue1, continue, Stream (Chunks), (>>==),- ($=),+ run_, ($$)) import qualified Data.Enumerator.List as EL import Data.Maybe (fromMaybe)@@ -83,12 +97,18 @@ where pth = personIdPath pid --- | Get an activity who matches the given activity ID+-- | Get an activity which matches the given activity ID getActivity :: ID -- ^ Specific ID to fetch -> GooglePlusM (Either Text Activity) getActivity aid = genericGet pth []- where pth = append "/plus/v1/activities/" $ encodeUtf8 aid+ where pth = "/plus/v1/activities/" `append` encodeUtf8 aid +-- | Get a comment which matches the given comment ID+getComment :: ID -- ^ Specific ID to fetch+ -> GooglePlusM (Either Text Comment)+getComment cid = genericGet pth []+ where pth = "/plus/v1/comments/" `append` encodeUtf8 cid+ -- | Get an activity who matches the given activity ID and collection to use. -- Default page size is (20) and only fetches the first page. -- You will receive an error from the server if the page size exceeds 100.@@ -97,7 +117,7 @@ -> Maybe Integer -- ^ Page size. Should be between 1 and 100. Default 20. -> GooglePlusM (Either Text ActivityFeed) getLatestActivityFeed pid coll perPage = do- feed <- getActivityFeedPage pid coll (perPage' perPage) Nothing+ feed <- getActivityFeedPage pid coll (perPageActivity perPage) Nothing return $ fst `fmap` feed -- | Paginating enumerator to consume a user's activity stream. Each chunk will@@ -113,25 +133,157 @@ -> Maybe Integer -- ^ Page size. Should be between 1 and 100. Defualt 20 -> Enumerator ActivityFeed GooglePlusM b enumActivityFeed pid coll perPage = EL.unfoldM depaginate FirstPage- where depaginate = depaginateActivityFeed pid coll $ perPage' perPage+ where depaginate = depaginateActivityFeed pid coll $ perPageActivity perPage +-- | Simplified version of enumActivityFeed which retrieves all pages of an+-- activity feed and merges them into one. Note that this will not be as+-- efficient as enumActivityFeed in terms of memory/time because it collects+-- them all in memory first. Note that this should incur 1 API call per page of+-- results, so the max page size of 100 is used.+getActivityFeed :: PersonID+ -> ActivityCollection+ -> GooglePlusM ActivityFeed+getActivityFeed pid coll = do+ feeds <- run_ $ enumActivityFeed pid coll (Just 100) $$ EL.consume+ return $ foldl1 mergeFeeds feeds+ where mergeFeeds a ActivityFeed { activityFeedItems = is} = a { activityFeedItems = activityFeedItems a ++ is }+ -- | Paginating enumerator yielding a Chunk for each page. Use this if you -- don't need the feed metadata that enumActivityFeed provides. enumActivities :: PersonID -- ^ Feed owner ID -> ActivityCollection -- ^ Indicates what type of feed to retrieve -> Maybe Integer -- ^ Page size. Should be between 1 and 100. Defualt 20 -> Enumerator Activity GooglePlusM b-enumActivities pid coll perPage = unfoldListM depaginate FirstPage- where depaginate = depaginateActivities pid coll (perPage' perPage)+enumActivities pid coll perPage = simpleDepaginator depaginate+ where depaginate = simpleDepaginationStep perPage' pth params+ pth = pidP `append` actP+ actP = "/activities/" `append` collectionPath coll+ pidP = personIdPath pid+ params = []+ perPage' = perPageActivity perPage +-- | Simplified version of enumActivities that fetches all the activitys of a+-- Person first, thus returning them. Note that this should incur 1 API call+-- per page of results, so the max page size of 100 is used.+getActivities :: PersonID -- ^ Feed owner ID+ -> ActivityCollection -- ^ Indicates what type of feed to retrieve+ -> GooglePlusM [Activity]+getActivities pid coll = run_ $ enumActivities pid coll (Just 100) $$ EL.consume+++-- | Search for a member of Google+. Paginating enumerator yielding a Chunk for+-- each page. Note that this Enumerator will abort if it encounters an error+-- from the server, thus cutting the list short.+enumPersonSearch :: Text -- ^ Search string+ -> Maybe Integer -- ^ Optional page size. Shold be between 1 and 20. Default 10+ -> Enumerator PersonSearchResult GooglePlusM b+enumPersonSearch search perPage = simpleDepaginator depaginate+ where depaginate = simpleDepaginationStep perPage' pth params+ pth = "/plus/v1/people"+ params = [("query", Just $ encodeUtf8 search)]+ perPage' = perPageSearch perPage++-- | Returns the full result set for a person search given a search string.+-- This interface is simpler to use but does not have the flexibility/memory+-- usage benefit of enumPersonSearch.+getPersonSearch :: Text -- ^ Search string+ -> GooglePlusM [PersonSearchResult]+getPersonSearch search = run_ $ enumPersonSearch search (Just 20) $$ EL.consume++-- | Find people associated with a particular Activity. Paginating enumerator+-- yielding a Chunk for each page. Paginating enumerator yielding a Chunk for+-- each page. Note that this Enumerator will abort if it encounters an error+-- from the server, thus cutting the list short.+enumPeopleByActivity :: ID -- ^ Activity ID+ -> ListByActivityCollection -- ^ Indicates which collection of people to list+ -> Maybe Integer -- ^ Optional page size. Should be between 1 and 100. Default 20.+ -> Enumerator Person GooglePlusM b+enumPeopleByActivity aid coll perPage = simpleDepaginator depaginate+ where depaginate = simpleDepaginationStep perPage' pth params+ pth = "/plus/v1/activities/" `append` encodeUtf8 aid `append` peopleP `append` collP coll+ peopleP = "/people/"+ collP PlusOners = "plusoners"+ collP Resharers = "resharers"+ params = []+ perPage' = perPageActivity perPage++-- | Returns the full result set for a person search given a search string.+-- This interface is simpler to use but does not have the flexibility/memory+-- usage benefit of enumPeopleByActivity.+getPeopleByActivity :: ID -- ^ Activity ID+ -> ListByActivityCollection -- ^ Indicates which collection of people to list+ -> GooglePlusM [Person]+getPeopleByActivity aid coll = run_ $ enumPeopleByActivity aid coll (Just 100) $$ EL.consume++-- | Search for an activity on Google+. Paginating enumerator yielding a Chunk+-- for each page. Note that this Enumerator will abort if it encounters an error+-- from the server, thus cutting the list short.+enumActivitySearch :: Text -- ^ Search string+ -> SearchOrderBy -- ^ Order of search results+ -> Maybe Integer -- ^ Optional page size. Shold be between 1 and 20. Default 10+ -> Enumerator Activity GooglePlusM b+enumActivitySearch search orderBy perPage = simpleDepaginator depaginate+ where depaginate = simpleDepaginationStep perPage' pth params+ pth = "/plus/v1/activities"+ params = [("query", Just $ encodeUtf8 search),+ ("orderBy", Just $ orderParam orderBy)]+ orderParam Best = "best"+ orderParam Recent = "recent"+ perPage' = perPageSearch perPage++-- | Returns the full result set for an activity search given a search string.+-- This interface is simpler to use but does not have the flexibility/memory+-- usage benefit of enumActivitySearch.+getActivitySearch :: Text -- ^ Search string+ -> SearchOrderBy -- ^ Order of search results+ -> GooglePlusM [Activity]+getActivitySearch search orderBy = run_ $ enumActivitySearch search orderBy (Just 20) $$ EL.consume++-- | Find comments for an activity on Google+. Paginating enumerator yielding a+-- Chunk for each page. Note that this Enumerator will abort if it encounters+-- an error from the server, thus cutting the list short.+enumComments :: ID -- ^ Activity ID+ -> Maybe Integer -- ^ Optional page size. Should be between 1 and 100. Default 20+ -> Enumerator Comment GooglePlusM b+enumComments aid perPage = simpleDepaginator depaginate+ where depaginate = simpleDepaginationStep perPage' pth params+ pth = "/plus/v1/activities/" `append` encodeUtf8 aid `append` "/comments"+ params = []+ perPage' = perPageActivity perPage++-- | Returns the full result set for an activity's comments. This interface is+-- simpler to use but does not have the flexibility/memory usage benefit of+-- enumComments.+getComments :: ID -- ^ Activity ID+ -> GooglePlusM [Comment]+getComments aid = run_ $ enumComments aid (Just 100) $$ EL.consume++-- | Specifies the type of Activities to get in an Activity listing. Currently+-- the API only allows public.+data ActivityCollection = PublicCollection deriving (Show, Eq)++data ListByActivityCollection = PlusOners | -- ^ List of people who have +1ed an activity+ Resharers -- ^ List of people who have reshared an activity+ deriving (Show, Eq)++data SearchOrderBy = Best | -- ^ Sort by relevance to the to the user, most relevant first+ Recent -- ^ Sort by most recent results first+ deriving (Show, Eq)+ ---- Helpers -perPage' :: Maybe Integer -> Integer-perPage' = fromMaybe defaultPageSize+simpleDepaginator :: Monad m => (DepaginationState -> m (Maybe ([a], DepaginationState)))+ -> Enumerator a m b+simpleDepaginator depaginate = unfoldListM depaginate FirstPage -defaultPageSize :: Integer-defaultPageSize = 20+perPageActivity :: Maybe Integer+ -> Integer+perPageActivity = fromMaybe 20 +perPageSearch :: Maybe Integer+ -> Integer+perPageSearch = fromMaybe 10+ type PageToken = Text type PaginatedActivityFeed = (ActivityFeed, Maybe PageToken) @@ -146,67 +298,125 @@ -- Exactly the same as unfoldM but takes the result of the stateful function -- and uses it as the chunks, rather than a Chunks with a singleton list-unfoldListM :: Monad m => (s -> m (Maybe ([a], s))) -> s -> Enumerator a m b+unfoldListM :: Monad m => (s -> m (Maybe ([a], s)))+ -> s+ -> Enumerator a m b unfoldListM f = checkContinue1 $ \loop s k -> do fs <- lift (f s) case fs of Nothing -> continue k Just (as, s') -> k (Chunks as) >>== loop s' -depaginateActivities:: PersonID -> ActivityCollection -> Integer -> DepaginationState -> GooglePlusM (Maybe ([Activity], DepaginationState))-depaginateActivities pid coll perPage state = (return . (fmap unwrap)) =<< depaginateActivityFeed pid coll perPage state- where unwrap (feed, s) = (activityFeedItems feed, s)+simpleGetFirstPage :: FromJSON a => Integer+ -> Ascii+ -> Query+ -> GooglePlusM (Maybe (PaginatedResource a))+simpleGetFirstPage perPage = simpleGetPage perPage Nothing -depaginateActivityFeed :: PersonID -> ActivityCollection -> Integer -> DepaginationState -> GooglePlusM (Maybe (ActivityFeed, DepaginationState))+simpleGetPage :: FromJSON a => Integer+ -> Maybe PageToken + -> Ascii + -> Query + -> GooglePlusM (Maybe (PaginatedResource a))+simpleGetPage perPage tok pth params = do+ page <- genericGet pth $ params ++ pageParams+ return $ eitherMaybe page+ where pageParam = BS8.pack . show $ perPage+ pageParams = case tok of+ Nothing -> [("maxResults", Just pageParam)]+ Just t -> [("maxResults", Just pageParam), ("pageToken", Just $ encodeUtf8 t)]++simpleDepaginationStep :: FromJSON a => Integer+ -> Ascii + -> Query + -> DepaginationState + -> GooglePlusM (Maybe ([a], DepaginationState))+simpleDepaginationStep perPage pth params FirstPage = (return . fmap paginatedState) =<< simpleGetFirstPage perPage pth params+simpleDepaginationStep perPage pth params (MorePages tok) = (return . fmap paginatedState) =<< simpleGetPage perPage (Just tok) pth params+simpleDepaginationStep _ _ _ NoMorePages = return Nothing++-- Activities Specifics++depaginateActivityFeed :: PersonID+ -> ActivityCollection+ -> Integer+ -> DepaginationState+ -> GooglePlusM (Maybe (ActivityFeed, DepaginationState)) depaginateActivityFeed pid coll perPage FirstPage = do page <- getFirstFeedPage pid coll perPage- return $ paginatedFeedState `fmap` page+ return $ paginatedState `fmap` page depaginateActivityFeed pid coll perPage (MorePages tok) = do page <- getActivityFeedPage pid coll perPage $ Just tok- return $ paginatedFeedState `fmap` eitherMaybe page+ return $ paginatedState `fmap` eitherMaybe page depaginateActivityFeed _ _ _ NoMorePages = return Nothing -paginatedFeedState :: PaginatedActivityFeed -> (ActivityFeed, DepaginationState)-paginatedFeedState (feed, token) = (feed, maybe NoMorePages MorePages token)--getFirstFeedPage :: PersonID -> ActivityCollection -> Integer -> GooglePlusM (Maybe PaginatedActivityFeed)+getFirstFeedPage :: PersonID+ -> ActivityCollection+ -> Integer+ -> GooglePlusM (Maybe PaginatedActivityFeed) getFirstFeedPage pid coll perPage = do page <- getActivityFeedPage pid coll perPage Nothing return $ eitherMaybe page -getActivityFeedPage :: PersonID -> ActivityCollection -> Integer -> Maybe PageToken -> GooglePlusM (Either Text PaginatedActivityFeed)+getActivityFeedPage :: PersonID+ -> ActivityCollection+ -> Integer+ -> Maybe PageToken+ -> GooglePlusM (Either Text PaginatedActivityFeed) getActivityFeedPage pid coll perPage tok = genericGet pth params- where pth = append pidP actP+ where pth = pidP `append` actP pidP = personIdPath pid- actP = append "/activities/" $ collectionPath coll+ actP = "/activities/" `append` collectionPath coll pageParam = BS8.pack . show $ perPage params = case tok of Nothing -> [("maxResults", Just pageParam)] Just t -> [("maxResults", Just pageParam), ("pageToken", Just $ encodeUtf8 t)] -eitherMaybe :: Either a b -> Maybe b+type PaginatedResource a = ([a], Maybe PageToken)++instance FromJSON a => FromJSON (PaginatedResource a) where+ parseJSON (Object v) = (,) <$> v .: "items"+ <*> v .:? "nextPageToken"+ parseJSON v = typeMismatch "PaginatedResource" v++paginatedState :: (a, Maybe PageToken)+ -> (a, DepaginationState)+paginatedState (results, token) = (results, maybe NoMorePages MorePages token)++-- Internals++eitherMaybe :: Either a b+ -> Maybe b eitherMaybe (Left _) = Nothing eitherMaybe (Right x) = Just x -genericGet :: FromJSON a => Ascii -> Query -> GooglePlusM (Either Text a)-genericGet pth qs = withEnv $ \auth -> do- resp <- doGet auth pth qs- return $ handleResponse resp+genericGet :: FromJSON a => Ascii+ -> Query+ -> GooglePlusM (Either Text a)+genericGet pth qs = withEnv $ \auth -> return . handleResponse =<< doGet auth pth qs -collectionPath :: ActivityCollection -> ByteString+collectionPath :: ActivityCollection+ -> ByteString collectionPath PublicCollection = "public" -personIdPath :: PersonID -> ByteString-personIdPath (PersonID i) = append "/plus/v1/people/" $ encodeUtf8 i+personIdPath :: PersonID+ -> ByteString+personIdPath (PersonID i) = "/plus/v1/people/" `append` encodeUtf8 i personIdPath Me = "/plus/v1/people/me" -doGet :: GooglePlusAuth -> Ascii -> Query -> GooglePlusM (Int, LBS.ByteString)+doGet :: GooglePlusAuth+ -> Ascii+ -> Query+ -> GooglePlusM (Int, LBS.ByteString) doGet auth pth q = liftIO $ withManager $ \manager -> do Response { statusCode = c, responseBody = b} <- httpLbsRedirect req manager return (c, b) where req = genRequest auth pth q -genRequest :: GooglePlusAuth -> Ascii -> Query -> Request m+genRequest :: GooglePlusAuth+ -> Ascii+ -> Query+ -> Request m genRequest auth pth q = def { host = h, path = pth, port = 443,@@ -216,11 +426,13 @@ authq = authParam auth q' = authq:q -authParam :: GooglePlusAuth -> QueryItem+authParam :: GooglePlusAuth+ -> QueryItem authParam (APIKey key) = ("key", Just $ encodeUtf8 key) authParam (OAuthToken tok) = ("access_token", Just $ encodeUtf8 tok) -handleResponse :: FromJSON a => (Int, LBS.ByteString) -> Either Text a+handleResponse :: FromJSON a => (Int, LBS.ByteString)+ -> Either Text a handleResponse (200, str) = packLeft $ fjson =<< parsed where fjson v = case fromJSON v of Success a -> Right a@@ -228,9 +440,11 @@ parsed = eitherResult $ parse json str handleResponse (_, str) = Left $ decodeUtf8 . BS.concat . LBS.toChunks $ str -packLeft :: Either String a -> Either Text a+packLeft :: Either String a+ -> Either Text a packLeft (Right x) = Right x packLeft (Left str) = Left $ pack str -withEnv :: (GooglePlusAuth -> GooglePlusM a) -> GooglePlusM a+withEnv :: (GooglePlusAuth -> GooglePlusM a)+ -> GooglePlusM a withEnv fn = fn =<< asks gpAuth
Web/GooglePlus/Monad.hs view
@@ -14,13 +14,17 @@ {-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving #-} module Web.GooglePlus.Monad where -import Data.Text (Text)+import Data.Text (Text,unpack) import Control.Monad.Reader -- |Represents authentication data with GooglePlus. Currently supports an OAuth -- token or an API key data GooglePlusAuth = APIKey Text | -- ^ Authentication using an API key OAuthToken Text -- ^ Authenticate using a token obtianed via OAuth V2. Currently no way in the library to obtain refresh tokens++instance Show GooglePlusAuth where+ show (APIKey a) = show $ unpack a+ show (OAuthToken a) = show $ unpack a --TODO: getting tokens and using refresh tokens
Web/GooglePlus/Types.hs view
@@ -13,8 +13,8 @@ {-# LANGUAGE OverloadedStrings #-} module Web.GooglePlus.Types (Person(..),+ PersonSearchResult(..), PersonID(..),- ActivityCollection(..), ID, Actor(..), Verb(..),@@ -40,9 +40,12 @@ Organization(..), OrganizationType(..), Place(..),- RelationshipStatus(..)) where+ RelationshipStatus(..),+ Comment(..),+ CommentObject(..),+ InReplyTo(..)) where -import Control.Applicative ((<$>), (<*>), pure)+import Control.Applicative ((<$>), (<*>), pure, Applicative) import Data.Aeson (Value(..), Object, FromJSON,@@ -50,6 +53,7 @@ (.:), (.:?)) import Data.Aeson.Types (Parser, typeMismatch)+import Data.List (intercalate) import qualified Data.Map as M import Data.Time.Calendar (Day(..)) import Data.Time.LocalTime (ZonedTime(..), zonedTimeToUTC)@@ -69,9 +73,6 @@ activityFeedItems :: [Activity] -- ^ Activities in the feed (currently limited to first page } deriving (Show, Eq) --- |Specifies the type of Activities to get. Currently the API only allows public.-data ActivityCollection = PublicCollection deriving (Show, Eq)- instance FromJSON ActivityFeed where parseJSON (Object v) = ActivityFeed <$> v .: "title" <*> v .: "updated"@@ -125,7 +126,7 @@ a == b = zonedTimeToUTC a == zonedTimeToUTC b instance FromJSON ZonedTime where- parseJSON (String str) = maybe (fail $ "Failed to parse ZonedTime " ++ unpack str) pure parsed+ parseJSON (String str) = maybeToParser parsed $ "Failed to parse ZonedTime " ++ unpack str where parsed = readRFC3339 . unpack $ str parseJSON v = typeMismatch "ZonedTime" v @@ -339,6 +340,20 @@ <*> v .:? "hasApp" parseJSON v = typeMismatch "Person" v +-- |A Person search result with limited informaiton. The full person's profile must be retrieved to get the rest+data PersonSearchResult = PersonSearchResult { personSRId :: ID, -- ^ Id of the Person+ personSRDisplayName :: Text, -- ^ Name of the Person, suitable for display+ personSRImage :: Image, -- ^ Profile image for the Person+ personSRProfileURL :: URL -- ^ URL to the person's profile+ } deriving (Show, Eq)++instance FromJSON PersonSearchResult where+ parseJSON (Object v) = PersonSearchResult <$> v .: "id"+ <*> v .: "displayName"+ <*> v .: "image"+ <*> v .: "url"+ parseJSON v = typeMismatch "PersonSearchResult" v+ instance FromJSON Day where parseJSON (String str) = pure . read . unpack $ str parseJSON v = typeMismatch "Day" v@@ -492,7 +507,7 @@ } deriving (Show, Eq) instance FromJSON Place where- parseJSON (Object v) = Place <$> v .: "primary"+ parseJSON (Object v) = Place <$> v .:| ("primary", False) <*> v .: "value" parseJSON v = typeMismatch "Place" v @@ -522,11 +537,61 @@ parseJSON (String "in_civil_union") = pure InCivilUnion parseJSON v = typeMismatch "RelationshipStatus" v -(.:|) :: (FromJSON a) => Object -> (Text, a) -> Parser a+-- |Activity comment on Google++data Comment = Comment { commentId :: ID, -- ^ ID of the comment+ commentPublished :: ZonedTime, -- ^ Date originally published+ commentUpdated :: ZonedTime, -- ^ Date updated+ commentActor :: Actor, -- ^ The actor who posted the comment+ commentVerb :: Verb, -- ^ Indicates what action was performed+ commentObject :: CommentObject, -- ^ The content of the object+ commentUrl :: URL, -- ^ URL to the comment+ commentActivities :: [InReplyTo] -- ^ The activities to which this comment is a reply+ } deriving (Show, Eq)++instance FromJSON Comment where+ parseJSON (Object v) = Comment <$> v .: "id"+ <*> v .: "published"+ <*> v .: "updated"+ <*> v .: "actor"+ <*> v .:| ("verb", Post)+ <*> v .: "object"+ <*> v .: "selfLink"+ <*> v .: "inReplyTo"+ parseJSON v = typeMismatch "Comment" v+++data CommentObject = CommentObject { commentObjectContent :: Text -- ^ Text content of the comments+ } deriving (Show, Eq)++instance FromJSON CommentObject where+ parseJSON (Object v) = CommentObject <$> v .: "content"+ parseJSON v = typeMismatch "CommentObject" v++data InReplyTo = InReplyTo { inReplyToId :: ID, -- ^ ID of the article+ inReplyToUrl :: URL -- ^ URL of the article+ } deriving (Show, Eq)++instance FromJSON InReplyTo where+ parseJSON (Object v) = InReplyTo <$> v .: "id"+ <*> v .: "url"+ parseJSON v = typeMismatch "InReplyTo" v++---- Helpers++(.:|) :: (FromJSON a) => Object+ -> (Text, a)+ -> Parser a obj .:| (key, d) = case M.lookup key obj of Nothing -> pure d Just v -> parseJSON v -spanSkip :: Char -> Text -> (Text, Text)+spanSkip :: Char+ -> Text+ -> (Text, Text) spanSkip cond xs = (left, T.tail right) where (left, right) = T.span (/= cond) xs++maybeToParser :: (Applicative m, Monad m) => Maybe a+ -> String+ -> m a+maybeToParser parsed msg = maybe (fail msg) pure parsed
googleplus.cabal view
@@ -1,13 +1,15 @@ name: googleplus-version: 0.2.2-synopsis: Haskell implementation of the Google+ API+version: 0.3.0+synopsis: Haskell implementation of the Google+ API v1 description: Will implement the Google+ REST API. Google+ is a social network made by Google. Found out more at <http://plus.google.com>. - Currently supports the API-key authentication only. OAuth should be coming- along at some point. Only features read-only API access beacuse that is all- that Google has published thus far.+ Implements the full API as of Oct 15, 2011. Only features read-only API+ access beacuse that is all that Google has published thus far. Both API and+ OAuth authentication is supported, but note that if you intend to use OAuth,+ this library does not currently provide the means to procure and renew OAuth+ tokens. category: Web license: BSD3