-------------------------------------------------------------------- -- | -- Module : Web.GooglePlus.Types -- Description : Types returned by the Google+ API -- Copyright : (c) Michael Xavier 2011 -- License : MIT -- -- Maintainer: Michael Xavier -- Stability : provisional -- Portability: portable -- -------------------------------------------------------------------- {-# LANGUAGE OverloadedStrings #-} module Web.GooglePlus.Types (Person(..), PersonSearchResult(..), PersonID(..), ID, Actor(..), Verb(..), ActivityObject(..), ActivityObjectType(..), Provider(..), Access(..), AccessItem(..), AccessItemType(..), Geocode(..), Activity(..), ActivityFeed(..), Attachment(..), AttachmentType(..), Embed(..), Gender(..), PersonName(..), Image(..), Email(..), EmailType(..), PersonURL(..), PersonURLType(..), Organization(..), OrganizationType(..), Place(..), RelationshipStatus(..), Comment(..), CommentObject(..), InReplyTo(..)) where import Control.Applicative ((<$>), (<*>), pure, Applicative) import Data.Aeson (Value(..), Object, FromJSON, parseJSON, (.:), (.:?)) 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) import Data.Time.RFC3339 (readRFC3339) import Data.Text (Text, unpack) import qualified Data.Text as T import Network.URL (URL(..), importURL) type ID = Text ---------- Activity Types -- |A feed of user activity data ActivityFeed = ActivityFeed { activityFeedTitle :: Text, -- ^ Title of the feed in Google+ activityFeedUpdated :: ZonedTime, -- ^ Time updated activityFeedId :: ID, -- ^ Unique ID of the feed activityFeedItems :: [Activity] -- ^ Activities in the feed (currently limited to first page } deriving (Show, Eq) instance FromJSON ActivityFeed where parseJSON (Object v) = ActivityFeed <$> v .: "title" <*> v .: "updated" <*> v .: "id" <*> v .: "items" parseJSON v = typeMismatch "ActivityFeed" v -- |Activity on Google+, such as a post data Activity = Activity { activityPlaceholder :: Maybe Bool, -- ^ Meaning undocumented activityTitle :: Text, -- ^ Title of the activity activityPublished :: ZonedTime, -- ^ Date originally published activityUpdated :: ZonedTime, -- ^ Date updated activityId :: ID, -- ^ Activity ID activityURL :: URL, -- ^ URL to view the Activity activityActor :: Actor, -- ^ The person who performed the Activity activityVerb :: Verb, -- ^ Indicates what action was performed activityObject :: ActivityObject, -- ^ The object of the Activity activityAnnotation :: Maybe Text, -- ^ Additional content added by the person who shared this activity, applicable only when resharing an activity activityCrosspostSource :: Maybe ID, -- ^ ID of original activity if this activity is a crosspost from another system activityProvider :: Provider, -- ^ Service provider initially providing the activity activityAccess :: Access, -- ^ Identifies who has access to this activity activityGeocode :: Maybe Geocode, -- ^ Where the activity occurred (Latitude/Longitude) activityAddress :: Maybe Text, -- ^ Street address where the activity occurred activityRadius :: Maybe Integer, -- ^ Radius of the region where the activity ocurred, centered at the Geocode activityPlaceId :: Maybe ID, -- ^ ID of the place where the activity occurred activityPlaceName :: Maybe Text } deriving (Show, Eq) instance FromJSON Activity where parseJSON (Object v) = Activity <$> v .:? "placeholder" <*> v .: "title" <*> v .: "published" <*> v .: "updated" <*> v .: "id" <*> v .: "url" <*> v .: "actor" <*> v .:| ("verb", Post) <*> v .: "object" <*> v .:? "annotation" <*> v .:? "crosspostSource" <*> v .: "provider" <*> v .: "access" <*> v .:? "geocode" <*> v .:? "address" <*> v .:? "radius" <*> v .:? "placeId" <*> v .:? "placeName" parseJSON v = typeMismatch "Activity" v instance Eq ZonedTime where a == b = zonedTimeToUTC a == zonedTimeToUTC b instance FromJSON ZonedTime where parseJSON (String str) = maybeToParser parsed $ "Failed to parse ZonedTime " ++ unpack str where parsed = readRFC3339 . unpack $ str parseJSON v = typeMismatch "ZonedTime" v -- |A person who may be associated with an Activity data Actor = Actor { actorDisplayName :: Text, -- ^ The public display name of the Actor actorId :: ID, -- ^ The ID of the Actor actorImage :: Image, -- ^ Data pertaining to the Actor's main profile image actorUrl :: URL -- ^ URL of the user's profile } deriving (Show, Eq) instance FromJSON Actor where parseJSON (Object v) = Actor <$> v .: "displayName" <*> v .: "id" <*> v .: "image" <*> v .: "url" parseJSON v = typeMismatch "Actor" v -- |Type of activity being performed data Verb = Post | -- ^ Publish content to the stream Checkin | -- ^ Check into a location Share -- ^ Reshare an activity deriving (Show, Eq) instance FromJSON Verb where parseJSON (String "post") = pure Post parseJSON (String "checkin") = pure Checkin parseJSON (String "share") = pure Share parseJSON v = typeMismatch "Verb" v -- |Object to which an activity pertains data ActivityObject = ActivityObject { activityObjectActor :: Maybe Actor, -- ^ If the object is another Activity, this refers to the actor for that Activity activityObjectAttachments :: [Attachment], -- ^ Media objects attached to this activity object activityObjectContent :: Text, -- ^ Snipped of text if the object is an article activityObjectId :: Maybe ID, -- ^ ID of the media object's resource activityObjectType :: ActivityObjectType, -- ^ Type of Object activityObjectOriginalContent :: Maybe Text, -- ^ Content text as provided by the author without any HTML formatting activityObjectPlusOners :: Integer, -- ^ Number of people giving the Activity a +1 activityObjectReplies :: Integer, -- ^ Number of replies to the Activity activityObjectResharers :: Integer, -- ^ Number of people resharing the Activity activityObjectURL :: URL -- ^ URL pointing to the linked resource } deriving (Show, Eq) instance FromJSON ActivityObject where parseJSON (Object v) = ActivityObject <$> v .:? "actor" <*> v .:| ("attachments", []) <*> v .: "content" <*> v .:? "id" <*> v .:| ("objectType", Note) <*> v .:? "originalContent" <*> parseTotalItems "plusoners" <*> parseTotalItems "replies" <*> parseTotalItems "resharers" <*> v .: "url" where parseTotalItems key = maybe (fail $ "failed to find " ++ unpack key ++ "/totalItems in " ++ show v) parseJSON $ totalItems' key totalItems' key = case M.lookup key v of Just (Object obj) -> M.lookup "totalItems" obj _ -> Nothing parseJSON v = typeMismatch "ActivityObject" v -- |Types of objects that can be associated with an Activity data ActivityObjectType = Note | -- ^ Textual content GooglePlusActivity -- ^ A Google+ Activity deriving (Show, Eq) instance FromJSON ActivityObjectType where parseJSON (String "note") = pure Note parseJSON (String "activity") = pure GooglePlusActivity parseJSON v = typeMismatch "ActivityObjectType" v -- |Media attached to an Activity data Attachment = Attachment { attachmentContent :: Maybe Text, -- ^ Snippet of text if the Attachment is an article attachmentDisplayName :: Maybe Text, -- ^ Title of the Attachment attachmentEmbed :: Maybe Embed, -- ^ Embeddable link if the Attachment is a video attachmentFullImage :: Maybe Image, -- ^ Full image if the Attachment is a photo attachmentId :: Maybe ID, -- ^ ID of the Attachment's resource attachmentImage :: Maybe Image, -- ^ Preview image attachmentType :: AttachmentType, -- ^ Type of attachment attachmentURL :: Maybe URL -- ^ Lin k to text/html attachment } deriving (Show, Eq) instance FromJSON Attachment where parseJSON (Object v) = Attachment <$> v .:? "content" <*> v .:? "displayName" <*> v .:? "embed" <*> v .:? "fullImage" <*> v .:? "id" <*> v .:? "image" <*> v .: "objectType" <*> v .:? "url" parseJSON v = typeMismatch "AttachmentType" v -- |Embeddable link for an attachment if it is a video data Embed = Embed { embedType :: Text, -- ^ Type of embeddable link embedUrl :: URL -- ^ Embeddable link } deriving (Show, Eq) instance FromJSON Embed where parseJSON (Object v) = Embed <$> v .: "type" <*> v .: "url" parseJSON v = typeMismatch "Embed" v -- |Type of Activity Attachment data AttachmentType = Photo | PhotoAlbum | -- ^ A type that occurs in the wild but is not mentioned in the Google+ API docs Video | Article -- ^ An article attachment specified by a link by the poster deriving (Show, Eq) instance FromJSON AttachmentType where parseJSON (String "photo") = pure Photo parseJSON (String "photo-album") = pure PhotoAlbum parseJSON (String "video") = pure Video parseJSON (String "article") = pure Article parseJSON v = typeMismatch "AttachmentType" v -- |Geolocation based on longitude and latitude data Geocode = Geocode { latitude :: Double, longitude :: Double } deriving (Show, Eq) instance FromJSON Geocode where parseJSON (String str) = pure $ Geocode long lat where (longT, latT) = spanSkip ' ' str long = read . unpack $ longT lat = read . unpack $ latT parseJSON v = typeMismatch "Geocode" v -- |Describes who has access to a given Activity resource data Access = Access { accessDescription :: Maybe Text, -- ^ Description of the access, suitable for display accessItems :: [AccessItem] -- ^ List of access entries } deriving (Show, Eq) instance FromJSON Access where parseJSON (Object v) = Access <$> v .:? "description" <*> v .: "items" parseJSON v = typeMismatch "Access" v -- |AccessEntry that describes the type of access someone may have to an Activity data AccessItem = AccessItem { accessItemId :: Maybe ID, -- ^ ID of the entry. Only set when this AccessItem refers to a Person or Circle accessItemType :: AccessItemType -- ^ Type of entity which has access to the associated Activity } deriving (Show, Eq) instance FromJSON AccessItem where parseJSON (Object v) = AccessItem <$> v .:? "id" <*> v .: "type" parseJSON v = typeMismatch "AccessItem" v -- |Type of entity which may access an Activity data AccessItemType = PersonAccess | CircleAccess | MyCirclesAccess | -- ^ Access granted to all members of the Actor's circles ExtendedCirclesAccess | -- ^ Access granted to members of the Actor's circles and their circles as well PublicAccess -- ^ Access to anyone on the internet deriving (Show, Eq) instance FromJSON AccessItemType where parseJSON (String "person") = pure PersonAccess parseJSON (String "circle") = pure CircleAccess parseJSON (String "myCircles") = pure MyCirclesAccess parseJSON (String "extendedCircles") = pure ExtendedCirclesAccess parseJSON (String "public") = pure PublicAccess parseJSON v = typeMismatch "AccessItemType" v -- |Service provider who originally published an Activity data Provider = Provider { providerTitle :: Text } deriving (Show, Eq) instance FromJSON Provider where parseJSON (Object v) = Provider <$> v .: "title" parseJSON v = typeMismatch "Provider" v ---------- Person Types -- |A member of Google+ data Person = Person { personId :: ID, -- ^ Id of the Person personDisplayName :: Text, -- ^ Name of the Person, suitable for display personName :: Maybe PersonName, -- ^ Person's actual, full name personNickName :: Maybe Text, -- ^ Optional nickname of the Person personTagline :: Maybe Text, -- ^ Brief description of the Person personBirthday :: Maybe Day, -- ^ Person's Birthday personGender :: Maybe Gender, -- ^ Person's gender personAboutMe :: Maybe Text, -- ^ About Me profile section personCurrentLocation :: Maybe Text, -- ^ Current location of the Person personRelationshipStatus :: Maybe RelationshipStatus, -- ^ Person's current relationship status personProfileURL :: URL, -- ^ URL to the person's profile personImage :: Image, -- ^ Profile image for the Person personEmails :: [Email], -- ^ Email addresses that the person uses personURLs :: [PersonURL], -- ^ External URLs on the Person's profile personOrganizations :: [Organization], -- ^ Organizations that the Person has belonged to, past and present personPlacesLived :: [Place], -- ^ Places in which the Person has lived personLanguagesSpoken :: [Language], -- ^ Languages the Person speaks personHasApp :: Maybe Bool } deriving (Show, Eq) instance FromJSON Person where parseJSON (Object v) = Person <$> v .: "id" <*> v .: "displayName" <*> v .:? "name" <*> v .:? "nickname" <*> v .:? "tagline" <*> v .:? "birthday" <*> v .:? "gender" <*> v .:? "aboutMe" <*> v .:? "currentLocation" <*> v .:? "relationshipStatus" <*> v .: "url" <*> v .: "image" <*> v .:| ("emails", []) <*> v .:| ("urls", []) <*> v .:| ("organizations", []) <*> v .:| ("placesLived", []) <*> v .:| ("languagesSpoken", []) <*> 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 -- |Person's gender data Gender = Male | Female | OtherGender deriving (Show, Eq) instance FromJSON Gender where parseJSON (String "male") = pure Male parseJSON (String "female") = pure Female parseJSON (String "other") = pure OtherGender parseJSON v = typeMismatch "Gender" v -- |Identifier used for finding a Person data PersonID = PersonID Text | -- ^ ID for a specific user Me -- ^ The authenticated user deriving (Show, Eq) -- |Full, real name of a Person data PersonName = PersonName { familyName :: Text, formatted :: Text, -- ^ Fully formatted name of a Person including middle names, suffixes, etc. givenName :: Text, -- ^ The given (first) name of a Person honorificPrefix :: Text, -- ^ Prefix to a Person's name such as Dr. or Mrs. honorificSuffix :: Text, -- ^ Suffix of a Person's name such as Jr. middleName :: Text } deriving (Show, Eq) instance FromJSON PersonName where parseJSON (Object v) = PersonName <$> v .: "familyName" <*> v .: "formatted" <*> v .: "givenName" <*> v .: "honorificPrefix" <*> v .: "honorificSuffix" <*> v .: "middleName" parseJSON v = typeMismatch "PersonName" v -- |Image resource on Google+ data Image = Image { imageURL :: URL, imageType :: Maybe Text, -- ^ Media type of the link imageWidth :: Maybe Integer, imageHeight :: Maybe Integer } deriving (Show, Eq) instance FromJSON Image where parseJSON (Object v) = Image <$> v .: "url" <*> v .:? "type" <*> v .:? "width" <*> v .:? "height" parseJSON v = typeMismatch "Image" v -- |Email address belonging to the User data Email = Email { emailPrimary :: Bool, -- ^ Whether or not the Email is the Person's primary Email emailType :: EmailType, -- ^ Type/context of the Email address emailAddressValue :: Text -- ^ The actual text address of the Email } deriving (Show, Eq) instance FromJSON Email where parseJSON (Object v) = Email <$> v .: "primary" <*> v .: "type" <*> v .: "value" parseJSON v = typeMismatch "Email" v -- |Context/types of Emails that a Person can have data EmailType = HomeEmail | WorkEmail | OtherEmail deriving (Show, Eq) instance FromJSON EmailType where parseJSON (String "home") = pure HomeEmail parseJSON (String "work") = pure WorkEmail parseJSON (String "other") = pure OtherEmail parseJSON v = typeMismatch "EmailType" v -- |External URLS that the Person has published data PersonURL = PersonURL { personUrlPrimary :: Bool, -- ^ Whether or not the URL is the Person's primary URl personUrlType :: Maybe PersonURLType, -- ^ Type of URL personURLValue :: URL -- ^ Actual text URl for the Person } deriving (Show, Eq) instance FromJSON PersonURL where parseJSON (Object v) = PersonURL <$> v .:| ("primary", False) <*> v .:? "type" <*> v .: "value" parseJSON v = typeMismatch "PersonURL" v -- |Context/types of URLS that a Person can have data PersonURLType = HomeURL | WorkURL | BlogURL | ProfileURL | JsonURL | -- ^ This is not a documented value, yet I've encountered it in the wild. I have no idea what it means. OtherURL deriving (Show, Eq) instance FromJSON PersonURLType where parseJSON (String "home") = pure HomeURL parseJSON (String "work") = pure WorkURL parseJSON (String "blog") = pure BlogURL parseJSON (String "profile") = pure ProfileURL parseJSON (String "other") = pure OtherURL parseJSON (String "json") = pure JsonURL parseJSON v = typeMismatch "PersonURLType" v instance FromJSON URL where parseJSON (String str) = maybe (fail $ "Failed to parse URL " ++ unpack str) pure parsed where parsed = importURL . unpack $ str parseJSON v = typeMismatch "URL" v -- |Organization to which a Persion currently or previously may belong data Organization = Organization { organizationDepartment :: Maybe Text, -- ^ Department of an Organization in which the Person resided organizationDescription :: Maybe Text, -- ^ General description of the Organization organizationEndDate :: Maybe Text, -- ^ Date on which the user stopped at the organization in an unspecified text format organizationLocation :: Maybe Text, -- ^ Location of the Organization organizationName :: Text, -- ^ Name of the Organization organizationPrimary :: Bool, -- ^ Whether or not this Organization was the Person's primary one organizationstartDate :: Maybe Text, -- ^ Date on which the user started at the organization in an unspecified text format organizationTitle :: Text, -- ^ The Person's role at the Organization organizationType :: OrganizationType -- ^ The type of Organization } deriving (Show, Eq) instance FromJSON Organization where parseJSON (Object v) = Organization <$> v .:? "department" <*> v .:? "description" <*> v .:? "endDate" <*> v .:? "location" <*> v .: "name" <*> v .:| ("primary", False) <*> v .:? "startDate" <*> v .: "title" <*> v .: "type" parseJSON v = typeMismatch "Organization" v -- |The capacity in which the Perosn participated in an Organization data OrganizationType = Work | School deriving (Show, Eq) instance FromJSON OrganizationType where parseJSON (String "work") = pure Work -- spec says job. fail. parseJSON (String "school") = pure School parseJSON v = typeMismatch "OrganizationType" v -- |A physical location where a Person resides/resided data Place = Place { placePrimary :: Bool, -- ^ Whether or not this is/was the Person's primary residence placeValue :: Text -- ^ Text description of the Place } deriving (Show, Eq) instance FromJSON Place where parseJSON (Object v) = Place <$> v .:| ("primary", False) <*> v .: "value" parseJSON v = typeMismatch "Place" v type Language = Text -- |Relationship status of a Person data RelationshipStatus = Single | InARelationship | Engaged | Married | ItsComplicated | OpenRelationship | Widowed | InDomesticPartnership | InCivilUnion deriving (Show, Eq) instance FromJSON RelationshipStatus where parseJSON (String "single") = pure Single parseJSON (String "in_a_relationship") = pure InARelationship parseJSON (String "engaged") = pure Engaged parseJSON (String "married") = pure Married parseJSON (String "its_complicated") = pure ItsComplicated parseJSON (String "open_relationship") = pure OpenRelationship parseJSON (String "widowed") = pure Widowed parseJSON (String "in_domestic_partnership") = pure InDomesticPartnership parseJSON (String "in_civil_union") = pure InCivilUnion parseJSON v = typeMismatch "RelationshipStatus" v -- |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 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