pinboard (empty) → 0.1
raw patch · 11 files changed
+1016/−0 lines, 11 filesdep +HsOpenSSLdep +aesondep +basesetup-changed
Dependencies added: HsOpenSSL, aeson, base, bytestring, containers, either, http-streams, http-types, io-streams, mtl, old-locale, random, text, time, transformers, unordered-containers
Files
- LICENSE +21/−0
- Setup.lhs +4/−0
- pinboard.cabal +67/−0
- src/Pinboard.hs +35/−0
- src/Pinboard/Api.hs +224/−0
- src/Pinboard/ApiTypes.hs +237/−0
- src/Pinboard/Client.hs +41/−0
- src/Pinboard/Client/Error.hs +53/−0
- src/Pinboard/Client/Internal.hs +165/−0
- src/Pinboard/Client/Types.hs +69/−0
- src/Pinboard/Client/Util.hs +100/−0
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2015 Jon Schoning++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ Setup.lhs view
@@ -0,0 +1,4 @@+#! /usr/bin/env runhaskell++> import Distribution.Simple+> main = defaultMain
+ pinboard.cabal view
@@ -0,0 +1,67 @@+name: pinboard+version: 0.1+synopsis: Access to the Pinboard API+license: MIT+license-file: LICENSE+author: Jon Schoning+maintainer: jonschoning@gmail.com+copyright: Copyright (c) 2015 Jon Schoning+homepage: https://github.com/jonschoning/pinboard+bug-reports: https://github.com/jonschoning/pinboard/issues+category: Network APIs+build-type: Simple+cabal-version: >=1.10+Description: + .+ The Pinboard API is a way to interact programatically with+ your bookmarks, notes and other Pinboard data. This+ library wraps the API exposing functions and data+ structures suitable for usage in Haskell programs.+ .+ Example:+ .+ > import Pinboard+ > + > main :: IO ()+ > main = do+ > let config = fromApiToken "api token"+ > result <- runPinboardJson config $ getPostsRecent Nothing Nothing+ > case result of+ > Right details -> print details+ > Left pinboardError -> print pinboardError+ .+library + hs-source-dirs: src+ build-depends: base >=4.6 && < 5.0+ , aeson+ , bytestring+ , containers+ , either+ , HsOpenSSL+ , http-streams+ , http-types+ , io-streams+ , mtl >= 2.1.3.1+ , old-locale+ , random >= 1.1+ , text+ , time+ , transformers+ , unordered-containers+ + default-language: Haskell2010+ other-modules: + exposed-modules: + Pinboard+ Pinboard.Api+ Pinboard.ApiTypes + Pinboard.Client+ Pinboard.Client.Internal+ Pinboard.Client.Error+ Pinboard.Client.Types+ Pinboard.Client.Util+ ghc-options: -Wall -rtsopts++source-repository head+ type: git+ location: git://github.com/jonschoning/pinboard.git
+ src/Pinboard.hs view
@@ -0,0 +1,35 @@+-------------------------------------------+-- |+-- Module : Pinboard+-- Copyright : (c) Jon Schoning, 2015+-- Maintainer : jonschoning@gmail.com+-- Stability : experimental+-- Portability : POSIX+--+-- @+-- import Pinboard+-- +-- main :: IO ()+-- main = do+-- let config = fromApiToken "api token"+-- result <- runPinboardJson config $ getPostsRecent Nothing Nothing+-- case result of+-- Right details -> print details+-- Left pinboardError -> print pinboardError+-- @++module Pinboard (+ -- * Pinboard.Client+ -- | Executes the methods defined in Pinboard.Api+ module Pinboard.Client + -- * Pinboard.Api+ -- | Provides Pinboard Api Methods+ , module Pinboard.Api+ -- * Pinboard.ApiTypes+ -- | Pinboard Data Structures returned by the Api+ , module Pinboard.ApiTypes+ ) where++import Pinboard.Client+import Pinboard.Api+import Pinboard.ApiTypes
+ src/Pinboard/Api.hs view
@@ -0,0 +1,224 @@+{-# LANGUAGE OverloadedStrings #-}++-------------------------------------------+-- |+-- Module : Pinboard.Api+-- Copyright : (c) Jon Schoning, 2015+-- Maintainer : jonschoning@gmail.com+-- Stability : experimental+-- Portability : POSIX+--+-- < https://pinboard.in/api/ >++module Pinboard.Api+ ( + -- ** Posts+ getPostsRecent,+ getPostsForDate,+ getPostsAll,+ getPostsDates,+ getPostsMRUTime,+ getSuggestedTags,+ addPost,+ deletePost,+ -- ** Tags+ getTags,+ renameTag,+ deleteTag,+ -- ** User+ getUserSecretRssKey,+ getUserApiToken,+ -- ** Notes+ getNoteList,+ getNote,+ ) where++import Prelude hiding (unwords)+import Pinboard.Client.Internal (pinboardJson)+import Pinboard.Client.Types (Pinboard, PinboardRequest (..), Param (..))+import Pinboard.Client.Util ((</>))+import Control.Applicative ((<$>))+import Data.Text (Text, unwords)+import Data.Time (UTCTime)+import Data.Maybe (catMaybes)+import Pinboard.ApiTypes + +-- POSTS ---------------------------------------------------------------------++-- | posts/recent : Returns a list of the user's most recent posts, filtered by tag.+getPostsRecent + :: Maybe [Tag] -- ^ filter by up to three tags+ -> Maybe Count -- ^ number of results to return. Default is 15, max is 100 + -> Pinboard Posts+getPostsRecent tags count = pinboardJson (PinboardRequest path params)+ where + path = "posts/recent" + params = catMaybes [ Tag . unwords <$> tags+ , Count <$> count ]++-- | posts/all : Returns all bookmarks in the user's account.+getPostsAll+ :: Maybe [Tag] -- ^ filter by up to three tags+ -> Maybe StartOffset -- ^ offset value (default is 0)+ -> Maybe NumResults -- ^ number of results to return. Default is all+ -> Maybe FromDateTime -- ^ return only bookmarks created after this time+ -> Maybe ToDateTime -- ^ return only bookmarks created before this time+ -> Maybe Meta -- ^ include a change detection signature for each bookmark+ -> Pinboard Posts+getPostsAll tags start results fromdt todt meta = + pinboardJson (PinboardRequest path params)+ where + path = "posts/all" + params = catMaybes [ Tag . unwords <$> tags+ , Start <$> start+ , Results <$> results+ , FromDateTime <$> fromdt+ , ToDateTime <$> todt+ , Meta <$> meta+ ]++-- | posts/get : Returns one or more posts on a single day matching the arguments. +-- If no date or url is given, date of most recent bookmark will be used.+getPostsForDate+ :: Maybe [Tag] -- ^ filter by up to three tags+ -> Maybe Date -- ^ return results bookmarked on this day+ -> Maybe Url -- ^ return bookmark for this URL+ -> Pinboard Posts+getPostsForDate tags date url = pinboardJson (PinboardRequest path params)+ where + path = "posts/get" + params = catMaybes [ Tag . unwords <$> tags+ , Date <$> date+ , Url <$> url ]+++-- | posts/dates : Returns a list of dates with the number of posts at each date.+getPostsDates+ :: Maybe [Tag] -- ^ filter by up to three tags+ -> Pinboard PostDates+getPostsDates tags = pinboardJson (PinboardRequest path params)+ where + path = "posts/dates" + params = catMaybes [ Tag . unwords <$> tags ]+++-- | posts/update : Returns the most recent time a bookmark was added, updated or deleted.+getPostsMRUTime :: Pinboard UTCTime+getPostsMRUTime = fromUpdateTime <$> pinboardJson (PinboardRequest path params)+ where + path = "posts/update" + params = []++-- | posts/suggest : Returns a list of popular tags and recommended tags for a given URL. +-- Popular tags are tags used site-wide for the url; +-- Recommended tags are drawn from the user's own tags.+getSuggestedTags+ :: Url+ -> Pinboard [Suggested]+getSuggestedTags url = pinboardJson (PinboardRequest path params)+ where + path = "posts/suggest" + params = [ Url url ]++-- | posts/delete : Delete an existing bookmark.+deletePost + :: Url+ -> Pinboard ()+deletePost url = fromDoneResult <$> pinboardJson (PinboardRequest path params)+ where + path = "posts/delete" + params = [Url url]+++-- | posts/add : Add a bookmark+addPost+ :: Url -- ^ the URL of the item+ -> Description -- ^ Title of the item. This field is unfortunately named 'description' for backwards compatibility with the delicious API+ -> Maybe Extended -- ^ Description of the item. Called 'extended' for backwards compatibility with delicious API+ -> Maybe [Tag] -- ^ List of up to 100 tags+ -> Maybe DateTime -- ^ creation time for this bookmark. Defaults to current time. Datestamps more than 10 minutes ahead of server time will be reset to current server time+ -> Maybe Replace -- ^ Replace any existing bookmark with this URL. Default is yes. If set to no, will throw an error if bookmark exists+ -> Maybe Shared -- ^ Make bookmark public. Default is "yes" unless user has enabled the "save all bookmarks as private" user setting, in which case default is "no"+ -> Maybe ToRead -- ^ Marks the bookmark as unread. Default is "no"+ -> Pinboard ()+addPost url descr ext tags ctime repl shared toread = + fromDoneResult <$> pinboardJson (PinboardRequest path params)+ where + path = "posts/add" + params = catMaybes [ Just $ Url url+ , Just $ Description descr+ , Extended <$> ext+ , Tags . unwords <$> tags+ , DateTime <$> ctime+ , Replace <$> repl+ , Shared <$> shared+ , ToRead <$> toread ]++-- TAGS ----------------------------------------------------------------------+++-- | tags/get : Returns a full list of the user's tags along with the number of +-- times they were used.+getTags :: Pinboard TagMap+getTags = fromJsonTagMap <$> pinboardJson (PinboardRequest path params)+ where + path = "tags/get" + params = []+++-- | tags/delete : Delete an existing tag.+deleteTag + :: Tag + -> Pinboard ()+deleteTag tag = fromDoneResult <$> pinboardJson (PinboardRequest path params)+ where + path = "tags/delete" + params = [Tag tag]+++-- | tags/rename : Rename an tag, or fold it in to an existing tag+renameTag + :: Old -- ^ note: match is not case sensitive+ -> New -- ^ if empty, nothing will happen+ -> Pinboard ()+renameTag old new = fromDoneResult <$> pinboardJson (PinboardRequest path params)+ where + path = "tags/rename" + params = [Old old, New new]+++-- USER ----------------------------------------------------------------------++-- | user/secret : Returns the user's secret RSS key (for viewing private feeds)+getUserSecretRssKey :: Pinboard Text+getUserSecretRssKey = fromTextResult <$> pinboardJson (PinboardRequest path params)+ where + path = "user/secret" + params = []++-- | user/api_token : Returns the user's API token (for making API calls without a password)+getUserApiToken :: Pinboard Text+getUserApiToken = fromTextResult <$> pinboardJson (PinboardRequest path params)+ where + path = "user/api_token" + params = []+++-- NOTES ---------------------------------------------------------------------++-- | notes/list : Returns a list of the user's notes (note text detail is not included)+getNoteList :: Pinboard NoteList+getNoteList = pinboardJson (PinboardRequest path params)+ where + path = "notes/list" + params = []++-- | notes/id : Returns an individual user note. The hash property is a 20 character long sha1 hash of the note text.+getNote + :: NoteId+ -> Pinboard Note+getNote noteid = pinboardJson (PinboardRequest path params)+ where + path = "notes" </> noteid+ params = []+
+ src/Pinboard/ApiTypes.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module : Pinboard.Types+-- Copyright : (c) Jon Schoning, 2015+-- Maintainer : jonschoning@gmail.com+-- Stability : experimental+-- Portability : POSIX+module Pinboard.ApiTypes where++import Prelude hiding (words)+import Control.Applicative ((<$>), (<*>), (<|>))+import Data.Aeson (FromJSON (parseJSON), Value (String, Object), ( .:))+import Data.Aeson.Types (Parser)+import Data.HashMap.Strict (HashMap, member, toList)+import Data.Text (Text, words, unpack)+import Data.Time (UTCTime)+import Data.Time.Calendar (Day)+import Data.Time.Format (readTime)+import System.Locale (defaultTimeLocale)+import qualified Data.HashMap.Strict as HM+++-- * Posts++data Posts = Posts {+ postsDate :: UTCTime+ , postsUser :: Text+ , posts :: [Post]+ } deriving (Show, Eq)++instance FromJSON Posts where+ parseJSON (Object o) =+ Posts <$> o .: "date"+ <*> o .: "user"+ <*> o .: "posts"+ parseJSON _ = error "bad parse"++data Post = Post {+ postHref :: Text+ , postDescription :: Text+ , postExtended :: Text+ , postMeta :: Text+ , postHash :: Text+ , postTime :: UTCTime+ , postShared :: Bool+ , postToread :: Bool+ , postTags :: [Text]+ } deriving (Show, Eq)++instance FromJSON Post where+ parseJSON (Object o) =+ Post <$> o .: "href"+ <*> o .: "description"+ <*> o .: "extended"+ <*> o .: "meta"+ <*> o .: "hash"+ <*> o .: "time"+ <*> (boolFromYesNo <$> o .: "shared")+ <*> (boolFromYesNo <$> o .: "toread")+ <*> (words <$> o .: "tags")+ parseJSON _ = error "bad parse"++boolFromYesNo :: Text -> Bool+boolFromYesNo "yes" = True+boolFromYesNo _ = False++data PostDates = PostDates {+ postDatesUser :: Text+ , postDatesTag :: Text+ , postDatesCount :: [DateCount]+ } deriving (Show, Eq)++instance FromJSON PostDates where+ parseJSON (Object o) =+ PostDates <$> o .: "user"+ <*> o .: "tag"+ <*> (parseDates <$> o .: "dates")+ where+ parseDates :: Value -> [DateCount]+ parseDates (Object o')= do+ (dateStr, String countStr) <- toList o'+ return (read (unpack dateStr), read (unpack countStr))+ parseDates _ = []+ parseJSON _ = error "bad parse"++type DateCount = (Day, Int)+++-- * Notes++data NoteList = NoteList {+ noteListCount :: Int+ , noteListItems :: [NoteListItem]+ } deriving (Show, Eq)++instance FromJSON NoteList where+ parseJSON (Object o) =+ NoteList <$> o .: "count"+ <*> o .: "notes"+ parseJSON _ = error "bad parse"++data NoteListItem = NoteListItem {+ noteListItemId :: Text+ , noteListItemHash :: Text+ , noteListItemTitle :: Text+ , noteListItemLength :: Int+ , noteListItemCreatedAt :: UTCTime+ , noteListItemUpdatedAt :: UTCTime+ } deriving (Show, Eq)++instance FromJSON NoteListItem where+ parseJSON (Object o) =+ NoteListItem <$> o .: "id"+ <*> o .: "hash"+ <*> o .: "title"+ <*> (read <$> (o .: "length"))+ <*> (readNoteTime <$> o .: "created_at")+ <*> (readNoteTime <$> o .: "updated_at")+ parseJSON _ = error "bad parse"++++data Note = Note {+ noteId :: Text+ , noteHash :: Text+ , noteTitle :: Text+ , noteText :: Text+ , noteLength :: Int+ , noteCreatedAt :: UTCTime+ , noteUpdatedAt :: UTCTime+ } deriving (Show, Eq)++instance FromJSON Note where+ parseJSON (Object o) =+ Note <$> o .: "id"+ <*> o .: "hash"+ <*> o .: "title"+ <*> o .: "text"+ <*> o .: "length"+ <*> (readNoteTime <$> o .: "created_at")+ <*> (readNoteTime <$> o .: "updated_at")+ parseJSON _ = error "bad parse"++readNoteTime :: String -> UTCTime+readNoteTime = readTime defaultTimeLocale "%F %T"+++-- * Tags++type TagMap = HashMap Text Int++newtype JsonTagMap = ToJsonTagMap {fromJsonTagMap :: TagMap}+ deriving (Show, Eq)++instance FromJSON JsonTagMap where+ parseJSON = return . toTags+ where toTags (Object o) = ToJsonTagMap $ HM.map (\(String s)-> read (unpack s)) o+ toTags _ = error "bad parse"+++data Suggested = Popular [Text]+ | Recommended [Text]+ deriving (Show, Eq)++instance FromJSON Suggested where+ parseJSON (Object o)+ | member "popular" o = Popular <$> (o .: "popular")+ | member "recommended" o = Recommended <$> (o .: "recommended")+ | otherwise = error "bad parse" + parseJSON _ = error "bad parse"+++-- * Scalars++newtype DoneResult = ToDoneResult {fromDoneResult :: ()}+ deriving (Show, Eq)++instance FromJSON DoneResult where+ parseJSON (Object o) = parseDone =<< (o .: "result" <|> o .: "result_code")+ where+ parseDone :: Text -> Parser DoneResult+ parseDone "done" = return $ ToDoneResult ()+ parseDone msg = ( fail . unpack ) msg+ parseJSON _ = error "bad parse"++newtype TextResult = ToTextResult {fromTextResult :: Text}+ deriving (Show, Eq)++instance FromJSON TextResult where+ parseJSON (Object o) = ToTextResult <$> (o .: "result")+ parseJSON _ = error "bad parse"++newtype UpdateTime = ToUpdateTime {fromUpdateTime :: UTCTime}+ deriving (Show, Eq)++instance FromJSON UpdateTime where+ parseJSON (Object o) = ToUpdateTime <$> (o .: "update_time")+ parseJSON _ = error "bad parse"++-- * Aliases++-- | as defined by RFC 3986. Allowed schemes are http, https, javascript, mailto, ftp and file. The Safari-specific feed scheme is allowed but will be treated as a synonym for http.+type Url = Text++-- | up to 255 characters long+type Description = Text ++-- | up to 65536 characters long. Any URLs will be auto-linkified when displayed.+type Extended = Text++-- | up to 255 characters. May not contain commas or whitespace.+type Tag = Text +type Old = Tag+type New = Tag++type Count = Int+type NumResults = Int+type StartOffset = Int++type Shared = Bool+type Replace = Bool+type ToRead = Bool++-- | UTC date in this format: 2010-12-11. Same range as datetime above+type Date = Day++-- | UTC timestamp in this format: 2010-12-11T19:48:02Z. Valid date range is Jan 1, 1 AD to January 1, 2100 (but see note below about future timestamps).+type DateTime = UTCTime+type FromDateTime = DateTime+type ToDateTime = DateTime++type Meta = Int++type NoteId = Text+
+ src/Pinboard/Client.hs view
@@ -0,0 +1,41 @@+-- |+-- Module : Pinboard.Client+-- Copyright : (c) Jon Schoning+-- Maintainer : jonschoning@gmail.com+-- Stability : experimental+-- Portability : POSIX+--+-- @+-- import Pinboard+-- +-- main :: IO ()+-- main = do+-- let config = fromApiToken "api token"+-- result <- runPinboardJson config $ getPostsRecent Nothing Nothing+-- case result of+-- Right details -> print details+-- Left pinboardError -> print pinboardError+-- @+module Pinboard.Client+ ( + -- * Client+ -- | Executes the methods defined in Pinboard.Api+ runPinboardJson+ -- | Create a default PinboardConfig using the supplied apiToken+ , fromApiToken+ -- | The PinboardConfig provides authentication via apiToken+ , PinboardConfig (..)+ -- * Client Dependencies+ , module Pinboard.Client.Error+ , module Pinboard.Client.Types+ , module Pinboard.Client.Util+ ) where++import Pinboard.Client.Internal+import Pinboard.Client.Types+import Pinboard.Client.Error+import Pinboard.Client.Util+import Data.ByteString (ByteString)++fromApiToken :: ByteString -> PinboardConfig+fromApiToken token = PinboardConfig { debug = False, apiToken = token }
+ src/Pinboard/Client/Error.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Pinboard.Client.Error+-- Copyright : (c) Jon Schoning, 2015+-- Maintainer : jonschoning@gmail.com+-- Stability : experimental+-- Portability : POSIX+module Pinboard.Client.Error + ( defaultPinboardError+ , PinboardErrorHTTPCode (..)+ , PinboardErrorType (..)+ , PinboardErrorCode (..)+ , PinboardError (..)+ ) where++import Data.Text (Text)+import Data.Monoid(mempty)++------------------------------------------------------------------------------+data PinboardErrorHTTPCode = + BadRequest -- ^ 400+ | UnAuthorized -- ^ 401+ | RequestFailed -- ^ 402+ | Forbidden -- ^ 403+ | NotFound -- ^ 404+ | TooManyRequests -- ^ 429+ | PinboardServerError -- ^ (>=500)+ | UnknownHTTPCode -- ^ All other codes+ deriving Show++------------------------------------------------------------------------------+data PinboardErrorType =+ ConnectionFailure+ | ParseFailure+ | UnknownErrorType + deriving Show++------------------------------------------------------------------------------+data PinboardErrorCode =+ UnknownError + deriving Show++------------------------------------------------------------------------------+data PinboardError = PinboardError {+ errorType :: PinboardErrorType+ , errorMsg :: Text+ , errorCode :: Maybe PinboardErrorCode+ , errorParam :: Maybe Text+ , errorHTTP :: Maybe PinboardErrorHTTPCode+ } deriving Show++defaultPinboardError :: PinboardError+defaultPinboardError = PinboardError UnknownErrorType mempty Nothing Nothing Nothing
+ src/Pinboard/Client/Internal.hs view
@@ -0,0 +1,165 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+------------------------------------------------------------------------------+-- | +-- Module : Pinboard.Client.Internal+-- Copyright : (c) Jon Schoning, 2015+-- Maintainer : jonschoning@gmail.com+-- Stability : experimental+-- Portability : POSIX+------------------------------------------------------------------------------++module Pinboard.Client.Internal+ ( + runPinboardSingleRaw+ , runPinboardSingleRawBS+ , runPinboardSingleJson+ , runPinboardJson+ , pinboardJson+ , sendPinboardRequestBS+ ) where+++import Control.Applicative ((<$>))+import Control.Exception (catch, SomeException, try, bracket)+import Control.Monad (when)+import Control.Monad.IO.Class (MonadIO (liftIO))+import Control.Monad.Reader (ask, runReaderT)+import Control.Monad.Trans.Either (left, runEitherT, right)+import Data.Aeson (FromJSON, Value(..), eitherDecodeStrict)+import Data.Monoid ((<>))+import qualified Data.ByteString as S+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.Http.Client (Connection, Method (GET),+ baselineContextSSL, buildRequest,+ closeConnection, concatHandler, concatHandler', + getStatusCode, http,+ openConnectionSSL,+ receiveResponse, sendRequest,+ setHeader, emptyBody, Response)+import Network.HTTP.Types(urlEncode)+import OpenSSL (withOpenSSL)+import System.IO.Streams (InputStream)+import Pinboard.Client.Error (PinboardError (..),+ PinboardErrorHTTPCode (..),+ PinboardErrorType (..),+ defaultPinboardError)+import Pinboard.Client.Types (Pinboard,+ PinboardConfig (..),+ PinboardRequest (..),+ Param (..))+import Pinboard.Client.Util (encodeParams, paramsToByteString, toText)++--------------------------------------------------------------------------------++runPinboardJson+ :: FromJSON a+ => PinboardConfig+ -> Pinboard a+ -> IO (Either PinboardError a)+runPinboardJson config requests = withOpenSSL $+ bracket connOpen connClose (either (connFail ConnectionFailure) go)+ where go conn = runReaderT (runEitherT requests) (config, conn) + `catch` connFail UnknownErrorType++runPinboardSingleRaw+ :: PinboardConfig + -> PinboardRequest+ -> (Response -> InputStream S.ByteString -> IO a)+ -> IO (Either PinboardError a)+runPinboardSingleRaw config req handler = withOpenSSL $ + bracket connOpen connClose (either (connFail ConnectionFailure) go)+ where go conn = (Right <$> sendPinboardRequest req config conn handler)+ `catch` connFail UnknownErrorType ++runPinboardSingleRawBS+ :: PinboardConfig + -> PinboardRequest+ -> IO (Either PinboardError S.ByteString)+runPinboardSingleRawBS config req = runPinboardSingleRaw config req concatHandler'++runPinboardSingleJson+ :: FromJSON a+ => PinboardConfig + -> PinboardRequest+ -> IO (Either PinboardError a)+runPinboardSingleJson config = runPinboardJson config . pinboardJson++--------------------------------------------------------------------------------++connOpenRaw :: IO Connection+connOpenRaw = do+ ctx <- baselineContextSSL+ openConnectionSSL ctx "api.pinboard.in" 443++connOpen :: IO (Either SomeException Connection)+connOpen = try connOpenRaw++connClose :: Either a Connection -> IO ()+connClose = either (const $ return ()) closeConnection++connFail :: PinboardErrorType -> SomeException -> IO (Either PinboardError b)+connFail e msg = return $ Left $ PinboardError e (toText msg) Nothing Nothing Nothing++--------------------------------------------------------------------------------++pinboardJson :: FromJSON a => PinboardRequest -> Pinboard a+pinboardJson req = do + (config, conn) <- ask+ result <- liftIO (sendPinboardRequestBS reqJson config conn)+ handleResultBS (debug config) result+ where+ reqJson = req { requestParams = Format "json" : requestParams req }+ handleDecodeError dbg resultBS msg = do+ when dbg $ liftIO $ print (eitherDecodeStrict resultBS :: Either String Value)+ left $ PinboardError ParseFailure (T.pack msg) Nothing Nothing Nothing + handleResultBS dbg (response, resultBS) =+ case getStatusCode response of+ 200 -> either (handleDecodeError dbg resultBS) right (eitherDecodeStrict resultBS)+ code | code >= 400 ->+ let pinboardError err = left $ defaultPinboardError { errorMsg = toText resultBS, errorHTTP = Just err } in+ case code of+ 400 -> pinboardError BadRequest+ 401 -> pinboardError UnAuthorized+ 402 -> pinboardError RequestFailed+ 403 -> pinboardError Forbidden+ 404 -> pinboardError NotFound+ 429 -> pinboardError TooManyRequests+ 500 -> pinboardError PinboardServerError+ 502 -> pinboardError PinboardServerError+ 503 -> pinboardError PinboardServerError+ 504 -> pinboardError PinboardServerError+ _ -> pinboardError UnknownHTTPCode+ _ -> left defaultPinboardError++--------------------------------------------------------------------------------++sendPinboardRequest+ :: PinboardRequest + -> PinboardConfig + -> Connection + -> (Response -> InputStream S.ByteString -> IO a)+ -> IO a+sendPinboardRequest PinboardRequest{..} PinboardConfig{..} conn handler = do+ let url = S.concat [ T.encodeUtf8 requestPath + , "?" + , paramsToByteString $ ("auth_token", urlEncode False apiToken) : encodeParams requestParams ]+ req <- buildReq url+ sendRequest conn req emptyBody+ receiveResponse conn handler+ where+ buildReq url = buildRequest $ do+ http GET ("/v1/" <> url)+ setHeader "Connection" "Keep-Alive" + setHeader "User-Agent" "pinboard.hs/0.1" ++sendPinboardRequestBS + :: PinboardRequest + -> PinboardConfig + -> Connection + -> IO (Response, S.ByteString) +sendPinboardRequestBS request config conn = sendPinboardRequest request config conn handler+ where handler response responseInputStream = do resultBS <- concatHandler response responseInputStream+ return (response, resultBS)+
+ src/Pinboard/Client/Types.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module : Pinboard.Client.Types+-- Copyright : (c) Jon Schoning, 2015+-- Maintainer : jonschoning@gmail.com+-- Stability : experimental+-- Portability : POSIX+module Pinboard.Client.Types+ ( Pinboard+ , PinboardRequest (..)+ , PinboardConfig (..)+ , Param (..)+ , ParamsBS+ ) where++import Control.Monad.Reader (ReaderT)+import Control.Monad.Trans.Either (EitherT)+import Data.ByteString (ByteString)+import Data.Text (Text)+import Network.Http.Client (Connection)+import Pinboard.Client.Error (PinboardError (..))+import Data.Time.Calendar(Day)+import Data.Time.Clock(UTCTime)++------------------------------------------------------------------------------++type Pinboard = EitherT PinboardError (ReaderT (PinboardConfig, Connection) IO)++------------------------------------------------------------------------------++data PinboardRequest = PinboardRequest+ { requestPath :: Text -- ^ url path of PinboardRequest+ , requestParams :: [Param] -- ^ Query Parameters of PinboardRequest+ } deriving Show++------------------------------------------------------------------------------+data PinboardConfig = PinboardConfig+ { apiToken :: ByteString+ , debug :: Bool+ } deriving Show++------------------------------------------------------------------------------++type ParamsBS = [(ByteString, ByteString)]++------------------------------------------------------------------------------++data Param = Format Text+ | Tag Text+ | Tags Text+ | Old Text+ | New Text+ | Count Int+ | Start Int+ | Results Int+ | Url Text+ | Date Day+ | DateTime UTCTime+ | FromDateTime UTCTime+ | ToDateTime UTCTime+ | Replace Bool+ | Shared Bool+ | ToRead Bool+ | Description Text+ | Extended Text+ | Meta Int+ deriving (Show, Eq)+
+ src/Pinboard/Client/Util.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Pinboard.Client.Util+-- Copyright : (c) Jon Schoning, 2015+-- Maintainer : jonschoning@gmail.com+-- Stability : experimental+-- Portability : POSIX+module Pinboard.Client.Util+ ( + mkConfig+ , paramsToByteString+ , toText+ , toTextLower+ , (</>)+ , paramToName+ , paramToText+ , encodeParams+ ) where++import Data.Monoid (Monoid, mconcat, mempty, (<>))+import Data.String (IsString)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Pinboard.Client.Types (PinboardConfig (..), Param (..), ParamsBS)+import Network.HTTP.Types(urlEncode)++------------------------------------------------------------------------------++mkConfig :: PinboardConfig+mkConfig = PinboardConfig { debug = False, apiToken = mempty }++------------------------------------------------------------------------------+-- | Conversion from a `Show` constrained type to `Text`+toText+ :: Show a+ => a + -> Text +toText = T.pack . show++------------------------------------------------------------------------------+-- | Conversion from a `Show` constrained type to lowercase `Text`+toTextLower+ :: Show a+ => a + -> Text +toTextLower = T.toLower . T.pack . show++------------------------------------------------------------------------------+-- | Conversion of a key value pair to a query parameterized string+paramsToByteString+ :: (Monoid m, IsString m)+ => [(m, m)]+ -> m+paramsToByteString [] = mempty+paramsToByteString [(x,y)] = x <> "=" <> y+paramsToByteString ((x,y) : xs) =+ mconcat [ x, "=", y, "&" ] <> paramsToByteString xs++-- | Retrieve and encode the optional parameters+encodeParams :: [Param] -> ParamsBS+encodeParams xs = do + x <- xs + let (k, v) = paramToText x+ return ( T.encodeUtf8 k+ , (urlEncode True . T.encodeUtf8 ) v+ )++paramToText :: Param -> (Text, Text)+paramToText (Tag a) = ("tag", a)+paramToText (Tags a) = ("tags", a)+paramToText (Old a) = ("old", a)+paramToText (New a) = ("new", a)+paramToText (Format a) = ("format", a)+paramToText (Count a) = ("count", toText a)+paramToText (Start a) = ("start", toText a)+paramToText (Results a) = ("results", toText a)+paramToText (Url a) = ("url", a)+paramToText (Date a) = ("dt", toText a)+paramToText (DateTime a) = ("dt", toText a)+paramToText (FromDateTime a) = ("fromdt", toText a)+paramToText (ToDateTime a) = ("todt", toText a)+paramToText (Replace a) = ("replace", if a then "yes" else "no")+paramToText (Shared a) = ("shared", if a then "yes" else "no")+paramToText (ToRead a) = ("toread", if a then "yes" else "no")+paramToText (Description a) = ("description", a)+paramToText (Extended a) = ("extended", a)+paramToText (Meta a) = ("meta", toText a)++paramToName :: Param -> Text+paramToName = fst . paramToText+------------------------------------------------------------------------------+-- | Forward slash interspersion on `Monoid` and `IsString`+-- constrained types+(</>)+ :: (Monoid m, IsString m)+ => m+ -> m+ -> m+m1 </> m2 = m1 <> "/" <> m2