googleplus-0.3.1: Web/GooglePlus/Types.hs
--------------------------------------------------------------------
-- |
-- Module : Web.GooglePlus.Types
-- Description : Types returned by the Google+ API
-- Copyright : (c) Michael Xavier 2011
-- License : MIT
--
-- Maintainer: Michael Xavier <michael@michaelxavier.net>
-- 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 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