packages feed

twitter-types 0.10.1 → 0.11.0

raw patch · 8 files changed

+1505/−1251 lines, 8 filesdep ~aesonsetup-changedPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson

API changes (from Hackage documentation)

+ Web.Twitter.Types: Variant :: Maybe Int -> Text -> URIString -> Variant
+ Web.Twitter.Types: VideoInfo :: [Int] -> Maybe Int -> [Variant] -> VideoInfo
+ Web.Twitter.Types: [exeVideoInfo] :: ExtendedEntity -> Maybe VideoInfo
+ Web.Twitter.Types: [vBitrate] :: Variant -> Maybe Int
+ Web.Twitter.Types: [vContentType] :: Variant -> Text
+ Web.Twitter.Types: [vUrl] :: Variant -> URIString
+ Web.Twitter.Types: [vsAspectRatio] :: VideoInfo -> [Int]
+ Web.Twitter.Types: [vsDurationMillis] :: VideoInfo -> Maybe Int
+ Web.Twitter.Types: [vsVariants] :: VideoInfo -> [Variant]
+ Web.Twitter.Types: data Variant
+ Web.Twitter.Types: data VideoInfo
+ Web.Twitter.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Twitter.Types.Variant
+ Web.Twitter.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Twitter.Types.VideoInfo
+ Web.Twitter.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Twitter.Types.Variant
+ Web.Twitter.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Twitter.Types.VideoInfo
+ Web.Twitter.Types: instance Data.Data.Data Web.Twitter.Types.Variant
+ Web.Twitter.Types: instance Data.Data.Data Web.Twitter.Types.VideoInfo
+ Web.Twitter.Types: instance GHC.Classes.Eq Web.Twitter.Types.Variant
+ Web.Twitter.Types: instance GHC.Classes.Eq Web.Twitter.Types.VideoInfo
+ Web.Twitter.Types: instance GHC.Generics.Generic Web.Twitter.Types.Variant
+ Web.Twitter.Types: instance GHC.Generics.Generic Web.Twitter.Types.VideoInfo
+ Web.Twitter.Types: instance GHC.Generics.Generic1 Web.Twitter.Types.Entity
+ Web.Twitter.Types: instance GHC.Show.Show Web.Twitter.Types.Variant
+ Web.Twitter.Types: instance GHC.Show.Show Web.Twitter.Types.VideoInfo
- Web.Twitter.Types: ExtendedEntity :: StatusId -> URIString -> URIString -> HashMap Text MediaSize -> Text -> Maybe Double -> Maybe String -> URLEntity -> ExtendedEntity
+ Web.Twitter.Types: ExtendedEntity :: StatusId -> URIString -> URIString -> HashMap Text MediaSize -> Text -> Maybe VideoInfo -> Maybe Double -> Maybe String -> URLEntity -> ExtendedEntity

Files

