├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Setup.hs ├── src └── Web │ └── Twitter │ ├── Feed.hs │ └── Types.hs ├── stack.yaml ├── test ├── Suite.hs └── Web │ └── Twitter │ └── Feed │ └── Tests.hs └── twitter-feed.cabal /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | cabal-dev 3 | *.o 4 | *.hi 5 | *.chi 6 | *.chs.h 7 | *.dyn_o 8 | *.dyn_hi 9 | .virtualenv 10 | .hpc 11 | .hsenv 12 | .cabal-sandbox/ 13 | cabal.sandbox.config 14 | cabal.config 15 | *.prof 16 | *.aux 17 | *.hp 18 | .stack-work/ 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This file is based on a generated file. 2 | # See https://github.com/hvr/multi-ghc-travis. 3 | 4 | language: c 5 | 6 | sudo: false 7 | 8 | # GHC depends on GMP. You can add other dependencies here as well. 9 | addons: 10 | apt: 11 | packages: 12 | - libgmp-dev 13 | 14 | cache: 15 | directories: 16 | - $HOME/.stack 17 | 18 | matrix: 19 | include: 20 | - env: ARGS="--resolver lts-2" 21 | compiler: ": # lts-2 gch-7.8.4" 22 | - env: ARGS="--resolver lts-3" 23 | compiler: ": # lts-3 gch-7.10.2" 24 | - env: ARGS="--resolver lts" 25 | compiler: ": # lts" 26 | 27 | before_install: 28 | # Using compiler above sets CC to an invalid value, so unset it 29 | - unset CC 30 | 31 | # Download and unpack the stack executable 32 | - mkdir -p ~/.local/bin 33 | - export PATH=$HOME/.local/bin:$PATH 34 | - curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 35 | 36 | # This line does all of the work: installs GHC if necessary, build the library, 37 | # executables, and test suites, and runs the test suites. --no-terminal works 38 | # around some quirks in Travis's terminal implementation. 39 | script: stack $ARGS --no-terminal --install-ghc test --haddock --no-haddock-deps 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0.11 2 | 3 | - Allow HUnit 1.5. 4 | 5 | ## 0.2.0.10 6 | 7 | - Allow HUnit 1.4. 8 | 9 | ## 0.2.0.9 10 | 11 | - Allow http-conduit 2.2. 12 | 13 | ## 0.2.0.8 14 | 15 | - Allow aeson 1.0. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Stack Builders Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/stackbuilders/twitter-feed.svg?branch=master)](https://travis-ci.org/stackbuilders/twitter-feed) [![Hackage](https://img.shields.io/hackage/v/twitter-feed.svg)](http://hackage.haskell.org/package/twitter-feed) 2 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 3 | 4 | > **⚠️ Warning:** This library has been deprecated and is no longer maintained. It will not receive any further security patches, features, or bug fixes and is preserved here at GitHub for archival purposes. If you want to use it, we suggest forking the repository and auditing the codebase before use. For more information, contact us at info@stackbuilders.com. 5 | 6 | # DEPRECATED - Twitter Feed 7 | 8 | This package is used for retrieving a users' timeline via the Twitter timeline 9 | API (OAuth). It retrieves the timeline with entities, and links the usernames 10 | and links found in the feed. 11 | 12 | It is currently used to retrieve the Twitter feed that is displayed on our 13 | web site, . 14 | 15 | ## Usage 16 | 17 | You must pass your OAuth credentials to the client. Create them as follows: 18 | 19 | ```haskell 20 | import Web.Authenticate.OAuth 21 | 22 | myoauth :: OAuth 23 | myoauth = newOAuth 24 | { oauthServerName = "api.twitter.com" 25 | , oauthConsumerKey = "your consumer key" 26 | , oauthConsumerSecret = "your consumer secret" 27 | } 28 | 29 | mycred :: Credential 30 | mycred = newCredential "your oauth token" 31 | "your oauth token secret" 32 | ``` 33 | 34 | Next, you can call the `timeline` function directly. The arguments to the 35 | timeline function are: 36 | 37 | 1. OAuth token 38 | 1. OAuth Credential 39 | 1. Number of tweets to retrieve 40 | 1. Whether to exclude replies in feed 41 | 1. Username for which to retrieve tweets 42 | 43 | ```haskell 44 | λ: res <- timeline myoauth mycred 3 False "stackbuilders" 45 | λ: res 46 | Right [SimpleTweet {body = "Ven a nuestro evento de Stack U en Quito el 22 de febrero - Ruby y programaci\243n funcional stackbuilders.com/news/ven-al-ev\8230", tweetId = "434472043862433792"},SimpleTweet {body = "@_eightb @filipebarcos prove that we didn't use ghcjs! :)", tweetId = "431932790423420929"},SimpleTweet {body = "RT @filipebarcos: w00t!! @stackbuilders just launched their new website! stackbuilders.com and it's built in haskell!", tweetId = "431929704388775936"}] 47 | ``` 48 | 49 | Your response will be an `IO Either String [SimpleTweet]`. 50 | 51 | ## Contributing 52 | 53 | Contributions are welcome to this library. Fork, modify, make sure the tests 54 | pass, and open a PR. 55 | 56 | ## LICENSE 57 | 58 | MIT, see [the LICENSE file](LICENSE). 59 | 60 | ## Authors 61 | 62 | Justin Leitgeb (Twitter: [@justinleitgeb](http://twitter.com/justinleitgeb), 63 | Github: [@jsl](https://github.com/jsl)) and 64 | Andrés Torres (Github: [AndresRicardoTorres](https://github.com/AndresRicardoTorres)). 65 | 66 | --- 67 | Stack Builders 68 | [Check out our libraries](https://github.com/stackbuilders/) | [Join our team](https://www.stackbuilders.com/join-us/) 69 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /src/Web/Twitter/Feed.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings, CPP #-} 2 | 3 | ----------------------------------------------------------------------------- 4 | -- | 5 | -- Module : Web.Twitter.Feed 6 | -- License : MIT (see the file LICENSE) 7 | -- Maintainer : Justin Leitgeb 8 | -- 9 | -- Functions for fetching (and linkifying) timelines of Twitter users. 10 | -- 11 | ----------------------------------------------------------------------------- 12 | 13 | module Web.Twitter.Feed 14 | ( timeline 15 | , addLink 16 | , timelineUrl 17 | , sortLinks 18 | ) where 19 | 20 | import qualified Data.ByteString.Lazy as BS 21 | 22 | import Network.HTTP.Conduit 23 | import Network.HTTP.Client.Conduit (defaultManagerSettings) 24 | import Web.Authenticate.OAuth 25 | import Data.Aeson 26 | import Data.List (elemIndices, sort) 27 | import Data.Char (toLower) 28 | import Web.Twitter.Types 29 | 30 | -- | Get and decode a user timeline. 31 | timeline :: OAuth -- ^ OAuth client (consumer) 32 | -> Credential -- ^ OAuth credential 33 | -> Int -- ^ Count 34 | -> Bool -- ^ Exclude replies? 35 | -> String -- ^ Screen name 36 | -> IO (Either String [SimpleTweet]) -- ^ Either an error or 37 | -- the list of tweets 38 | timeline oauth credential count excludeReplies username = do 39 | req <- createRequest username count excludeReplies 40 | res <- getResponse oauth credential req 41 | return $ 42 | case decodeTweets res of 43 | Left message -> Left message 44 | Right ts -> Right $ map (simplifyTweet . linkifyTweet) ts 45 | 46 | createRequest :: String -> Int -> Bool -> IO Request 47 | createRequest username count excludeReplies = parseUrlThrow $ timelineUrl username count excludeReplies 48 | #if (!MIN_VERSION_http_conduit(2,1,11)) 49 | where 50 | parseUrlThrow = parseUrl 51 | #endif 52 | 53 | getResponse :: OAuth -> Credential -> Request -> IO (Response BS.ByteString) 54 | getResponse oauth credential req = do 55 | m <- newManager defaultManagerSettings 56 | signedreq <- signOAuth oauth credential req 57 | httpLbs signedreq m 58 | 59 | decodeTweets :: Response BS.ByteString -> Either String [Tweet] 60 | decodeTweets = eitherDecode . responseBody 61 | 62 | -- | Returns the URL for requesting a user timeline. 63 | timelineUrl :: String -- ^ Screen name 64 | -> Int -- ^ Count 65 | -> Bool -- ^ Exclude replies? 66 | -> String -- ^ The URL for requesting the user timeline 67 | timelineUrl user count excludeReplies = 68 | "https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=" ++ 69 | user ++ "&count=" ++ show count ++ "&exclude_replies=" ++ 70 | map toLower (show excludeReplies) 71 | 72 | linkifyTweet :: Tweet -> Tweet 73 | linkifyTweet tweet = Tweet (processText (text tweet) 74 | (userEntities $ entities tweet) 75 | (urlEntities $ entities tweet) 76 | (mediaEntities $ entities tweet)) 77 | (createdAt tweet) 78 | (idStr tweet) 79 | (entities tweet) 80 | 81 | processText :: String -> [UserEntity] -> [URLEntity] -> [MediaEntity] -> String 82 | processText message users urls medias = foldr addLink message sortedLinks 83 | where sortedLinks = sortLinks urls users medias 84 | 85 | -- | Sort lists of URL, user mention, and media entities. 86 | sortLinks :: [URLEntity] -- ^ A list of URL entities 87 | -> [UserEntity] -- ^ A list of user mention entities 88 | -> [MediaEntity] -- ^ A list of media entities 89 | -> [Link] -- ^ The sorted list of links 90 | sortLinks urls users medias = sort (map makeURLLink urls ++ 91 | map makeUserLink users ++ 92 | map makeMediaLink medias) 93 | 94 | makeURLLink :: URLEntity -> Link 95 | makeURLLink urlEntity = Link x y url 96 | where (x, y) = urlIndices urlEntity 97 | urlText = displayUrl urlEntity 98 | href = urlMessage urlEntity 99 | url = "" 100 | ++ urlText ++ "" 101 | 102 | makeMediaLink :: MediaEntity -> Link 103 | makeMediaLink mediaEntity = Link x y url 104 | where (x, y) = mediaIndices mediaEntity 105 | urlText = displayMediaUrl mediaEntity 106 | href = mediaUrl mediaEntity 107 | url = "" 108 | ++ urlText ++ "" 109 | 110 | makeUserLink :: UserEntity -> Link 111 | makeUserLink userEntity = Link x y mention 112 | where (x, y) = userIndices userEntity 113 | username = screenName userEntity 114 | mention = "@" ++ username ++ "" 116 | 117 | simplifyTweet :: Tweet -> SimpleTweet 118 | simplifyTweet tweet = 119 | SimpleTweet { body = text tweet 120 | , tweetId = idStr tweet 121 | , created_at = createdAt tweet } 122 | 123 | -- | Add a link to the text of a tweet. 124 | addLink :: Link -- ^ A link 125 | -> String -- ^ The text of a tweet without the link 126 | -> String -- ^ The text of the tweet with the link 127 | addLink (Link 139 140 link) tweet = before ++ " " ++ link ++ after 128 | where before = fst (splitAt (lastBlank tweet) tweet) 129 | after = snd (splitAt 140 tweet) 130 | lastBlank = last . elemIndices ' ' 131 | addLink (Link start end link) tweet = before ++ link ++ after 132 | where before = fst (splitAt start tweet) 133 | after = snd (splitAt end tweet) 134 | -------------------------------------------------------------------------------- /src/Web/Twitter/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | 4 | --------------------------------------------------------- 5 | -- | 6 | -- Module : Web.Twitter.Types 7 | -- License : MIT (see the file LICENSE) 8 | -- Maintainer : Justin Leitgeb 9 | -- 10 | -- Types for working with the twitter API. 11 | --------------------------------------------------------- 12 | module Web.Twitter.Types 13 | ( Tweet(..) 14 | , SimpleTweet(..) 15 | , Link(..) 16 | , Entities(..) 17 | , URLEntity(..) 18 | , UserEntity(..) 19 | , MediaEntity(..) 20 | ) where 21 | 22 | import Control.Applicative ((<*>), (<$>)) 23 | import Data.Aeson 24 | import Data.Aeson.Types 25 | import Data.Ord (comparing) 26 | import GHC.Generics (Generic) 27 | 28 | -- | Represents tweet start and end index 29 | type BoundingIndices = (Int, Int) 30 | 31 | -- | Represents a tweet as HTML 32 | data SimpleTweet = SimpleTweet 33 | { body :: String -- ^ Tweet content as HTML 34 | , tweetId :: String -- ^ Tweet id 35 | , created_at :: String -- ^ Tweet created at date 36 | } deriving (Show, Generic) 37 | 38 | -- | Represent a tweet as plain text 39 | data Tweet = Tweet 40 | { text :: String -- ^ Tweet content as plain text 41 | , createdAt :: String -- ^ Tweet created at date 42 | , idStr :: String -- ^ Tweet id 43 | , entities :: Entities -- ^ All references mentioned on the tweet 44 | } deriving (Show, Generic) 45 | 46 | -- | Represent a list of all tweet references 47 | data Entities = Entities 48 | { userEntities :: [UserEntity] -- ^ List of all user references mentioned on the tweet 49 | , urlEntities :: [URLEntity] -- ^ List of all urls references mentioned on the tweet 50 | , mediaEntities :: [MediaEntity] -- ^ List of all media references mentioned on the tweet 51 | } deriving (Show, Generic) 52 | 53 | -- | Represent a user reference 54 | data UserEntity = UserEntity 55 | { screenName :: String -- ^ User twitter name 56 | , userIndices :: BoundingIndices -- ^ Tweet start and end index 57 | } deriving (Show, Generic) 58 | 59 | -- | Represent a url reference 60 | data URLEntity = URLEntity 61 | { urlMessage :: String -- ^ Reference title 62 | , urlIndices :: BoundingIndices -- ^ Tweet start and end index 63 | , displayUrl :: String -- ^ Reference url 64 | } deriving (Show, Generic) 65 | 66 | -- | Represent a media reference 67 | data MediaEntity = MediaEntity 68 | { mediaUrl :: String -- ^ Reference url 69 | , mediaIndices :: BoundingIndices -- ^ Tweet start and end index 70 | , displayMediaUrl :: String -- ^ Reference title 71 | } deriving (Show, Generic) 72 | 73 | -- | Represent a link as HTML 74 | data Link = Link 75 | { startIndex :: Int -- ^ Tweet start index 76 | , endIndex :: Int -- ^ Tweet end index 77 | , newHtml :: String -- ^ Link as HTML 78 | } deriving (Show, Eq) 79 | 80 | instance ToJSON SimpleTweet 81 | 82 | instance ToJSON Tweet 83 | instance FromJSON Tweet where 84 | parseJSON = withObject "Tweet" parseTweet 85 | 86 | instance ToJSON Entities 87 | instance FromJSON Entities where 88 | parseJSON = withObject "Entities" parseEntities 89 | 90 | instance ToJSON UserEntity 91 | instance FromJSON UserEntity where 92 | parseJSON = withObject "UserEntity" parseUserEntity 93 | 94 | instance ToJSON URLEntity 95 | instance FromJSON URLEntity where 96 | parseJSON = withObject "URLEntity" parseURLEntity 97 | 98 | instance ToJSON MediaEntity 99 | instance FromJSON MediaEntity where 100 | parseJSON = withObject "MediaEntity" parseMediaEntity 101 | 102 | instance Ord Link where 103 | compare = comparing startIndex 104 | 105 | parseTweet :: Object -> Parser Tweet 106 | parseTweet v = Tweet <$> v .: "text" 107 | <*> v .: "created_at" 108 | <*> v .: "id_str" 109 | <*> v .: "entities" 110 | 111 | parseUserEntity :: Object -> Parser UserEntity 112 | parseUserEntity v = UserEntity <$> v .: "screen_name" 113 | <*> v .: "indices" 114 | 115 | parseURLEntity :: Object -> Parser URLEntity 116 | parseURLEntity v = URLEntity <$> v .: "url" 117 | <*> v .: "indices" 118 | <*> v .: "display_url" 119 | 120 | parseMediaEntity :: Object -> Parser MediaEntity 121 | parseMediaEntity v = MediaEntity <$> v .: "url" 122 | <*> v .: "indices" 123 | <*> v .: "display_url" 124 | 125 | parseEntities :: Object -> Parser Entities 126 | parseEntities v = Entities <$> v .:? "user_mentions" .!= [] 127 | <*> v .:? "urls" .!= [] 128 | <*> v .:? "media" .!= [] 129 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-7.14 2 | -------------------------------------------------------------------------------- /test/Suite.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Test.Framework (defaultMain) 4 | 5 | import qualified Web.Twitter.Feed.Tests 6 | 7 | main :: IO () 8 | main = defaultMain 9 | [ Web.Twitter.Feed.Tests.tests ] 10 | -------------------------------------------------------------------------------- /test/Web/Twitter/Feed/Tests.hs: -------------------------------------------------------------------------------- 1 | module Web.Twitter.Feed.Tests where 2 | 3 | import Test.HUnit hiding (Test) 4 | import Test.Framework.Providers.HUnit (testCase) 5 | import Test.Framework (Test, testGroup) 6 | 7 | import Web.Twitter.Feed 8 | import Web.Twitter.Types 9 | 10 | addLinkTest :: Assertion 11 | addLinkTest = addLink (Link 8 8 "not ") "this is a test" @?= "this is not a test" 12 | 13 | addLinkEllipsisTest :: Assertion 14 | addLinkEllipsisTest = addLink (Link 139 140 "link") tweet @?= expected 15 | where 16 | tweet = replicate 130 'a' ++ " http://ww" 17 | expected = replicate 130 'a' ++ " link" 18 | 19 | timelineUrlTest :: Assertion 20 | timelineUrlTest = timelineUrl "someone" 3 True @?= expected 21 | where 22 | expected = "https://api.twitter.com/1.1/statuses/user_timeline.json" ++ 23 | "?screen_name=someone&count=3&exclude_replies=true" 24 | 25 | sortLinkTest :: Assertion 26 | sortLinkTest = 27 | sortLinks urlEntitiesExamples userEntitiesExamples mediaEntitiesExamples 28 | @?= sortedLinks 29 | 30 | tests :: Test 31 | tests = testGroup "Web.Twitter.Feed" 32 | [ testCase "addLink" addLinkTest 33 | , testCase "addLinkEllipsis" addLinkEllipsisTest 34 | , testCase "timelineUrl" timelineUrlTest 35 | , testCase "sortLinks" sortLinkTest 36 | ] 37 | 38 | sortedLinks :: [Link] 39 | sortedLinks = 40 | [ Link 2 12 "t.co/g" 41 | , Link 13 20 "\ 42 | \@Hackage" 43 | , Link 20 39 "t.co/s" 44 | , Link 50 62 "\ 45 | \@StackBuilders" 46 | , Link 70 92 "\ 47 | \pic.twitter.com/xhzEs8NuQa" 48 | ] 49 | 50 | urlEntitiesExamples :: [URLEntity] 51 | urlEntitiesExamples = 52 | [ URLEntity "google.com" (2, 12) "t.co/g" 53 | , URLEntity "stackbuilders.com" (20, 39) "t.co/s" 54 | ] 55 | 56 | userEntitiesExamples :: [UserEntity] 57 | userEntitiesExamples = 58 | [ UserEntity "Hackage" (13, 20) 59 | , UserEntity "StackBuilders" (50, 62) 60 | ] 61 | 62 | mediaEntitiesExamples :: [MediaEntity] 63 | mediaEntitiesExamples = 64 | [ MediaEntity "http://t.co/xhzEs8NuQa" (70, 92) "pic.twitter.com/xhzEs8NuQa" ] 65 | -------------------------------------------------------------------------------- /twitter-feed.cabal: -------------------------------------------------------------------------------- 1 | name: twitter-feed 2 | version: 0.2.0.11 3 | synopsis: Client for fetching Twitter timeline via Oauth 4 | description: Fetches a user timeline from Twitter, and optionally linkifies the results using the Twitter entity API. 5 | homepage: https://github.com/stackbuilders/twitter-feed 6 | license: MIT 7 | license-file: LICENSE 8 | author: Justin Leitgeb, Andrés Torres 9 | maintainer: justin@stackbuilders.com 10 | category: Web 11 | build-type: Simple 12 | extra-source-files: 13 | cabal-version: >=1.10 14 | 15 | source-repository head 16 | type: git 17 | location: https://github.com/stackbuilders/twitter-feed.git 18 | 19 | library 20 | exposed-modules: Web.Twitter.Feed 21 | Web.Twitter.Types 22 | ghc-options: -Wall 23 | 24 | build-depends: 25 | base >= 4.6 && < 4.10, 26 | aeson >= 0.8 && < 1.3, 27 | authenticate-oauth >= 1.5 && < 1.7, 28 | http-conduit >= 2.1.4 && < 2.3, 29 | bytestring >= 0.10 && < 0.11 30 | 31 | hs-source-dirs: src 32 | default-language: Haskell2010 33 | 34 | test-suite twitter-library 35 | type: exitcode-stdio-1.0 36 | hs-source-dirs: test 37 | main-is: Suite.hs 38 | build-depends: 39 | base, 40 | twitter-feed, 41 | containers >= 0.5 && < 0.6, 42 | HUnit >= 1.2 && < 1.7, 43 | test-framework >= 0.8 && < 0.9, 44 | test-framework-hunit >= 0.3 && < 0.4 45 | 46 | default-language: Haskell2010 47 | ghc-options: -Wall 48 | other-modules: 49 | Web.Twitter.Feed.Tests 50 | --------------------------------------------------------------------------------