ig (empty) → 0.1
raw patch · 16 files changed
+1907/−0 lines, 16 filesdep +aesondep +attoparsecdep +attoparsec-conduitsetup-changed
Dependencies added: aeson, attoparsec, attoparsec-conduit, base, base16-bytestring, bytestring, conduit, crypto-api, cryptohash, cryptohash-cryptoapi, data-default, http-conduit, http-types, lifted-base, monad-control, resourcet, text, time, transformers, transformers-base, unordered-containers
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- ig.cabal +88/−0
- src/Instagram.hs +136/−0
- src/Instagram/Auth.hs +55/−0
- src/Instagram/Comments.hs +35/−0
- src/Instagram/Geographies.hs +39/−0
- src/Instagram/Likes.hs +33/−0
- src/Instagram/Locations.hs +87/−0
- src/Instagram/Media.hs +61/−0
- src/Instagram/Monad.hs +345/−0
- src/Instagram/RealTime.hs +140/−0
- src/Instagram/Relationships.hs +65/−0
- src/Instagram/Tags.hs +57/−0
- src/Instagram/Types.hs +607/−0
- src/Instagram/Users.hs +127/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013 Loyful++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Felipe Lessa <felipe.lessa@loyful.com>, JP Moresmau <jpmoresmau@gmail.com> nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ ig.cabal view
@@ -0,0 +1,88 @@+name: ig+version: 0.1+synopsis: Bindings to Instagram's API.+homepage: https://github.com/loyful/ig+license: BSD3+license-file: LICENSE+author: Felipe Lessa <felipe.lessa@loyful.com>, JP Moresmau <jpmoresmau@gmail.com>+maintainer: Loyful <opensource@loyful.com>+copyright: (c) 2013 Loyful+category: Web+build-type: Simple+cabal-version: >=1.8++description:+ This package exports bindings to Instagram's APIs (see+ <http://instagram.com/developer/>).+ .+ While we would like to have a complete binding to Instagram's+ API, this package is being developed on demand. If you need+ something that has not been implemented yet, please send a pull+ request or file an issue on GitHub+ (<https://github.com/loyful/ig/issues>).+ .+ A sample Yesod application demonstrating the API can be found at <https://github.com/loyful/ig-testapp>.++source-repository head+ type: git+ location: git://github.com/loyful/ig.git++flag debug+ default: False+ description: Print debugging info.++library+ hs-source-dirs: src+ ghc-options: -Wall+ exposed-modules:+ Instagram+-- other-modules:+ build-depends:+ base >= 4 && < 5+ , bytestring >= 0.9 && < 0.11+ , text == 0.11.*+ , transformers >= 0.2 && < 0.4+ , transformers-base+ , monad-control+ , resourcet+ , conduit == 1.0.*+ , http-types+ , http-conduit == 1.9.*+ , attoparsec == 0.10.*+ , attoparsec-conduit == 1.0.*+ , aeson >= 0.5 && < 0.7,+ time,+ data-default,+ lifted-base,+ unordered-containers+ , crypto-api >= 0.11 && < 0.13+ , cryptohash >= 0.7+ , cryptohash-cryptoapi == 0.1.*+ , base16-bytestring >= 0.1+ extensions:+ OverloadedStrings+ DeriveDataTypeable+ -- -- Copied from fb-0.14.6, may be useful =).+ -- EmptyDataDecls+ -- GADTs+ -- StandaloneDeriving+ -- ScopedTypeVariables+ -- GeneralizedNewtypeDeriving+ -- TypeFamilies+ -- FlexibleInstances+ -- MultiParamTypeClasses+ other-modules: + Instagram.Types,+ Instagram.Monad,+ Instagram.Auth,+ Instagram.Users,+ Instagram.RealTime,+ Instagram.Tags,+ Instagram.Relationships,+ Instagram.Media,+ Instagram.Comments,+ Instagram.Likes,+ Instagram.Locations,+ Instagram.Geographies+ if flag(debug)+ cpp-options: -DDEBUG
+ src/Instagram.hs view
@@ -0,0 +1,136 @@+-- | the public API for Instagram access+module Instagram+ (+ -- generic types and functions+ InstagramT+ ,runInstagramT+ ,runResourceInIs+ ,IGException++ -- authentication+ ,RedirectUri+ ,getUserAccessTokenURL1+ ,getUserAccessTokenURL2+ ,Credentials(..)+ ,OAuthToken(..)+ ,AccessToken(..) -- we open this type so that the api client can just use the token data outside of your type (as a simple Text)+ ,User(..)+ ,Scope(..)+ + -- data+ ,Envelope(..)+ ,Pagination(..)+ ,Media(..)+ ,Position(..)+ ,UserPosition(..)+ ,Location(..)+ ,ImageData(..)+ ,Images(..)+ ,Comment(..)+ ,Collection(..)+ ,NoResult+ + -- user+ ,UserID+ ,getUser+ ,SelfFeedParams(..)+ ,getSelfFeed+ ,RecentParams(..)+ ,getRecent+ ,SelfLikedParams(..)+ ,getSelfLiked+ ,UserSearchParams(..)+ ,searchUsers+ + -- real time+ ,Aspect -- do not export constructor since only media is supported+ ,media+ ,CallbackUrl+ ,Subscription(..)+ ,createSubscription+ ,listSubscriptions+ ,deleteSubscriptions+ ,SubscriptionRequest(..)+ ,SubscriptionParams(..)+ ,DeletionParams(..)+ ,Update(..)+ ,verifySignature+ + -- Tags+ ,Tag(..)+ ,TagName+ ,getTag+ ,RecentTagParams(..)+ ,getRecentTagged+ ,searchTags+ + -- relationships+ ,OutgoingStatus(..)+ ,IncomingStatus(..)+ ,Relationship(..)+ ,getFollows+ ,getFollowedBy+ ,getRequestedBy+ ,getRelationship+ ,setRelationShip+ ,RelationShipAction(..)+ + -- media+ ,MediaID+ ,getMedia+ ,getPopularMedia+ ,MediaSearchParams(..)+ ,searchMedia+ + -- comments+ ,CommentID+ ,getComments+ ,postComment+ ,deleteComment+ + -- likes+ ,getLikes+ ,like+ ,unlike+ + -- locations+ ,LocationID+ ,getLocation+ ,LocationMediaParams(..)+ ,getLocationRecentMedia+ ,LocationSearchParams(..)+ ,searchLocations+ + -- geographies+ ,GeographyID+ ,GeographyMediaParams(..)+ ,getGeographyRecentMedia++ ) where++import Instagram.Auth+import Instagram.Comments+import Instagram.Geographies+import Instagram.Likes+import Instagram.Locations+import Instagram.Media+import Instagram.Monad+import Instagram.RealTime+import Instagram.Relationships+import Instagram.Tags+import Instagram.Types+import Instagram.Users+++-- debugging+--import Data.Aeson+--import qualified Data.ByteString.Lazy as BS+--+--parse :: IO()+--parse = do+-- t<-BS.readFile "env1.json"+-- let d=eitherDecode t+-- print (d::Either String (Envelope [Media]))+-- return()+ +
+ src/Instagram/Auth.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE FlexibleContexts #-}+-- | handle the authorization mecanism for Instagram+-- <http://instagram.com/developer/authentication/#>+module Instagram.Auth (+ RedirectUri,+ getUserAccessTokenURL1,+ getUserAccessTokenURL2+) where++import Instagram.Monad+import Instagram.Types++import Data.Text hiding (map)+import Control.Monad (liftM)+import qualified Data.ByteString as BS (ByteString,intercalate)+import qualified Data.Text.Encoding as TE+import qualified Network.HTTP.Types as HT++import Data.Conduit++-- | the URI to redirect the user after she accepts/refuses to authorize the app+type RedirectUri = Text++-- | get the authorize url to redirect your user to+getUserAccessTokenURL1 :: Monad m => + RedirectUri -- ^ the URI to redirect the user after she accepts/refuses to authorize the app+ -> [Scope] -- ^ the requested scopes (can be empty for Basic)+ -> InstagramT m Text -- ^ the URL to redirect the user to+getUserAccessTokenURL1 url scopes= do+ cid<-liftM clientIDBS getCreds+ bsurl<-getQueryURL "/oauth/authorize/" $ buildQuery cid ++ buildScopes scopes+ return $ TE.decodeUtf8 bsurl+ where+ -- | build the query with client id and redirect URI+ buildQuery :: BS.ByteString -> HT.SimpleQuery+ buildQuery cid=[("client_id",cid),("redirect_uri",TE.encodeUtf8 url),("response_type","code")]+ buildScopes :: [Scope] -> HT.SimpleQuery+ buildScopes []=[]+ buildScopes l =[("scope",BS.intercalate "+" $ map (TE.encodeUtf8 . toLower . pack . show) l)]+ +-- | second step of authorization: get the access token once the user has been redirected with a code+getUserAccessTokenURL2 :: (MonadBaseControl IO m, MonadResource m) =>+ RedirectUri -- ^ the redirect uri+ -> Text -- ^ the code sent back to your app+ -> InstagramT m OAuthToken -- ^ the auth token+getUserAccessTokenURL2 url code= + addClientInfos buildQuery >>= getPostRequest "/oauth/access_token" >>= getJSONResponse+ where+ -- | build query parameters+ buildQuery :: HT.SimpleQuery+ buildQuery =[("redirect_uri",TE.encodeUtf8 url),("grant_type","authorization_code"),+ ("code",TE.encodeUtf8 code)]+ + +
+ src/Instagram/Comments.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE FlexibleContexts #-}+-- | comments handling+-- <http://instagram.com/developer/endpoints/comments/#>+module Instagram.Comments (+ getComments+ ,postComment+ ,deleteComment+)where++import Instagram.Monad+import Instagram.Types++import Data.Conduit+import qualified Network.HTTP.Types as HT+import Data.Text (Text)++-- | Get a full list of comments on a media.+getComments :: (MonadBaseControl IO m, MonadResource m) => MediaID + -> Maybe OAuthToken+ -> InstagramT m (Envelope [Comment])+getComments mid token =getGetEnvelopeM ["/v1/media/",mid,"/comments"] token ([]::HT.Query)++-- | Create a comment on a media.+postComment :: (MonadBaseControl IO m, MonadResource m) => MediaID + -> OAuthToken+ -> Text+ -> InstagramT m (Envelope NoResult)+postComment mid token txt =getPostEnvelope ["/v1/media/",mid,"/comments"] token ["text" ?+ txt]++-- | Remove a comment either on the authenticated user's media or authored by the authenticated user.+deleteComment :: (MonadBaseControl IO m, MonadResource m) => MediaID + -> CommentID+ -> OAuthToken+ -> InstagramT m (Envelope NoResult)+deleteComment mid cid token =getDeleteEnvelope ["/v1/media/",mid,"/comments/",cid] token ([]::HT.Query)
+ src/Instagram/Geographies.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE FlexibleContexts #-}+-- | geographies handling+-- <http://instagram.com/developer/endpoints/geographies/#>+module Instagram.Geographies (+ GeographyMediaParams(..)+ ,getGeographyRecentMedia+)where++import Instagram.Monad+import Instagram.Types++import qualified Network.HTTP.Types as HT +import qualified Data.Text as T (Text)+import Data.Conduit+import Data.Default+import Data.Typeable+import Data.Maybe (isJust)++-- | Parameters for call to recent media in geography search+data GeographyMediaParams = GeographyMediaParams {+ gmpCount :: Maybe Integer+ ,gmpMinId :: Maybe T.Text+ }+ deriving (Show,Typeable)+ +instance Default GeographyMediaParams where+ def=GeographyMediaParams Nothing Nothing+ +instance HT.QueryLike GeographyMediaParams where+ toQuery (GeographyMediaParams cnt minI)=filter (isJust .snd) + ["count" ?+ cnt+ ,"min_id" ?+ minI ]++-- | Get very recent media from a geography subscription that you created+getGeographyRecentMedia :: (MonadBaseControl IO m, MonadResource m) =>+ GeographyID+ -> GeographyMediaParams+ ->InstagramT m (Envelope [Media])+getGeographyRecentMedia gid = getGetEnvelopeM ["/v1/geographies/", gid,"/media/recent"] Nothing
+ src/Instagram/Likes.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE FlexibleContexts #-}+-- | Likes handling +-- <http://instagram.com/developer/endpoints/likes/#>+module Instagram.Likes (+ getLikes+ ,like+ ,unlike+)where++import Instagram.Monad+import Instagram.Types++import Data.Conduit+import qualified Network.HTTP.Types as HT+++-- | Get a list of users who have liked this media.+getLikes :: (MonadBaseControl IO m, MonadResource m) => MediaID + -> Maybe OAuthToken+ -> InstagramT m (Envelope [User])+getLikes mid token =getGetEnvelopeM ["/v1/media/",mid,"/likes"] token ([]::HT.Query)++-- | Set a like on this media by the currently authenticated user.+like :: (MonadBaseControl IO m, MonadResource m) => MediaID + -> OAuthToken+ -> InstagramT m (Envelope NoResult)+like mid token =getPostEnvelope ["/v1/media/",mid,"/likes"] token ([]::HT.Query)++-- | Remove a like on this media by the currently authenticated user.+unlike :: (MonadBaseControl IO m, MonadResource m) => MediaID + -> OAuthToken+ -> InstagramT m (Envelope NoResult)+unlike mid token =getDeleteEnvelope ["/v1/media/",mid,"/likes"] token ([]::HT.Query)
+ src/Instagram/Locations.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE FlexibleContexts #-}+-- | locations handling+-- <http://instagram.com/developer/endpoints/locations/#>+module Instagram.Locations (+ getLocation+ ,LocationMediaParams(..)+ ,getLocationRecentMedia+ ,LocationSearchParams(..)+ ,searchLocations+) where++import Instagram.Monad+import Instagram.Types++import qualified Network.HTTP.Types as HT +import Data.Time.Clock.POSIX (POSIXTime)+import qualified Data.Text as T (Text)+import Data.Conduit+import Data.Typeable+import Data.Default+import Data.Maybe (isJust)+++-- | Get information about a location. +getLocation :: (MonadBaseControl IO m, MonadResource m) =>+ LocationID+ -> Maybe OAuthToken+ ->InstagramT m (Envelope (Maybe Location))+getLocation lid token=getGetEnvelopeM ["/v1/locations/",lid] token ([]::HT.Query)+++-- | Parameters for call to recent media in location search+data LocationMediaParams = LocationMediaParams {+ lmspMaxTimestamp :: Maybe POSIXTime,+ lmspMinTimestamp :: Maybe POSIXTime,+ lmspMaxID :: Maybe T.Text,+ lmspMinId :: Maybe T.Text+ }+ deriving (Show,Typeable)+ +instance Default LocationMediaParams where+ def=LocationMediaParams Nothing Nothing Nothing Nothing+ +instance HT.QueryLike LocationMediaParams where+ toQuery (LocationMediaParams maxT minT maxI minI)=filter (isJust .snd) + ["max_timestamp" ?+ maxT+ ,"min_timestamp" ?+ minT+ ,"max_id" ?+ maxI+ ,"min_id" ?+ minI ]++-- | Get a list of recent media objects from a given location. +getLocationRecentMedia :: (MonadBaseControl IO m, MonadResource m) =>+ LocationID+ -> Maybe OAuthToken+ -> LocationMediaParams+ ->InstagramT m (Envelope [Media])+getLocationRecentMedia lid = getGetEnvelopeM ["/v1/locations/", lid,"/media/recent"]+++-- | Parameters for call to media search+data LocationSearchParams = LocationSearchParams {+ lspLatitude :: Maybe Double+ ,lspLongitude :: Maybe Double+ ,lspDistance :: Maybe Integer+ ,lspFoursquareIDv2 :: Maybe T.Text+ ,lspFoursquareID :: Maybe T.Text+ }+ deriving (Show,Typeable)+ +instance Default LocationSearchParams where+ def=LocationSearchParams Nothing Nothing Nothing Nothing Nothing+ +instance HT.QueryLike LocationSearchParams where+ toQuery (LocationSearchParams lat long dis fsidV2 fsid )=filter (isJust .snd) + ["lat" ?+ lat+ ,"lng" ?+ long+ ,"distance" ?+ dis+ ,"foursquare_v2_id" ?+ fsidV2+ ,"foursquare_id" ?+ fsid+ ]++-- | Search for a location by geographic coordinate. +searchLocations :: (MonadBaseControl IO m, MonadResource m) =>+ Maybe OAuthToken+ -> LocationSearchParams+ ->InstagramT m (Envelope [Location])+searchLocations = getGetEnvelopeM ["/v1/locations/search"]
+ src/Instagram/Media.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE FlexibleContexts #-}+-- | media handling+-- <http://instagram.com/developer/endpoints/media/#>+module Instagram.Media (+ getMedia+ ,getPopularMedia+ ,MediaSearchParams(..)+ ,searchMedia+)where++import Instagram.Monad+import Instagram.Types++import Data.Conduit+import qualified Network.HTTP.Types as HT+import Data.Default (Default(..))+import Data.Time.Clock.POSIX (POSIXTime)+import Data.Typeable (Typeable)+import Data.Maybe (isJust)++-- | Get information about a media object.+getMedia :: (MonadBaseControl IO m, MonadResource m) => MediaID + -> Maybe OAuthToken+ -> InstagramT m (Envelope (Maybe Media))+getMedia mid token =getGetEnvelopeM ["/v1/media/",mid] token ([]::HT.Query)++-- | Get a list of what media is most popular at the moment.+getPopularMedia :: (MonadBaseControl IO m, MonadResource m) =>+ Maybe OAuthToken+ -> InstagramT m (Envelope [Media])+getPopularMedia token =getGetEnvelopeM ["/v1/media/popular"] token ([]::HT.Query) ++-- | Parameters for call to media search+data MediaSearchParams = MediaSearchParams {+ mspLatitude :: Maybe Double+ ,mspLongitude :: Maybe Double+ ,mspDistance :: Maybe Integer+ ,mspMaxTimestamp :: Maybe POSIXTime+ ,mspMinTimestamp :: Maybe POSIXTime+ }+ deriving (Show,Typeable)+ +instance Default MediaSearchParams where+ def=MediaSearchParams Nothing Nothing Nothing Nothing Nothing+ +instance HT.QueryLike MediaSearchParams where+ toQuery (MediaSearchParams lat long dis maxT minT )=filter (isJust .snd) + ["lat" ?+ lat+ ,"lng" ?+ long+ ,"distance" ?+ dis+ ,"max_timestamp" ?+ maxT+ ,"min_timestamp" ?+ minT+ ]+ +-- | Search for media in a given area.+searchMedia :: (MonadBaseControl IO m, MonadResource m) =>+ Maybe OAuthToken+ -> MediaSearchParams+ -> InstagramT m (Envelope [Media])+searchMedia =getGetEnvelopeM ["/v1/media/search"]+
+ src/Instagram/Monad.hs view
@@ -0,0 +1,345 @@+{-# LANGUAGE ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances,+ MultiParamTypeClasses, UndecidableInstances, TypeFamilies,+ FlexibleContexts, RankNTypes,CPP #-}+-- | the instagram monad stack and helper functions+module Instagram.Monad (+ InstagramT+ ,runInstagramT+ ,getCreds+ ,getHost+ ,getPostRequest+ ,getGetRequest+ ,getDeleteRequest+ ,getQueryURL+ ,getJSONResponse+ ,getJSONEnvelope+ ,getGetEnvelope+ ,getGetEnvelopeM+ ,getPostEnvelope+ ,getPostEnvelopeM+ ,getDeleteEnvelope+ ,getDeleteEnvelopeM+ ,getManager+ ,runResourceInIs+ ,mapInstagramT+ ,addToken+ ,addTokenM+ ,addClientInfos+ ,ToHtQuery(..)+ ) where++import Instagram.Types++import Control.Applicative +import Control.Monad (MonadPlus, liftM)+import Control.Monad.Base (MonadBase(..))+import Control.Monad.Fix (MonadFix)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans.Class (MonadTrans(lift))+import Control.Monad.Trans.Control ( MonadTransControl(..), MonadBaseControl(..)+ , ComposeSt, defaultLiftBaseWith+ , defaultRestoreM )+import Control.Monad.Trans.Reader (ReaderT(..), ask, mapReaderT)+import Data.Typeable (Typeable)+import qualified Control.Monad.Trans.Resource as R+import qualified Control.Exception.Lifted as L++import qualified Data.Conduit as C+import qualified Network.HTTP.Conduit as H+import qualified Network.HTTP.Types as HT+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BSC+import Data.Aeson (json,fromJSON,Result(..),FromJSON)+import Data.Conduit.Attoparsec (sinkParser, ParseError)+import Control.Exception.Base (throw)+import qualified Data.Text.Encoding as TE+import qualified Data.Text as T (Text,concat)+import Data.Time.Clock.POSIX (POSIXTime)++#if DEBUG+import Data.Conduit.Binary (sinkHandle)+import System.IO (stdout)+import Data.Conduit.Util (zipSinks)+#endif++-- | the instagram monad transformer+-- this encapsulates the data necessary to pass the app credentials, etc+newtype InstagramT m a = Is { unIs :: ReaderT IsData m a }+ deriving ( Functor, Applicative, Alternative, Monad+ , MonadFix, MonadPlus, MonadIO, MonadTrans+ , R.MonadThrow, R.MonadActive, R.MonadResource )+ +instance MonadBase b m => MonadBase b (InstagramT m) where+ liftBase = lift . liftBase++instance MonadTransControl InstagramT where+ newtype StT InstagramT a = FbStT { unFbStT :: StT (ReaderT IsData) a }+ liftWith f = Is $ liftWith (\run -> f (liftM FbStT . run . unIs))+ restoreT = Is . restoreT . liftM unFbStT++instance MonadBaseControl b m => MonadBaseControl b (InstagramT m) where+ newtype StM (InstagramT m) a = StMT {unStMT :: ComposeSt InstagramT m a}+ liftBaseWith = defaultLiftBaseWith StMT+ restoreM = defaultRestoreM unStMT+ +-- | Run a computation in the 'InstagramT' monad transformer with+-- your credentials.+runInstagramT :: Credentials -- ^ Your app's credentials.+ -> H.Manager -- ^ Connection manager (see 'H.withManager').+ -> InstagramT m a -- ^ the action to run+ -> m a -- ^ the result+runInstagramT creds manager (Is act) =+ runReaderT act (IsData creds manager "api.instagram.com") -- potentially we could open to other hosts if there were test endpoints, etc...+ +-- | Get the user's credentials.+getCreds :: Monad m => InstagramT m Credentials+getCreds = isCreds `liftM` Is ask++-- | Get the instagram host+getHost :: Monad m => InstagramT m ByteString+getHost = isHost `liftM` Is ask++-- | build a post request to Instagram+getPostRequest :: (Monad m,HT.QueryLike q) => ByteString -- ^ the url path+ -> q -- ^ the query parameters+ -> InstagramT m (H.Request a) -- ^ the properly configured request+getPostRequest path query=do+ host<-getHost+ return $ H.def {+ H.secure=True+ , H.host = host+ , H.port = 443+ , H.path = path+ , H.method=HT.methodPost+ , H.requestBody=H.RequestBodyBS $ HT.renderQuery False $ HT.toQuery query+ }++-- | build a get request to Instagram+getGetRequest :: (Monad m,MonadIO m,HT.QueryLike q) => ByteString -- ^ the url path+ -> q -- ^ the query parameters+ -> InstagramT m (H.Request a) -- ^ the properly configured request+getGetRequest path query=do+ host<-getHost+ let qs=HT.renderQuery True $ HT.toQuery query+#if DEBUG+ liftIO $ BSC.putStrLn $ BS.append path qs+#endif + return $ H.def {+ H.secure=True+ , H.host = host+ , H.port = 443+ , H.path = path+ , H.method=HT.methodGet+ , H.queryString=qs+ }++-- | build a delete request to Instagram+getDeleteRequest :: (Monad m,MonadIO m,HT.QueryLike q) => ByteString -- ^ the url path+ -> q -- ^ the query parameters+ -> InstagramT m (H.Request a) -- ^ the properly configured request+getDeleteRequest path query=do+ get<-getGetRequest path query+ return $ get {H.method=HT.methodDelete}++-- | build a URL for a get operation with a single query+getQueryURL :: (Monad m,HT.QueryLike q) => ByteString -- ^ the url path+ -> q -- ^ the query parameters + -> InstagramT m ByteString -- ^ the URL+getQueryURL path query=do+ host<-getHost+ return $ BS.concat ["https://",host,path,HT.renderQuery True $ HT.toQuery query]++-- | perform a HTTP request and deal with the JSON result+igReq :: forall b (m :: * -> *) wrappedErr .+ (MonadBaseControl IO m, C.MonadResource m,FromJSON b,FromJSON wrappedErr) =>+ H.Request (InstagramT m)+ -> (wrappedErr -> IGError) -- ^ extract the error from the JSON+ -> InstagramT m b+igReq req extractError=do+ -- we check the status ourselves+ let req' = req { H.checkStatus = \_ _ _ -> Nothing }+ mgr<-getManager+ res<-H.http req' mgr+ let status = H.responseStatus res+ headers = H.responseHeaders res+ cookies = H.responseCookieJar res+ ok=isOkay status+ err=H.StatusCodeException status headers cookies+ L.catch (do +#if DEBUG+ (value,_)<-H.responseBody res C.$$+- zipSinks (sinkParser json) (sinkHandle stdout)+ liftIO $ BSC.putStrLn ""+#else + value<-H.responseBody res C.$$+- sinkParser json+#endif+ if ok+ then + -- parse response as the expected value+ case fromJSON value of+ Success ot->return ot+ Error jerr->throw $ JSONException jerr -- got an ok response we couldn't parse+ else+ -- parse response as an error+ case fromJSON value of+ Success ise-> throw $ IGAppException $ extractError ise+ _ -> throw err -- we can't even parse the error, throw the HTTP error+ ) (\(_::ParseError)->throw err) -- the error body wasn't even json+ +-- | get a JSON response from a request to Instagram+-- instagram returns either a result, or an error+getJSONResponse :: forall (m :: * -> *) v.+ (MonadBaseControl IO m, C.MonadResource m,FromJSON v) =>+ H.Request (InstagramT m)+ -> InstagramT+ m v+getJSONResponse req=igReq req id++-- | get an envelope from a request to Instagram+-- the error is wrapped inside the envelope+getJSONEnvelope :: forall (m :: * -> *) v.+ (MonadBaseControl IO m, C.MonadResource m,FromJSON v) =>+ H.Request (InstagramT m)+ -> InstagramT+ m (Envelope v)+getJSONEnvelope req=igReq req eeMeta++-- | get an envelope from Instagram+getGetEnvelope :: (MonadBaseControl IO m, C.MonadResource m,HT.QueryLike ql,FromJSON v) =>+ [T.Text] -- ^ the URL components, will be concatenated+ -> OAuthToken -- ^ the access token+ -> ql -- ^ the query parameters+ -> InstagramT m (Envelope v) -- ^ the resulting envelope+getGetEnvelope urlComponents token=getGetEnvelopeM urlComponents (Just token)++-- | get an envelope from Instagram, with optional authentication+getGetEnvelopeM :: (MonadBaseControl IO m, C.MonadResource m,HT.QueryLike ql,FromJSON v) =>+ [T.Text] -- ^ the URL components, will be concatenated+ -> Maybe OAuthToken -- ^ the access token+ -> ql -- ^ the query parameters+ -> InstagramT m (Envelope v) -- ^ the resulting envelope+getGetEnvelopeM=getEnvelopeM getGetRequest++-- | send a delete and get an envelope from Instagram+getDeleteEnvelope :: (MonadBaseControl IO m, C.MonadResource m,HT.QueryLike ql,FromJSON v) =>+ [T.Text] -- ^ the URL components, will be concatenated+ -> OAuthToken -- ^ the access token+ -> ql -- ^ the query parameters+ -> InstagramT m (Envelope v) -- ^ the resulting envelope+getDeleteEnvelope urlComponents token=getDeleteEnvelopeM urlComponents (Just token)++-- | send a delete and get an envelope from Instagram, with optional authentication+getDeleteEnvelopeM :: (MonadBaseControl IO m, C.MonadResource m,HT.QueryLike ql,FromJSON v) =>+ [T.Text] -- ^ the URL components, will be concatenated+ -> Maybe OAuthToken -- ^ the access token+ -> ql -- ^ the query parameters+ -> InstagramT m (Envelope v) -- ^ the resulting envelope+getDeleteEnvelopeM=getEnvelopeM getDeleteRequest++-- | post a request and get back an envelope from Instagram+getPostEnvelope :: (MonadBaseControl IO m, C.MonadResource m,HT.QueryLike ql,FromJSON v) =>+ [T.Text] -- ^ the URL components, will be concatenated+ -> OAuthToken -- ^ the access token+ -> ql -- ^ the query parameters+ -> InstagramT m (Envelope v) -- ^ the resulting envelope+getPostEnvelope urlComponents token=getPostEnvelopeM urlComponents (Just token)++-- | post a request and get back an envelope from Instagram, with optional authentication+getPostEnvelopeM :: (MonadBaseControl IO m, C.MonadResource m,HT.QueryLike ql,FromJSON v) =>+ [T.Text] -- ^ the URL components, will be concatenated+ -> Maybe OAuthToken -- ^ the access token+ -> ql -- ^ the query parameters+ -> InstagramT m (Envelope v) -- ^ the resulting envelope+getPostEnvelopeM=getEnvelopeM getPostRequest++-- | utility function to get an envelop, independently of how the request is built+getEnvelopeM :: (MonadBaseControl IO m, C.MonadResource m,HT.QueryLike ql,FromJSON v) =>+ (ByteString -> HT.Query -> InstagramT m (H.Request (InstagramT m))) -- ^ the request building method + -> [T.Text] -- ^ the URL components, will be concatenated+ -> Maybe OAuthToken -- ^ the access token+ -> ql -- ^ the query parameters+ -> InstagramT m (Envelope v) -- ^ the resulting envelope+getEnvelopeM f urlComponents token ql=do+ let url=TE.encodeUtf8 $ T.concat urlComponents+ addTokenM token ql >>= f url >>= getJSONEnvelope + +-- | Get the 'H.Manager'.+getManager :: Monad m => InstagramT m H.Manager+getManager = isManager `liftM` Is ask++-- | Run a 'ResourceT' inside a 'InstagramT'.+runResourceInIs :: (C.MonadResource m, MonadBaseControl IO m) =>+ InstagramT (C.ResourceT m) a+ -> InstagramT m a+runResourceInIs (Is inner) = Is $ ask >>= lift . C.runResourceT . runReaderT inner + +-- | Transform the computation inside a 'InstagramT'.+mapInstagramT :: (m a -> n b) -> InstagramT m a -> InstagramT n b+mapInstagramT f = Is . mapReaderT f . unIs + +-- | the data kept through the computations+data IsData = IsData {+ isCreds::Credentials -- ^ app credentials+ ,isManager::H.Manager -- ^ HTTP connection manager+ ,isHost:: ByteString -- ^ host name+ } + deriving (Typeable)+ +-- | @True@ if the the 'Status' is ok (i.e. @2XX@).+isOkay :: HT.Status -> Bool+isOkay status =+ let sc = HT.statusCode status+ in 200 <= sc && sc < 300+ +-- | add the access token to the query+addToken :: HT.QueryLike ql=> OAuthToken -> ql -> HT.Query+addToken (OAuthToken{oaAccessToken=(AccessToken t)}) ql=("access_token", Just $ TE.encodeUtf8 t) : HT.toQuery ql++-- | add an optional access token to the query+-- if we don't have a token, we'll pass the client_id+addTokenM :: (C.MonadResource m, MonadBaseControl IO m,HT.QueryLike ql)=> Maybe OAuthToken -> ql -> InstagramT m HT.Query+addTokenM (Just oat) ql=return $ addToken oat ql+addTokenM _ ql= do+ cid<-liftM clientIDBS getCreds+ return $ ("client_id",Just cid) : HT.toQuery ql++-- | add application client info to the query+addClientInfos :: (C.MonadResource m, MonadBaseControl IO m,HT.QueryLike ql) =>+ ql ->+ InstagramT m HT.Query+addClientInfos ql= do+ cid<-liftM clientIDBS getCreds+ csecret<-liftM clientSecretBS getCreds+ return $ ("client_id",Just cid):("client_secret", Just csecret) : HT.toQuery ql++-- | simple class used to hide the serialization of parameters ansd simplify the calling code +class ToHtQuery a where+ (?+) :: ByteString -> a -> (ByteString,Maybe ByteString)++instance ToHtQuery Double where+ n ?+ d=n ?+ show d++instance ToHtQuery (Maybe Double) where+ n ?+ d=n ?+ fmap show d++instance ToHtQuery Integer where+ n ?+ d=n ?+ show d+ +instance ToHtQuery (Maybe Integer) where+ n ?+ d=n ?+ fmap show d+ +instance ToHtQuery (Maybe POSIXTime) where+ n ?+ d=n ?+ fmap (show . (round :: POSIXTime -> Integer)) d+ +instance ToHtQuery (Maybe T.Text) where+ n ?+ d=(n,fmap TE.encodeUtf8 d)++instance ToHtQuery T.Text where+ n ?+ d=(n,Just $ TE.encodeUtf8 d)+ +instance ToHtQuery (Maybe String) where+ n ?+ d=(n,fmap BSC.pack d) ++instance ToHtQuery String where+ n ?+ d=(n,Just $ BSC.pack d) +
+ src/Instagram/RealTime.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE FlexibleContexts #-}+-- | Real time subscription management+-- <http://instagram.com/developer/realtime/#>+module Instagram.RealTime (+ createSubscription+ ,listSubscriptions+ ,deleteSubscriptions+ ,SubscriptionRequest(..)+ ,SubscriptionParams(..)+ ,DeletionParams(..)+ ,verifySignature+)++where++import Instagram.Monad+import Instagram.Types++import Data.Text (Text)+import Data.Typeable+import qualified Network.HTTP.Types as HT +import Data.Maybe (isJust)+import Data.Conduit+import Data.Aeson (Value(..))++import qualified Data.ByteString.Base16 as Base16+import qualified Crypto.Classes as Crypto+import qualified Crypto.HMAC as Crypto+import Crypto.Hash.CryptoAPI (SHA1)+import Control.Monad (liftM)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BSL++-- | create a subscription +createSubscription :: (MonadBaseControl IO m, MonadResource m) => + SubscriptionParams -- ^ the subscription parameters+ -> InstagramT m (Envelope Subscription) -- ^ the created subscription+createSubscription params=do+ let url="/v1/subscriptions/"+ addClientInfos params >>= getPostRequest url >>= getJSONEnvelope ++-- | list all subscriptions for the application+listSubscriptions :: (MonadBaseControl IO m, MonadResource m) => + InstagramT m (Envelope [Subscription]) -- ^ the ID of the subscription+listSubscriptions =do+ let url="/v1/subscriptions/"+ addClientInfos ([]::HT.Query) >>= getGetRequest url >>= getJSONEnvelope ++-- | delete subscriptions based on criteria+deleteSubscriptions :: (MonadBaseControl IO m, MonadResource m) => + DeletionParams -- ^ the parameters for the deletion+ -> InstagramT m (Envelope Value) -- ^ the ID of the subscription+deleteSubscriptions params=do+ let url="/v1/subscriptions/"+ addClientInfos params >>= getDeleteRequest url >>= getJSONEnvelope + +-- | parameters for the subscription creation+data SubscriptionParams= SubscriptionParams {+ spRequest :: SubscriptionRequest -- ^ the actual subscription request+ ,spCallback :: CallbackUrl -- ^ the url Instagram will post notifications to+ ,spAspect :: Aspect -- ^ the subscription aspect+ ,spVerifyToken :: Maybe Text -- ^ the verification token+ }+ deriving (Read,Show,Eq,Ord,Typeable)++-- | to HTTP query +instance HT.QueryLike SubscriptionParams where+ toQuery (SubscriptionParams req cb (Aspect asp) tok)=filter (isJust .snd) $ HT.toQuery req ++ + ["aspect" ?+ asp+ ,"callback_url" ?+ cb+ ,"verify_token" ?+ tok]++-- | details of subscription request +data SubscriptionRequest+ -- | when a user uploads a picture+ =UserRequest+ -- | when a picture is tagged with the given tag+ | TagRequest {+ trTag ::Text+ }+ -- | when a picture is tagged with a specific location+ | LocationRequest {+ lrID :: Text+ }+ -- | when a picture is tagged with a location inside the given region+ | GeographyRequest {+ grLatitude :: Double+ ,grLongitude :: Double+ ,grRadius :: Integer+ } + deriving (Read,Show,Eq,Ord,Typeable)++-- | to HTTP query +instance HT.QueryLike SubscriptionRequest where+ toQuery UserRequest=[("object",Just "user")]+ toQuery (TagRequest tag)=[("object",Just "tag"),"object_id" ?+ tag]+ toQuery (LocationRequest i)=[("object",Just "location"),"object_id" ?+ i]+ toQuery (GeographyRequest lat lng rad)=[("object",Just "geography"),"lat" ?+ lat+ ,"lng" ?+ lng+ ,"radius" ?+ rad]+ +-- | deletion parameters +data DeletionParams+ -- | delete all subscriptions+ =DeleteAll+ -- | delete one subscription, given its ID+ | DeleteOne {+ doID :: Text+ }+ -- | delete all user subscriptions+ | DeleteUsers+ -- | delete all tag subscriptions+ | DeleteTags+ -- | delete all location subscriptions + | DeleteLocations+ -- | delete all geography subscriptions + | DeleteGeographies+ deriving (Read,Show,Eq,Ord,Typeable)++-- | to HTTP query +instance HT.QueryLike DeletionParams where+ toQuery DeleteAll=[("object",Just "all")]+ toQuery (DeleteOne i)=["id" ?+ i]+ toQuery DeleteUsers=[("object",Just "user")]+ toQuery DeleteTags=[("object",Just "tag")]+ toQuery DeleteLocations=[("object",Just "location")]+ toQuery DeleteGeographies=[("object",Just "geography")]+ +-- | verify the signature with the content, using the secret as the key+verifySignature :: Monad m =>+ BS.ByteString -- ^ the signature+ -> BSL.ByteString -- ^ the content+ -> InstagramT m Bool+verifySignature sig content=do+ csecret<-liftM clientSecretBS getCreds+ let key :: Crypto.MacKey ctx SHA1+ key = Crypto.MacKey csecret -- secret is the key+ hash = Crypto.hmac key content+ expected = Base16.encode (Crypto.encode hash)+ return $! sig `Crypto.constTimeEq` expected
+ src/Instagram/Relationships.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE FlexibleContexts #-}+-- | relationship handling+-- <http://instagram.com/developer/endpoints/relationships/#>+module Instagram.Relationships (+ getFollows+ ,getFollowedBy+ ,getRequestedBy+ ,getRelationship+ ,setRelationShip+ ,RelationShipAction(..)+)where++import Instagram.Monad+import Instagram.Types++import Data.Typeable (Typeable)++import qualified Network.HTTP.Types as HT+import Data.Conduit+import Data.Char (toLower)+++-- | Get the list of users this user follows.+getFollows :: (MonadBaseControl IO m, MonadResource m) => UserID + -> Maybe OAuthToken+ -> InstagramT m (Envelope [User])+getFollows uid token =getGetEnvelopeM ["/v1/users/",uid,"/follows"] token ([]::HT.Query)++-- | Get the list of users this user is followed by.+getFollowedBy :: (MonadBaseControl IO m, MonadResource m) => UserID + -> Maybe OAuthToken+ -> InstagramT m (Envelope [User])+getFollowedBy uid token =getGetEnvelopeM ["/v1/users/",uid,"/followed-by"] token ([]::HT.Query)++-- | List the users who have requested this user's permission to follow.+getRequestedBy :: (MonadBaseControl IO m, MonadResource m) => + OAuthToken+ -> InstagramT m (Envelope [User])+getRequestedBy token =getGetEnvelope ["/v1/users/self/requested-by"] token ([]::HT.Query)++-- | Get information about a relationship to another user. +getRelationship :: (MonadBaseControl IO m, MonadResource m) => UserID + -> OAuthToken+ -> InstagramT m (Envelope Relationship)+getRelationship uid token =getGetEnvelope ["/v1/users/",uid,"/relationship"] token ([]::HT.Query)++-- | relationship action+data RelationShipAction = Follow+ | Unfollow+ | Block+ | Unblock+ | Approve+ | Deny+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)+ +instance HT.QueryLike RelationShipAction where+ toQuery a=+ ["action" ?+ map toLower (show a)] + +-- | Modify the relationship between the current user and the target user.+setRelationShip :: (MonadBaseControl IO m, MonadResource m) => UserID + -> OAuthToken+ -> RelationShipAction+ -> InstagramT m (Envelope Relationship)+setRelationShip uid=getPostEnvelope ["/v1/users/",uid,"/relationship"]
+ src/Instagram/Tags.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE FlexibleContexts #-}+-- | tag operations+-- <http://instagram.com/developer/endpoints/tags/#>+module Instagram.Tags (+ getTag+ ,RecentTagParams(..)+ ,getRecentTagged+ ,searchTags+)where++import Instagram.Monad+import Instagram.Types++import qualified Network.HTTP.Types as HT ++import qualified Data.Text as T (Text)+import Data.Conduit+import Data.Typeable+import Data.Default+import Data.Maybe (isJust)++-- | Get information about a tag object. +getTag :: (MonadBaseControl IO m, MonadResource m) =>+ TagName+ -> Maybe OAuthToken+ ->InstagramT m (Envelope (Maybe Tag))+getTag name token=getGetEnvelopeM ["/v1/tags/",name] token ([]::HT.Query)+ +-- | Get a list of recently tagged media.+getRecentTagged :: (MonadBaseControl IO m, MonadResource m) =>+ TagName+ -> Maybe OAuthToken+ -> RecentTagParams+ ->InstagramT m (Envelope [Media])+getRecentTagged name=getGetEnvelopeM ["/v1/tags/",name,"/media/recent/"]++-- | Search for tags by name. +searchTags :: (MonadBaseControl IO m, MonadResource m) =>+ TagName+ -> Maybe OAuthToken+ ->InstagramT m (Envelope [Tag])+searchTags name token=getGetEnvelopeM ["/v1/tags/search"] token ["q" ?+ name]+ +-- | parameters for recent tag pagination +data RecentTagParams=RecentTagParams{+ rtpMaxID :: Maybe T.Text+ ,rtpMinID :: Maybe T.Text+ }deriving (Show,Typeable)+ +instance Default RecentTagParams where+ def=RecentTagParams Nothing Nothing+ +instance HT.QueryLike RecentTagParams where+ toQuery (RecentTagParams maxI minI)=filter (isJust .snd) + ["max_tag_id" ?+ maxI+ ,"min_tag_id" ?+ minI]+
+ src/Instagram/Types.hs view
@@ -0,0 +1,607 @@+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, ScopedTypeVariables #-}+-- | Types definitions+module Instagram.Types (+ Credentials(..)+ ,clientIDBS+ ,clientSecretBS+ ,OAuthToken(..)+ ,AccessToken(..)+ ,UserID+ ,User(..)+ ,Scope(..)+ ,IGException(..)+ ,Envelope(..)+ ,ErrEnvelope(..)+ ,IGError+ ,Pagination(..)+ ,MediaID+ ,Media(..)+ ,Position(..)+ ,UserPosition(..)+ ,LocationID+ ,Location(..)+ ,ImageData(..)+ ,Images(..)+ ,CommentID+ ,Comment(..)+ ,Collection(..)+ ,Aspect(..)+ ,media+ ,CallbackUrl+ ,Subscription(..)+ ,Update(..)+ ,TagName+ ,Tag(..)+ ,OutgoingStatus(..)+ ,IncomingStatus(..)+ ,Relationship(..)+ ,NoResult+ ,GeographyID+)where++import Control.Applicative+import Data.Text+import Data.Typeable (Typeable)+import Data.ByteString (ByteString)++import Data.Aeson+++import qualified Data.Text.Encoding as TE+import Control.Exception.Base (Exception)+import Data.Time.Clock.POSIX (POSIXTime)+import qualified Data.Text as T (pack)+import Data.Aeson.Types (Parser)+import qualified Data.HashMap.Strict as HM (lookup)++-- | the app credentials+data Credentials = Credentials {+ cClientID :: Text -- ^ client id+ ,cClientSecret :: Text -- ^ client secret+ }+ deriving (Show,Read,Eq,Ord,Typeable)+ +-- | get client id in ByteString form+clientIDBS :: Credentials -> ByteString+clientIDBS=TE.encodeUtf8 . cClientID++-- | get client secret in ByteString form+clientSecretBS :: Credentials -> ByteString+clientSecretBS=TE.encodeUtf8 . cClientSecret+ +-- | the oauth token returned after authentication+data OAuthToken = OAuthToken {+ oaAccessToken :: AccessToken -- ^ the access token+ ,oaUser :: User -- ^ the user structure returned+ }+ deriving (Show,Read,Eq,Ord,Typeable)++-- | to json as per Instagram format+instance ToJSON OAuthToken where+ toJSON oa=object ["access_token" .= oaAccessToken oa, "user" .= oaUser oa] ++-- | from json as per Instagram format +instance FromJSON OAuthToken where+ parseJSON (Object v) =OAuthToken <$>+ v .: "access_token" <*>+ v .: "user" + parseJSON _= fail "OAuthToken"++-- | the access token is simply a Text+newtype AccessToken=AccessToken Text+ deriving (Eq, Ord, Read, Show, Typeable)+ +-- | simple string +instance ToJSON AccessToken where+ toJSON (AccessToken at)=String at ++-- | simple string+instance FromJSON AccessToken where+ parseJSON (String s)=pure $ AccessToken s+ parseJSON _= fail "AccessToken"++-- | User ID+type UserID = Text++-- | the User partial profile returned by the authentication +data User = User {+ uID :: UserID,+ uUsername :: Text,+ uFullName :: Text,+ uProfilePicture :: Maybe Text,+ uWebsite :: Maybe Text+ } + deriving (Show,Read,Eq,Ord,Typeable)+ +-- | to json as per Instagram format +instance ToJSON User where+ toJSON u=object ["id" .= uID u, "username" .= uUsername u , "full_name" .= uFullName u, "profile_picture" .= uProfilePicture u, "website" .= uWebsite u] ++-- | from json as per Instagram format+instance FromJSON User where+ parseJSON (Object v) =User <$>+ v .: "id" <*>+ v .: "username" <*>+ v .: "full_name" <*>+ v .:? "profile_picture" <*>+ v .:? "website"+ parseJSON _= fail "User"+ +-- | the scopes of the authentication+data Scope=Basic | Comments | Relationships | Likes+ deriving (Show,Read,Eq,Ord,Enum,Bounded,Typeable)++-- | an error returned to us by Instagram+data IGError = IGError {+ igeCode :: Int+ ,igeType :: Maybe Text+ ,igeMessage :: Maybe Text+ }+ deriving (Show,Read,Eq,Ord,Typeable)+ +-- | to json as per Instagram format +instance ToJSON IGError where+ toJSON e=object ["code" .= igeCode e, "error_type" .= igeType e , "error_message" .= igeMessage e] ++-- | from json as per Instagram format+instance FromJSON IGError where+ parseJSON (Object v) =IGError <$>+ v .: "code" <*>+ v .:? "error_type" <*>+ v .:? "error_message"+ parseJSON _= fail "IGError"++-- | an exception that a call to instagram may throw+data IGException = JSONException String -- ^ JSON parsingError+ | IGAppException IGError -- ^ application exception+ deriving (Show,Typeable)++-- | make our exception type a normal exception +instance Exception IGException ++-- | envelope for Instagram OK response+data Envelope d=Envelope{+ eMeta :: IGError -- ^ this should only say 200, no error, but put here for completeness+ ,eData :: d -- ^ data, garanteed to be present (otherwise we get an ErrEnvelope) + ,ePagination :: Maybe Pagination+ }+ deriving (Show,Read,Eq,Ord,Typeable)+ +-- | to json as per Instagram format +instance (ToJSON d)=>ToJSON (Envelope d) where+ toJSON e=object ["meta" .= eMeta e, "data" .= eData e, "pagination" .= ePagination e] + +-- | from json as per Instagram format+instance (FromJSON d)=>FromJSON (Envelope d) where+ parseJSON (Object v) =Envelope <$>+ v .: "meta" <*>+ v .: "data" <*>+ v .:? "pagination"+ parseJSON _= fail "Envelope"++-- | error envelope for Instagram error response+data ErrEnvelope=ErrEnvelope{+ eeMeta :: IGError+ }+ deriving (Show,Read,Eq,Ord,Typeable)+ +-- | to json as per Instagram format +instance ToJSON ErrEnvelope where+ toJSON e=object ["meta" .= eeMeta e] + +-- | from json as per Instagram format+instance FromJSON ErrEnvelope where+ parseJSON (Object v) =ErrEnvelope <$>+ v .: "meta"+ parseJSON _= fail "ErrEnvelope"++-- | pagination info for responses that can return a lot of data +data Pagination = Pagination {+ pNextUrl :: Maybe Text+ ,pNextMaxID :: Maybe Text+ ,pNextMinID :: Maybe Text+ ,pNextMaxTagID :: Maybe Text+ ,pMinTagID :: Maybe Text+ }+ deriving (Show,Read,Eq,Ord,Typeable)+ +-- | to json as per Instagram format +instance ToJSON Pagination where+ toJSON p=object ["next_url" .= pNextUrl p, "next_max_id" .= pNextMaxID p, "next_min_id" .= pNextMinID p, "next_max_tag_id" .= pNextMaxTagID p,"min_tag_id" .= pMinTagID p] ++-- | from json as per Instagram format+instance FromJSON Pagination where+ parseJSON (Object v) =Pagination <$>+ v .:? "next_url" <*>+ v .:? "next_max_id" <*>+ v .:? "next_min_id" <*>+ v .:? "next_max_tag_id" <*>+ v .:? "min_tag_id"+ parseJSON _= fail "Pagination" + +-- | Media ID+type MediaID=Text+ +-- | instagram media object+data Media = Media {+ mID :: MediaID+ ,mCaption :: Maybe Comment+ ,mLink :: Text+ ,mUser :: User + ,mCreated :: POSIXTime+ ,mImages :: Images+ ,mType :: Text+ ,mUsersInPhoto :: [UserPosition]+ ,mFilter :: Maybe Text+ ,mTags :: [Text]+ ,mLocation :: Maybe Location+ ,mComments :: Collection Comment+ ,mLikes :: Collection User+ ,mUserHasLiked :: Bool+ ,mAttribution :: Maybe Object -- ^ seems to be open format https://groups.google.com/forum/?fromgroups#!topic/instagram-api-developers/KvGH1cnjljQ+ } + deriving (Show,Eq,Typeable)+ +-- | to json as per Instagram format +instance ToJSON Media where+ toJSON m=object ["id" .= mID m,"caption" .= mCaption m,"user".= mUser m,"link" .= mLink m, "created_time" .= toJSON (show ((round $ mCreated m) :: Integer))+ ,"images" .= mImages m,"type" .= mType m,"users_in_photo" .= mUsersInPhoto m, "filter" .= mFilter m,"tags" .= mTags m+ ,"location" .= mLocation m,"comments" .= mComments m,"likes" .= mLikes m,"user_has_liked" .= mUserHasLiked m,"attribution" .= mAttribution m] ++-- | from json as per Instagram format+instance FromJSON Media where+ parseJSON (Object v) =do+ ct::String<-v .: "created_time"+ Media <$>+ v .: "id" <*>+ v .:? "caption" <*>+ v .: "link" <*>+ v .: "user" <*>+ pure (fromIntegral (read ct::Integer)) <*>+ v .: "images" <*>+ v .: "type" <*>+ v .: "users_in_photo" <*>+ v .:? "filter" <*>+ v .: "tags" <*>+ v .:? "location" <*>+ v .:? "comments" .!= Collection 0 [] <*>+ v .:? "likes" .!= Collection 0 [] <*>+ v .:? "user_has_liked" .!= False <*>+ v .:? "attribution"+ parseJSON _= fail "Media" ++-- | position in picture+data Position = Position {+ pX ::Double+ ,pY :: Double+} deriving (Show,Eq,Typeable)++ +-- | to json as per Instagram format +instance ToJSON Position where+ toJSON p=object ["x" .= pX p,"y" .= pY p]+ +-- | from json as per Instagram format+instance FromJSON Position where+ parseJSON (Object v) = Position <$>+ v .: "x" <*>+ v .: "y" + parseJSON _=fail "Position"+ +-- | position of a user+data UserPosition = UserPosition {+ upPosition :: Position+ ,upUser :: User+ } deriving (Show,Eq,Typeable)++ +-- | to json as per Instagram format +instance ToJSON UserPosition where+ toJSON p=object ["position" .= upPosition p,"user" .= upUser p]+ +-- | from json as per Instagram format+instance FromJSON UserPosition where+ parseJSON (Object v) = UserPosition <$>+ v .: "position" <*>+ v .: "user" + parseJSON _=fail "UserPosition"++-- | location ID+type LocationID = Text++-- | geographical location info+data Location = Location {+ lID :: Maybe LocationID+ ,lLatitude :: Maybe Double+ ,lLongitude :: Maybe Double+ ,lStreetAddress :: Maybe Text+ ,lName :: Maybe Text+ } + deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per Instagram format +instance ToJSON Location where+ toJSON l=object ["id" .= lID l,"latitude" .= lLatitude l,"longitude" .= lLongitude l, "street_address" .= lStreetAddress l,"name" .= lName l]+ +-- | from json as per Instagram format+instance FromJSON Location where+ parseJSON (Object v) = + Location <$>+ parseID v <*>+ v .:? "latitude" <*>+ v .:? "longitude" <*>+ v .:? "street_address" <*>+ v .:? "name"+ where + -- | the Instagram API hasn't made its mind up, sometimes location id is an int, sometimes a string+ parseID :: Object -> Parser (Maybe LocationID)+ parseID obj=case HM.lookup "id" obj of+ Just (String s)->pure $ Just s+ Just (Number n)->pure $ Just $ T.pack $ show n+ Nothing->pure Nothing+ _->fail "LocationID"+ parseJSON _= fail "Location"+ +-- | data for a single image+data ImageData = ImageData {+ idURL :: Text,+ idWidth :: Integer,+ idHeight :: Integer+ } + deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per Instagram format +instance ToJSON ImageData where+ toJSON i=object ["url" .= idURL i,"width" .= idWidth i,"height" .= idHeight i]+ +-- | from json as per Instagram format+instance FromJSON ImageData where+ parseJSON (Object v) = ImageData <$>+ v .: "url" <*>+ v .: "width" <*>+ v .: "height"+ parseJSON _= fail "ImageData" + +-- | different images for the same media+data Images = Images {+ iLowRes :: ImageData+ ,iThumbnail :: ImageData+ ,iStandardRes :: ImageData+ }+ deriving (Show,Eq,Ord,Typeable) + +-- | to json as per Instagram format +instance ToJSON Images where+ toJSON i=object ["low_resolution" .= iLowRes i,"thumbnail" .= iThumbnail i,"standard_resolution" .= iStandardRes i]+ +-- | from json as per Instagram format+instance FromJSON Images where+ parseJSON (Object v) = Images <$>+ v .: "low_resolution" <*>+ v .: "thumbnail" <*>+ v .: "standard_resolution"+ parseJSON _= fail "Images" + +-- | comment id+type CommentID = Text+ +-- | Commenton on a medium+data Comment = Comment {+ cID :: CommentID+ ,cCreated :: POSIXTime+ ,cText :: Text+ ,cFrom :: User+ }+ deriving (Show,Eq,Ord,Typeable) ++-- | to json asCommentstagram format +instance ToJSON Comment where+ toJSON c=object ["id" .= cID c,"created_time" .= toJSON (show ((round $ cCreated c) :: Integer))+ ,"text" .= cText c,"from" .= cFrom c] ++-- | from json asCommentstagram format+instance FromJSON Comment where+ parseJSON (Object v) =do+ ct::String<-v .: "created_time"+ Comment <$>+ v .: "id" <*>+ pure (fromIntegral (read ct::Integer)) <*>+ v .: "text" <*>+ v .: "from" + parseJSON _= fail "Caption" ++-- | a collection of items (count + data)+-- data can only be a subset+data Collection a= Collection {+ cCount :: Integer+ ,cData :: [a]+ }+ deriving (Show,Eq,Ord,Typeable) ++ +-- | to json as per Instagram format +instance (ToJSON a)=>ToJSON (Collection a) where+ toJSON igc=object ["count" .= cCount igc,"data" .= cData igc]++-- | from json as per Instagram format+instance (FromJSON a)=>FromJSON (Collection a) where+ parseJSON (Object v) = Collection <$>+ v .: "count" <*>+ v .: "data" + parseJSON _= fail "Collection" + ++-- | the URL to receive notifications to+type CallbackUrl = Text + +-- | notification aspect+data Aspect = Aspect Text+ deriving (Show, Read, Eq, Ord, Typeable)+ +-- | to json as per Instagram format +instance ToJSON Aspect where+ toJSON (Aspect t)=String t++-- | from json as per Instagram format+instance FromJSON Aspect where+ parseJSON (String t) = pure $ Aspect t+ parseJSON _= fail "Aspect" ++-- | the media Aspect, the only one supported for now +media :: Aspect+media = Aspect "media"++-- | a subscription to a real time notification+data Subscription= Subscription {+ sID :: Text+ ,sType :: Text+ ,sObject :: Text+ ,sObjectID :: Maybe Text+ ,sAspect :: Aspect+ ,sCallbackUrl :: CallbackUrl+ ,sLatitude :: Maybe Double+ ,sLongitude :: Maybe Double+ ,sRadius :: Maybe Integer+ } + deriving (Show,Eq,Typeable)+ +-- | to json as per Instagram format +instance ToJSON Subscription where+ toJSON s=object ["id" .= sID s,"type" .= sType s,"object" .= sObject s,"object_id" .= sObjectID s,"aspect" .= sAspect s+ ,"callback_url".=sCallbackUrl s,"lat".= sLatitude s,"lng".=sLongitude s,"radius".=sRadius s]++-- | from json as per Instagram format+instance FromJSON Subscription where+ parseJSON (Object v) = Subscription <$>+ v .: "id" <*>+ v .: "type" <*>+ v .: "object" <*>+ v .:? "object_id" <*>+ v .: "aspect" <*>+ v .: "callback_url" <*>+ v .:? "lat" <*>+ v .:? "lng" <*>+ v .:? "radius"+ parseJSON _= fail "Subscription" + +-- | an update from a subscription +data Update = Update {+ uSubscriptionID :: Integer+ ,uObject :: Text+ ,uObjectID :: Text+ ,uChangedAspect :: Aspect+ ,uTime :: POSIXTime+ }+ deriving (Show,Eq,Typeable)+ +-- | to json as per Instagram format +instance ToJSON Update where+ toJSON u=object ["subscription_id" .= uSubscriptionID u ,"object" .= uObject u,"object_id" .= uObjectID u+ ,"changed_aspect" .= uChangedAspect u,"time" .= toJSON ((round $ uTime u) :: Integer)] ++-- | from json as per Instagram format+instance FromJSON Update where+ parseJSON (Object v) =do+ ct::Integer<-v .: "time"+ Update <$>+ v .: "subscription_id" <*>+ v .: "object" <*>+ v .: "object_id" <*>+ v .: "changed_aspect" <*>+ pure (fromIntegral ct)+ parseJSON _= fail "Update" ++-- | Tag Name+type TagName = Text++-- | a Tag +data Tag = Tag {+ tName :: TagName,+ tMediaCount :: Integer+ }+ deriving (Show,Read,Eq,Ord,Typeable) + +-- | to json as per Instagram format +instance ToJSON Tag where+ toJSON t=object ["name" .= tName t,"media_count" .= tMediaCount t] ++-- | from json as per Instagram format+instance FromJSON Tag where+ parseJSON (Object v) = Tag <$>+ v .: "name" <*>+ v .:? "media_count" .!= 0+ parseJSON _= fail "Tag" + +-- | outgoing relationship status +data OutgoingStatus = Follows | Requested | OutNone+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++-- | to json as per Instagram format +instance ToJSON OutgoingStatus where+ toJSON Follows = String "follows"+ toJSON Requested = String "requested"+ toJSON OutNone = String "none"++-- | from json as per Instagram format+instance FromJSON OutgoingStatus where+ parseJSON (String "follows")=pure Follows+ parseJSON (String "requested")=pure Requested+ parseJSON (String "none")=pure OutNone+ parseJSON _= fail "OutgoingStatus" + +-- | incoming relationship status +data IncomingStatus = FollowedBy | RequestedBy | BlockedByYou | InNone+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++-- | to json as per Instagram format +instance ToJSON IncomingStatus where+ toJSON FollowedBy = String "followed_by"+ toJSON RequestedBy = String "requested_by"+ toJSON BlockedByYou = String "blocked_by_you"+ toJSON InNone = String "none"++-- | from json as per Instagram format+instance FromJSON IncomingStatus where+ parseJSON (String "followed_by")=pure FollowedBy+ parseJSON (String "requested_by")=pure RequestedBy+ parseJSON (String "blocked_by_you")=pure BlockedByYou+ parseJSON (String "none")=pure InNone+ parseJSON _= fail "IncomingStatus" + +-- | a relationship between two users+data Relationship = Relationship {+ rOutgoing :: OutgoingStatus+ ,rIncoming :: IncomingStatus+ ,rTargetUserPrivate :: Bool -- ^ not present in doc+ }+ deriving (Show,Read,Eq,Ord,Typeable) ++-- | to json as per Instagram format +instance ToJSON Relationship where+ toJSON r=object ["outgoing_status" .= rOutgoing r,"incoming_status" .= rIncoming r,"target_user_is_private" .= rTargetUserPrivate r] ++-- | from json as per Instagram format+instance FromJSON Relationship where+ parseJSON (Object v) = Relationship <$>+ v .:? "outgoing_status" .!= OutNone <*>+ v .:? "incoming_status" .!= InNone <*>+ v .:? "target_user_is_private" .!= False+ parseJSON _= fail "Relationship" ++-- | Instagram returns data:null for nothing, but Aeson considers that () maps to an empty array...+-- so we model the fact that we expect null via NoResult +data NoResult = NoResult+ deriving (Show,Read,Eq,Ord,Typeable)++-- | to json as per Instagram format +instance ToJSON NoResult where+ toJSON _=Null++-- | from json as per Instagram format+instance FromJSON NoResult where+ parseJSON Null = pure NoResult + parseJSON _= fail "NoResult" ++-- | geography ID +type GeographyID = Text+
+ src/Instagram/Users.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE FlexibleContexts #-}+-- | Users end point handling+-- <http://instagram.com/developer/endpoints/users/#>+module Instagram.Users (+ getUser+ ,SelfFeedParams(..)+ ,getSelfFeed+ ,RecentParams(..)+ ,getRecent+ ,SelfLikedParams(..)+ ,getSelfLiked+ ,UserSearchParams(..)+ ,searchUsers+) where++import Instagram.Monad+import Instagram.Types++import Data.Time.Clock.POSIX (POSIXTime)+import Data.Typeable (Typeable)+ +import qualified Network.HTTP.Types as HT+import Data.Maybe (isJust)+import qualified Data.Text as T (Text)+import Data.Conduit+import Data.Default++++-- | Get basic information about a user. +getUser :: (MonadBaseControl IO m, MonadResource m) => UserID + -> Maybe OAuthToken+ -> InstagramT m (Envelope (Maybe User))+getUser uid token =getGetEnvelopeM ["/v1/users/",uid] token ([]::HT.Query)+ +-- | Parameters for call to self feed+data SelfFeedParams = SelfFeedParams {+ sfpCount :: Maybe Integer,+ sfpMaxID :: Maybe T.Text,+ sfpMinId :: Maybe T.Text+ }+ deriving (Show,Typeable)+ +instance Default SelfFeedParams where+ def=SelfFeedParams Nothing Nothing Nothing+ +instance HT.QueryLike SelfFeedParams where+ toQuery (SelfFeedParams c maxI minI)=filter (isJust .snd) + ["count" ?+ c+ ,"max_id" ?+ maxI+ ,"min_id" ?+ minI]+ + +-- | See the authenticated user's feed. +getSelfFeed :: (MonadBaseControl IO m, MonadResource m) =>+ OAuthToken+ -> SelfFeedParams + -> InstagramT m (Envelope [Media])+getSelfFeed =getGetEnvelope ["/v1/users/self/feed/"] ++-- | Parameters for call to recent media+data RecentParams = RecentParams {+ rpCount :: Maybe Integer,+ rpMaxTimestamp :: Maybe POSIXTime,+ rpMinTimestamp :: Maybe POSIXTime,+ rpMaxID :: Maybe T.Text,+ rpMinId :: Maybe T.Text+ }+ deriving (Show,Typeable)+ +instance Default RecentParams where+ def=RecentParams Nothing Nothing Nothing Nothing Nothing+ +instance HT.QueryLike RecentParams where+ toQuery (RecentParams c maxT minT maxI minI)=filter (isJust .snd) + ["count" ?+ c+ ,"max_timestamp" ?+ maxT+ ,"min_timestamp" ?+ minT+ ,"max_id" ?+ maxI+ ,"min_id" ?+ minI]+ + +-- | Get the most recent media published by a user. +getRecent :: (MonadBaseControl IO m, MonadResource m) => UserID + -> OAuthToken+ -> RecentParams + -> InstagramT m (Envelope [Media])+getRecent uid=getGetEnvelope ["/v1/users/",uid,"/media/recent/"]++-- | parameters for self liked call+data SelfLikedParams = SelfLikedParams {+ slpCount :: Maybe Integer,+ slpMaxLikeID :: Maybe T.Text+ }+ deriving (Show,Typeable)+ +instance Default SelfLikedParams where+ def=SelfLikedParams Nothing Nothing+ +instance HT.QueryLike SelfLikedParams where+ toQuery (SelfLikedParams c maxI)=filter (isJust .snd) + ["count" ?+ c + ,"max_like_id" ?+ maxI] ++-- | See the authenticated user's list of media they've liked.+getSelfLiked :: (MonadBaseControl IO m, MonadResource m) => OAuthToken + -> SelfLikedParams+ -> InstagramT m (Envelope [Media]) +getSelfLiked =getGetEnvelope ["/v1/users/self/media/liked"] ++-- | parameters for self liked call+data UserSearchParams = UserSearchParams {+ uspQuery :: T.Text,+ uspCount :: Maybe Integer+ }+ deriving (Show,Typeable)+ +instance HT.QueryLike UserSearchParams where+ toQuery (UserSearchParams q c )=filter (isJust .snd) + ["count" ?+ c -- does not seem to be taken into account...+ ,"q" ?+ q] ++-- | Search for a user by name. +searchUsers :: (MonadBaseControl IO m, MonadResource m) => Maybe OAuthToken + -> UserSearchParams+ -> InstagramT m (Envelope [User]) +searchUsers =getGetEnvelopeM ["/v1/users/search"]