README.md view
@@ -1,6 +1,8 @@ twitter-types  ============= -[![Build Status](https://secure.travis-ci.org/himura/twitter-types.png)](http://travis-ci.org/himura/twitter-types)+![CI](https://github.com/himura/twitter-types/workflows/CI/badge.svg)+[![Hackage](https://img.shields.io/hackage/v/twitter-types.svg?style=flat)](https://hackage.haskell.org/package/twitter-types)+[![Hackage-Deps](https://img.shields.io/hackage-deps/v/twitter-types.svg)](http://packdeps.haskellers.com/feed?needle=twitter-types)  This library treats the Twitter JSON API in the Haskell way.
Setup.hs view
@@ -1,3 +1,5 @@ #!/usr/bin/env runhaskell+ import Distribution.Simple+ main = defaultMain
Web/Twitter/Types.hs view
@@ -1,997 +1,1160 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}--module Web.Twitter.Types-       ( UserId-       , Friends-       , URIString-       , UserName-       , StatusId-       , LanguageCode-       , StreamingAPI(..)-       , Status(..)-       , SearchResult(..)-       , SearchStatus(..)-       , SearchMetadata(..)-       , RetweetedStatus(..)-       , DirectMessage(..)-       , EventTarget(..)-       , Event(..)-       , Delete(..)-       , User(..)-       , List(..)-       , Entities(..)-       , EntityIndices-       , ExtendedEntities(..)-       , ExtendedEntity(..)-       , Entity(..)-       , HashTagEntity(..)-       , UserEntity(..)-       , URLEntity(..)-       , MediaEntity(..)-       , MediaSize(..)-       , Coordinates(..)-       , Place(..)-       , BoundingBox(..)-       , Contributor(..)-       , UploadedMedia (..)-       , ImageSizeType (..)-       , DisplayTextRange (..)-       , checkError-       , twitterTimeFormat-       )-       where--import Control.Applicative-import Control.Monad-import Data.Aeson-import Data.Aeson.Types (Parser)-import Data.Data-import Data.HashMap.Strict (HashMap, fromList, union)-import Data.Int-import Data.Ratio-import Data.Text (Text, unpack, pack)-import Data.Text.Read (decimal)-import Data.Time-import Data.Time.Clock.POSIX-import GHC.Generics--newtype TwitterTime = TwitterTime { fromTwitterTime :: UTCTime }--type UserId       = Integer-type Friends      = [UserId]-type URIString    = Text-type UserName     = Text-type StatusId     = Integer-type LanguageCode = String--data StreamingAPI = SStatus Status-                  | SRetweetedStatus RetweetedStatus-                  | SEvent Event-                  | SDelete Delete-                  -- | SScrubGeo ScrubGeo-                  | SFriends Friends-                  | SDirectMessage DirectMessage-                  | SUnknown Value-                  deriving (Show, Eq, Data, Typeable, Generic)--checkError :: Object -> Parser ()-checkError o = do-    err <- o .:? "error"-    case err of-        Just msg -> fail msg-        Nothing -> return ()--twitterTimeFormat :: String-twitterTimeFormat = "%a %b %d %T %z %Y"--instance FromJSON TwitterTime where-    parseJSON = withText "TwitterTime" $ \t ->-        case parseTimeM True defaultTimeLocale twitterTimeFormat (unpack t) of-            Just  d -> pure $ TwitterTime d-            Nothing -> fail $ "Could not parse twitter time. Text was: " ++ unpack t--instance ToJSON TwitterTime where-    toJSON t = String $ pack $ formatTime defaultTimeLocale twitterTimeFormat $ fromTwitterTime t--instance FromJSON StreamingAPI where-    parseJSON v@(Object o) =-        SRetweetedStatus <$> js <|>-        SStatus <$> js <|>-        SEvent <$> js <|>-        SDelete <$> js <|>-        SFriends <$> (o .: "friends") <|>-        SDirectMessage <$> (o .: "direct_message") <|>-        return (SUnknown v)-      where-        js :: FromJSON a => Parser a-        js = parseJSON v-    parseJSON v = fail $ "couldn't parse StreamingAPI from: " ++ show v--instance ToJSON StreamingAPI where-    toJSON (SStatus          s) = toJSON s-    toJSON (SRetweetedStatus s) = toJSON s-    toJSON (SEvent           e) = toJSON e-    toJSON (SDelete          d) = toJSON d-    toJSON (SFriends         f) = toJSON f-    toJSON (SDirectMessage   m) = toJSON m-    toJSON (SUnknown         v) = v---- | This type represents a Twitter tweet structure.--- See <https://dev.twitter.com/docs/platform-objects/tweets>.-data Status = Status-    { statusContributors :: Maybe [Contributor]-    , statusCoordinates :: Maybe Coordinates-    , statusCreatedAt :: UTCTime-    , statusCurrentUserRetweet :: Maybe StatusId-    , statusEntities :: Maybe Entities-    , statusExtendedEntities :: Maybe ExtendedEntities-    , statusFavoriteCount :: Integer-    , statusFavorited :: Maybe Bool-    , statusFilterLevel :: Maybe Text-    , statusId :: StatusId-    , statusInReplyToScreenName :: Maybe Text-    , statusInReplyToStatusId :: Maybe StatusId-    , statusInReplyToUserId :: Maybe UserId-    , statusLang :: Maybe LanguageCode-    , statusPlace :: Maybe Place-    , statusPossiblySensitive :: Maybe Bool-    , statusScopes :: Maybe Object-    , statusQuotedStatusId :: Maybe StatusId-    , statusQuotedStatus :: Maybe Status-    , statusRetweetCount :: Integer-    , statusRetweeted :: Maybe Bool-    , statusRetweetedStatus :: Maybe Status-    , statusSource :: Text-    , statusText :: Text-    , statusTruncated :: Bool-    , statusUser :: User-    , statusWithheldCopyright :: Maybe Bool-    , statusWithheldInCountries :: Maybe [Text]-    , statusWithheldScope :: Maybe Text-    , statusDisplayTextRange :: Maybe DisplayTextRange-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON Status where-    parseJSON (Object o) = checkError o >>-        Status <$> o .:? "contributors" .!= Nothing-               <*> o .:? "coordinates" .!= Nothing-               <*> (o .:  "created_at" >>= return . fromTwitterTime)-               <*> ((o .: "current_user_retweet" >>= (.: "id")) <|> return Nothing)-               <*> o .:? "entities"-               <*> o .:? "extended_entities"-               <*> o .:? "favorite_count" .!= 0-               <*> o .:? "favorited"-               <*> o .:? "filter_level"-               <*> o .:  "id"-               <*> o .:? "in_reply_to_screen_name" .!= Nothing-               <*> o .:? "in_reply_to_status_id" .!= Nothing-               <*> o .:? "in_reply_to_user_id" .!= Nothing-               <*> o .:? "lang"-               <*> o .:? "place" .!= Nothing-               <*> o .:? "possibly_sensitive"-               <*> o .:? "scopes"-               <*> o .:? "quoted_status_id"-               <*> o .:? "quoted_status"-               <*> o .:? "retweet_count" .!= 0-               <*> o .:? "retweeted"-               <*> o .:? "retweeted_status"-               <*> o .:  "source"-               <*> (o .: "full_text" <|> o .: "text")-               <*> o .:  "truncated"-               <*> o .:  "user"-               <*> o .:? "withheld_copyright"-               <*> o .:? "withheld_in_countries"-               <*> o .:? "withheld_scope"-               <*> o .:? "display_text_range"-    parseJSON v = fail $ "couldn't parse status from: " ++ show v--instance ToJSON Status where-    toJSON Status{..} = object [ "contributors"             .= statusContributors-                               , "coordinates"              .= statusCoordinates-                               , "created_at"               .= TwitterTime statusCreatedAt-                               , "current_user_retweet"     .= object [ "id"     .= statusCurrentUserRetweet-                                                                      , "id_str" .= show statusCurrentUserRetweet-                                                                      ]-                               , "entities"                 .= statusEntities-                               , "extended_entities"        .= statusExtendedEntities-                               , "favorite_count"           .= statusFavoriteCount-                               , "favorited"                .= statusFavorited-                               , "filter_level"             .= statusFilterLevel-                               , "id"                       .= statusId-                               , "in_reply_to_screen_name"  .= statusInReplyToScreenName-                               , "in_reply_to_status_id"    .= statusInReplyToStatusId-                               , "in_reply_to_user_id"      .= statusInReplyToUserId-                               , "lang"                     .= statusLang-                               , "place"                    .= statusPlace-                               , "possibly_sensitive"       .= statusPossiblySensitive-                               , "scopes"                   .= statusScopes-                               , "quoted_status_id"         .= statusQuotedStatusId-                               , "quoted_status"            .= statusQuotedStatus-                               , "retweet_count"            .= statusRetweetCount-                               , "retweeted"                .= statusRetweeted-                               , "retweeted_status"         .= statusRetweetedStatus-                               , "source"                   .= statusSource-                               , "text"                     .= statusText-                               , "truncated"                .= statusTruncated-                               , "user"                     .= statusUser-                               , "withheld_copyright"       .= statusWithheldCopyright-                               , "withheld_in_countries"    .= statusWithheldInCountries-                               , "withheld_scope"           .= statusWithheldScope-                               , "display_text_range"       .= statusDisplayTextRange-                               ]--data SearchResult body =-    SearchResult-    { searchResultStatuses :: body-    , searchResultSearchMetadata :: SearchMetadata-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON body =>-         FromJSON (SearchResult body) where-    parseJSON (Object o) = checkError o >>-        SearchResult <$> o .:  "statuses"-                     <*> o .:  "search_metadata"-    parseJSON v = fail $ "couldn't parse search result from: " ++ show v--instance ToJSON body =>-         ToJSON (SearchResult body) where-    toJSON SearchResult{..} = object [ "statuses"        .= searchResultStatuses-                                     , "search_metadata" .= searchResultSearchMetadata-                                     ]--data SearchStatus =-    SearchStatus-    { searchStatusCreatedAt     :: UTCTime-    , searchStatusId            :: StatusId-    , searchStatusText          :: Text-    , searchStatusSource        :: Text-    , searchStatusUser          :: User-    , searchStatusCoordinates   :: Maybe Coordinates-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON SearchStatus where-    parseJSON (Object o) = checkError o >>-        SearchStatus <$> (o .:  "created_at" >>= return . fromTwitterTime)-                     <*> o .:  "id"-                     <*> o .:  "text"-                     <*> o .:  "source"-                     <*> o .:  "user"-                     <*> o .:? "coordinates"-    parseJSON v = fail $ "couldn't parse status search result from: " ++ show v--instance ToJSON SearchStatus where-    toJSON SearchStatus{..} = object [ "created_at"     .= TwitterTime searchStatusCreatedAt-                                     , "id"             .= searchStatusId-                                     , "text"           .= searchStatusText-                                     , "source"         .= searchStatusSource-                                     , "user"           .= searchStatusUser-                                     , "coordinates"    .= searchStatusCoordinates-                                     ]--data SearchMetadata =-    SearchMetadata-    { searchMetadataMaxId         :: StatusId-    , searchMetadataSinceId       :: StatusId-    , searchMetadataRefreshURL    :: URIString-    , searchMetadataNextResults   :: Maybe URIString-    , searchMetadataCount         :: Int-    , searchMetadataCompletedIn   :: Maybe Float-    , searchMetadataSinceIdStr    :: String-    , searchMetadataQuery         :: String-    , searchMetadataMaxIdStr      :: String-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON SearchMetadata where-    parseJSON (Object o) = checkError o >>-        SearchMetadata <$> o .:  "max_id"-                       <*> o .:  "since_id"-                       <*> o .:  "refresh_url"-                       <*> o .:? "next_results"-                       <*> o .:  "count"-                       <*> o .:? "completed_in"-                       <*> o .:  "since_id_str"-                       <*> o .:  "query"-                       <*> o .:  "max_id_str"-    parseJSON v = fail $ "couldn't parse search metadata from: " ++ show v--instance ToJSON SearchMetadata where-    toJSON SearchMetadata{..} = object [ "max_id"       .= searchMetadataMaxId-                                       , "since_id"     .= searchMetadataSinceId-                                       , "refresh_url"  .= searchMetadataRefreshURL-                                       , "next_results" .= searchMetadataNextResults-                                       , "count"        .= searchMetadataCount-                                       , "completed_in" .= searchMetadataCompletedIn-                                       , "since_id_str" .= searchMetadataSinceIdStr-                                       , "query"        .= searchMetadataQuery-                                       , "max_id_str"   .= searchMetadataMaxIdStr-                                       ]--data RetweetedStatus =-    RetweetedStatus-    { rsCreatedAt       :: UTCTime-    , rsId              :: StatusId-    , rsText            :: Text-    , rsSource          :: Text-    , rsTruncated       :: Bool-    , rsEntities        :: Maybe Entities-    , rsUser            :: User-    , rsRetweetedStatus :: Status-    , rsCoordinates     :: Maybe Coordinates-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON RetweetedStatus where-    parseJSON (Object o) = checkError o >>-        RetweetedStatus <$> (o .:  "created_at" >>= return . fromTwitterTime)-                        <*> o .:  "id"-                        <*> o .:  "text"-                        <*> o .:  "source"-                        <*> o .:  "truncated"-                        <*> o .:? "entities"-                        <*> o .:  "user"-                        <*> o .:  "retweeted_status"-                        <*> o .:? "coordinates"-    parseJSON v = fail $ "couldn't parse retweeted status from: " ++ show v--instance ToJSON RetweetedStatus where-    toJSON RetweetedStatus{..} = object [ "created_at"          .= TwitterTime rsCreatedAt-                                        , "id"                  .= rsId-                                        , "text"                .= rsText-                                        , "source"              .= rsSource-                                        , "truncated"           .= rsTruncated-                                        , "entities"            .= rsEntities-                                        , "user"                .= rsUser-                                        , "retweeted_status"    .= rsRetweetedStatus-                                        , "coordinates"         .= rsCoordinates-                                        ]--type EventId = Integer--data DirectMessage = DirectMessage-    { dmId :: EventId-    , dmCreatedTimestamp :: UTCTime-    , dmTargetRecipientId :: UserId-    , dmSenderId :: UserId-    , dmText :: Text-    , dmEntities :: Entities-    } deriving (Show, Eq, Data, Typeable, Generic)--parseIntegral :: Integral a => Text -> Parser a-parseIntegral v = either (\_ -> fail $ "couldn't parse stringized int: " ++ show v) (return . fst) $ decimal v--epochMsToUTCTime :: Int64 -> UTCTime-epochMsToUTCTime = posixSecondsToUTCTime . fromRational . (% 1000) . fromIntegral--parseUnixTimeString :: Text -> Parser UTCTime-parseUnixTimeString = fmap epochMsToUTCTime <$> parseIntegral--unixTimeToEpochInt :: UTCTime -> Int-unixTimeToEpochInt = floor . (* 1000) . utcTimeToPOSIXSeconds--instance FromJSON DirectMessage where-    parseJSON (Object o) = do-        _ <- checkError o-        messageCreate <- o .: "message_create"-        messageData <- messageCreate .: "message_data"--        DirectMessage-            <$> (o .: "id" >>= parseIntegral)-            <*> (o .: "created_timestamp" >>= parseUnixTimeString)-            <*> (messageCreate .: "target" >>= (.: "recipient_id") >>= parseIntegral)-            <*> (messageCreate .: "sender_id" >>= parseIntegral)-            <*> messageData .: "text"-            <*> messageData .: "entities"-    parseJSON v = fail $ "couldn't parse direct message create event from: " ++ show v--instance ToJSON DirectMessage where-    toJSON DirectMessage {..} =-        object-            [ "id" .= show dmId-            , "created_timestamp" .= show (unixTimeToEpochInt dmCreatedTimestamp)-            , "message_create" .= object ["message_data" .= object ["text" .= dmText, "entities" .= dmEntities]-                                         , "target" .= object ["recipient_id" .= show dmTargetRecipientId]-                                         , "sender_id" .= show dmSenderId-                                         ]-            ]--data EventType = Favorite | Unfavorite-               | ListCreated | ListUpdated | ListMemberAdded-               | UserUpdate | Block | Unblock | Follow-               deriving (Show, Eq, Data, Typeable, Generic)--data EventTarget = ETUser User | ETStatus Status | ETList List | ETUnknown Value-                 deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON EventTarget where-    parseJSON v@(Object o) = checkError o >>-        ETUser <$> parseJSON v <|>-        ETStatus <$> parseJSON v <|>-        ETList <$> parseJSON v <|>-        return (ETUnknown v)-    parseJSON v = fail $ "couldn't parse event target from: " ++ show v--instance ToJSON EventTarget where-    toJSON (ETUser    u) = toJSON u-    toJSON (ETStatus  s) = toJSON s-    toJSON (ETList    l) = toJSON l-    toJSON (ETUnknown v) = v--data Event =-    Event-    { evCreatedAt       :: UTCTime-    , evTargetObject    :: Maybe EventTarget-    , evEvent           :: Text-    , evTarget          :: EventTarget-    , evSource          :: EventTarget-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON Event where-    parseJSON (Object o) = checkError o >>-        Event <$> (o .:  "created_at" >>= return . fromTwitterTime)-              <*> o .:? "target_object"-              <*> o .:  "event"-              <*> o .:  "target"-              <*> o .:  "source"-    parseJSON v = fail $ "couldn't parse event from: " ++ show v--instance ToJSON Event where-    toJSON Event{..} = object [ "created_at"    .= TwitterTime evCreatedAt-                              , "target_object" .= evTargetObject-                              , "event"         .= evEvent-                              , "target"        .= evTarget-                              , "source"        .= evSource-                              ]--data Delete =-    Delete-    { delId  :: StatusId-    , delUserId :: UserId-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON Delete where-    parseJSON (Object o) = checkError o >> do-        s <- o .: "delete" >>= (.: "status")-        Delete <$> s .: "id"-               <*> s .: "user_id"-    parseJSON v = fail $ "couldn't parse delete from: " ++ show v--instance ToJSON Delete where-    toJSON Delete{..} = object [ "delete" .= object [ "status" .= object [ "id"      .= delId-                                                                         , "user_id" .= delUserId-                                                                         ]-                                                    ]-                               ]---- | This type represents the Twitter user.--- See <https://dev.twitter.com/docs/platform-objects/users>.-data User = User-    { userContributorsEnabled :: Bool-    , userCreatedAt :: UTCTime-    , userDefaultProfile :: Bool-    , userDefaultProfileImage :: Bool-    , userDescription :: Maybe Text-    , userEmail :: Maybe Text-    , userFavoritesCount :: Int-    , userFollowRequestSent :: Maybe Bool-    , userFollowing :: Maybe Bool-    , userFollowersCount :: Int-    , userFriendsCount :: Int-    , userGeoEnabled :: Bool-    , userId :: UserId-    , userIsTranslator :: Bool-    , userLang :: Maybe LanguageCode-    , userListedCount :: Int-    , userLocation :: Maybe Text-    , userName :: Text-    , userNotifications :: Maybe Bool-    , userProfileBackgroundColor :: Maybe Text-    , userProfileBackgroundImageURL :: Maybe URIString-    , userProfileBackgroundImageURLHttps :: Maybe URIString-    , userProfileBackgroundTile :: Maybe Bool-    , userProfileBannerURL :: Maybe URIString-    , userProfileImageURL :: Maybe URIString-    , userProfileImageURLHttps :: Maybe URIString-    , userProfileLinkColor :: Text-    , userProfileSidebarBorderColor :: Text-    , userProfileSidebarFillColor :: Text-    , userProfileTextColor :: Text-    , userProfileUseBackgroundImage :: Bool-    , userProtected :: Bool-    , userScreenName :: Text-    , userShowAllInlineMedia :: Maybe Bool-    , userStatusesCount :: Int-    , userTimeZone :: Maybe Text-    , userURL :: Maybe URIString-    , userUtcOffset :: Maybe Int-    , userVerified :: Bool-    , userWithheldInCountries :: Maybe [Text]-    , userWithheldScope :: Maybe Text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON User where-    parseJSON (Object o) = checkError o >>-        User <$> o .:  "contributors_enabled"-             <*> (o .:  "created_at" >>= return . fromTwitterTime)-             <*> o .:  "default_profile"-             <*> o .:  "default_profile_image"-             <*> o .:? "description"-             <*> fmap join (o .:? "email") -- The field can be a null value-             <*> o .:  "favourites_count"-             <*> o .:? "follow_request_sent" .!= Nothing-             <*> o .:? "following" .!= Nothing-             <*> o .:  "followers_count"-             <*> o .:  "friends_count"-             <*> o .:  "geo_enabled"-             <*> o .:  "id"-             <*> o .:  "is_translator"-             <*> o .:  "lang"-             <*> o .:  "listed_count"-             <*> o .:? "location"-             <*> o .:  "name"-             <*> o .:? "notifications" .!= Nothing-             <*> o .:? "profile_background_color"-             <*> o .:? "profile_background_image_url"-             <*> o .:? "profile_background_image_url_https"-             <*> o .:? "profile_background_tile"-             <*> o .:? "profile_banner_url"-             <*> o .:? "profile_image_url"-             <*> o .:? "profile_image_url_https"-             <*> o .:  "profile_link_color"-             <*> o .:  "profile_sidebar_border_color"-             <*> o .:  "profile_sidebar_fill_color"-             <*> o .:  "profile_text_color"-             <*> o .:  "profile_use_background_image"-             <*> o .:  "protected"-             <*> o .:  "screen_name"-             <*> o .:? "show_all_inline_media"-             <*> o .:  "statuses_count"-             <*> o .:? "time_zone"-             <*> o .:? "url" .!= Nothing-             <*> o .:? "utc_offset"-             <*> o .:  "verified"-             <*> o .:? "withheld_in_countries"-             <*> o .:? "withheld_scope"-    parseJSON v = fail $ "couldn't parse user from: " ++ show v--instance ToJSON User where-    toJSON User{..} = object [ "contributors_enabled"               .= userContributorsEnabled-                             , "created_at"                         .= TwitterTime userCreatedAt-                             , "default_profile"                    .= userDefaultProfile-                             , "default_profile_image"              .= userDefaultProfileImage-                             , "description"                        .= userDescription-                             , "email"                              .= userEmail-                             , "favourites_count"                   .= userFavoritesCount-                             , "follow_request_sent"                .= userFollowRequestSent-                             , "following"                          .= userFollowing-                             , "followers_count"                    .= userFollowersCount-                             , "friends_count"                      .= userFriendsCount-                             , "geo_enabled"                        .= userGeoEnabled-                             , "id"                                 .= userId-                             , "is_translator"                      .= userIsTranslator-                             , "lang"                               .= userLang-                             , "listed_count"                       .= userListedCount-                             , "location"                           .= userLocation-                             , "name"                               .= userName-                             , "notifications"                      .= userNotifications-                             , "profile_background_color"           .= userProfileBackgroundColor-                             , "profile_background_image_url"       .= userProfileBackgroundImageURL-                             , "profile_background_image_url_https" .= userProfileBackgroundImageURLHttps-                             , "profile_background_tile"            .= userProfileBackgroundTile-                             , "profile_banner_url"                 .= userProfileBannerURL-                             , "profile_image_url"                  .= userProfileImageURL-                             , "profile_image_url_https"            .= userProfileImageURLHttps-                             , "profile_link_color"                 .= userProfileLinkColor-                             , "profile_sidebar_border_color"       .= userProfileSidebarBorderColor-                             , "profile_sidebar_fill_color"         .= userProfileSidebarFillColor-                             , "profile_text_color"                 .= userProfileTextColor-                             , "profile_use_background_image"       .= userProfileUseBackgroundImage-                             , "protected"                          .= userProtected-                             , "screen_name"                        .= userScreenName-                             , "show_all_inline_media"              .= userShowAllInlineMedia-                             , "statuses_count"                     .= userStatusesCount-                             , "time_zone"                          .= userTimeZone-                             , "url"                                .= userURL-                             , "utc_offset"                         .= userUtcOffset-                             , "verified"                           .= userVerified-                             , "withheld_in_countries"              .= userWithheldInCountries-                             , "withheld_scope"                     .= userWithheldScope-                             ]--data List =-    List-    { listId :: Int-    , listName :: Text-    , listFullName :: Text-    , listMemberCount :: Int-    , listSubscriberCount :: Int-    , listMode :: Text-    , listUser :: User-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON List where-    parseJSON (Object o) = checkError o >>-        List <$> o .:  "id"-             <*> o .:  "name"-             <*> o .:  "full_name"-             <*> o .:  "member_count"-             <*> o .:  "subscriber_count"-             <*> o .:  "mode"-             <*> o .:  "user"-    parseJSON v = fail $ "couldn't parse List from: " ++ show v--instance ToJSON List where-    toJSON List{..} = object [ "id"                 .= listId-                             , "name"               .= listName-                             , "full_name"          .= listFullName-                             , "member_count"       .= listMemberCount-                             , "subscriber_count"   .= listSubscriberCount-                             , "mode"               .= listMode-                             , "user"               .= listUser-                             ]---- | Hashtag entity.--- See <https://dev.twitter.com/docs/platform-objects/entities#obj-hashtags>.-data HashTagEntity =-    HashTagEntity-    { hashTagText :: Text -- ^ The Hashtag text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON HashTagEntity where-    parseJSON (Object o) =-        HashTagEntity <$> o .: "text"-    parseJSON v = fail $ "couldn't parse hashtag entity from: " ++ show v--instance ToJSON HashTagEntity where-    toJSON HashTagEntity{..} = object [ "text" .= hashTagText ]---- | User mention entity.--- See <https://dev.twitter.com/docs/platform-objects/entities#obj-usermention>.-data UserEntity =-    UserEntity-    { userEntityUserId              :: UserId-    , userEntityUserName            :: UserName-    , userEntityUserScreenName      :: Text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON UserEntity where-    parseJSON (Object o) =-        UserEntity <$> o .:  "id"-                   <*> o .:  "name"-                   <*> o .:  "screen_name"-    parseJSON v = fail $ "couldn't parse user entity from: " ++ show v--instance ToJSON UserEntity where-    toJSON UserEntity{..} = object [ "id"           .= userEntityUserId-                                   , "name"         .= userEntityUserName-                                   , "screen_name"  .= userEntityUserScreenName-                                   ]---- | URL entity.--- See <https://dev.twitter.com/docs/platform-objects/entities#obj-url>.-data URLEntity =-    URLEntity-    { ueURL      :: URIString -- ^ The URL that was extracted-    , ueExpanded :: URIString -- ^ The fully resolved URL (only for t.co links)-    , ueDisplay  :: Text    -- ^ Not a URL but a string to display instead of the URL (only for t.co links)-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON URLEntity where-    parseJSON (Object o) =-        URLEntity <$> o .:  "url"-                  <*> o .:  "expanded_url"-                  <*> o .:  "display_url"-    parseJSON v = fail $ "couldn't parse url entity from: " ++ show v--instance ToJSON URLEntity where-    toJSON URLEntity{..} = object [ "url"           .= ueURL-                                  , "expanded_url"  .= ueExpanded-                                  , "display_url"   .= ueDisplay-                                  ]--data MediaEntity =-    MediaEntity-    { meType :: Text-    , meId :: StatusId-    , meSizes :: HashMap Text MediaSize-    , meMediaURL :: URIString-    , meMediaURLHttps :: URIString-    , meURL :: URLEntity-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON MediaEntity where-    parseJSON v@(Object o) =-        MediaEntity <$> o .: "type"-                    <*> o .: "id"-                    <*> o .: "sizes"-                    <*> o .: "media_url"-                    <*> o .: "media_url_https"-                    <*> parseJSON v-    parseJSON v = fail $ "couldn't parse media entity from: " ++ show v--instance ToJSON MediaEntity where-    toJSON MediaEntity{..} = object [ "type"            .= meType-                                    , "id"              .= meId-                                    , "sizes"           .= meSizes-                                    , "media_url"       .= meMediaURL-                                    , "media_url_https" .= meMediaURLHttps-                                    , "url"             .= ueURL meURL-                                    , "expanded_url"    .= ueExpanded meURL-                                    , "display_url"     .= ueDisplay meURL-                                    ]---- | Size entity.--- See <https://dev.twitter.com/docs/platform-objects/entities#obj-size>.-data MediaSize =-    MediaSize-    { msWidth :: Int-    , msHeight :: Int-    , msResize :: Text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON MediaSize where-    parseJSON (Object o) =-        MediaSize <$> o .: "w"-                  <*> o .: "h"-                  <*> o .: "resize"-    parseJSON v = fail $ "couldn't parse media size from: " ++ show v--instance ToJSON MediaSize where-    toJSON MediaSize{..} = object [ "w"      .= msWidth-                                  , "h"      .= msHeight-                                  , "resize" .= msResize-                                  ]--data Coordinates =-    Coordinates-    { coordinates :: [Double]-    , coordinatesType :: Text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON Coordinates where-    parseJSON (Object o) =-        Coordinates <$> o .: "coordinates"-                    <*> o .: "type"-    parseJSON v = fail $ "couldn't parse coordinates from: " ++ show v--instance ToJSON Coordinates where-    toJSON Coordinates{..} = object [ "coordinates" .= coordinates-                                    , "type"        .= coordinatesType-                                    ]---- | This type represents a place, named locations with corresponding geo coordinates.--- See <https://dev.twitter.com/docs/platform-objects/places>.-data Place =-    Place-    { placeAttributes   :: HashMap Text Text-    , placeBoundingBox  :: Maybe BoundingBox-    , placeCountry      :: Text-    , placeCountryCode  :: Text-    , placeFullName     :: Text-    , placeId           :: Text-    , placeName         :: Text-    , placeType         :: Text-    , placeURL          :: Text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON Place where-    parseJSON (Object o) =-        Place <$> o .: "attributes"-              <*> o .:? "bounding_box"-              <*> o .: "country"-              <*> o .: "country_code"-              <*> o .: "full_name"-              <*> o .: "id"-              <*> o .: "name"-              <*> o .: "place_type"-              <*> o .: "url"-    parseJSON v = fail $ "couldn't parse place from: " ++ show v--instance ToJSON Place where-    toJSON Place{..} = object [ "attributes"    .= placeAttributes-                              , "bounding_box"  .= placeBoundingBox-                              , "country"       .= placeCountry-                              , "country_code"  .= placeCountryCode-                              , "full_name"     .= placeFullName-                              , "id"            .= placeId-                              , "name"          .= placeName-                              , "place_type"    .= placeType-                              , "url"           .= placeURL-                              ]---- | A bounding box of coordinates which encloses the place.--- See <https://dev.twitter.com/docs/platform-objects/places#obj-boundingbox>.-data BoundingBox =-    BoundingBox-    { boundingBoxCoordinates  :: [[[Double]]]-    , boundingBoxType         :: Text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON BoundingBox where-    parseJSON (Object o) =-        BoundingBox <$> o .: "coordinates"-                    <*> o .: "type"-    parseJSON v = fail $ "couldn't parse bounding box from: " ++ show v--instance ToJSON BoundingBox where-    toJSON BoundingBox{..} = object [ "coordinates" .= boundingBoxCoordinates-                                    , "type"        .= boundingBoxType-                                    ]---- | Entity handling.--- See <https://dev.twitter.com/docs/platform-objects/entities>.-data Entities =-    Entities-    { enHashTags     :: [Entity HashTagEntity]-    , enUserMentions :: [Entity UserEntity]-    , enURLs         :: [Entity URLEntity]-    , enMedia        :: [Entity MediaEntity]-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON Entities where-    parseJSON (Object o) =-        Entities <$> o .:? "hashtags" .!= []-                 <*> o .:? "user_mentions" .!= []-                 <*> o .:? "urls" .!= []-                 <*> o .:? "media" .!= []-    parseJSON v = fail $ "couldn't parse entities from: " ++ show v--instance ToJSON Entities where-    toJSON Entities{..} = object [ "hashtags"       .= enHashTags-                                 , "user_mentions"  .= enUserMentions-                                 , "urls"           .= enURLs-                                 , "media"          .= enMedia-                                 ]----- | The character positions the Entity was extracted from------   This is experimental implementation.---   This may be replaced by more definite types.-type EntityIndices = [Int]--data Entity a =-    Entity-    { entityBody    :: a             -- ^ The detail information of the specific entity types (HashTag, URL, User)-    , entityIndices :: EntityIndices -- ^ The character positions the Entity was extracted from-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON a => FromJSON (Entity a) where-    parseJSON v@(Object o) =-        Entity <$> parseJSON v-               <*> o .: "indices"-    parseJSON v = fail $ "couldn't parse entity wrapper from: " ++ show v--instance ToJSON a => ToJSON (Entity a) where-    toJSON Entity{..} = case toJSON entityBody of-                            (Object o) -> Object $ union o $ fromList [("indices"::Text, toJSON entityIndices)]-                            _          -> error "Entity body must produce an object."--data ExtendedEntities =-  ExtendedEntities {-    exeMedia :: [Entity ExtendedEntity]-  } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON ExtendedEntities where-  parseJSON (Object o) =-    ExtendedEntities <$> o .:? "media" .!= []--  parseJSON v = fail $ "couldn't parse extended entity from: " ++ show v--instance ToJSON ExtendedEntities where-    toJSON ExtendedEntities{..} = object [ "media" .= exeMedia ]----- Extended entities are like entities, but contain special media features like--- video or multiple photos-data ExtendedEntity =-  ExtendedEntity-  {-    exeID :: StatusId,-    exeMediaUrl :: URIString,-    exeMediaUrlHttps :: URIString,-    exeSizes :: HashMap Text MediaSize,-    exeType :: Text,-    -- exeVideoInfo :: ???-    exeDurationMillis :: Maybe Double,-    -- exeVariants :: ??-    exeExtAltText :: Maybe String,-    exeURL :: URLEntity-  } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON ExtendedEntity where-  parseJSON v@(Object o) =-    ExtendedEntity <$> o .:  "id"-                   <*> o .:  "media_url"-                   <*> o .:  "media_url_https"-                   <*> o .:  "sizes"-                   <*> o .:  "type"-                   <*> o .:? "duration_millis"-                   <*> o .:?  "ext_alt_text"-                   <*> parseJSON v--  parseJSON v = fail $ "couldn't parse extended entity from:" ++ show v--instance ToJSON ExtendedEntity where-  toJSON ExtendedEntity{..} = object [ "id"                .= exeID-                                     , "media_url"       .= exeMediaUrl-                                     , "media_url_https" .= exeMediaUrlHttps-                                     , "sizes"           .= exeSizes-                                     , "type"            .= exeType-                                     , "duration_millis" .= exeDurationMillis-                                     , "ext_alt_text"    .= exeExtAltText-                                     , "url"             .= ueURL exeURL-                                     , "expanded_url"    .= ueExpanded exeURL-                                     , "display_url"     .= ueDisplay exeURL-                                     ]---data Contributor = Contributor-    { contributorId :: UserId-    , contributorScreenName :: Maybe Text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON Contributor where-    parseJSON (Object o) =-        Contributor <$>  o .:  "id"-                    <*>  o .:?  "screen_name"-    parseJSON v@(Number _) =-        Contributor <$> parseJSON v <*> pure Nothing-    parseJSON v = fail $ "couldn't parse contributor from: " ++ show v--instance ToJSON Contributor where-    toJSON Contributor{..} = object [ "id"          .= contributorId-                                    , "screen_name" .= contributorScreenName-                                    ]---- | Image size type. This type is included in the API response of \"\/1.1\/media\/upload.json\".-data ImageSizeType = ImageSizeType-    { imageSizeTypeWidth :: Int-    , imageSizeTypeHeight :: Int-    , imageSizeTypeType :: Text-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON ImageSizeType where-    parseJSON (Object o) =-        ImageSizeType <$> o .:  "w"-                      <*> o .:  "h"-                      <*> o .:  "image_type"-    parseJSON v = fail $ "unknown value: " ++ show v--instance ToJSON ImageSizeType where-    toJSON ImageSizeType{..} = object [ "w"          .= imageSizeTypeWidth-                                      , "h"          .= imageSizeTypeHeight-                                      , "image_type" .= imageSizeTypeType-                                      ]---- | This type is represents the API response of \"\/1.1\/media\/upload.json\".--- See <https://dev.twitter.com/docs/api/multiple-media-extended-entities>.-data UploadedMedia = UploadedMedia-    { uploadedMediaId :: Integer-    , uploadedMediaSize :: Integer-    , uploadedMediaImage :: ImageSizeType-    } deriving (Show, Eq, Data, Typeable, Generic)--instance FromJSON UploadedMedia where-    parseJSON (Object o) =-        UploadedMedia <$> o .:  "media_id"-                      <*> o .:  "size"-                      <*> o .:  "image"-    parseJSON v = fail $ "unknown value: " ++ show v--instance ToJSON UploadedMedia where-    toJSON UploadedMedia{..} = object [ "media_id"  .= uploadedMediaId-                                      , "size"      .= uploadedMediaSize-                                      , "image"     .= uploadedMediaImage-                                      ]---- | unicode code point indices, identifying the inclusive start and exclusive end of the displayable content of the Tweet.-data DisplayTextRange = DisplayTextRange-    { displayTextRangeStart :: Int-    , displayTextRangeEnd :: Int -- ^ exclusive-    } deriving (Show, Eq, Ord, Data, Typeable, Generic)+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Web.Twitter.Types (+    UserId,+    Friends,+    URIString,+    UserName,+    StatusId,+    LanguageCode,+    StreamingAPI (..),+    Status (..),+    SearchResult (..),+    SearchStatus (..),+    SearchMetadata (..),+    RetweetedStatus (..),+    DirectMessage (..),+    EventTarget (..),+    Event (..),+    Delete (..),+    User (..),+    List (..),+    Entities (..),+    EntityIndices,+    ExtendedEntities (..),+    Variant (..),+    VideoInfo (..),+    ExtendedEntity (..),+    Entity (..),+    HashTagEntity (..),+    UserEntity (..),+    URLEntity (..),+    MediaEntity (..),+    MediaSize (..),+    Coordinates (..),+    Place (..),+    BoundingBox (..),+    Contributor (..),+    UploadedMedia (..),+    ImageSizeType (..),+    DisplayTextRange (..),+    checkError,+    twitterTimeFormat,+) where++import Control.Applicative+import Control.Monad+import Data.Aeson+import Data.Aeson.Types (Parser)+import Data.Data+import Data.HashMap.Strict (HashMap)++#if MIN_VERSION_aeson(2, 0, 0)+import qualified Data.Aeson.KeyMap as KeyMap+#else+import qualified Data.HashMap.Strict as KeyMap+#endif+import Data.Int+import Data.Ratio+import Data.Text (Text, pack, unpack)+import Data.Text.Read (decimal)+import Data.Time+import Data.Time.Clock.POSIX+import GHC.Generics++newtype TwitterTime = TwitterTime {fromTwitterTime :: UTCTime}++type UserId = Integer+type Friends = [UserId]+type URIString = Text+type UserName = Text+type StatusId = Integer+type LanguageCode = String++data StreamingAPI+    = SStatus Status+    | SRetweetedStatus RetweetedStatus+    | SEvent Event+    | SDelete Delete+    | -- | SScrubGeo ScrubGeo+      SFriends Friends+    | SDirectMessage DirectMessage+    | SUnknown Value+    deriving (Show, Eq, Data, Typeable, Generic)++checkError :: Object -> Parser ()+checkError o = do+    err <- o .:? "error"+    case err of+        Just msg -> fail msg+        Nothing -> return ()++twitterTimeFormat :: String+twitterTimeFormat = "%a %b %d %T %z %Y"++instance FromJSON TwitterTime where+    parseJSON = withText "TwitterTime" $ \t ->+        case parseTimeM True defaultTimeLocale twitterTimeFormat (unpack t) of+            Just d -> pure $ TwitterTime d+            Nothing -> fail $ "Could not parse twitter time. Text was: " ++ unpack t++instance ToJSON TwitterTime where+    toJSON t = String $ pack $ formatTime defaultTimeLocale twitterTimeFormat $ fromTwitterTime t++instance FromJSON StreamingAPI where+    parseJSON v@(Object o) =+        SRetweetedStatus <$> js+            <|> SStatus <$> js+            <|> SEvent <$> js+            <|> SDelete <$> js+            <|> SFriends <$> (o .: "friends")+            <|> SDirectMessage <$> (o .: "direct_message")+            <|> return (SUnknown v)+      where+        js :: FromJSON a => Parser a+        js = parseJSON v+    parseJSON v = fail $ "couldn't parse StreamingAPI from: " ++ show v++instance ToJSON StreamingAPI where+    toJSON (SStatus s) = toJSON s+    toJSON (SRetweetedStatus s) = toJSON s+    toJSON (SEvent e) = toJSON e+    toJSON (SDelete d) = toJSON d+    toJSON (SFriends f) = toJSON f+    toJSON (SDirectMessage m) = toJSON m+    toJSON (SUnknown v) = v++-- | This type represents a Twitter tweet structure.+-- See <https://dev.twitter.com/docs/platform-objects/tweets>.+data Status = Status+    { statusContributors :: Maybe [Contributor]+    , statusCoordinates :: Maybe Coordinates+    , statusCreatedAt :: UTCTime+    , statusCurrentUserRetweet :: Maybe StatusId+    , statusEntities :: Maybe Entities+    , statusExtendedEntities :: Maybe ExtendedEntities+    , statusFavoriteCount :: Integer+    , statusFavorited :: Maybe Bool+    , statusFilterLevel :: Maybe Text+    , statusId :: StatusId+    , statusInReplyToScreenName :: Maybe Text+    , statusInReplyToStatusId :: Maybe StatusId+    , statusInReplyToUserId :: Maybe UserId+    , statusLang :: Maybe LanguageCode+    , statusPlace :: Maybe Place+    , statusPossiblySensitive :: Maybe Bool+    , statusScopes :: Maybe Object+    , statusQuotedStatusId :: Maybe StatusId+    , statusQuotedStatus :: Maybe Status+    , statusRetweetCount :: Integer+    , statusRetweeted :: Maybe Bool+    , statusRetweetedStatus :: Maybe Status+    , statusSource :: Text+    , statusText :: Text+    , statusTruncated :: Bool+    , statusUser :: User+    , statusWithheldCopyright :: Maybe Bool+    , statusWithheldInCountries :: Maybe [Text]+    , statusWithheldScope :: Maybe Text+    , statusDisplayTextRange :: Maybe DisplayTextRange+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Status where+    parseJSON (Object o) =+        checkError o+            >> Status <$> o .:? "contributors" .!= Nothing+            <*> o .:? "coordinates" .!= Nothing+            <*> (o .: "created_at" >>= return . fromTwitterTime)+            <*> ((o .: "current_user_retweet" >>= (.: "id")) <|> return Nothing)+            <*> o .:? "entities"+            <*> o .:? "extended_entities"+            <*> o .:? "favorite_count" .!= 0+            <*> o .:? "favorited"+            <*> o .:? "filter_level"+            <*> o .: "id"+            <*> o .:? "in_reply_to_screen_name" .!= Nothing+            <*> o .:? "in_reply_to_status_id" .!= Nothing+            <*> o .:? "in_reply_to_user_id" .!= Nothing+            <*> o .:? "lang"+            <*> o .:? "place" .!= Nothing+            <*> o .:? "possibly_sensitive"+            <*> o .:? "scopes"+            <*> o .:? "quoted_status_id"+            <*> o .:? "quoted_status"+            <*> o .:? "retweet_count" .!= 0+            <*> o .:? "retweeted"+            <*> o .:? "retweeted_status"+            <*> o .: "source"+            <*> (o .: "full_text" <|> o .: "text")+            <*> o .: "truncated"+            <*> o .: "user"+            <*> o .:? "withheld_copyright"+            <*> o .:? "withheld_in_countries"+            <*> o .:? "withheld_scope"+            <*> o .:? "display_text_range"+    parseJSON v = fail $ "couldn't parse status from: " ++ show v++instance ToJSON Status where+    toJSON Status {..} =+        object+            [ "contributors" .= statusContributors+            , "coordinates" .= statusCoordinates+            , "created_at" .= TwitterTime statusCreatedAt+            , "current_user_retweet"+                .= object+                    [ "id" .= statusCurrentUserRetweet+                    , "id_str" .= show statusCurrentUserRetweet+                    ]+            , "entities" .= statusEntities+            , "extended_entities" .= statusExtendedEntities+            , "favorite_count" .= statusFavoriteCount+            , "favorited" .= statusFavorited+            , "filter_level" .= statusFilterLevel+            , "id" .= statusId+            , "in_reply_to_screen_name" .= statusInReplyToScreenName+            , "in_reply_to_status_id" .= statusInReplyToStatusId+            , "in_reply_to_user_id" .= statusInReplyToUserId+            , "lang" .= statusLang+            , "place" .= statusPlace+            , "possibly_sensitive" .= statusPossiblySensitive+            , "scopes" .= statusScopes+            , "quoted_status_id" .= statusQuotedStatusId+            , "quoted_status" .= statusQuotedStatus+            , "retweet_count" .= statusRetweetCount+            , "retweeted" .= statusRetweeted+            , "retweeted_status" .= statusRetweetedStatus+            , "source" .= statusSource+            , "text" .= statusText+            , "truncated" .= statusTruncated+            , "user" .= statusUser+            , "withheld_copyright" .= statusWithheldCopyright+            , "withheld_in_countries" .= statusWithheldInCountries+            , "withheld_scope" .= statusWithheldScope+            , "display_text_range" .= statusDisplayTextRange+            ]++data SearchResult body = SearchResult+    { searchResultStatuses :: body+    , searchResultSearchMetadata :: SearchMetadata+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance+    FromJSON body =>+    FromJSON (SearchResult body)+    where+    parseJSON (Object o) =+        checkError o+            >> SearchResult <$> o .: "statuses"+            <*> o .: "search_metadata"+    parseJSON v = fail $ "couldn't parse search result from: " ++ show v++instance+    ToJSON body =>+    ToJSON (SearchResult body)+    where+    toJSON SearchResult {..} =+        object+            [ "statuses" .= searchResultStatuses+            , "search_metadata" .= searchResultSearchMetadata+            ]++data SearchStatus = SearchStatus+    { searchStatusCreatedAt :: UTCTime+    , searchStatusId :: StatusId+    , searchStatusText :: Text+    , searchStatusSource :: Text+    , searchStatusUser :: User+    , searchStatusCoordinates :: Maybe Coordinates+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON SearchStatus where+    parseJSON (Object o) =+        checkError o+            >> SearchStatus <$> (o .: "created_at" >>= return . fromTwitterTime)+            <*> o .: "id"+            <*> o .: "text"+            <*> o .: "source"+            <*> o .: "user"+            <*> o .:? "coordinates"+    parseJSON v = fail $ "couldn't parse status search result from: " ++ show v++instance ToJSON SearchStatus where+    toJSON SearchStatus {..} =+        object+            [ "created_at" .= TwitterTime searchStatusCreatedAt+            , "id" .= searchStatusId+            , "text" .= searchStatusText+            , "source" .= searchStatusSource+            , "user" .= searchStatusUser+            , "coordinates" .= searchStatusCoordinates+            ]++data SearchMetadata = SearchMetadata+    { searchMetadataMaxId :: StatusId+    , searchMetadataSinceId :: StatusId+    , searchMetadataRefreshURL :: URIString+    , searchMetadataNextResults :: Maybe URIString+    , searchMetadataCount :: Int+    , searchMetadataCompletedIn :: Maybe Float+    , searchMetadataSinceIdStr :: String+    , searchMetadataQuery :: String+    , searchMetadataMaxIdStr :: String+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON SearchMetadata where+    parseJSON (Object o) =+        checkError o+            >> SearchMetadata <$> o .: "max_id"+            <*> o .: "since_id"+            <*> o .: "refresh_url"+            <*> o .:? "next_results"+            <*> o .: "count"+            <*> o .:? "completed_in"+            <*> o .: "since_id_str"+            <*> o .: "query"+            <*> o .: "max_id_str"+    parseJSON v = fail $ "couldn't parse search metadata from: " ++ show v++instance ToJSON SearchMetadata where+    toJSON SearchMetadata {..} =+        object+            [ "max_id" .= searchMetadataMaxId+            , "since_id" .= searchMetadataSinceId+            , "refresh_url" .= searchMetadataRefreshURL+            , "next_results" .= searchMetadataNextResults+            , "count" .= searchMetadataCount+            , "completed_in" .= searchMetadataCompletedIn+            , "since_id_str" .= searchMetadataSinceIdStr+            , "query" .= searchMetadataQuery+            , "max_id_str" .= searchMetadataMaxIdStr+            ]++data RetweetedStatus = RetweetedStatus+    { rsCreatedAt :: UTCTime+    , rsId :: StatusId+    , rsText :: Text+    , rsSource :: Text+    , rsTruncated :: Bool+    , rsEntities :: Maybe Entities+    , rsUser :: User+    , rsRetweetedStatus :: Status+    , rsCoordinates :: Maybe Coordinates+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON RetweetedStatus where+    parseJSON (Object o) =+        checkError o+            >> RetweetedStatus <$> (o .: "created_at" >>= return . fromTwitterTime)+            <*> o .: "id"+            <*> o .: "text"+            <*> o .: "source"+            <*> o .: "truncated"+            <*> o .:? "entities"+            <*> o .: "user"+            <*> o .: "retweeted_status"+            <*> o .:? "coordinates"+    parseJSON v = fail $ "couldn't parse retweeted status from: " ++ show v++instance ToJSON RetweetedStatus where+    toJSON RetweetedStatus {..} =+        object+            [ "created_at" .= TwitterTime rsCreatedAt+            , "id" .= rsId+            , "text" .= rsText+            , "source" .= rsSource+            , "truncated" .= rsTruncated+            , "entities" .= rsEntities+            , "user" .= rsUser+            , "retweeted_status" .= rsRetweetedStatus+            , "coordinates" .= rsCoordinates+            ]++type EventId = Integer++data DirectMessage = DirectMessage+    { dmId :: EventId+    , dmCreatedTimestamp :: UTCTime+    , dmTargetRecipientId :: UserId+    , dmSenderId :: UserId+    , dmText :: Text+    , dmEntities :: Entities+    }+    deriving (Show, Eq, Data, Typeable, Generic)++parseIntegral :: Integral a => Text -> Parser a+parseIntegral v = either (\_ -> fail $ "couldn't parse stringized int: " ++ show v) (return . fst) $ decimal v++epochMsToUTCTime :: Int64 -> UTCTime+epochMsToUTCTime = posixSecondsToUTCTime . fromRational . (% 1000) . fromIntegral++parseUnixTimeString :: Text -> Parser UTCTime+parseUnixTimeString = fmap epochMsToUTCTime <$> parseIntegral++unixTimeToEpochInt :: UTCTime -> Int+unixTimeToEpochInt = floor . (* 1000) . utcTimeToPOSIXSeconds++instance FromJSON DirectMessage where+    parseJSON (Object o) = do+        _ <- checkError o+        messageCreate <- o .: "message_create"+        messageData <- messageCreate .: "message_data"++        DirectMessage+            <$> (o .: "id" >>= parseIntegral)+            <*> (o .: "created_timestamp" >>= parseUnixTimeString)+            <*> (messageCreate .: "target" >>= (.: "recipient_id") >>= parseIntegral)+            <*> (messageCreate .: "sender_id" >>= parseIntegral)+            <*> messageData .: "text"+            <*> messageData .: "entities"+    parseJSON v = fail $ "couldn't parse direct message create event from: " ++ show v++instance ToJSON DirectMessage where+    toJSON DirectMessage {..} =+        object+            [ "id" .= show dmId+            , "created_timestamp" .= show (unixTimeToEpochInt dmCreatedTimestamp)+            , "message_create"+                .= object+                    [ "message_data" .= object ["text" .= dmText, "entities" .= dmEntities]+                    , "target" .= object ["recipient_id" .= show dmTargetRecipientId]+                    , "sender_id" .= show dmSenderId+                    ]+            ]++data EventType+    = Favorite+    | Unfavorite+    | ListCreated+    | ListUpdated+    | ListMemberAdded+    | UserUpdate+    | Block+    | Unblock+    | Follow+    deriving (Show, Eq, Data, Typeable, Generic)++data EventTarget = ETUser User | ETStatus Status | ETList List | ETUnknown Value+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON EventTarget where+    parseJSON v@(Object o) =+        checkError o+            >> ETUser <$> parseJSON v+            <|> ETStatus <$> parseJSON v+            <|> ETList <$> parseJSON v+            <|> return (ETUnknown v)+    parseJSON v = fail $ "couldn't parse event target from: " ++ show v++instance ToJSON EventTarget where+    toJSON (ETUser u) = toJSON u+    toJSON (ETStatus s) = toJSON s+    toJSON (ETList l) = toJSON l+    toJSON (ETUnknown v) = v++data Event = Event+    { evCreatedAt :: UTCTime+    , evTargetObject :: Maybe EventTarget+    , evEvent :: Text+    , evTarget :: EventTarget+    , evSource :: EventTarget+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Event where+    parseJSON (Object o) =+        checkError o+            >> Event <$> (o .: "created_at" >>= return . fromTwitterTime)+            <*> o .:? "target_object"+            <*> o .: "event"+            <*> o .: "target"+            <*> o .: "source"+    parseJSON v = fail $ "couldn't parse event from: " ++ show v++instance ToJSON Event where+    toJSON Event {..} =+        object+            [ "created_at" .= TwitterTime evCreatedAt+            , "target_object" .= evTargetObject+            , "event" .= evEvent+            , "target" .= evTarget+            , "source" .= evSource+            ]++data Delete = Delete+    { delId :: StatusId+    , delUserId :: UserId+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Delete where+    parseJSON (Object o) =+        checkError o >> do+            s <- o .: "delete" >>= (.: "status")+            Delete <$> s .: "id"+                <*> s .: "user_id"+    parseJSON v = fail $ "couldn't parse delete from: " ++ show v++instance ToJSON Delete where+    toJSON Delete {..} =+        object+            [ "delete"+                .= object+                    [ "status"+                        .= object+                            [ "id" .= delId+                            , "user_id" .= delUserId+                            ]+                    ]+            ]++-- | This type represents the Twitter user.+-- See <https://dev.twitter.com/docs/platform-objects/users>.+data User = User+    { userContributorsEnabled :: Bool+    , userCreatedAt :: UTCTime+    , userDefaultProfile :: Bool+    , userDefaultProfileImage :: Bool+    , userDescription :: Maybe Text+    , userEmail :: Maybe Text+    , userFavoritesCount :: Int+    , userFollowRequestSent :: Maybe Bool+    , userFollowing :: Maybe Bool+    , userFollowersCount :: Int+    , userFriendsCount :: Int+    , userGeoEnabled :: Bool+    , userId :: UserId+    , userIsTranslator :: Bool+    , userLang :: Maybe LanguageCode+    , userListedCount :: Int+    , userLocation :: Maybe Text+    , userName :: Text+    , userNotifications :: Maybe Bool+    , userProfileBackgroundColor :: Maybe Text+    , userProfileBackgroundImageURL :: Maybe URIString+    , userProfileBackgroundImageURLHttps :: Maybe URIString+    , userProfileBackgroundTile :: Maybe Bool+    , userProfileBannerURL :: Maybe URIString+    , userProfileImageURL :: Maybe URIString+    , userProfileImageURLHttps :: Maybe URIString+    , userProfileLinkColor :: Text+    , userProfileSidebarBorderColor :: Text+    , userProfileSidebarFillColor :: Text+    , userProfileTextColor :: Text+    , userProfileUseBackgroundImage :: Bool+    , userProtected :: Bool+    , userScreenName :: Text+    , userShowAllInlineMedia :: Maybe Bool+    , userStatusesCount :: Int+    , userTimeZone :: Maybe Text+    , userURL :: Maybe URIString+    , userUtcOffset :: Maybe Int+    , userVerified :: Bool+    , userWithheldInCountries :: Maybe [Text]+    , userWithheldScope :: Maybe Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON User where+    parseJSON (Object o) =+        checkError o+            >> User <$> o .: "contributors_enabled"+            <*> (o .: "created_at" >>= return . fromTwitterTime)+            <*> o .: "default_profile"+            <*> o .: "default_profile_image"+            <*> o .:? "description"+            <*> fmap join (o .:? "email") -- The field can be a null value+            <*> o .: "favourites_count"+            <*> o .:? "follow_request_sent" .!= Nothing+            <*> o .:? "following" .!= Nothing+            <*> o .: "followers_count"+            <*> o .: "friends_count"+            <*> o .: "geo_enabled"+            <*> o .: "id"+            <*> o .: "is_translator"+            <*> o .: "lang"+            <*> o .: "listed_count"+            <*> o .:? "location"+            <*> o .: "name"+            <*> o .:? "notifications" .!= Nothing+            <*> o .:? "profile_background_color"+            <*> o .:? "profile_background_image_url"+            <*> o .:? "profile_background_image_url_https"+            <*> o .:? "profile_background_tile"+            <*> o .:? "profile_banner_url"+            <*> o .:? "profile_image_url"+            <*> o .:? "profile_image_url_https"+            <*> o .: "profile_link_color"+            <*> o .: "profile_sidebar_border_color"+            <*> o .: "profile_sidebar_fill_color"+            <*> o .: "profile_text_color"+            <*> o .: "profile_use_background_image"+            <*> o .: "protected"+            <*> o .: "screen_name"+            <*> o .:? "show_all_inline_media"+            <*> o .: "statuses_count"+            <*> o .:? "time_zone"+            <*> o .:? "url" .!= Nothing+            <*> o .:? "utc_offset"+            <*> o .: "verified"+            <*> o .:? "withheld_in_countries"+            <*> o .:? "withheld_scope"+    parseJSON v = fail $ "couldn't parse user from: " ++ show v++instance ToJSON User where+    toJSON User {..} =+        object+            [ "contributors_enabled" .= userContributorsEnabled+            , "created_at" .= TwitterTime userCreatedAt+            , "default_profile" .= userDefaultProfile+            , "default_profile_image" .= userDefaultProfileImage+            , "description" .= userDescription+            , "email" .= userEmail+            , "favourites_count" .= userFavoritesCount+            , "follow_request_sent" .= userFollowRequestSent+            , "following" .= userFollowing+            , "followers_count" .= userFollowersCount+            , "friends_count" .= userFriendsCount+            , "geo_enabled" .= userGeoEnabled+            , "id" .= userId+            , "is_translator" .= userIsTranslator+            , "lang" .= userLang+            , "listed_count" .= userListedCount+            , "location" .= userLocation+            , "name" .= userName+            , "notifications" .= userNotifications+            , "profile_background_color" .= userProfileBackgroundColor+            , "profile_background_image_url" .= userProfileBackgroundImageURL+            , "profile_background_image_url_https" .= userProfileBackgroundImageURLHttps+            , "profile_background_tile" .= userProfileBackgroundTile+            , "profile_banner_url" .= userProfileBannerURL+            , "profile_image_url" .= userProfileImageURL+            , "profile_image_url_https" .= userProfileImageURLHttps+            , "profile_link_color" .= userProfileLinkColor+            , "profile_sidebar_border_color" .= userProfileSidebarBorderColor+            , "profile_sidebar_fill_color" .= userProfileSidebarFillColor+            , "profile_text_color" .= userProfileTextColor+            , "profile_use_background_image" .= userProfileUseBackgroundImage+            , "protected" .= userProtected+            , "screen_name" .= userScreenName+            , "show_all_inline_media" .= userShowAllInlineMedia+            , "statuses_count" .= userStatusesCount+            , "time_zone" .= userTimeZone+            , "url" .= userURL+            , "utc_offset" .= userUtcOffset+            , "verified" .= userVerified+            , "withheld_in_countries" .= userWithheldInCountries+            , "withheld_scope" .= userWithheldScope+            ]++data List = List+    { listId :: Int+    , listName :: Text+    , listFullName :: Text+    , listMemberCount :: Int+    , listSubscriberCount :: Int+    , listMode :: Text+    , listUser :: User+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON List where+    parseJSON (Object o) =+        checkError o+            >> List <$> o .: "id"+            <*> o .: "name"+            <*> o .: "full_name"+            <*> o .: "member_count"+            <*> o .: "subscriber_count"+            <*> o .: "mode"+            <*> o .: "user"+    parseJSON v = fail $ "couldn't parse List from: " ++ show v++instance ToJSON List where+    toJSON List {..} =+        object+            [ "id" .= listId+            , "name" .= listName+            , "full_name" .= listFullName+            , "member_count" .= listMemberCount+            , "subscriber_count" .= listSubscriberCount+            , "mode" .= listMode+            , "user" .= listUser+            ]++-- | Hashtag entity.+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-hashtags>.+data HashTagEntity = HashTagEntity+    { -- | The Hashtag text+      hashTagText :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON HashTagEntity where+    parseJSON (Object o) =+        HashTagEntity <$> o .: "text"+    parseJSON v = fail $ "couldn't parse hashtag entity from: " ++ show v++instance ToJSON HashTagEntity where+    toJSON HashTagEntity {..} = object ["text" .= hashTagText]++-- | User mention entity.+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-usermention>.+data UserEntity = UserEntity+    { userEntityUserId :: UserId+    , userEntityUserName :: UserName+    , userEntityUserScreenName :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON UserEntity where+    parseJSON (Object o) =+        UserEntity <$> o .: "id"+            <*> o .: "name"+            <*> o .: "screen_name"+    parseJSON v = fail $ "couldn't parse user entity from: " ++ show v++instance ToJSON UserEntity where+    toJSON UserEntity {..} =+        object+            [ "id" .= userEntityUserId+            , "name" .= userEntityUserName+            , "screen_name" .= userEntityUserScreenName+            ]++-- | URL entity.+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-url>.+data URLEntity = URLEntity+    { -- | The URL that was extracted+      ueURL :: URIString+    , -- | The fully resolved URL (only for t.co links)+      ueExpanded :: URIString+    , -- | Not a URL but a string to display instead of the URL (only for t.co links)+      ueDisplay :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON URLEntity where+    parseJSON (Object o) =+        URLEntity <$> o .: "url"+            <*> o .: "expanded_url"+            <*> o .: "display_url"+    parseJSON v = fail $ "couldn't parse url entity from: " ++ show v++instance ToJSON URLEntity where+    toJSON URLEntity {..} =+        object+            [ "url" .= ueURL+            , "expanded_url" .= ueExpanded+            , "display_url" .= ueDisplay+            ]++data MediaEntity = MediaEntity+    { meType :: Text+    , meId :: StatusId+    , meSizes :: HashMap Text MediaSize+    , meMediaURL :: URIString+    , meMediaURLHttps :: URIString+    , meURL :: URLEntity+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON MediaEntity where+    parseJSON v@(Object o) =+        MediaEntity <$> o .: "type"+            <*> o .: "id"+            <*> o .: "sizes"+            <*> o .: "media_url"+            <*> o .: "media_url_https"+            <*> parseJSON v+    parseJSON v = fail $ "couldn't parse media entity from: " ++ show v++instance ToJSON MediaEntity where+    toJSON MediaEntity {..} =+        object+            [ "type" .= meType+            , "id" .= meId+            , "sizes" .= meSizes+            , "media_url" .= meMediaURL+            , "media_url_https" .= meMediaURLHttps+            , "url" .= ueURL meURL+            , "expanded_url" .= ueExpanded meURL+            , "display_url" .= ueDisplay meURL+            ]++-- | Size entity.+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-size>.+data MediaSize = MediaSize+    { msWidth :: Int+    , msHeight :: Int+    , msResize :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON MediaSize where+    parseJSON (Object o) =+        MediaSize <$> o .: "w"+            <*> o .: "h"+            <*> o .: "resize"+    parseJSON v = fail $ "couldn't parse media size from: " ++ show v++instance ToJSON MediaSize where+    toJSON MediaSize {..} =+        object+            [ "w" .= msWidth+            , "h" .= msHeight+            , "resize" .= msResize+            ]++data Coordinates = Coordinates+    { coordinates :: [Double]+    , coordinatesType :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Coordinates where+    parseJSON (Object o) =+        Coordinates <$> o .: "coordinates"+            <*> o .: "type"+    parseJSON v = fail $ "couldn't parse coordinates from: " ++ show v++instance ToJSON Coordinates where+    toJSON Coordinates {..} =+        object+            [ "coordinates" .= coordinates+            , "type" .= coordinatesType+            ]++-- | This type represents a place, named locations with corresponding geo coordinates.+-- See <https://dev.twitter.com/docs/platform-objects/places>.+data Place = Place+    { placeAttributes :: HashMap Text Text+    , placeBoundingBox :: Maybe BoundingBox+    , placeCountry :: Text+    , placeCountryCode :: Text+    , placeFullName :: Text+    , placeId :: Text+    , placeName :: Text+    , placeType :: Text+    , placeURL :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Place where+    parseJSON (Object o) =+        Place <$> o .: "attributes"+            <*> o .:? "bounding_box"+            <*> o .: "country"+            <*> o .: "country_code"+            <*> o .: "full_name"+            <*> o .: "id"+            <*> o .: "name"+            <*> o .: "place_type"+            <*> o .: "url"+    parseJSON v = fail $ "couldn't parse place from: " ++ show v++instance ToJSON Place where+    toJSON Place {..} =+        object+            [ "attributes" .= placeAttributes+            , "bounding_box" .= placeBoundingBox+            , "country" .= placeCountry+            , "country_code" .= placeCountryCode+            , "full_name" .= placeFullName+            , "id" .= placeId+            , "name" .= placeName+            , "place_type" .= placeType+            , "url" .= placeURL+            ]++-- | A bounding box of coordinates which encloses the place.+-- See <https://dev.twitter.com/docs/platform-objects/places#obj-boundingbox>.+data BoundingBox = BoundingBox+    { boundingBoxCoordinates :: [[[Double]]]+    , boundingBoxType :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON BoundingBox where+    parseJSON (Object o) =+        BoundingBox <$> o .: "coordinates"+            <*> o .: "type"+    parseJSON v = fail $ "couldn't parse bounding box from: " ++ show v++instance ToJSON BoundingBox where+    toJSON BoundingBox {..} =+        object+            [ "coordinates" .= boundingBoxCoordinates+            , "type" .= boundingBoxType+            ]++-- | Entity handling.+-- See <https://dev.twitter.com/docs/platform-objects/entities>.+data Entities = Entities+    { enHashTags :: [Entity HashTagEntity]+    , enUserMentions :: [Entity UserEntity]+    , enURLs :: [Entity URLEntity]+    , enMedia :: [Entity MediaEntity]+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Entities where+    parseJSON (Object o) =+        Entities <$> o .:? "hashtags" .!= []+            <*> o .:? "user_mentions" .!= []+            <*> o .:? "urls" .!= []+            <*> o .:? "media" .!= []+    parseJSON v = fail $ "couldn't parse entities from: " ++ show v++instance ToJSON Entities where+    toJSON Entities {..} =+        object+            [ "hashtags" .= enHashTags+            , "user_mentions" .= enUserMentions+            , "urls" .= enURLs+            , "media" .= enMedia+            ]++-- | The character positions the Entity was extracted from+--+--   This is experimental implementation.+--   This may be replaced by more definite types.+type EntityIndices = [Int]++data Entity a = Entity+    { -- | The detail information of the specific entity types (HashTag, URL, User)+      entityBody :: a+    , -- | The character positions the Entity was extracted from+      entityIndices :: EntityIndices+    }+    deriving (Show, Eq, Data, Typeable, Generic, Generic1)++instance FromJSON a => FromJSON (Entity a) where+    parseJSON v@(Object o) =+        Entity <$> parseJSON v+            <*> o .: "indices"+    parseJSON v = fail $ "couldn't parse entity wrapper from: " ++ show v++instance ToJSON a => ToJSON (Entity a) where+    toJSON Entity {..} =+        case toJSON entityBody of+            (Object o) -> Object $ KeyMap.insert "indices" (toJSON entityIndices) o+            _ -> error "Entity body must produce an object."++data ExtendedEntities = ExtendedEntities+    { exeMedia :: [Entity ExtendedEntity]+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON ExtendedEntities where+    parseJSON (Object o) =+        ExtendedEntities <$> o .:? "media" .!= []+    parseJSON v = fail $ "couldn't parse extended entity from: " ++ show v++instance ToJSON ExtendedEntities where+    toJSON ExtendedEntities {..} = object ["media" .= exeMedia]++-- "video_info": {+--   "aspect_ratio": [+--     9,+--     16+--   ],+--   "duration_millis": 10704,+--   "variants": [+--     {+--       "bitrate": 320000,+--       "content_type": "video/mp4",+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/180x320/FMei8yCw7yc_Z7e-.mp4"+--     },+--     {+--       "bitrate": 2176000,+--       "content_type": "video/mp4",+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/720x1280/octt5pFbISkef8RB.mp4"+--     },+--     {+--       "bitrate": 832000,+--       "content_type": "video/mp4",+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/360x640/2OmqK74SQ9jNX8mZ.mp4"+--     },+--     {+--       "content_type": "application/x-mpegURL",+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/pl/wcJQJ2nxiFU4ZZng.m3u8"+--     }+--   ]+-- }++data Variant = Variant+    { vBitrate :: Maybe Int+    , vContentType :: Text+    , vUrl :: URIString+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Variant where+    parseJSON (Object o) =+        Variant <$> o .:? "bitrate"+            <*> o .: "content_type"+            <*> o .: "url"+    parseJSON v = fail $ "couldn't parse variant from:" ++ show v++instance ToJSON Variant where+    toJSON Variant {..} =+        object+            [ "bitrate" .= vBitrate+            , "content_type" .= vContentType+            , "url" .= vUrl+            ]++data VideoInfo = VideoInfo+    { vsAspectRatio :: [Int]+    , vsDurationMillis :: Maybe Int+    , vsVariants :: [Variant]+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON VideoInfo where+    parseJSON (Object o) =+        VideoInfo <$> o .: "aspect_ratio" .!= []+            <*> o .:? "duration_millis"+            <*> o .: "variants" .!= []+    parseJSON v = fail $ "couldn't parse video info from:" ++ show v++instance ToJSON VideoInfo where+    toJSON VideoInfo {..} =+        object+            [ "aspect_ratio" .= vsAspectRatio+            , "duration_millis" .= vsDurationMillis+            , "variants" .= vsVariants+            ]++-- Extended entities are like entities, but contain special media features like+-- video or multiple photos+data ExtendedEntity = ExtendedEntity+    { exeID :: StatusId+    , exeMediaUrl :: URIString+    , exeMediaUrlHttps :: URIString+    , exeSizes :: HashMap Text MediaSize+    , exeType :: Text+    , exeVideoInfo :: Maybe VideoInfo+    , exeDurationMillis :: Maybe Double+    , exeExtAltText :: Maybe String+    , exeURL :: URLEntity+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON ExtendedEntity where+    parseJSON v@(Object o) =+        ExtendedEntity <$> o .: "id"+            <*> o .: "media_url"+            <*> o .: "media_url_https"+            <*> o .: "sizes"+            <*> o .: "type"+            <*> o .:? "video_info"+            <*> o .:? "duration_millis"+            <*> o .:? "ext_alt_text"+            <*> parseJSON v+    parseJSON v = fail $ "couldn't parse extended entity from:" ++ show v++instance ToJSON ExtendedEntity where+    toJSON ExtendedEntity {..} =+        object+            [ "id" .= exeID+            , "media_url" .= exeMediaUrl+            , "media_url_https" .= exeMediaUrlHttps+            , "sizes" .= exeSizes+            , "type" .= exeType+            , "video_info" .= exeVideoInfo+            , "duration_millis" .= exeDurationMillis+            , "ext_alt_text" .= exeExtAltText+            , "url" .= ueURL exeURL+            , "expanded_url" .= ueExpanded exeURL+            , "display_url" .= ueDisplay exeURL+            ]++data Contributor = Contributor+    { contributorId :: UserId+    , contributorScreenName :: Maybe Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Contributor where+    parseJSON (Object o) =+        Contributor <$> o .: "id"+            <*> o .:? "screen_name"+    parseJSON v@(Number _) =+        Contributor <$> parseJSON v <*> pure Nothing+    parseJSON v = fail $ "couldn't parse contributor from: " ++ show v++instance ToJSON Contributor where+    toJSON Contributor {..} =+        object+            [ "id" .= contributorId+            , "screen_name" .= contributorScreenName+            ]++-- | Image size type. This type is included in the API response of \"\/1.1\/media\/upload.json\".+data ImageSizeType = ImageSizeType+    { imageSizeTypeWidth :: Int+    , imageSizeTypeHeight :: Int+    , imageSizeTypeType :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON ImageSizeType where+    parseJSON (Object o) =+        ImageSizeType <$> o .: "w"+            <*> o .: "h"+            <*> o .: "image_type"+    parseJSON v = fail $ "unknown value: " ++ show v++instance ToJSON ImageSizeType where+    toJSON ImageSizeType {..} =+        object+            [ "w" .= imageSizeTypeWidth+            , "h" .= imageSizeTypeHeight+            , "image_type" .= imageSizeTypeType+            ]++-- | This type is represents the API response of \"\/1.1\/media\/upload.json\".+-- See <https://dev.twitter.com/docs/api/multiple-media-extended-entities>.+data UploadedMedia = UploadedMedia+    { uploadedMediaId :: Integer+    , uploadedMediaSize :: Integer+    , uploadedMediaImage :: ImageSizeType+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON UploadedMedia where+    parseJSON (Object o) =+        UploadedMedia <$> o .: "media_id"+            <*> o .: "size"+            <*> o .: "image"+    parseJSON v = fail $ "unknown value: " ++ show v++instance ToJSON UploadedMedia where+    toJSON UploadedMedia {..} =+        object+            [ "media_id" .= uploadedMediaId+            , "size" .= uploadedMediaSize+            , "image" .= uploadedMediaImage+            ]++-- | unicode code point indices, identifying the inclusive start and exclusive end of the displayable content of the Tweet.+data DisplayTextRange = DisplayTextRange+    { displayTextRangeStart :: Int+    , -- | exclusive+      displayTextRangeEnd :: Int+    }+    deriving (Show, Eq, Ord, Data, Typeable, Generic)  instance FromJSON DisplayTextRange where     parseJSON v = do
tests/Instances.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeOperators #-}@@ -7,10 +8,16 @@  import Control.Applicative import Data.Aeson++#if MIN_VERSION_aeson(2, 0, 0)+import Data.Aeson.KeyMap as KeyMap+#else+import Data.HashMap.Strict as KeyMap+#endif import Data.HashMap.Strict as HashMap import Data.String import qualified Data.Text as T-import Data.Time (UTCTime (..), readTime, fromGregorian, defaultTimeLocale)+import Data.Time (UTCTime (..), defaultTimeLocale, fromGregorian, readTime) import Generic.Random import Test.Tasty.QuickCheck import Web.Twitter.Types@@ -20,19 +27,22 @@  instance Arbitrary UTCTime where     arbitrary =-        do randomDay <- choose (1, 29) :: Gen Int-           randomMonth <- choose (1, 12) :: Gen Int-           randomYear <- choose (2001, 2002) :: Gen Integer-           randomTime <- choose (0, 86401) :: Gen Int-           return $ UTCTime (fromGregorian randomYear randomMonth randomDay) (fromIntegral randomTime)+        do+            randomDay <- choose (1, 29) :: Gen Int+            randomMonth <- choose (1, 12) :: Gen Int+            randomYear <- choose (2001, 2002) :: Gen Integer+            randomTime <- choose (0, 86401) :: Gen Int+            return $ UTCTime (fromGregorian randomYear randomMonth randomDay) (fromIntegral randomTime)  instance Arbitrary T.Text where     arbitrary = T.pack <$> arbitrary  instance Arbitrary Value where-    arbitrary = elements [ Object HashMap.empty-                         , Object (HashMap.fromList [("test", Number 2), ("value", String "non empty")])-                         ]+    arbitrary =+        elements+            [ Object KeyMap.empty+            , Object (KeyMap.fromList [("test", Number 2), ("value", String "non empty")])+            ]  -- derive makeArbitrary ''StreamingAPI @@ -41,35 +51,35 @@         qt <- frequency [(5, Just <$> arbitrary), (95, pure Nothing)] :: Gen (Maybe Status)         rt <- frequency [(5, Just <$> arbitrary), (95, pure Nothing)] :: Gen (Maybe Status)         Status <$> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> pure Nothing-               <*> pure (statusId <$> qt)-               <*> pure qt-               <*> arbitrary-               <*> arbitrary-               <*> pure rt-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary-               <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> pure Nothing+            <*> pure (statusId <$> qt)+            <*> pure qt+            <*> arbitrary+            <*> arbitrary+            <*> pure rt+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary  instance Arbitrary SearchStatus where     arbitrary = genericArbitraryU@@ -136,18 +146,23 @@     arbitrary = genericArbitraryU instance Arbitrary ExtendedEntities where     arbitrary = genericArbitraryU+instance Arbitrary Variant where+    arbitrary = genericArbitraryU+instance Arbitrary VideoInfo where+    arbitrary = genericArbitraryU instance Arbitrary ExtendedEntity where-  arbitrary = do-    ms <- arbitrary-    ExtendedEntity-        <$> arbitrary-        <*> arbitrary-        <*> arbitrary-        <*> pure (HashMap.fromList [("medium", ms)])-        <*> arbitrary-        <*> arbitrary-        <*> arbitrary-        <*> arbitrary+    arbitrary = do+        ms <- arbitrary+        ExtendedEntity+            <$> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> pure (HashMap.fromList [("medium", ms)])+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary  instance Arbitrary a => Arbitrary (Entity a) where     arbitrary = do
tests/PropFromToJSONTest.hs view
@@ -4,7 +4,7 @@  import Data.Aeson import qualified Data.Aeson.Types as Aeson-import Instances()+import Instances () import Test.Tasty import Test.Tasty.QuickCheck import Test.Tasty.TH
tests/StatusTest.hs view
@@ -1,5 +1,5 @@-{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}  module StatusTest where @@ -8,7 +8,7 @@ import qualified Data.HashMap.Strict as M import Data.Maybe import Fixtures-import Instances()+import Instances () import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.TH@@ -21,109 +21,128 @@     statusText obj @?= "Peek-a-boo! https://t.co/R3P6waHxRa"     statusSource obj @?= "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>"     statusTruncated obj @?= False-    statusEntities obj @?=-        Just-            (Entities-             { enHashTags = []-             , enUserMentions = []-             , enURLs = []-             , enMedia =-                   [ Entity-                     { entityBody =-                           MediaEntity-                           { meType = "photo"-                           , meId = 706860403746181121-                           , meSizes =-                                 M.fromList-                                     [ ( "small"-                                       , MediaSize-                                         { msWidth = 340-                                         , msHeight = 226-                                         , msResize = "fit"-                                         })-                                     , ( "large"-                                       , MediaSize-                                         { msWidth = 1024-                                         , msHeight = 680-                                         , msResize = "fit"-                                         })-                                     , ( "medium"-                                       , MediaSize-                                         { msWidth = 600-                                         , msHeight = 398-                                         , msResize = "fit"-                                         })-                                     , ( "thumb"-                                       , MediaSize-                                         { msWidth = 150-                                         , msHeight = 150-                                         , msResize = "crop"-                                         })-                                     ]-                           , meMediaURL = "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"-                           , meMediaURLHttps = "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"-                           , meURL =-                                 URLEntity-                                 { ueURL = "https://t.co/R3P6waHxRa"-                                 , ueExpanded = "http://twitter.com/jeremycloud/status/706860403981099008/photo/1"-                                 , ueDisplay = "pic.twitter.com/R3P6waHxRa"-                                 }-                           }-                     , entityIndices = [12, 35]-                     }-                   ]-             })-    statusExtendedEntities obj @?=-        Just-            (ExtendedEntities-             { exeMedia =-                   [ Entity-                     { entityBody =-                           ExtendedEntity-                           { exeID = 706860403746181121-                           , exeMediaUrl = "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"-                           , exeMediaUrlHttps = "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"-                           , exeSizes =-                                 M.fromList-                                     [ ( "small"-                                       , MediaSize-                                         { msWidth = 340-                                         , msHeight = 226-                                         , msResize = "fit"-                                         })-                                     , ( "large"-                                       , MediaSize-                                         { msWidth = 1024-                                         , msHeight = 680-                                         , msResize = "fit"-                                         })-                                     , ( "medium"-                                       , MediaSize-                                         { msWidth = 600-                                         , msHeight = 398-                                         , msResize = "fit"-                                         })-                                     , ( "thumb"-                                       , MediaSize-                                         { msWidth = 150-                                         , msHeight = 150-                                         , msResize = "crop"-                                         })-                                     ]-                           , exeType = "photo"-                           , exeDurationMillis = Nothing-                           , exeExtAltText = Nothing-                           , exeURL =-                                 URLEntity-                                 { ueURL = "https://t.co/R3P6waHxRa"-                                 , ueExpanded = "http://twitter.com/jeremycloud/status/706860403981099008/photo/1"-                                 , ueDisplay = "pic.twitter.com/R3P6waHxRa"-                                 }-                           }-                     , entityIndices = [12, 35]-                     }-                   ]-             })+    statusEntities obj+        @?= Just+            ( Entities+                { enHashTags = []+                , enUserMentions = []+                , enURLs = []+                , enMedia =+                    [ Entity+                        { entityBody =+                            MediaEntity+                                { meType = "photo"+                                , meId = 706860403746181121+                                , meSizes =+                                    M.fromList+                                        [+                                            ( "small"+                                            , MediaSize+                                                { msWidth = 340+                                                , msHeight = 226+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "large"+                                            , MediaSize+                                                { msWidth = 1024+                                                , msHeight = 680+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "medium"+                                            , MediaSize+                                                { msWidth = 600+                                                , msHeight = 398+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "thumb"+                                            , MediaSize+                                                { msWidth = 150+                                                , msHeight = 150+                                                , msResize = "crop"+                                                }+                                            )+                                        ]+                                , meMediaURL = "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"+                                , meMediaURLHttps = "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"+                                , meURL =+                                    URLEntity+                                        { ueURL = "https://t.co/R3P6waHxRa"+                                        , ueExpanded = "http://twitter.com/jeremycloud/status/706860403981099008/photo/1"+                                        , ueDisplay = "pic.twitter.com/R3P6waHxRa"+                                        }+                                }+                        , entityIndices = [12, 35]+                        }+                    ]+                }+            )+    statusExtendedEntities obj+        @?= Just+            ( ExtendedEntities+                { exeMedia =+                    [ Entity+                        { entityBody =+                            ExtendedEntity+                                { exeID = 706860403746181121+                                , exeMediaUrl = "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"+                                , exeMediaUrlHttps = "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"+                                , exeSizes =+                                    M.fromList+                                        [+                                            ( "small"+                                            , MediaSize+                                                { msWidth = 340+                                                , msHeight = 226+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "large"+                                            , MediaSize+                                                { msWidth = 1024+                                                , msHeight = 680+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "medium"+                                            , MediaSize+                                                { msWidth = 600+                                                , msHeight = 398+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "thumb"+                                            , MediaSize+                                                { msWidth = 150+                                                , msHeight = 150+                                                , msResize = "crop"+                                                }+                                            )+                                        ]+                                , exeType = "photo"+                                , exeVideoInfo = Nothing+                                , exeDurationMillis = Nothing+                                , exeExtAltText = Nothing+                                , exeURL =+                                    URLEntity+                                        { ueURL = "https://t.co/R3P6waHxRa"+                                        , ueExpanded = "http://twitter.com/jeremycloud/status/706860403981099008/photo/1"+                                        , ueDisplay = "pic.twitter.com/R3P6waHxRa"+                                        }+                                }+                        , entityIndices = [12, 35]+                        }+                    ]+                }+            )     statusInReplyToStatusId obj @?= Nothing     statusInReplyToUserId obj @?= Nothing     statusFavorited obj @?= Just False@@ -155,9 +174,67 @@ -- case_compatibilityplus_extended_13997 = withFixtureJSON "tweet-updates/compatibilityplus_extended_13997" $ \obj -> do -- case_extended_classic_14002 :: Assertion -- case_extended_classic_14002 = withFixtureJSON "tweet-updates/extended_classic_14002" $ \obj -> do--- case_extended_classic_hidden_13761 :: Assertion--- case_extended_classic_hidden_13761 = withFixtureJSON "tweet-updates/extended_classic_hidden_13761" $ \obj -> do +case_extended_classic_hidden_13761 :: Assertion+case_extended_classic_hidden_13761 = withFixtureJSON "tweet-updates/extended_classic_hidden_13761.json" $ \obj -> do+    statusTruncated obj @?= False+    statusExtendedEntities obj+        @?= Just+            ( ExtendedEntities+                { exeMedia =+                    [ Entity+                        { entityBody =+                            ExtendedEntity+                                { exeID = 743479379079004160+                                , exeMediaUrl = "http://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg"+                                , exeMediaUrlHttps = "https://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg"+                                , exeSizes =+                                    M.fromList+                                        [+                                            ( "small"+                                            , MediaSize+                                                { msWidth = 340+                                                , msHeight = 190+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "large"+                                            , MediaSize+                                                { msWidth = 480+                                                , msHeight = 268+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "medium"+                                            , MediaSize+                                                { msWidth = 480+                                                , msHeight = 268+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "thumb"+                                            , MediaSize+                                                { msWidth = 150+                                                , msHeight = 150+                                                , msResize = "crop"+                                                }+                                            )+                                        ]+                                , exeType = "animated_gif"+                                , exeVideoInfo = Just (VideoInfo {vsAspectRatio = [120, 67], vsDurationMillis = Nothing, vsVariants = [Variant {vBitrate = Just 0, vContentType = "video/mp4", vUrl = "https://pbs.twimg.com/tweet_video/ClFejl_VAAAo9Xk.mp4"}]})+                                , exeDurationMillis = Nothing+                                , exeExtAltText = Nothing+                                , exeURL = URLEntity {ueURL = "https://t.co/VnJMDg3cbS", ueExpanded = "http://twitter.com/beyond_oneforty/status/743479431658758145/photo/1", ueDisplay = "pic.twitter.com/VnJMDg3cbS"}+                                }+                        , entityIndices = [48, 71]+                        }+                    ]+                }+            )+ case_extended_extended_14001 :: Assertion case_extended_extended_14001 = withFixtureJSON "tweet-updates/extended_extended_14001.json" $ \obj -> do     statusText obj @?= "@twitter @twitterdev has more details about these changes at https://t.co/ZnXoRQy8mK.  Thanks for making @twitter more expressive! https://t.co/AWmiH870F7"@@ -166,4 +243,3 @@  tests :: TestTree tests = $(testGroupGenerator)-
tests/TypesTest.hs view
@@ -1,5 +1,5 @@-{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}  module TypesTest where @@ -9,7 +9,7 @@ import Data.Maybe import Data.Time.Clock.POSIX import Fixtures-import Instances()+import Instances () import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.TH@@ -73,7 +73,6 @@     statusPossiblySensitive qs @?= Nothing     statusCoordinates qs @?= Nothing - case_parseStatusWithPhoto :: Assertion case_parseStatusWithPhoto = withFixtureJSON "status_thimura_with_photo.json" $ \obj -> do     statusId obj @?= 491143410770657280@@ -112,7 +111,7 @@     statusRetweetCount obj @?= 0     (userScreenName . statusUser) obj @?= "imeoin"     let ent = fromMaybe (Entities [] [] [] []) $ statusEntities obj-    (map entityIndices . enHashTags) ent @?= [[32,42]]+    (map entityIndices . enHashTags) ent @?= [[32, 42]]     (hashTagText . entityBody . head . enHashTags) ent @?= "tcdisrupt"  case_parseSearchStatusMetadata :: Assertion@@ -145,47 +144,48 @@  data DMList = DMList     { dmList :: [DirectMessage]-    } deriving (Show, Eq)+    }+    deriving (Show, Eq) instance FromJSON DMList where     parseJSON = withObject "DMList" $ \obj -> DMList <$> obj .: "events"  case_parseDirectMessageList :: Assertion case_parseDirectMessageList =     withFixtureJSON "direct_message_event_list.json" $ \obj -> do-        dmList obj @?=-            [ DirectMessage-                  { dmId = 123123123123123123-                  , dmCreatedTimestamp = read "2019-10-13 18:15:48.951 UTC"-                  , dmTargetRecipientId = 186712193-                  , dmSenderId = 69179963-                  , dmText = "hello @thimura"-                  , dmEntities =+        dmList obj+            @?= [ DirectMessage+                    { dmId = 123123123123123123+                    , dmCreatedTimestamp = read "2019-10-13 18:15:48.951 UTC"+                    , dmTargetRecipientId = 186712193+                    , dmSenderId = 69179963+                    , dmText = "hello @thimura"+                    , dmEntities =                         Entities-                        { enHashTags = []-                        , enUserMentions =-                              [ Entity-                                { entityBody =-                                      UserEntity-                                      { userEntityUserId = 69179963-                                      , userEntityUserName = "ちむら"-                                      , userEntityUserScreenName = "thimura"-                                      }-                                , entityIndices = [6, 14]-                                }-                              ]-                        , enURLs = []-                        , enMedia = []-                        }-                  }-            , DirectMessage-                  { dmId = 25252525252525-                  , dmCreatedTimestamp = read "2019-10-13 18:06:46.14 UTC"-                  , dmTargetRecipientId = 186712193-                  , dmSenderId = 69179963-                  , dmText = "hello"-                  , dmEntities = Entities {enHashTags = [], enUserMentions = [], enURLs = [], enMedia = []}-                  }-            ]+                            { enHashTags = []+                            , enUserMentions =+                                [ Entity+                                    { entityBody =+                                        UserEntity+                                            { userEntityUserId = 69179963+                                            , userEntityUserName = "ちむら"+                                            , userEntityUserScreenName = "thimura"+                                            }+                                    , entityIndices = [6, 14]+                                    }+                                ]+                            , enURLs = []+                            , enMedia = []+                            }+                    }+                , DirectMessage+                    { dmId = 25252525252525+                    , dmCreatedTimestamp = read "2019-10-13 18:06:46.14 UTC"+                    , dmTargetRecipientId = 186712193+                    , dmSenderId = 69179963+                    , dmText = "hello"+                    , dmEntities = Entities {enHashTags = [], enUserMentions = [], enURLs = [], enMedia = []}+                    }+                ]  case_parseEventFavorite :: Assertion case_parseEventFavorite = withFixtureJSON "event_favorite_thimura.json" $ \obj -> do
twitter-types.cabal view
@@ -1,69 +1,65 @@-name:              twitter-types-version:           0.10.1-license:           BSD3-license-file:      LICENSE-author:            Takahiro HIMURA-maintainer:        Takahiro HIMURA <taka@himura.jp>-synopsis:          Twitter JSON parser and types-description:       Please see the README on Github at <https://github.com/himura/twitter-types#readme>-category:          Web-stability:         Experimental-build-type:        Simple-cabal-version:     >= 1.10-homepage:          https://github.com/himura/twitter-types--tested-with:       GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.4, GHC == 8.6.5+cabal-version:      >=1.10+name:               twitter-types+version:            0.11.0+license:            BSD3+license-file:       LICENSE+maintainer:         Takahiro HIMURA <taka@himura.jp>+author:             Takahiro HIMURA+stability:          Experimental+tested-with:        ghc ==8.8.4 ghc ==8.10.4 ghc ==9.0.1+homepage:           https://github.com/himura/twitter-types+synopsis:           Twitter JSON parser and types+description:+    Please see the README on Github at <https://github.com/himura/twitter-types#readme> +category:           Web+build-type:         Simple extra-source-files:-  README.md-  tests/fixtures/*.json-  tests/fixtures/tweet-updates/*.json+    README.md+    tests/fixtures/*.json+    tests/fixtures/tweet-updates/*.json  source-repository head-  type: git-  location: git://github.com/himura/twitter-types.git+    type:     git+    location: git://github.com/himura/twitter-types.git  library-  ghc-options: -Wall--  build-depends:-      base >= 4.9 && < 5-    , aeson >= 0.3.2.2-    , text-    , time >= 1.5-    , unordered-containers--  exposed-modules:-    Web.Twitter.Types--  default-language:    Haskell2010+    exposed-modules:  Web.Twitter.Types+    default-language: Haskell2010+    ghc-options:      -Wall+    build-depends:+        base >=4.9 && <5,+        aeson >=0.3.2.2,+        text,+        time >=1.5,+        unordered-containers  test-suite tests-  type:              exitcode-stdio-1.0-  hs-source-dirs:    tests-  main-is:           spec_main.hs-  build-depends:-      base >= 4.9 && < 5-    , twitter-types-    , aeson-    , attoparsec-    , bytestring-    , generic-random-    , directory-    , filepath-    , tasty >= 0.7-    , tasty-hunit-    , tasty-quickcheck-    , tasty-th-    , text-    , time-    , unordered-containers-  other-modules:-    Fixtures-    Instances-    PropFromToJSONTest-    StatusTest-    TypesTest+    type:             exitcode-stdio-1.0+    main-is:          spec_main.hs+    hs-source-dirs:   tests+    other-modules:+        Fixtures+        Instances+        PropFromToJSONTest+        StatusTest+        TypesTest -  ghc-options:       -Wall-  default-language:    Haskell2010+    default-language: Haskell2010+    ghc-options:      -Wall+    build-depends:+        base >=4.9 && <5,+        twitter-types,+        aeson,+        attoparsec,+        bytestring,+        generic-random,+        directory,+        filepath,+        tasty >=0.7,+        tasty-hunit,+        tasty-quickcheck,+        tasty-th,+        text,+        time,+        unordered-containers