add functions/vendor files

This commit is contained in:
Reed Allman
2017-06-11 02:05:36 -07:00
parent 6ee9c1fa0a
commit f2c7aa5ee6
7294 changed files with 1629834 additions and 0 deletions

2
vendor/github.com/dghubble/go-twitter/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
/coverage
/bin

11
vendor/github.com/dghubble/go-twitter/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,11 @@
language: go
go:
- 1.6
- 1.7
- 1.8
- tip
install:
- go get github.com/golang/lint/golint
- go get -v -t ./twitter
script:
- ./test

21
vendor/github.com/dghubble/go-twitter/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Dalton Hubble
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.

270
vendor/github.com/dghubble/go-twitter/README.md generated vendored Normal file
View File

@@ -0,0 +1,270 @@
# go-twitter [![Build Status](https://travis-ci.org/dghubble/go-twitter.png)](https://travis-ci.org/dghubble/go-twitter) [![GoDoc](https://godoc.org/github.com/dghubble/go-twitter?status.png)](https://godoc.org/github.com/dghubble/go-twitter)
<img align="right" src="https://storage.googleapis.com/dghubble/gopher-on-bird.png">
go-twitter is a Go client library for the [Twitter API](https://dev.twitter.com/rest/public). Check the [usage](#usage) section or try the [examples](/examples) to see how to access the Twitter API.
### Features
* Twitter REST API:
* Accounts
* Direct Messages
* Favorites
* Friends
* Friendships
* Followers
* Search
* Statuses
* Timelines
* Users
* Twitter Streaming API
* Public Streams
* User Streams
* Site Streams
* Firehose Streams
## Install
go get github.com/dghubble/go-twitter/twitter
## Documentation
Read [GoDoc](https://godoc.org/github.com/dghubble/go-twitter/twitter)
## Usage
### REST API
The `twitter` package provides a `Client` for accessing the Twitter API. Here are some example requests.
```go
config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("accessToken", "accessSecret")
httpClient := config.Client(oauth1.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
// Home Timeline
tweets, resp, err := client.Timelines.HomeTimeline(&twitter.HomeTimelineParams{
Count: 20,
})
// Send a Tweet
tweet, resp, err := client.Statuses.Update("just setting up my twttr", nil)
// Status Show
tweet, resp, err := client.Statuses.Show(585613041028431872, nil)
// Search Tweets
search, resp, err := client.Search.Tweets(&twitter.SearchTweetParams{
Query: "gopher",
})
// User Show
user, resp, err := client.Users.Show(&twitter.UserShowParams{
ScreenName: "dghubble",
})
// Followers
followers, resp, err := client.Followers.List(&twitter.FollowerListParams{})
```
Authentication is handled by the `http.Client` passed to `NewClient` to handle user auth (OAuth1) or application auth (OAuth2). See the [Authentication](#authentication) section.
Required parameters are passed as positional arguments. Optional parameters are passed typed params structs (or nil).
## Streaming API
The Twitter Public, User, Site, and Firehose Streaming APIs can be accessed through the `Client` `StreamService` which provides methods `Filter`, `Sample`, `User`, `Site`, and `Firehose`.
Create a `Client` with an authenticated `http.Client`. All stream endpoints require a user auth context so choose an OAuth1 `http.Client`.
client := twitter.NewClient(httpClient)
Next, request a managed `Stream` be started.
#### Filter
Filter Streams return Tweets that match one or more filtering predicates such as `Track`, `Follow`, and `Locations`.
```go
params := &twitter.StreamFilterParams{
Track: []string{"kitten"},
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.Filter(params)
```
#### User
User Streams provide messages specific to the authenticate User and possibly those they follow.
```go
params := &twitter.StreamUserParams{
With: "followings",
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.User(params)
```
*Note* To see Direct Message events, your consumer application must ask Users for read/write/DM access to their account.
#### Sample
Sample Streams return a small sample of public Tweets.
```go
params := &twitter.StreamSampleParams{
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.Sample(params)
```
#### Site, Firehose
Site and Firehose Streams require your application to have special permissions, but their API works the same way.
### Receiving Messages
Each `Stream` maintains the connection to the Twitter Streaming API endpoint, receives messages, and sends them on the `Stream.Messages` channel.
Go channels support range iterations which allow you to read the messages which are of type `interface{}`.
```go
for message := range stream.Messages {
fmt.Println(message)
}
```
If you run this in your main goroutine, it will receive messages forever unless the stream stops. To continue execution, receive messages using a separate goroutine.
### Demux
Receiving messages of type `interface{}` isn't very nice, it means you'll have to type switch and probably filter out message types you don't care about.
For this, try a `Demux`, like `SwitchDemux`, which receives messages and type switches them to call functions with typed messages.
For example, say you're only interested in Tweets and Direct Messages.
```go
demux := twitter.NewSwitchDemux()
demux.Tweet = func(tweet *twitter.Tweet) {
fmt.Println(tweet.Text)
}
demux.DM = func(dm *twitter.DirectMessage) {
fmt.Println(dm.SenderID)
}
```
Pass the `Demux` each message or give it the entire `Stream.Message` channel.
```go
for message := range stream.Messages {
demux.Handle(message)
}
// or pass the channel
demux.HandleChan(stream.Messages)
```
### Stopping
The `Stream` will stop itself if the stream disconnects and retrying produces unrecoverable errors. When this occurs, `Stream` will close the `stream.Messages` channel, so execution will break out of any message *for range* loops.
When you are finished receiving from a `Stream`, call `Stop()` which closes the connection, channels, and stops the goroutine **before** returning. This ensures resources are properly cleaned up.
### Pitfalls
**Bad**: In this example, `Stop()` is unlikely to be reached. Control stays in the message loop unless the `Stream` becomes disconnected and cannot retry.
```go
// program does not terminate :(
stream, _ := client.Streams.Sample(params)
for message := range stream.Messages {
demux.Handle(message)
}
stream.Stop()
```
**Bad**: Here, messages are received on a non-main goroutine, but then `Stop()` is called immediately. The `Stream` is stopped and the program exits.
```go
// got no messages :(
stream, _ := client.Streams.Sample(params)
go demux.HandleChan(stream.Messages)
stream.Stop()
```
**Good**: For main package scripts, one option is to receive messages in a goroutine and wait for CTRL-C to be pressed, then explicitly stop the `Stream`.
```go
stream, err := client.Streams.Sample(params)
go demux.HandleChan(stream.Messages)
// Wait for SIGINT and SIGTERM (HIT CTRL-C)
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
log.Println(<-ch)
stream.Stop()
```
## Authentication
The API client accepts an any `http.Client` capable of making user auth (OAuth1) or application auth (OAuth2) authorized requests. See the [dghubble/oauth1](https://github.com/dghubble/oauth1) and [golang/oauth2](https://github.com/golang/oauth2/) packages which can provide such agnostic clients.
Passing an `http.Client` directly grants you control over the underlying transport, avoids dependencies on particular OAuth1 or OAuth2 packages, and keeps client APIs separate from authentication protocols.
See the [google/go-github](https://github.com/google/go-github) client which takes the same approach.
For example, make requests as a consumer application on behalf of a user who has granted access, with OAuth1.
```go
// OAuth1
import (
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("accessToken", "accessSecret")
// http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
```
If no user auth context is needed, make requests as your application with application auth.
```go
// OAuth2
import (
"github.com/dghubble/go-twitter/twitter"
"golang.org/x/oauth2"
)
config := &oauth2.Config{}
token := &oauth2.Token{AccessToken: accessToken}
// http.Client will automatically authorize Requests
httpClient := config.Client(oauth2.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
```
To implement Login with Twitter for web or mobile, see the gologin [package](https://github.com/dghubble/gologin) and [examples](https://github.com/dghubble/gologin/tree/master/examples/twitter).
## Roadmap
* Support gzipped streams
* Auto-stop streams in the event of long stalls
## Contributing
See the [Contributing Guide](https://gist.github.com/dghubble/be682c123727f70bcfe7).
## License
[MIT License](LICENSE)

View File

@@ -0,0 +1,42 @@
# Examples
Get the dependencies and examples
cd examples
go get .
## User Auth (OAuth1)
A user access token (OAuth1) grants a consumer application access to a user's Twitter resources.
Setup an OAuth1 `http.Client` with the consumer key and secret and oauth token and secret.
export TWITTER_CONSUMER_KEY=xxx
export TWITTER_CONSUMER_SECRET=xxx
export TWITTER_ACCESS_TOKEN=xxx
export TWITTER_ACCESS_SECRET=xxx
To make requests as an application, on behalf of a user, create a `twitter` `Client` to get the home timeline, mention timeline, and more (example will **not** post Tweets).
go run user-auth.go
## App Auth (OAuth2)
An application access token (OAuth2) allows an application to make Twitter API requests for public content, with rate limits counting against the app itself. App auth requests can be made to API endpoints which do not require a user context.
Setup an OAuth2 `http.Client` with the Twitter application access token.
export TWITTER_APP_ACCESS_TOKEN=xxx
To make requests as an application, create a `twitter` `Client` and get public Tweets or timelines or other public content.
go run app-auth.go
## Streaming API
A user access token (OAuth1) is required for Streaming API requests. See above.
go run streaming.go
Hit CTRL-C to stop streaming. Uncomment different examples in code to try different streams.

View File

@@ -0,0 +1,71 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/coreos/pkg/flagutil"
"github.com/dghubble/go-twitter/twitter"
"golang.org/x/oauth2"
)
func main() {
flags := flag.NewFlagSet("app-auth", flag.ExitOnError)
accessToken := flags.String("app-access-token", "", "Twitter Application Access Token")
flags.Parse(os.Args[1:])
flagutil.SetFlagsFromEnv(flags, "TWITTER")
if *accessToken == "" {
log.Fatal("Application Access Token required")
}
config := &oauth2.Config{}
token := &oauth2.Token{AccessToken: *accessToken}
// OAuth2 http.Client will automatically authorize Requests
httpClient := config.Client(oauth2.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
// user show
userShowParams := &twitter.UserShowParams{ScreenName: "golang"}
user, _, _ := client.Users.Show(userShowParams)
fmt.Printf("USERS SHOW:\n%+v\n", user)
// users lookup
userLookupParams := &twitter.UserLookupParams{ScreenName: []string{"golang", "gophercon"}}
users, _, _ := client.Users.Lookup(userLookupParams)
fmt.Printf("USERS LOOKUP:\n%+v\n", users)
// status show
statusShowParams := &twitter.StatusShowParams{}
tweet, _, _ := client.Statuses.Show(584077528026849280, statusShowParams)
fmt.Printf("STATUSES SHOW:\n%+v\n", tweet)
// statuses lookup
statusLookupParams := &twitter.StatusLookupParams{ID: []int64{20}, TweetMode: "extended"}
tweets, _, _ := client.Statuses.Lookup([]int64{573893817000140800}, statusLookupParams)
fmt.Printf("STATUSES LOOKUP:\n%+v\n", tweets)
// oEmbed status
statusOembedParams := &twitter.StatusOEmbedParams{ID: 691076766878691329, MaxWidth: 500}
oembed, _, _ := client.Statuses.OEmbed(statusOembedParams)
fmt.Printf("OEMBED TWEET:\n%+v\n", oembed)
// user timeline
userTimelineParams := &twitter.UserTimelineParams{ScreenName: "golang", Count: 2}
tweets, _, _ = client.Timelines.UserTimeline(userTimelineParams)
fmt.Printf("USER TIMELINE:\n%+v\n", tweets)
// search tweets
searchTweetParams := &twitter.SearchTweetParams{
Query: "happy birthday",
TweetMode: "extended",
Count: 3,
}
search, _, _ := client.Search.Tweets(searchTweetParams)
fmt.Printf("SEARCH TWEETS:\n%+v\n", search)
fmt.Printf("SEARCH METADATA:\n%+v\n", search.Metadata)
}

View File

@@ -0,0 +1,91 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/coreos/pkg/flagutil"
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
func main() {
flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
consumerKey := flags.String("consumer-key", "", "Twitter Consumer Key")
consumerSecret := flags.String("consumer-secret", "", "Twitter Consumer Secret")
accessToken := flags.String("access-token", "", "Twitter Access Token")
accessSecret := flags.String("access-secret", "", "Twitter Access Secret")
flags.Parse(os.Args[1:])
flagutil.SetFlagsFromEnv(flags, "TWITTER")
if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" {
log.Fatal("Consumer key/secret and Access token/secret required")
}
config := oauth1.NewConfig(*consumerKey, *consumerSecret)
token := oauth1.NewToken(*accessToken, *accessSecret)
// OAuth1 http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// Twitter Client
client := twitter.NewClient(httpClient)
// Convenience Demux demultiplexed stream messages
demux := twitter.NewSwitchDemux()
demux.Tweet = func(tweet *twitter.Tweet) {
fmt.Println(tweet.Text)
}
demux.DM = func(dm *twitter.DirectMessage) {
fmt.Println(dm.SenderID)
}
demux.Event = func(event *twitter.Event) {
fmt.Printf("%#v\n", event)
}
fmt.Println("Starting Stream...")
// FILTER
filterParams := &twitter.StreamFilterParams{
Track: []string{"cat"},
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.Filter(filterParams)
if err != nil {
log.Fatal(err)
}
// USER (quick test: auth'd user likes a tweet -> event)
// userParams := &twitter.StreamUserParams{
// StallWarnings: twitter.Bool(true),
// With: "followings",
// Language: []string{"en"},
// }
// stream, err := client.Streams.User(userParams)
// if err != nil {
// log.Fatal(err)
// }
// SAMPLE
// sampleParams := &twitter.StreamSampleParams{
// StallWarnings: twitter.Bool(true),
// }
// stream, err := client.Streams.Sample(sampleParams)
// if err != nil {
// log.Fatal(err)
// }
// Receive messages until stopped or stream quits
go demux.HandleChan(stream.Messages)
// Wait for SIGINT and SIGTERM (HIT CTRL-C)
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
log.Println(<-ch)
fmt.Println("Stopping Stream...")
stream.Stop()
}

View File

@@ -0,0 +1,70 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/coreos/pkg/flagutil"
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
func main() {
flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
consumerKey := flags.String("consumer-key", "", "Twitter Consumer Key")
consumerSecret := flags.String("consumer-secret", "", "Twitter Consumer Secret")
accessToken := flags.String("access-token", "", "Twitter Access Token")
accessSecret := flags.String("access-secret", "", "Twitter Access Secret")
flags.Parse(os.Args[1:])
flagutil.SetFlagsFromEnv(flags, "TWITTER")
if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" {
log.Fatal("Consumer key/secret and Access token/secret required")
}
config := oauth1.NewConfig(*consumerKey, *consumerSecret)
token := oauth1.NewToken(*accessToken, *accessSecret)
// OAuth1 http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
// Verify Credentials
verifyParams := &twitter.AccountVerifyParams{
SkipStatus: twitter.Bool(true),
IncludeEmail: twitter.Bool(true),
}
user, _, _ := client.Accounts.VerifyCredentials(verifyParams)
fmt.Printf("User's ACCOUNT:\n%+v\n", user)
// Home Timeline
homeTimelineParams := &twitter.HomeTimelineParams{
Count: 2,
TweetMode: "extended",
}
tweets, _, _ := client.Timelines.HomeTimeline(homeTimelineParams)
fmt.Printf("User's HOME TIMELINE:\n%+v\n", tweets)
// Mention Timeline
mentionTimelineParams := &twitter.MentionTimelineParams{
Count: 2,
TweetMode: "extended",
}
tweets, _, _ = client.Timelines.MentionTimeline(mentionTimelineParams)
fmt.Printf("User's MENTION TIMELINE:\n%+v\n", tweets)
// Retweets of Me Timeline
retweetTimelineParams := &twitter.RetweetsOfMeTimelineParams{
Count: 2,
TweetMode: "extended",
}
tweets, _, _ = client.Timelines.RetweetsOfMeTimeline(retweetTimelineParams)
fmt.Printf("User's 'RETWEETS OF ME' TIMELINE:\n%+v\n", tweets)
// Update (POST!) Tweet (uncomment to run)
// tweet, _, _ := client.Statuses.Update("just setting up my twttr", nil)
// fmt.Printf("Posted Tweet\n%v\n", tweet)
}

23
vendor/github.com/dghubble/go-twitter/test generated vendored Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -e
PKGS=$(go list ./... | grep -v /examples)
FORMATTABLE="$(ls -d */)"
LINTABLE=$(go list ./...)
go test $PKGS -cover
go vet $PKGS
echo "Checking gofmt..."
fmtRes=$(gofmt -l $FORMATTABLE)
if [ -n "${fmtRes}" ]; then
echo -e "gofmt checking failed:\n${fmtRes}"
exit 2
fi
echo "Checking golint..."
lintRes=$(echo $LINTABLE | xargs -n 1 golint)
if [ -n "${lintRes}" ]; then
echo -e "golint checking failed:\n${lintRes}"
exit 2
fi

View File

@@ -0,0 +1,37 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// AccountService provides a method for account credential verification.
type AccountService struct {
sling *sling.Sling
}
// newAccountService returns a new AccountService.
func newAccountService(sling *sling.Sling) *AccountService {
return &AccountService{
sling: sling.Path("account/"),
}
}
// AccountVerifyParams are the params for AccountService.VerifyCredentials.
type AccountVerifyParams struct {
IncludeEntities *bool `url:"include_entities,omitempty"`
SkipStatus *bool `url:"skip_status,omitempty"`
IncludeEmail *bool `url:"include_email,omitempty"`
}
// VerifyCredentials returns the authorized user if credentials are valid and
// returns an error otherwise.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/account/verify_credentials
func (s *AccountService) VerifyCredentials(params *AccountVerifyParams) (*User, *http.Response, error) {
user := new(User)
apiError := new(APIError)
resp, err := s.sling.New().Get("verify_credentials.json").QueryStruct(params).Receive(user, apiError)
return user, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,27 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAccountService_VerifyCredentials(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/account/verify_credentials.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"include_entities": "false", "include_email": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"name": "Dalton Hubble", "id": 623265148}`)
})
client := NewClient(httpClient)
user, _, err := client.Accounts.VerifyCredentials(&AccountVerifyParams{IncludeEntities: Bool(false), IncludeEmail: Bool(true)})
expected := &User{Name: "Dalton Hubble", ID: 623265148}
assert.Nil(t, err)
assert.Equal(t, expected, user)
}

View File

@@ -0,0 +1,25 @@
package twitter
import (
"time"
"github.com/cenkalti/backoff"
)
func newExponentialBackOff() *backoff.ExponentialBackOff {
b := backoff.NewExponentialBackOff()
b.InitialInterval = 5 * time.Second
b.Multiplier = 2.0
b.MaxInterval = 320 * time.Second
b.Reset()
return b
}
func newAggressiveExponentialBackOff() *backoff.ExponentialBackOff {
b := backoff.NewExponentialBackOff()
b.InitialInterval = 1 * time.Minute
b.Multiplier = 2.0
b.MaxInterval = 16 * time.Minute
b.Reset()
return b
}

View File

@@ -0,0 +1,37 @@
package twitter
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewExponentialBackOff(t *testing.T) {
b := newExponentialBackOff()
assert.Equal(t, 5*time.Second, b.InitialInterval)
assert.Equal(t, 2.0, b.Multiplier)
assert.Equal(t, 320*time.Second, b.MaxInterval)
}
func TestNewAggressiveExponentialBackOff(t *testing.T) {
b := newAggressiveExponentialBackOff()
assert.Equal(t, 1*time.Minute, b.InitialInterval)
assert.Equal(t, 2.0, b.Multiplier)
assert.Equal(t, 16*time.Minute, b.MaxInterval)
}
// BackoffRecorder is an implementation of backoff.BackOff that records
// calls to NextBackOff and Reset for later inspection in tests.
type BackOffRecorder struct {
Count int
}
func (b *BackOffRecorder) NextBackOff() time.Duration {
b.Count++
return 1 * time.Nanosecond
}
func (b *BackOffRecorder) Reset() {
b.Count = 0
}

88
vendor/github.com/dghubble/go-twitter/twitter/demux.go generated vendored Normal file
View File

@@ -0,0 +1,88 @@
package twitter
// A Demux receives interface{} messages individually or from a channel and
// sends those messages to one or more outputs determined by the
// implementation.
type Demux interface {
Handle(message interface{})
HandleChan(messages <-chan interface{})
}
// SwitchDemux receives messages and uses a type switch to send each typed
// message to a handler function.
type SwitchDemux struct {
All func(message interface{})
Tweet func(tweet *Tweet)
DM func(dm *DirectMessage)
StatusDeletion func(deletion *StatusDeletion)
LocationDeletion func(LocationDeletion *LocationDeletion)
StreamLimit func(limit *StreamLimit)
StatusWithheld func(statusWithheld *StatusWithheld)
UserWithheld func(userWithheld *UserWithheld)
StreamDisconnect func(disconnect *StreamDisconnect)
Warning func(warning *StallWarning)
FriendsList func(friendsList *FriendsList)
Event func(event *Event)
Other func(message interface{})
}
// NewSwitchDemux returns a new SwitchMux which has NoOp handler functions.
func NewSwitchDemux() SwitchDemux {
return SwitchDemux{
All: func(message interface{}) {},
Tweet: func(tweet *Tweet) {},
DM: func(dm *DirectMessage) {},
StatusDeletion: func(deletion *StatusDeletion) {},
LocationDeletion: func(LocationDeletion *LocationDeletion) {},
StreamLimit: func(limit *StreamLimit) {},
StatusWithheld: func(statusWithheld *StatusWithheld) {},
UserWithheld: func(userWithheld *UserWithheld) {},
StreamDisconnect: func(disconnect *StreamDisconnect) {},
Warning: func(warning *StallWarning) {},
FriendsList: func(friendsList *FriendsList) {},
Event: func(event *Event) {},
Other: func(message interface{}) {},
}
}
// Handle determines the type of a message and calls the corresponding receiver
// function with the typed message. All messages are passed to the All func.
// Messages with unmatched types are passed to the Other func.
func (d SwitchDemux) Handle(message interface{}) {
d.All(message)
switch msg := message.(type) {
case *Tweet:
d.Tweet(msg)
case *DirectMessage:
d.DM(msg)
case *StatusDeletion:
d.StatusDeletion(msg)
case *LocationDeletion:
d.LocationDeletion(msg)
case *StreamLimit:
d.StreamLimit(msg)
case *StatusWithheld:
d.StatusWithheld(msg)
case *UserWithheld:
d.UserWithheld(msg)
case *StreamDisconnect:
d.StreamDisconnect(msg)
case *StallWarning:
d.Warning(msg)
case *FriendsList:
d.FriendsList(msg)
case *Event:
d.Event(msg)
default:
d.Other(msg)
}
}
// HandleChan receives messages and calls the corresponding receiver function
// with the typed message. All messages are passed to the All func. Messages
// with unmatched type are passed to the Other func.
func (d SwitchDemux) HandleChan(messages <-chan interface{}) {
for message := range messages {
d.Handle(message)
}
}

View File

@@ -0,0 +1,135 @@
package twitter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDemux_Handle(t *testing.T) {
messages, expectedCounts := exampleMessages()
counts := &counter{}
demux := newCounterDemux(counts)
for _, message := range messages {
demux.Handle(message)
}
assert.Equal(t, expectedCounts, counts)
}
func TestDemux_HandleChan(t *testing.T) {
messages, expectedCounts := exampleMessages()
counts := &counter{}
demux := newCounterDemux(counts)
ch := make(chan interface{})
// stream messages into channel
go func() {
for _, msg := range messages {
ch <- msg
}
close(ch)
}()
// handle channel messages until exhausted
demux.HandleChan(ch)
assert.Equal(t, expectedCounts, counts)
}
// counter counts stream messages by type for testing.
type counter struct {
all int
tweet int
dm int
statusDeletion int
locationDeletion int
streamLimit int
statusWithheld int
userWithheld int
streamDisconnect int
stallWarning int
friendsList int
event int
other int
}
// newCounterDemux returns a Demux which counts message types.
func newCounterDemux(counter *counter) Demux {
demux := NewSwitchDemux()
demux.All = func(interface{}) {
counter.all++
}
demux.Tweet = func(*Tweet) {
counter.tweet++
}
demux.DM = func(*DirectMessage) {
counter.dm++
}
demux.StatusDeletion = func(*StatusDeletion) {
counter.statusDeletion++
}
demux.LocationDeletion = func(*LocationDeletion) {
counter.locationDeletion++
}
demux.StreamLimit = func(*StreamLimit) {
counter.streamLimit++
}
demux.StatusWithheld = func(*StatusWithheld) {
counter.statusWithheld++
}
demux.UserWithheld = func(*UserWithheld) {
counter.userWithheld++
}
demux.StreamDisconnect = func(*StreamDisconnect) {
counter.streamDisconnect++
}
demux.Warning = func(*StallWarning) {
counter.stallWarning++
}
demux.FriendsList = func(*FriendsList) {
counter.friendsList++
}
demux.Event = func(*Event) {
counter.event++
}
demux.Other = func(interface{}) {
counter.other++
}
return demux
}
// examples messages returns a test stream of messages and the expected
// counts of each message type.
func exampleMessages() (messages []interface{}, expectedCounts *counter) {
var (
tweet = &Tweet{}
dm = &DirectMessage{}
statusDeletion = &StatusDeletion{}
locationDeletion = &LocationDeletion{}
streamLimit = &StreamLimit{}
statusWithheld = &StatusWithheld{}
userWithheld = &UserWithheld{}
streamDisconnect = &StreamDisconnect{}
stallWarning = &StallWarning{}
friendsList = &FriendsList{}
event = &Event{}
otherA = func() {}
otherB = struct{}{}
)
messages = []interface{}{tweet, dm, statusDeletion, locationDeletion,
streamLimit, statusWithheld, userWithheld, streamDisconnect,
stallWarning, friendsList, event, otherA, otherB}
expectedCounts = &counter{
all: len(messages),
tweet: 1,
dm: 1,
statusDeletion: 1,
locationDeletion: 1,
streamLimit: 1,
statusWithheld: 1,
userWithheld: 1,
streamDisconnect: 1,
stallWarning: 1,
friendsList: 1,
event: 1,
other: 2,
}
return messages, expectedCounts
}

View File

@@ -0,0 +1,130 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// DirectMessage is a direct message to a single recipient.
type DirectMessage struct {
CreatedAt string `json:"created_at"`
Entities *Entities `json:"entities"`
ID int64 `json:"id"`
IDStr string `json:"id_str"`
Recipient *User `json:"recipient"`
RecipientID int64 `json:"recipient_id"`
RecipientScreenName string `json:"recipient_screen_name"`
Sender *User `json:"sender"`
SenderID int64 `json:"sender_id"`
SenderScreenName string `json:"sender_screen_name"`
Text string `json:"text"`
}
// DirectMessageService provides methods for accessing Twitter direct message
// API endpoints.
type DirectMessageService struct {
baseSling *sling.Sling
sling *sling.Sling
}
// newDirectMessageService returns a new DirectMessageService.
func newDirectMessageService(sling *sling.Sling) *DirectMessageService {
return &DirectMessageService{
baseSling: sling.New(),
sling: sling.Path("direct_messages/"),
}
}
// directMessageShowParams are the parameters for DirectMessageService.Show
type directMessageShowParams struct {
ID int64 `url:"id,omitempty"`
}
// Show returns the requested Direct Message.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/get/direct_messages/show
func (s *DirectMessageService) Show(id int64) (*DirectMessage, *http.Response, error) {
params := &directMessageShowParams{ID: id}
dm := new(DirectMessage)
apiError := new(APIError)
resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(dm, apiError)
return dm, resp, relevantError(err, *apiError)
}
// DirectMessageGetParams are the parameters for DirectMessageService.Get
type DirectMessageGetParams struct {
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
Count int `url:"count,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
SkipStatus *bool `url:"skip_status,omitempty"`
}
// Get returns recent Direct Messages received by the authenticated user.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/get/direct_messages
func (s *DirectMessageService) Get(params *DirectMessageGetParams) ([]DirectMessage, *http.Response, error) {
dms := new([]DirectMessage)
apiError := new(APIError)
resp, err := s.baseSling.New().Get("direct_messages.json").QueryStruct(params).Receive(dms, apiError)
return *dms, resp, relevantError(err, *apiError)
}
// DirectMessageSentParams are the parameters for DirectMessageService.Sent
type DirectMessageSentParams struct {
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
Count int `url:"count,omitempty"`
Page int `url:"page,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
}
// Sent returns recent Direct Messages sent by the authenticated user.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/get/direct_messages/sent
func (s *DirectMessageService) Sent(params *DirectMessageSentParams) ([]DirectMessage, *http.Response, error) {
dms := new([]DirectMessage)
apiError := new(APIError)
resp, err := s.sling.New().Get("sent.json").QueryStruct(params).Receive(dms, apiError)
return *dms, resp, relevantError(err, *apiError)
}
// DirectMessageNewParams are the parameters for DirectMessageService.New
type DirectMessageNewParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Text string `url:"text"`
}
// New sends a new Direct Message to a specified user as the authenticated
// user.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/post/direct_messages/new
func (s *DirectMessageService) New(params *DirectMessageNewParams) (*DirectMessage, *http.Response, error) {
dm := new(DirectMessage)
apiError := new(APIError)
resp, err := s.sling.New().Post("new.json").BodyForm(params).Receive(dm, apiError)
return dm, resp, relevantError(err, *apiError)
}
// DirectMessageDestroyParams are the parameters for DirectMessageService.Destroy
type DirectMessageDestroyParams struct {
ID int64 `url:"id,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
}
// Destroy deletes the Direct Message with the given id and returns it if
// successful.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/post/direct_messages/destroy
func (s *DirectMessageService) Destroy(id int64, params *DirectMessageDestroyParams) (*DirectMessage, *http.Response, error) {
if params == nil {
params = &DirectMessageDestroyParams{}
}
params.ID = id
dm := new(DirectMessage)
apiError := new(APIError)
resp, err := s.sling.New().Post("destroy.json").BodyForm(params).Receive(dm, apiError)
return dm, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,110 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
var (
testDM = DirectMessage{
ID: 240136858829479936,
Recipient: &User{ScreenName: "theSeanCook"},
Sender: &User{ScreenName: "s0c1alm3dia"},
Text: "hello world",
}
testDMIDStr = "240136858829479936"
testDMJSON = `{"id": 240136858829479936,"recipient": {"screen_name": "theSeanCook"},"sender": {"screen_name": "s0c1alm3dia"},"text": "hello world"}`
)
func TestDirectMessageService_Show(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages/show.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": testDMIDStr}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, testDMJSON)
})
client := NewClient(httpClient)
dms, _, err := client.DirectMessages.Show(testDM.ID)
assert.Nil(t, err)
assert.Equal(t, &testDM, dms)
}
func TestDirectMessageService_Get(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"since_id": "589147592367431680", "count": "1"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[`+testDMJSON+`]`)
})
client := NewClient(httpClient)
params := &DirectMessageGetParams{SinceID: 589147592367431680, Count: 1}
dms, _, err := client.DirectMessages.Get(params)
expected := []DirectMessage{testDM}
assert.Nil(t, err)
assert.Equal(t, expected, dms)
}
func TestDirectMessageService_Sent(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages/sent.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"since_id": "589147592367431680", "count": "1"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[`+testDMJSON+`]`)
})
client := NewClient(httpClient)
params := &DirectMessageSentParams{SinceID: 589147592367431680, Count: 1}
dms, _, err := client.DirectMessages.Sent(params)
expected := []DirectMessage{testDM}
assert.Nil(t, err)
assert.Equal(t, expected, dms)
}
func TestDirectMessageService_New(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages/new.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"screen_name": "theseancook", "text": "hello world"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, testDMJSON)
})
client := NewClient(httpClient)
params := &DirectMessageNewParams{ScreenName: "theseancook", Text: "hello world"}
dm, _, err := client.DirectMessages.New(params)
assert.Nil(t, err)
assert.Equal(t, &testDM, dm)
}
func TestDirectMessageService_Destroy(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages/destroy.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"id": testDMIDStr}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, testDMJSON)
})
client := NewClient(httpClient)
dm, _, err := client.DirectMessages.Destroy(testDM.ID, nil)
assert.Nil(t, err)
assert.Equal(t, &testDM, dm)
}

70
vendor/github.com/dghubble/go-twitter/twitter/doc.go generated vendored Normal file
View File

@@ -0,0 +1,70 @@
/*
Package twitter provides a Client for the Twitter API.
The twitter package provides a Client for accessing the Twitter API. Here are
some example requests.
// Twitter client
client := twitter.NewClient(httpClient)
// Home Timeline
tweets, resp, err := client.Timelines.HomeTimeline(&HomeTimelineParams{})
// Send a Tweet
tweet, resp, err := client.Statuses.Update("just setting up my twttr", nil)
// Status Show
tweet, resp, err := client.Statuses.Show(585613041028431872, nil)
// User Show
params := &twitter.UserShowParams{ScreenName: "dghubble"}
user, resp, err := client.Users.Show(params)
// Followers
followers, resp, err := client.Followers.List(&FollowerListParams{})
Required parameters are passed as positional arguments. Optional parameters
are passed in a typed params struct (or pass nil).
Authentication
By design, the Twitter Client accepts any http.Client so user auth (OAuth1) or
application auth (OAuth2) requests can be made by using the appropriate
authenticated client. Use the https://github.com/dghubble/oauth1 and
https://github.com/golang/oauth2 packages to obtain an http.Client which
transparently authorizes requests.
For example, make requests as a consumer application on behalf of a user who
has granted access, with OAuth1.
// OAuth1
import (
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("accessToken", "accessSecret")
// http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// twitter client
client := twitter.NewClient(httpClient)
If no user auth context is needed, make requests as your application with
application auth.
// OAuth2
import (
"github.com/dghubble/go-twitter/twitter"
"golang.org/x/oauth2"
)
config := &oauth2.Config{}
token := &oauth2.Token{AccessToken: accessToken}
// http.Client will automatically authorize Requests
httpClient := config.Client(oauth2.NoContext, token)
// twitter client
client := twitter.NewClient(httpClient)
To implement Login with Twitter, see https://github.com/dghubble/gologin.
*/
package twitter

View File

@@ -0,0 +1,104 @@
package twitter
// Entities represent metadata and context info parsed from Twitter components.
// https://dev.twitter.com/overview/api/entities
// TODO: symbols
type Entities struct {
Hashtags []HashtagEntity `json:"hashtags"`
Media []MediaEntity `json:"media"`
Urls []URLEntity `json:"urls"`
UserMentions []MentionEntity `json:"user_mentions"`
}
// HashtagEntity represents a hashtag which has been parsed from text.
type HashtagEntity struct {
Indices Indices `json:"indices"`
Text string `json:"text"`
}
// URLEntity represents a URL which has been parsed from text.
type URLEntity struct {
Indices Indices `json:"indices"`
DisplayURL string `json:"display_url"`
ExpandedURL string `json:"expanded_url"`
URL string `json:"url"`
}
// MediaEntity represents media elements associated with a Tweet.
type MediaEntity struct {
URLEntity
ID int64 `json:"id"`
IDStr string `json:"id_str"`
MediaURL string `json:"media_url"`
MediaURLHttps string `json:"media_url_https"`
SourceStatusID int64 `json:"source_status_id"`
SourceStatusIDStr string `json:"source_status_id_str"`
Type string `json:"type"`
Sizes MediaSizes `json:"sizes"`
VideoInfo VideoInfo `json:"video_info"`
}
// MentionEntity represents Twitter user mentions parsed from text.
type MentionEntity struct {
Indices Indices `json:"indices"`
ID int64 `json:"id"`
IDStr string `json:"id_str"`
Name string `json:"name"`
ScreenName string `json:"screen_name"`
}
// UserEntities contain Entities parsed from User url and description fields.
// https://dev.twitter.com/overview/api/entities-in-twitter-objects#users
type UserEntities struct {
URL Entities `json:"url"`
Description Entities `json:"description"`
}
// ExtendedEntity contains media information.
// https://dev.twitter.com/overview/api/entities-in-twitter-objects#extended_entities
type ExtendedEntity struct {
Media []MediaEntity `json:"media"`
}
// Indices represent the start and end offsets within text.
type Indices [2]int
// Start returns the index at which an entity starts, inclusive.
func (i Indices) Start() int {
return i[0]
}
// End returns the index at which an entity ends, exclusive.
func (i Indices) End() int {
return i[1]
}
// MediaSizes contain the different size media that are available.
// https://dev.twitter.com/overview/api/entities#obj-sizes
type MediaSizes struct {
Thumb MediaSize `json:"thumb"`
Large MediaSize `json:"large"`
Medium MediaSize `json:"medium"`
Small MediaSize `json:"small"`
}
// MediaSize describes the height, width, and resizing method used.
type MediaSize struct {
Width int `json:"w"`
Height int `json:"h"`
Resize string `json:"resize"`
}
// VideoInfo is available on video media objects.
type VideoInfo struct {
AspectRatio [2]int `json:"aspect_ratio"`
DurationMillis int `json:"duration_millis"`
Variants []VideoVariant `json:"variants"`
}
// VideoVariant describes one of the available video formats.
type VideoVariant struct {
ContentType string `json:"content_type"`
Bitrate int `json:"bitrate"`
URL string `json:"url"`
}

View File

@@ -0,0 +1,22 @@
package twitter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIndices(t *testing.T) {
cases := []struct {
pair Indices
expectedStart int
expectedEnd int
}{
{Indices{}, 0, 0},
{Indices{25, 47}, 25, 47},
}
for _, c := range cases {
assert.Equal(t, c.expectedStart, c.pair.Start())
assert.Equal(t, c.expectedEnd, c.pair.End())
}
}

View File

@@ -0,0 +1,47 @@
package twitter
import (
"fmt"
)
// APIError represents a Twitter API Error response
// https://dev.twitter.com/overview/api/response-codes
type APIError struct {
Errors []ErrorDetail `json:"errors"`
}
// ErrorDetail represents an individual item in an APIError.
type ErrorDetail struct {
Message string `json:"message"`
Code int `json:"code"`
}
func (e APIError) Error() string {
if len(e.Errors) > 0 {
err := e.Errors[0]
return fmt.Sprintf("twitter: %d %v", err.Code, err.Message)
}
return ""
}
// Empty returns true if empty. Otherwise, at least 1 error message/code is
// present and false is returned.
func (e APIError) Empty() bool {
if len(e.Errors) == 0 {
return true
}
return false
}
// relevantError returns any non-nil http-related error (creating the request,
// getting the response, decoding) if any. If the decoded apiError is non-zero
// the apiError is returned. Otherwise, no errors occurred, returns nil.
func relevantError(httpError error, apiError APIError) error {
if httpError != nil {
return httpError
}
if apiError.Empty() {
return nil
}
return apiError
}

View File

@@ -0,0 +1,48 @@
package twitter
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
var errAPI = APIError{
Errors: []ErrorDetail{
ErrorDetail{Message: "Status is a duplicate", Code: 187},
},
}
var errHTTP = fmt.Errorf("unknown host")
func TestAPIError_Error(t *testing.T) {
err := APIError{}
if assert.Error(t, err) {
assert.Equal(t, "", err.Error())
}
if assert.Error(t, errAPI) {
assert.Equal(t, "twitter: 187 Status is a duplicate", errAPI.Error())
}
}
func TestAPIError_Empty(t *testing.T) {
err := APIError{}
assert.True(t, err.Empty())
assert.False(t, errAPI.Empty())
}
func TestRelevantError(t *testing.T) {
cases := []struct {
httpError error
apiError APIError
expected error
}{
{nil, APIError{}, nil},
{nil, errAPI, errAPI},
{errHTTP, APIError{}, errHTTP},
{errHTTP, errAPI, errHTTP},
}
for _, c := range cases {
err := relevantError(c.httpError, c.apiError)
assert.Equal(t, c.expected, err)
}
}

View File

@@ -0,0 +1,72 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// FavoriteService provides methods for accessing Twitter favorite API endpoints.
//
// Note: the like action was known as favorite before November 3, 2015; the
// historical naming remains in API methods and object properties.
type FavoriteService struct {
sling *sling.Sling
}
// newFavoriteService returns a new FavoriteService.
func newFavoriteService(sling *sling.Sling) *FavoriteService {
return &FavoriteService{
sling: sling.Path("favorites/"),
}
}
// FavoriteListParams are the parameters for FavoriteService.List.
type FavoriteListParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// List returns liked Tweets from the specified user.
// https://dev.twitter.com/rest/reference/get/favorites/list
func (s *FavoriteService) List(params *FavoriteListParams) ([]Tweet, *http.Response, error) {
favorites := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(favorites, apiError)
return *favorites, resp, relevantError(err, *apiError)
}
// FavoriteCreateParams are the parameters for FavoriteService.Create.
type FavoriteCreateParams struct {
ID int64 `url:"id,omitempty"`
}
// Create favorites the specified tweet.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/favorites/create
func (s *FavoriteService) Create(params *FavoriteCreateParams) (*Tweet, *http.Response, error) {
tweet := new(Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Post("create.json").QueryStruct(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// FavoriteDestroyParams are the parameters for FavoriteService.Destroy.
type FavoriteDestroyParams struct {
ID int64 `url:"id,omitempty"`
}
// Destroy un-favorites the specified tweet.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/favorites/destroy
func (s *FavoriteService) Destroy(params *FavoriteDestroyParams) (*Tweet, *http.Response, error) {
tweet := new(Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Post("destroy.json").QueryStruct(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,65 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFavoriteService_List(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/favorites/list.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "113419064", "since_id": "101492475", "include_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "Gophercon talks!"}, {"text": "Why gophers are so adorable"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Favorites.List(&FavoriteListParams{UserID: 113419064, SinceID: 101492475, IncludeEntities: Bool(false)})
expected := []Tweet{Tweet{Text: "Gophercon talks!"}, Tweet{Text: "Why gophers are so adorable"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestFavoriteService_Create(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/favorites/create.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"id": "12345"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very informative tweet"}`)
})
client := NewClient(httpClient)
params := &FavoriteCreateParams{ID: 12345}
tweet, _, err := client.Favorites.Create(params)
assert.Nil(t, err)
expected := &Tweet{ID: 581980947630845953, Text: "very informative tweet"}
assert.Equal(t, expected, tweet)
}
func TestFavoriteService_Destroy(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/favorites/destroy.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"id": "12345"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very unhappy tweet"}`)
})
client := NewClient(httpClient)
params := &FavoriteDestroyParams{ID: 12345}
tweet, _, err := client.Favorites.Destroy(params)
assert.Nil(t, err)
expected := &Tweet{ID: 581980947630845953, Text: "very unhappy tweet"}
assert.Equal(t, expected, tweet)
}

View File

@@ -0,0 +1,73 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// FollowerIDs is a cursored collection of follower ids.
type FollowerIDs struct {
IDs []int64 `json:"ids"`
NextCursor int64 `json:"next_cursor"`
NextCursorStr string `json:"next_cursor_str"`
PreviousCursor int64 `json:"previous_cursor"`
PreviousCursorStr string `json:"previous_cursor_str"`
}
// Followers is a cursored collection of followers.
type Followers struct {
Users []User `json:"users"`
NextCursor int64 `json:"next_cursor"`
NextCursorStr string `json:"next_cursor_str"`
PreviousCursor int64 `json:"previous_cursor"`
PreviousCursorStr string `json:"previous_cursor_str"`
}
// FollowerService provides methods for accessing Twitter followers endpoints.
type FollowerService struct {
sling *sling.Sling
}
// newFollowerService returns a new FollowerService.
func newFollowerService(sling *sling.Sling) *FollowerService {
return &FollowerService{
sling: sling.Path("followers/"),
}
}
// FollowerIDParams are the parameters for FollowerService.Ids
type FollowerIDParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Cursor int64 `url:"cursor,omitempty"`
Count int `url:"count,omitempty"`
}
// IDs returns a cursored collection of user ids following the specified user.
// https://dev.twitter.com/rest/reference/get/followers/ids
func (s *FollowerService) IDs(params *FollowerIDParams) (*FollowerIDs, *http.Response, error) {
ids := new(FollowerIDs)
apiError := new(APIError)
resp, err := s.sling.New().Get("ids.json").QueryStruct(params).Receive(ids, apiError)
return ids, resp, relevantError(err, *apiError)
}
// FollowerListParams are the parameters for FollowerService.List
type FollowerListParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Cursor int64 `url:"cursor,omitempty"`
Count int `url:"count,omitempty"`
SkipStatus *bool `url:"skip_status,omitempty"`
IncludeUserEntities *bool `url:"include_user_entities,omitempty"`
}
// List returns a cursored collection of Users following the specified user.
// https://dev.twitter.com/rest/reference/get/followers/list
func (s *FollowerService) List(params *FollowerListParams) (*Followers, *http.Response, error) {
followers := new(Followers)
apiError := new(APIError)
resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(followers, apiError)
return followers, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,69 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFollowerService_Ids(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/followers/ids.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "623265148", "count": "5", "cursor": "1516933260114270762"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &FollowerIDs{
IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FollowerIDParams{
UserID: 623265148,
Count: 5,
Cursor: 1516933260114270762,
}
followerIDs, _, err := client.Followers.IDs(params)
assert.Nil(t, err)
assert.Equal(t, expected, followerIDs)
}
func TestFollowerService_List(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/followers/list.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"screen_name": "dghubble", "count": "5", "cursor": "1516933260114270762", "skip_status": "true", "include_user_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"users": [{"id": 123}], "next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &Followers{
Users: []User{User{ID: 123}},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FollowerListParams{
ScreenName: "dghubble",
Count: 5,
Cursor: 1516933260114270762,
SkipStatus: Bool(true),
IncludeUserEntities: Bool(false),
}
followers, _, err := client.Followers.List(params)
assert.Nil(t, err)
assert.Equal(t, expected, followers)
}

View File

@@ -0,0 +1,73 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// FriendIDs is a cursored collection of friend ids.
type FriendIDs struct {
IDs []int64 `json:"ids"`
NextCursor int64 `json:"next_cursor"`
NextCursorStr string `json:"next_cursor_str"`
PreviousCursor int64 `json:"previous_cursor"`
PreviousCursorStr string `json:"previous_cursor_str"`
}
// Friends is a cursored collection of friends.
type Friends struct {
Users []User `json:"users"`
NextCursor int64 `json:"next_cursor"`
NextCursorStr string `json:"next_cursor_str"`
PreviousCursor int64 `json:"previous_cursor"`
PreviousCursorStr string `json:"previous_cursor_str"`
}
// FriendService provides methods for accessing Twitter friends endpoints.
type FriendService struct {
sling *sling.Sling
}
// newFriendService returns a new FriendService.
func newFriendService(sling *sling.Sling) *FriendService {
return &FriendService{
sling: sling.Path("friends/"),
}
}
// FriendIDParams are the parameters for FriendService.Ids
type FriendIDParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Cursor int64 `url:"cursor,omitempty"`
Count int `url:"count,omitempty"`
}
// IDs returns a cursored collection of user ids that the specified user is following.
// https://dev.twitter.com/rest/reference/get/friends/ids
func (s *FriendService) IDs(params *FriendIDParams) (*FriendIDs, *http.Response, error) {
ids := new(FriendIDs)
apiError := new(APIError)
resp, err := s.sling.New().Get("ids.json").QueryStruct(params).Receive(ids, apiError)
return ids, resp, relevantError(err, *apiError)
}
// FriendListParams are the parameters for FriendService.List
type FriendListParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Cursor int64 `url:"cursor,omitempty"`
Count int `url:"count,omitempty"`
SkipStatus *bool `url:"skip_status,omitempty"`
IncludeUserEntities *bool `url:"include_user_entities,omitempty"`
}
// List returns a cursored collection of Users that the specified user is following.
// https://dev.twitter.com/rest/reference/get/friends/list
func (s *FriendService) List(params *FriendListParams) (*Friends, *http.Response, error) {
friends := new(Friends)
apiError := new(APIError)
resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(friends, apiError)
return friends, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,69 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFriendService_Ids(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friends/ids.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "623265148", "count": "5", "cursor": "1516933260114270762"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &FriendIDs{
IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FriendIDParams{
UserID: 623265148,
Count: 5,
Cursor: 1516933260114270762,
}
friendIDs, _, err := client.Friends.IDs(params)
assert.Nil(t, err)
assert.Equal(t, expected, friendIDs)
}
func TestFriendService_List(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friends/list.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"screen_name": "dghubble", "count": "5", "cursor": "1516933260114270762", "skip_status": "true", "include_user_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"users": [{"id": 123}], "next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &Friends{
Users: []User{User{ID: 123}},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FriendListParams{
ScreenName: "dghubble",
Count: 5,
Cursor: 1516933260114270762,
SkipStatus: Bool(true),
IncludeUserEntities: Bool(false),
}
friends, _, err := client.Friends.List(params)
assert.Nil(t, err)
assert.Equal(t, expected, friends)
}

View File

@@ -0,0 +1,134 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// FriendshipService provides methods for accessing Twitter friendship API
// endpoints.
type FriendshipService struct {
sling *sling.Sling
}
// newFriendshipService returns a new FriendshipService.
func newFriendshipService(sling *sling.Sling) *FriendshipService {
return &FriendshipService{
sling: sling.Path("friendships/"),
}
}
// FriendshipCreateParams are parameters for FriendshipService.Create
type FriendshipCreateParams struct {
ScreenName string `url:"screen_name,omitempty"`
UserID int64 `url:"user_id,omitempty"`
Follow *bool `url:"follow,omitempty"`
}
// Create creates a friendship to (i.e. follows) the specified user and
// returns the followed user.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/friendships/create
func (s *FriendshipService) Create(params *FriendshipCreateParams) (*User, *http.Response, error) {
user := new(User)
apiError := new(APIError)
resp, err := s.sling.New().Post("create.json").QueryStruct(params).Receive(user, apiError)
return user, resp, relevantError(err, *apiError)
}
// FriendshipShowParams are paramenters for FriendshipService.Show
type FriendshipShowParams struct {
SourceID int64 `url:"source_id,omitempty"`
SourceScreenName string `url:"source_screen_name,omitempty"`
TargetID int64 `url:"target_id,omitempty"`
TargetScreenName string `url:"target_screen_name,omitempty"`
}
// Show returns the relationship between two arbitrary users.
// Requires a user auth or an app context.
// https://dev.twitter.com/rest/reference/get/friendships/show
func (s *FriendshipService) Show(params *FriendshipShowParams) (*Relationship, *http.Response, error) {
response := new(RelationshipResponse)
apiError := new(APIError)
resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(response, apiError)
return response.Relationship, resp, relevantError(err, *apiError)
}
// RelationshipResponse contains a relationship.
type RelationshipResponse struct {
Relationship *Relationship `json:"relationship"`
}
// Relationship represents the relation between a source user and target user.
type Relationship struct {
Source RelationshipSource `json:"source"`
Target RelationshipTarget `json:"target"`
}
// RelationshipSource represents the source user.
type RelationshipSource struct {
ID int64 `json:"id"`
IDStr string `json:"id_str"`
ScreenName string `json:"screen_name"`
Following bool `json:"following"`
FollowedBy bool `json:"followed_by"`
CanDM bool `json:"can_dm"`
Blocking bool `json:"blocking"`
Muting bool `json:"muting"`
AllReplies bool `json:"all_replies"`
WantRetweets bool `json:"want_retweets"`
MarkedSpam bool `json:"marked_spam"`
NotificationsEnabled bool `json:"notifications_enabled"`
}
// RelationshipTarget represents the target user.
type RelationshipTarget struct {
ID int64 `json:"id"`
IDStr string `json:"id_str"`
ScreenName string `json:"screen_name"`
Following bool `json:"following"`
FollowedBy bool `json:"followed_by"`
}
// FriendshipDestroyParams are paramenters for FriendshipService.Destroy
type FriendshipDestroyParams struct {
ScreenName string `url:"screen_name,omitempty"`
UserID int64 `url:"user_id,omitempty"`
}
// Destroy destroys a friendship to (i.e. unfollows) the specified user and
// returns the unfollowed user.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/friendships/destroy
func (s *FriendshipService) Destroy(params *FriendshipDestroyParams) (*User, *http.Response, error) {
user := new(User)
apiError := new(APIError)
resp, err := s.sling.New().Post("destroy.json").QueryStruct(params).Receive(user, apiError)
return user, resp, relevantError(err, *apiError)
}
// FriendshipPendingParams are paramenters for FriendshipService.Outgoing
type FriendshipPendingParams struct {
Cursor int64 `url:"cursor,omitempty"`
}
// Outgoing returns a collection of numeric IDs for every protected user for whom the authenticating
// user has a pending follow request.
// https://dev.twitter.com/rest/reference/get/friendships/outgoing
func (s *FriendshipService) Outgoing(params *FriendshipPendingParams) (*FriendIDs, *http.Response, error) {
ids := new(FriendIDs)
apiError := new(APIError)
resp, err := s.sling.New().Get("outgoing.json").QueryStruct(params).Receive(ids, apiError)
return ids, resp, relevantError(err, *apiError)
}
// Incoming returns a collection of numeric IDs for every user who has a pending request to
// follow the authenticating user.
// https://dev.twitter.com/rest/reference/get/friendships/incoming
func (s *FriendshipService) Incoming(params *FriendshipPendingParams) (*FriendIDs, *http.Response, error) {
ids := new(FriendIDs)
apiError := new(APIError)
resp, err := s.sling.New().Get("incoming.json").QueryStruct(params).Receive(ids, apiError)
return ids, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,123 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFriendshipService_Create(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/create.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"user_id": "12345"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 12345, "name": "Doug Williams"}`)
})
client := NewClient(httpClient)
params := &FriendshipCreateParams{UserID: 12345}
user, _, err := client.Friendships.Create(params)
assert.Nil(t, err)
expected := &User{ID: 12345, Name: "Doug Williams"}
assert.Equal(t, expected, user)
}
func TestFriendshipService_Show(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/show.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"source_screen_name": "foo", "target_screen_name": "bar"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{ "relationship": { "source": { "can_dm": false, "muting": true, "id_str": "8649302", "id": 8649302, "screen_name": "foo"}, "target": { "id_str": "12148", "id": 12148, "screen_name": "bar", "following": true, "followed_by": false } } }`)
})
client := NewClient(httpClient)
params := &FriendshipShowParams{SourceScreenName: "foo", TargetScreenName: "bar"}
relationship, _, err := client.Friendships.Show(params)
assert.Nil(t, err)
expected := &Relationship{
Source: RelationshipSource{ID: 8649302, ScreenName: "foo", IDStr: "8649302", CanDM: false, Muting: true, WantRetweets: false},
Target: RelationshipTarget{ID: 12148, ScreenName: "bar", IDStr: "12148", Following: true, FollowedBy: false},
}
assert.Equal(t, expected, relationship)
}
func TestFriendshipService_Destroy(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/destroy.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"user_id": "12345"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 12345, "name": "Doug Williams"}`)
})
client := NewClient(httpClient)
params := &FriendshipDestroyParams{UserID: 12345}
user, _, err := client.Friendships.Destroy(params)
assert.Nil(t, err)
expected := &User{ID: 12345, Name: "Doug Williams"}
assert.Equal(t, expected, user)
}
func TestFriendshipService_Outgoing(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/outgoing.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"cursor": "1516933260114270762"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &FriendIDs{
IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FriendshipPendingParams{
Cursor: 1516933260114270762,
}
friendIDs, _, err := client.Friendships.Outgoing(params)
assert.Nil(t, err)
assert.Equal(t, expected, friendIDs)
}
func TestFriendshipService_Incoming(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/incoming.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"cursor": "1516933260114270762"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &FriendIDs{
IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FriendshipPendingParams{
Cursor: 1516933260114270762,
}
friendIDs, _, err := client.Friendships.Incoming(params)
assert.Nil(t, err)
assert.Equal(t, expected, friendIDs)
}

View File

@@ -0,0 +1,62 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// Search represents the result of a Tweet search.
type Search struct {
Statuses []Tweet `json:"statuses"`
Metadata *SearchMetadata `json:"search_metadata"`
}
// SearchMetadata describes a Search result.
type SearchMetadata struct {
Count int `json:"count"`
SinceID int64 `json:"since_id"`
SinceIDStr string `json:"since_id_str"`
MaxID int64 `json:"max_id"`
MaxIDStr string `json:"max_id_str"`
RefreshURL string `json:"refresh_url"`
NextResults string `json:"next_results"`
CompletedIn float64 `json:"completed_in"`
Query string `json:"query"`
}
// SearchService provides methods for accessing Twitter search API endpoints.
type SearchService struct {
sling *sling.Sling
}
// newSearchService returns a new SearchService.
func newSearchService(sling *sling.Sling) *SearchService {
return &SearchService{
sling: sling.Path("search/"),
}
}
// SearchTweetParams are the parameters for SearchService.Tweets
type SearchTweetParams struct {
Query string `url:"q,omitempty"`
Geocode string `url:"geocode,omitempty"`
Lang string `url:"lang,omitempty"`
Locale string `url:"locale,omitempty"`
ResultType string `url:"result_type,omitempty"`
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
Until string `url:"until,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Tweets returns a collection of Tweets matching a search query.
// https://dev.twitter.com/rest/reference/get/search/tweets
func (s *SearchService) Tweets(params *SearchTweetParams) (*Search, *http.Response, error) {
search := new(Search)
apiError := new(APIError)
resp, err := s.sling.New().Get("tweets.json").QueryStruct(params).Receive(search, apiError)
return search, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,46 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSearchService_Tweets(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/search/tweets.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"q": "happy birthday", "result_type": "popular", "count": "1"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"statuses":[{"id":781760642139250689}],"search_metadata":{"completed_in":0.043,"max_id":781760642139250689,"max_id_str":"781760642139250689","next_results":"?max_id=781760640104828927&q=happy+birthday&count=1&include_entities=1","query":"happy birthday","refresh_url":"?since_id=781760642139250689&q=happy+birthday&include_entities=1","count":1,"since_id":0,"since_id_str":"0"}}`)
})
client := NewClient(httpClient)
search, _, err := client.Search.Tweets(&SearchTweetParams{
Query: "happy birthday",
Count: 1,
ResultType: "popular",
})
expected := &Search{
Statuses: []Tweet{
Tweet{ID: 781760642139250689},
},
Metadata: &SearchMetadata{
Count: 1,
SinceID: 0,
SinceIDStr: "0",
MaxID: 781760642139250689,
MaxIDStr: "781760642139250689",
RefreshURL: "?since_id=781760642139250689&q=happy+birthday&include_entities=1",
NextResults: "?max_id=781760640104828927&q=happy+birthday&count=1&include_entities=1",
CompletedIn: 0.043,
Query: "happy birthday",
},
}
assert.Nil(t, err)
assert.Equal(t, expected, search)
}

View File

@@ -0,0 +1,303 @@
package twitter
import (
"fmt"
"net/http"
"github.com/dghubble/sling"
)
// Tweet represents a Twitter Tweet, previously called a status.
// https://dev.twitter.com/overview/api/tweets
// Deprecated fields: Contributors, Geo, Annotations
type Tweet struct {
Coordinates *Coordinates `json:"coordinates"`
CreatedAt string `json:"created_at"`
CurrentUserRetweet *TweetIdentifier `json:"current_user_retweet"`
Entities *Entities `json:"entities"`
FavoriteCount int `json:"favorite_count"`
Favorited bool `json:"favorited"`
FilterLevel string `json:"filter_level"`
ID int64 `json:"id"`
IDStr string `json:"id_str"`
InReplyToScreenName string `json:"in_reply_to_screen_name"`
InReplyToStatusID int64 `json:"in_reply_to_status_id"`
InReplyToStatusIDStr string `json:"in_reply_to_status_id_str"`
InReplyToUserID int64 `json:"in_reply_to_user_id"`
InReplyToUserIDStr string `json:"in_reply_to_user_id_str"`
Lang string `json:"lang"`
PossiblySensitive bool `json:"possibly_sensitive"`
RetweetCount int `json:"retweet_count"`
Retweeted bool `json:"retweeted"`
RetweetedStatus *Tweet `json:"retweeted_status"`
Source string `json:"source"`
Scopes map[string]interface{} `json:"scopes"`
Text string `json:"text"`
FullText string `json:"full_text"`
DisplayTextRange Indices `json:"display_text_range"`
Place *Place `json:"place"`
Truncated bool `json:"truncated"`
User *User `json:"user"`
WithheldCopyright bool `json:"withheld_copyright"`
WithheldInCountries []string `json:"withheld_in_countries"`
WithheldScope string `json:"withheld_scope"`
ExtendedEntities *ExtendedEntity `json:"extended_entities"`
ExtendedTweet *ExtendedTweet `json:"extended_tweet"`
QuotedStatusID int64 `json:"quoted_status_id"`
QuotedStatusIDStr string `json:"quoted_status_id_str"`
QuotedStatus *Tweet `json:"quoted_status"`
}
// ExtendedTweet represents fields embedded in extended Tweets when served in
// compatibility mode (default).
// https://dev.twitter.com/overview/api/upcoming-changes-to-tweets
type ExtendedTweet struct {
FullText string `json:"full_text"`
DisplayTextRange Indices `json:"display_text_range"`
}
// Place represents a Twitter Place / Location
// https://dev.twitter.com/overview/api/places
type Place struct {
Attributes map[string]string `json:"attributes"`
BoundingBox *BoundingBox `json:"bounding_box"`
Country string `json:"country"`
CountryCode string `json:"country_code"`
FullName string `json:"full_name"`
Geometry *BoundingBox `json:"geometry"`
ID string `json:"id"`
Name string `json:"name"`
PlaceType string `json:"place_type"`
Polylines []string `json:"polylines"`
URL string `json:"url"`
}
// BoundingBox represents the bounding coordinates (longitude, latitutde)
// defining the bounds of a box containing a Place entity.
type BoundingBox struct {
Coordinates [][][2]float64 `json:"coordinates"`
Type string `json:"type"`
}
// Coordinates are pairs of longitude and latitude locations.
type Coordinates struct {
Coordinates [2]float64 `json:"coordinates"`
Type string `json:"type"`
}
// TweetIdentifier represents the id by which a Tweet can be identified.
type TweetIdentifier struct {
ID int64 `json:"id"`
IDStr string `json:"id_str"`
}
// StatusService provides methods for accessing Twitter status API endpoints.
type StatusService struct {
sling *sling.Sling
}
// newStatusService returns a new StatusService.
func newStatusService(sling *sling.Sling) *StatusService {
return &StatusService{
sling: sling.Path("statuses/"),
}
}
// StatusShowParams are the parameters for StatusService.Show
type StatusShowParams struct {
ID int64 `url:"id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
IncludeMyRetweet *bool `url:"include_my_retweet,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Show returns the requested Tweet.
// https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid
func (s *StatusService) Show(id int64, params *StatusShowParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusShowParams{}
}
params.ID = id
tweet := new(Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// StatusLookupParams are the parameters for StatusService.Lookup
type StatusLookupParams struct {
ID []int64 `url:"id,omitempty,comma"`
TrimUser *bool `url:"trim_user,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
Map *bool `url:"map,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Lookup returns the requested Tweets as a slice. Combines ids from the
// required ids argument and from params.Id.
// https://dev.twitter.com/rest/reference/get/statuses/lookup
func (s *StatusService) Lookup(ids []int64, params *StatusLookupParams) ([]Tweet, *http.Response, error) {
if params == nil {
params = &StatusLookupParams{}
}
params.ID = append(params.ID, ids...)
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("lookup.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// StatusUpdateParams are the parameters for StatusService.Update
type StatusUpdateParams struct {
Status string `url:"status,omitempty"`
InReplyToStatusID int64 `url:"in_reply_to_status_id,omitempty"`
PossiblySensitive *bool `url:"possibly_sensitive,omitempty"`
Lat *float64 `url:"lat,omitempty"`
Long *float64 `url:"long,omitempty"`
PlaceID string `url:"place_id,omitempty"`
DisplayCoordinates *bool `url:"display_coordinates,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
MediaIds []int64 `url:"media_ids,omitempty,comma"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Update updates the user's status, also known as Tweeting.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/statuses/update
func (s *StatusService) Update(status string, params *StatusUpdateParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusUpdateParams{}
}
params.Status = status
tweet := new(Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Post("update.json").BodyForm(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// StatusRetweetParams are the parameters for StatusService.Retweet
type StatusRetweetParams struct {
ID int64 `url:"id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Retweet retweets the Tweet with the given id and returns the original Tweet
// with embedded retweet details.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/statuses/retweet/%3Aid
func (s *StatusService) Retweet(id int64, params *StatusRetweetParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusRetweetParams{}
}
params.ID = id
tweet := new(Tweet)
apiError := new(APIError)
path := fmt.Sprintf("retweet/%d.json", params.ID)
resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// StatusUnretweetParams are the parameters for StatusService.Unretweet
type StatusUnretweetParams struct {
ID int64 `url:"id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Unretweet unretweets the Tweet with the given id and returns the original Tweet.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/statuses/unretweet/%3Aid
func (s *StatusService) Unretweet(id int64, params *StatusUnretweetParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusUnretweetParams{}
}
params.ID = id
tweet := new(Tweet)
apiError := new(APIError)
path := fmt.Sprintf("unretweet/%d.json", params.ID)
resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// StatusRetweetsParams are the parameters for StatusService.Retweets
type StatusRetweetsParams struct {
ID int64 `url:"id,omitempty"`
Count int `url:"count,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Retweets returns the most recent retweets of the Tweet with the given id.
// https://dev.twitter.com/rest/reference/get/statuses/retweets/%3Aid
func (s *StatusService) Retweets(id int64, params *StatusRetweetsParams) ([]Tweet, *http.Response, error) {
if params == nil {
params = &StatusRetweetsParams{}
}
params.ID = id
tweets := new([]Tweet)
apiError := new(APIError)
path := fmt.Sprintf("retweets/%d.json", params.ID)
resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// StatusDestroyParams are the parameters for StatusService.Destroy
type StatusDestroyParams struct {
ID int64 `url:"id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Destroy deletes the Tweet with the given id and returns it if successful.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/statuses/destroy/%3Aid
func (s *StatusService) Destroy(id int64, params *StatusDestroyParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusDestroyParams{}
}
params.ID = id
tweet := new(Tweet)
apiError := new(APIError)
path := fmt.Sprintf("destroy/%d.json", params.ID)
resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// OEmbedTweet represents a Tweet in oEmbed format.
type OEmbedTweet struct {
URL string `json:"url"`
ProviderURL string `json:"provider_url"`
ProviderName string `json:"provider_name"`
AuthorName string `json:"author_name"`
Version string `json:"version"`
AuthorURL string `json:"author_url"`
Type string `json:"type"`
HTML string `json:"html"`
Height int64 `json:"height"`
Width int64 `json:"width"`
CacheAge string `json:"cache_age"`
}
// StatusOEmbedParams are the parameters for StatusService.OEmbed
type StatusOEmbedParams struct {
ID int64 `url:"id,omitempty"`
URL string `url:"url,omitempty"`
Align string `url:"align,omitempty"`
MaxWidth int64 `url:"maxwidth,omitempty"`
HideMedia *bool `url:"hide_media,omitempty"`
HideThread *bool `url:"hide_media,omitempty"`
OmitScript *bool `url:"hide_media,omitempty"`
WidgetType string `url:"widget_type,omitempty"`
HideTweet *bool `url:"hide_tweet,omitempty"`
}
// OEmbed returns the requested Tweet in oEmbed format.
// https://dev.twitter.com/rest/reference/get/statuses/oembed
func (s *StatusService) OEmbed(params *StatusOEmbedParams) (*OEmbedTweet, *http.Response, error) {
oEmbedTweet := new(OEmbedTweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("oembed.json").QueryStruct(params).Receive(oEmbedTweet, apiError)
return oEmbedTweet, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,275 @@
package twitter
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStatusService_Show(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/show.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "589488862814076930", "include_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"user": {"screen_name": "dghubble"}, "text": ".@audreyr use a DONTREADME file if you really want people to read it :P"}`)
})
client := NewClient(httpClient)
params := &StatusShowParams{ID: 5441, IncludeEntities: Bool(false)}
tweet, _, err := client.Statuses.Show(589488862814076930, params)
expected := &Tweet{User: &User{ScreenName: "dghubble"}, Text: ".@audreyr use a DONTREADME file if you really want people to read it :P"}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_ShowHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/show.json", func(w http.ResponseWriter, r *http.Request) {
assertQuery(t, map[string]string{"id": "589488862814076930"}, r)
})
client := NewClient(httpClient)
client.Statuses.Show(589488862814076930, nil)
}
func TestStatusService_Lookup(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/lookup.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "20,573893817000140800", "trim_user": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"id": 20, "text": "just setting up my twttr"}, {"id": 573893817000140800, "text": "Don't get lost #PaxEast2015"}]`)
})
client := NewClient(httpClient)
params := &StatusLookupParams{ID: []int64{20}, TrimUser: Bool(true)}
tweets, _, err := client.Statuses.Lookup([]int64{573893817000140800}, params)
expected := []Tweet{Tweet{ID: 20, Text: "just setting up my twttr"}, Tweet{ID: 573893817000140800, Text: "Don't get lost #PaxEast2015"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestStatusService_LookupHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/lookup.json", func(w http.ResponseWriter, r *http.Request) {
assertQuery(t, map[string]string{"id": "20,573893817000140800"}, r)
})
client := NewClient(httpClient)
client.Statuses.Lookup([]int64{20, 573893817000140800}, nil)
}
func TestStatusService_Update(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{}, r)
assertPostForm(t, map[string]string{"status": "very informative tweet", "media_ids": "123456789,987654321", "lat": "37.826706", "long": "-122.42219"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very informative tweet"}`)
})
client := NewClient(httpClient)
params := &StatusUpdateParams{MediaIds: []int64{123456789, 987654321}, Lat: Float(37.826706), Long: Float(-122.422190)}
tweet, _, err := client.Statuses.Update("very informative tweet", params)
expected := &Tweet{ID: 581980947630845953, Text: "very informative tweet"}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_UpdateHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
assertPostForm(t, map[string]string{"status": "very informative tweet"}, r)
})
client := NewClient(httpClient)
client.Statuses.Update("very informative tweet", nil)
}
func TestStatusService_APIError(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
assertPostForm(t, map[string]string{"status": "very informative tweet"}, r)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(403)
fmt.Fprintf(w, `{"errors": [{"message": "Status is a duplicate", "code": 187}]}`)
})
client := NewClient(httpClient)
_, _, err := client.Statuses.Update("very informative tweet", nil)
expected := APIError{
Errors: []ErrorDetail{
ErrorDetail{Message: "Status is a duplicate", Code: 187},
},
}
if assert.Error(t, err) {
assert.Equal(t, expected, err)
}
}
func TestStatusService_HTTPError(t *testing.T) {
httpClient, _, server := testServer()
server.Close()
client := NewClient(httpClient)
_, _, err := client.Statuses.Update("very informative tweet", nil)
if err == nil || !strings.Contains(err.Error(), "connection refused") {
t.Errorf("Statuses.Update error expected connection refused, got: \n %+v", err)
}
}
func TestStatusService_Retweet(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweet/20.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{}, r)
assertPostForm(t, map[string]string{"id": "20", "trim_user": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630202020, "text": "RT @jack: just setting up my twttr", "retweeted_status": {"id": 20, "text": "just setting up my twttr"}}`)
})
client := NewClient(httpClient)
params := &StatusRetweetParams{TrimUser: Bool(true)}
tweet, _, err := client.Statuses.Retweet(20, params)
expected := &Tweet{ID: 581980947630202020, Text: "RT @jack: just setting up my twttr", RetweetedStatus: &Tweet{ID: 20, Text: "just setting up my twttr"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_RetweetHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweet/20.json", func(w http.ResponseWriter, r *http.Request) {
assertPostForm(t, map[string]string{"id": "20"}, r)
})
client := NewClient(httpClient)
client.Statuses.Retweet(20, nil)
}
func TestStatusService_Unretweet(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/unretweet/20.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{}, r)
assertPostForm(t, map[string]string{"id": "20", "trim_user": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630202020, "text":"RT @jack: just setting up my twttr", "retweeted_status": {"id": 20, "text": "just setting up my twttr"}}`)
})
client := NewClient(httpClient)
params := &StatusUnretweetParams{TrimUser: Bool(true)}
tweet, _, err := client.Statuses.Unretweet(20, params)
expected := &Tweet{ID: 581980947630202020, Text: "RT @jack: just setting up my twttr", RetweetedStatus: &Tweet{ID: 20, Text: "just setting up my twttr"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_Retweets(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweets/20.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "20", "count": "2"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "RT @jack: just setting up my twttr"}, {"text": "RT @jack: just setting up my twttr"}]`)
})
client := NewClient(httpClient)
params := &StatusRetweetsParams{Count: 2}
retweets, _, err := client.Statuses.Retweets(20, params)
expected := []Tweet{Tweet{Text: "RT @jack: just setting up my twttr"}, Tweet{Text: "RT @jack: just setting up my twttr"}}
assert.Nil(t, err)
assert.Equal(t, expected, retweets)
}
func TestStatusService_RetweetsHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweets/20.json", func(w http.ResponseWriter, r *http.Request) {
assertQuery(t, map[string]string{"id": "20"}, r)
})
client := NewClient(httpClient)
client.Statuses.Retweets(20, nil)
}
func TestStatusService_Destroy(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/destroy/40.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{}, r)
assertPostForm(t, map[string]string{"id": "40", "trim_user": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 40, "text": "wishing I had another sammich"}`)
})
client := NewClient(httpClient)
params := &StatusDestroyParams{TrimUser: Bool(true)}
tweet, _, err := client.Statuses.Destroy(40, params)
// feed Biz Stone a sammich, he deletes sammich Tweet
expected := &Tweet{ID: 40, Text: "wishing I had another sammich"}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_DestroyHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/destroy/40.json", func(w http.ResponseWriter, r *http.Request) {
assertPostForm(t, map[string]string{"id": "40"}, r)
})
client := NewClient(httpClient)
client.Statuses.Destroy(40, nil)
}
func TestStatusService_OEmbed(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/oembed.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "691076766878691329", "maxwidth": "400", "hide_media": "true"}, r)
w.Header().Set("Content-Type", "application/json")
// abbreviated oEmbed response
fmt.Fprintf(w, `{"url": "https://twitter.com/dghubble/statuses/691076766878691329", "width": 400, "html": "<blockquote></blockquote>"}`)
})
client := NewClient(httpClient)
params := &StatusOEmbedParams{
ID: 691076766878691329,
MaxWidth: 400,
HideMedia: Bool(true),
}
oembed, _, err := client.Statuses.OEmbed(params)
expected := &OEmbedTweet{
URL: "https://twitter.com/dghubble/statuses/691076766878691329",
Width: 400,
HTML: "<blockquote></blockquote>",
}
assert.Nil(t, err)
assert.Equal(t, expected, oembed)
}

View File

@@ -0,0 +1,110 @@
package twitter
// StatusDeletion indicates that a given Tweet has been deleted.
// https://dev.twitter.com/streaming/overview/messages-types#status_deletion_notices_delete
type StatusDeletion struct {
ID int64 `json:"id"`
IDStr string `json:"id_str"`
UserID int64 `json:"user_id"`
UserIDStr string `json:"user_id_str"`
}
type statusDeletionNotice struct {
Delete struct {
StatusDeletion *StatusDeletion `json:"status"`
} `json:"delete"`
}
// LocationDeletion indicates geolocation data must be stripped from a range
// of Tweets.
// https://dev.twitter.com/streaming/overview/messages-types#Location_deletion_notices_scrub_geo
type LocationDeletion struct {
UserID int64 `json:"user_id"`
UserIDStr string `json:"user_id_str"`
UpToStatusID int64 `json:"up_to_status_id"`
UpToStatusIDStr string `json:"up_to_status_id_str"`
}
type locationDeletionNotice struct {
ScrubGeo *LocationDeletion `json:"scrub_geo"`
}
// StreamLimit indicates a stream matched more statuses than its rate limit
// allowed. The track number is the number of undelivered matches.
// https://dev.twitter.com/streaming/overview/messages-types#limit_notices
type StreamLimit struct {
Track int64 `json:"track"`
}
type streamLimitNotice struct {
Limit *StreamLimit `json:"limit"`
}
// StatusWithheld indicates a Tweet with the given ID, belonging to UserId,
// has been withheld in certain countries.
// https://dev.twitter.com/streaming/overview/messages-types#withheld_content_notices
type StatusWithheld struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
WithheldInCountries []string `json:"withheld_in_countries"`
}
type statusWithheldNotice struct {
StatusWithheld *StatusWithheld `json:"status_withheld"`
}
// UserWithheld indicates a User with the given ID has been withheld in
// certain countries.
// https://dev.twitter.com/streaming/overview/messages-types#withheld_content_notices
type UserWithheld struct {
ID int64 `json:"id"`
WithheldInCountries []string `json:"withheld_in_countries"`
}
type userWithheldNotice struct {
UserWithheld *UserWithheld `json:"user_withheld"`
}
// StreamDisconnect indicates the stream has been shutdown for some reason.
// https://dev.twitter.com/streaming/overview/messages-types#disconnect_messages
type StreamDisconnect struct {
Code int64 `json:"code"`
StreamName string `json:"stream_name"`
Reason string `json:"reason"`
}
type streamDisconnectNotice struct {
StreamDisconnect *StreamDisconnect `json:"disconnect"`
}
// StallWarning indicates the client is falling behind in the stream.
// https://dev.twitter.com/streaming/overview/messages-types#stall_warnings
type StallWarning struct {
Code string `json:"code"`
Message string `json:"message"`
PercentFull int `json:"percent_full"`
}
type stallWarningNotice struct {
StallWarning *StallWarning `json:"warning"`
}
// FriendsList is a list of some of a user's friends.
// https://dev.twitter.com/streaming/overview/messages-types#friends_list_friends
type FriendsList struct {
Friends []int64 `json:"friends"`
}
type directMessageNotice struct {
DirectMessage *DirectMessage `json:"direct_message"`
}
// Event is a non-Tweet notification message (e.g. like, retweet, follow).
// https://dev.twitter.com/streaming/overview/messages-types#Events_event
type Event struct {
Event string `json:"event"`
CreatedAt string `json:"created_at"`
Target *User `json:"target"`
Source *User `json:"source"`
// TODO: add List or deprecate it
TargetObject *Tweet `json:"target_object"`
}

View File

@@ -0,0 +1,56 @@
package twitter
import (
"strings"
"time"
)
// stopped returns true if the done channel receives, false otherwise.
func stopped(done <-chan struct{}) bool {
select {
case <-done:
return true
default:
return false
}
}
// sleepOrDone pauses the current goroutine until the done channel receives
// or until at least the duration d has elapsed, whichever comes first. This
// is similar to time.Sleep(d), except it can be interrupted.
func sleepOrDone(d time.Duration, done <-chan struct{}) {
select {
case <-time.After(d):
return
case <-done:
return
}
}
// scanLines is a split function for a Scanner that returns each line of text
// stripped of the end-of-line marker "\r\n" used by Twitter Streaming APIs.
// This differs from the bufio.ScanLines split function which considers the
// '\r' optional.
// https://dev.twitter.com/streaming/overview/processing
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := strings.Index(string(data), "\r\n"); i >= 0 {
// We have a full '\r\n' terminated line.
return i + 2, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), dropCR(data), nil
}
// Request more data.
return 0, nil, nil
}
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\n' {
return data[0 : len(data)-1]
}
return data
}

View File

@@ -0,0 +1,64 @@
package twitter
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestStopped(t *testing.T) {
done := make(chan struct{})
assert.False(t, stopped(done))
close(done)
assert.True(t, stopped(done))
}
func TestSleepOrDone_Sleep(t *testing.T) {
wait := time.Nanosecond * 20
done := make(chan struct{})
completed := make(chan struct{})
go func() {
sleepOrDone(wait, done)
close(completed)
}()
// wait for goroutine SleepOrDone to sleep
assertDone(t, completed, defaultTestTimeout)
}
func TestSleepOrDone_Done(t *testing.T) {
wait := time.Second * 5
done := make(chan struct{})
completed := make(chan struct{})
go func() {
sleepOrDone(wait, done)
close(completed)
}()
// close done, interrupting SleepOrDone
close(done)
// assert that SleepOrDone exited, closing completed
assertDone(t, completed, defaultTestTimeout)
}
func TestScanLines(t *testing.T) {
cases := []struct {
input []byte
atEOF bool
advance int
token []byte
}{
{[]byte("Line 1\r\n"), false, 8, []byte("Line 1")},
{[]byte("Line 1\n"), false, 0, nil},
{[]byte("Line 1"), false, 0, nil},
{[]byte(""), false, 0, nil},
{[]byte("Line 1\r\n"), true, 8, []byte("Line 1")},
{[]byte("Line 1\n"), true, 7, []byte("Line 1")},
{[]byte("Line 1"), true, 6, []byte("Line 1")},
{[]byte(""), true, 0, nil},
}
for _, c := range cases {
advance, token, _ := scanLines(c.input, c.atEOF)
assert.Equal(t, c.advance, advance)
assert.Equal(t, c.token, token)
}
}

View File

@@ -0,0 +1,327 @@
package twitter
import (
"bufio"
"encoding/json"
"io"
"net/http"
"sync"
"time"
"github.com/cenkalti/backoff"
"github.com/dghubble/sling"
)
const (
userAgent = "go-twitter v0.1"
publicStream = "https://stream.twitter.com/1.1/"
userStream = "https://userstream.twitter.com/1.1/"
siteStream = "https://sitestream.twitter.com/1.1/"
)
// StreamService provides methods for accessing the Twitter Streaming API.
type StreamService struct {
client *http.Client
public *sling.Sling
user *sling.Sling
site *sling.Sling
}
// newStreamService returns a new StreamService.
func newStreamService(client *http.Client, sling *sling.Sling) *StreamService {
sling.Set("User-Agent", userAgent)
return &StreamService{
client: client,
public: sling.New().Base(publicStream).Path("statuses/"),
user: sling.New().Base(userStream),
site: sling.New().Base(siteStream),
}
}
// StreamFilterParams are parameters for StreamService.Filter.
type StreamFilterParams struct {
FilterLevel string `url:"filter_level,omitempty"`
Follow []string `url:"follow,omitempty,comma"`
Language []string `url:"language,omitempty,comma"`
Locations []string `url:"locations,omitempty,comma"`
StallWarnings *bool `url:"stall_warnings,omitempty"`
Track []string `url:"track,omitempty,comma"`
}
// Filter returns messages that match one or more filter predicates.
// https://dev.twitter.com/streaming/reference/post/statuses/filter
func (srv *StreamService) Filter(params *StreamFilterParams) (*Stream, error) {
req, err := srv.public.New().Post("filter.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// StreamSampleParams are the parameters for StreamService.Sample.
type StreamSampleParams struct {
StallWarnings *bool `url:"stall_warnings,omitempty"`
Language []string `url:"language,omitempty,comma"`
}
// Sample returns a small sample of public stream messages.
// https://dev.twitter.com/streaming/reference/get/statuses/sample
func (srv *StreamService) Sample(params *StreamSampleParams) (*Stream, error) {
req, err := srv.public.New().Get("sample.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// StreamUserParams are the parameters for StreamService.User.
type StreamUserParams struct {
FilterLevel string `url:"filter_level,omitempty"`
Language []string `url:"language,omitempty,comma"`
Locations []string `url:"locations,omitempty,comma"`
Replies string `url:"replies,omitempty"`
StallWarnings *bool `url:"stall_warnings,omitempty"`
Track []string `url:"track,omitempty,comma"`
With string `url:"with,omitempty"`
}
// User returns a stream of messages specific to the authenticated User.
// https://dev.twitter.com/streaming/reference/get/user
func (srv *StreamService) User(params *StreamUserParams) (*Stream, error) {
req, err := srv.user.New().Get("user.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// StreamSiteParams are the parameters for StreamService.Site.
type StreamSiteParams struct {
FilterLevel string `url:"filter_level,omitempty"`
Follow []string `url:"follow,omitempty,comma"`
Language []string `url:"language,omitempty,comma"`
Replies string `url:"replies,omitempty"`
StallWarnings *bool `url:"stall_warnings,omitempty"`
With string `url:"with,omitempty"`
}
// Site returns messages for a set of users.
// Requires special permission to access.
// https://dev.twitter.com/streaming/reference/get/site
func (srv *StreamService) Site(params *StreamSiteParams) (*Stream, error) {
req, err := srv.site.New().Get("site.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// StreamFirehoseParams are the parameters for StreamService.Firehose.
type StreamFirehoseParams struct {
Count int `url:"count,omitempty"`
FilterLevel string `url:"filter_level,omitempty"`
Language []string `url:"language,omitempty,comma"`
StallWarnings *bool `url:"stall_warnings,omitempty"`
}
// Firehose returns all public messages and statuses.
// Requires special permission to access.
// https://dev.twitter.com/streaming/reference/get/statuses/firehose
func (srv *StreamService) Firehose(params *StreamFirehoseParams) (*Stream, error) {
req, err := srv.public.New().Get("firehose.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// Stream maintains a connection to the Twitter Streaming API, receives
// messages from the streaming response, and sends them on the Messages
// channel from a goroutine. The stream goroutine stops itself if an EOF is
// reached or retry errors occur, also closing the Messages channel.
//
// The client must Stop() the stream when finished receiving, which will
// wait until the stream is properly stopped.
type Stream struct {
client *http.Client
Messages chan interface{}
done chan struct{}
group *sync.WaitGroup
body io.Closer
}
// newStream creates a Stream and starts a goroutine to retry connecting and
// receive from a stream response. The goroutine may stop due to retry errors
// or be stopped by calling Stop() on the stream.
func newStream(client *http.Client, req *http.Request) *Stream {
s := &Stream{
client: client,
Messages: make(chan interface{}),
done: make(chan struct{}),
group: &sync.WaitGroup{},
}
s.group.Add(1)
go s.retry(req, newExponentialBackOff(), newAggressiveExponentialBackOff())
return s
}
// Stop signals retry and receiver to stop, closes the Messages channel, and
// blocks until done.
func (s *Stream) Stop() {
close(s.done)
// Scanner does not have a Stop() or take a done channel, so for low volume
// streams Scan() blocks until the next keep-alive. Close the resp.Body to
// escape and stop the stream in a timely fashion.
if s.body != nil {
s.body.Close()
}
// block until the retry goroutine stops
s.group.Wait()
}
// retry retries making the given http.Request and receiving the response
// according to the Twitter backoff policies. Callers should invoke in a
// goroutine since backoffs sleep between retries.
// https://dev.twitter.com/streaming/overview/connecting
func (s *Stream) retry(req *http.Request, expBackOff backoff.BackOff, aggExpBackOff backoff.BackOff) {
// close Messages channel and decrement the wait group counter
defer close(s.Messages)
defer s.group.Done()
var wait time.Duration
for !stopped(s.done) {
resp, err := s.client.Do(req)
if err != nil {
// stop retrying for HTTP protocol errors
s.Messages <- err
return
}
// when err is nil, resp contains a non-nil Body which must be closed
defer resp.Body.Close()
s.body = resp.Body
switch resp.StatusCode {
case 200:
// receive stream response Body, handles closing
s.receive(resp.Body)
expBackOff.Reset()
aggExpBackOff.Reset()
case 503:
// exponential backoff
wait = expBackOff.NextBackOff()
case 420, 429:
// aggressive exponential backoff
wait = aggExpBackOff.NextBackOff()
default:
// stop retrying for other response codes
resp.Body.Close()
return
}
// close response before each retry
resp.Body.Close()
if wait == backoff.Stop {
return
}
sleepOrDone(wait, s.done)
}
}
// receive scans a stream response body, JSON decodes tokens to messages, and
// sends messages to the Messages channel. Receiving continues until an EOF,
// scan error, or the done channel is closed.
func (s *Stream) receive(body io.ReadCloser) {
defer body.Close()
// A bufio.Scanner steps through 'tokens' of data on each Scan() using a
// SplitFunc. SplitFunc tokenizes input bytes to return the number of bytes
// to advance, the token slice of bytes, and any errors.
scanner := bufio.NewScanner(body)
// default ScanLines SplitFunc is incorrect for Twitter Streams, set custom
scanner.Split(scanLines)
for !stopped(s.done) && scanner.Scan() {
token := scanner.Bytes()
if len(token) == 0 {
// empty keep-alive
continue
}
select {
// send messages, data, or errors
case s.Messages <- getMessage(token):
continue
// allow client to Stop(), even if not receiving
case <-s.done:
return
}
}
}
// getMessage unmarshals the token and returns a message struct, if the type
// can be determined. Otherwise, returns the token unmarshalled into a data
// map[string]interface{} or the unmarshal error.
func getMessage(token []byte) interface{} {
var data map[string]interface{}
// unmarshal JSON encoded token into a map for
err := json.Unmarshal(token, &data)
if err != nil {
return err
}
return decodeMessage(token, data)
}
// decodeMessage determines the message type from known data keys, allocates
// at most one message struct, and JSON decodes the token into the message.
// Returns the message struct or the data map if the message type could not be
// determined.
func decodeMessage(token []byte, data map[string]interface{}) interface{} {
if hasPath(data, "retweet_count") {
tweet := new(Tweet)
json.Unmarshal(token, tweet)
return tweet
} else if hasPath(data, "direct_message") {
notice := new(directMessageNotice)
json.Unmarshal(token, notice)
return notice.DirectMessage
} else if hasPath(data, "delete") {
notice := new(statusDeletionNotice)
json.Unmarshal(token, notice)
return notice.Delete.StatusDeletion
} else if hasPath(data, "scrub_geo") {
notice := new(locationDeletionNotice)
json.Unmarshal(token, notice)
return notice.ScrubGeo
} else if hasPath(data, "limit") {
notice := new(streamLimitNotice)
json.Unmarshal(token, notice)
return notice.Limit
} else if hasPath(data, "status_withheld") {
notice := new(statusWithheldNotice)
json.Unmarshal(token, notice)
return notice.StatusWithheld
} else if hasPath(data, "user_withheld") {
notice := new(userWithheldNotice)
json.Unmarshal(token, notice)
return notice.UserWithheld
} else if hasPath(data, "disconnect") {
notice := new(streamDisconnectNotice)
json.Unmarshal(token, notice)
return notice.StreamDisconnect
} else if hasPath(data, "warning") {
notice := new(stallWarningNotice)
json.Unmarshal(token, notice)
return notice.StallWarning
} else if hasPath(data, "friends") {
friendsList := new(FriendsList)
json.Unmarshal(token, friendsList)
return friendsList
} else if hasPath(data, "event") {
event := new(Event)
json.Unmarshal(token, event)
return event
}
// message type unknown, return the data map[string]interface{}
return data
}
// hasPath returns true if the map contains the given key, false otherwise.
func hasPath(data map[string]interface{}, key string) bool {
_, ok := data[key]
return ok
}

View File

@@ -0,0 +1,352 @@
package twitter
import (
"fmt"
"net/http"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStream_MessageJSONError(t *testing.T) {
badJSON := []byte(`{`)
msg := getMessage(badJSON)
assert.EqualError(t, msg.(error), "unexpected end of JSON input")
}
func TestStream_GetMessageTweet(t *testing.T) {
msgJSON := []byte(`{"id": 20, "text": "just setting up my twttr", "retweet_count": "68535"}`)
msg := getMessage(msgJSON)
assert.IsType(t, &Tweet{}, msg)
}
func TestStream_GetMessageDirectMessage(t *testing.T) {
msgJSON := []byte(`{"direct_message": {"id": 666024290140217347}}`)
msg := getMessage(msgJSON)
assert.IsType(t, &DirectMessage{}, msg)
}
func TestStream_GetMessageDelete(t *testing.T) {
msgJSON := []byte(`{"delete": { "id": 20}}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StatusDeletion{}, msg)
}
func TestStream_GetMessageLocationDeletion(t *testing.T) {
msgJSON := []byte(`{"scrub_geo": { "up_to_status_id": 20}}`)
msg := getMessage(msgJSON)
assert.IsType(t, &LocationDeletion{}, msg)
}
func TestStream_GetMessageStreamLimit(t *testing.T) {
msgJSON := []byte(`{"limit": { "track": 10 }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StreamLimit{}, msg)
}
func TestStream_StatusWithheld(t *testing.T) {
msgJSON := []byte(`{"status_withheld": { "id": 20, "user_id": 12, "withheld_in_countries":["USA", "China"] }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StatusWithheld{}, msg)
}
func TestStream_UserWithheld(t *testing.T) {
msgJSON := []byte(`{"user_withheld": { "id": 12, "withheld_in_countries":["USA", "China"] }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &UserWithheld{}, msg)
}
func TestStream_StreamDisconnect(t *testing.T) {
msgJSON := []byte(`{"disconnect": { "code": "420", "stream_name": "streaming stuff", "reason": "too many connections" }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StreamDisconnect{}, msg)
}
func TestStream_StallWarning(t *testing.T) {
msgJSON := []byte(`{"warning": { "code": "420", "percent_full": 90, "message": "a lot of messages" }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StallWarning{}, msg)
}
func TestStream_FriendsList(t *testing.T) {
msgJSON := []byte(`{"friends": [666024290140217347, 666024290140217349, 666024290140217342]}`)
msg := getMessage(msgJSON)
assert.IsType(t, &FriendsList{}, msg)
}
func TestStream_Event(t *testing.T) {
msgJSON := []byte(`{"event": "block", "target": {"name": "XKCD Comic", "favourites_count": 2}, "source": {"name": "XKCD Comic2", "favourites_count": 3}, "created_at": "Sat Sep 4 16:10:54 +0000 2010"}`)
msg := getMessage(msgJSON)
assert.IsType(t, &Event{}, msg)
}
func TestStream_Unknown(t *testing.T) {
msgJSON := []byte(`{"unknown_data": {"new_twitter_type":"unexpected"}}`)
msg := getMessage(msgJSON)
assert.IsType(t, map[string]interface{}{}, msg)
}
func TestStream_Filter(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/statuses/filter.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{"track": "gophercon,golang"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w,
`{"text": "Gophercon talks!"}`+"\r\n"+
`{"text": "Gophercon super talks!"}`+"\r\n",
)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamFilterParams := &StreamFilterParams{
Track: []string{"gophercon", "golang"},
}
stream, err := client.Streams.Filter(streamFilterParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 2, other: 2}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_Sample(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/statuses/sample.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"stall_warnings": "true"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w,
`{"text": "Gophercon talks!"}`+"\r\n"+
`{"text": "Gophercon super talks!"}`+"\r\n",
)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamSampleParams := &StreamSampleParams{
StallWarnings: Bool(true),
}
stream, err := client.Streams.Sample(streamSampleParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 2, other: 2}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_User(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/user.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"stall_warnings": "true", "with": "followings"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w, `{"friends": [666024290140217347, 666024290140217349, 666024290140217342]}`+"\r\n"+"\r\n")
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamUserParams := &StreamUserParams{
StallWarnings: Bool(true),
With: "followings",
}
stream, err := client.Streams.User(streamUserParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 1, friendsList: 1}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_Site(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/site.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"follow": "666024290140217347,666024290140217349"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w,
`{"text": "Gophercon talks!"}`+"\r\n"+
`{"text": "Gophercon super talks!"}`+"\r\n",
)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamSiteParams := &StreamSiteParams{
Follow: []string{"666024290140217347", "666024290140217349"},
}
stream, err := client.Streams.Site(streamSiteParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 2, other: 2}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_PublicFirehose(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/statuses/firehose.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"count": "100"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w,
`{"text": "Gophercon talks!"}`+"\r\n"+
`{"text": "Gophercon super talks!"}`+"\r\n",
)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamFirehoseParams := &StreamFirehoseParams{
Count: 100,
}
stream, err := client.Streams.Firehose(streamFirehoseParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 2, other: 2}
assert.Equal(t, expectedCounts, counts)
}
func TestStreamRetry_ExponentialBackoff(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch reqCount {
case 0:
http.Error(w, "Service Unavailable", 503)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
stream := &Stream{
client: httpClient,
Messages: make(chan interface{}),
done: make(chan struct{}),
group: &sync.WaitGroup{},
}
stream.group.Add(1)
req, _ := http.NewRequest("GET", "http://example.com/", nil)
expBackoff := &BackOffRecorder{}
// receive messages and throw them away
go NewSwitchDemux().HandleChan(stream.Messages)
stream.retry(req, expBackoff, nil)
defer stream.Stop()
// assert exponential backoff in response to 503
assert.Equal(t, 1, expBackoff.Count)
}
func TestStreamRetry_AggressiveBackoff(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch reqCount {
case 0:
http.Error(w, "Enhance Your Calm", 420)
case 1:
http.Error(w, "Too Many Requests", 429)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
stream := &Stream{
client: httpClient,
Messages: make(chan interface{}),
done: make(chan struct{}),
group: &sync.WaitGroup{},
}
stream.group.Add(1)
req, _ := http.NewRequest("GET", "http://example.com/", nil)
aggExpBackoff := &BackOffRecorder{}
// receive messages and throw them away
go NewSwitchDemux().HandleChan(stream.Messages)
stream.retry(req, nil, aggExpBackoff)
defer stream.Stop()
// assert aggressive exponential backoff in response to 420 and 429
assert.Equal(t, 2, aggExpBackoff.Count)
}

View File

@@ -0,0 +1,109 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// TimelineService provides methods for accessing Twitter status timeline
// API endpoints.
type TimelineService struct {
sling *sling.Sling
}
// newTimelineService returns a new TimelineService.
func newTimelineService(sling *sling.Sling) *TimelineService {
return &TimelineService{
sling: sling.Path("statuses/"),
}
}
// UserTimelineParams are the parameters for TimelineService.UserTimeline.
type UserTimelineParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
ExcludeReplies *bool `url:"exclude_replies,omitempty"`
IncludeRetweets *bool `url:"include_rts,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// UserTimeline returns recent Tweets from the specified user.
// https://dev.twitter.com/rest/reference/get/statuses/user_timeline
func (s *TimelineService) UserTimeline(params *UserTimelineParams) ([]Tweet, *http.Response, error) {
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("user_timeline.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// HomeTimelineParams are the parameters for TimelineService.HomeTimeline.
type HomeTimelineParams struct {
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
ExcludeReplies *bool `url:"exclude_replies,omitempty"`
ContributorDetails *bool `url:"contributor_details,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// HomeTimeline returns recent Tweets and retweets from the user and those
// users they follow.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/statuses/home_timeline
func (s *TimelineService) HomeTimeline(params *HomeTimelineParams) ([]Tweet, *http.Response, error) {
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("home_timeline.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// MentionTimelineParams are the parameters for TimelineService.MentionTimeline.
type MentionTimelineParams struct {
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
ContributorDetails *bool `url:"contributor_details,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// MentionTimeline returns recent Tweet mentions of the authenticated user.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/statuses/mentions_timeline
func (s *TimelineService) MentionTimeline(params *MentionTimelineParams) ([]Tweet, *http.Response, error) {
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("mentions_timeline.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// RetweetsOfMeTimelineParams are the parameters for
// TimelineService.RetweetsOfMeTimeline.
type RetweetsOfMeTimelineParams struct {
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
IncludeUserEntities *bool `url:"include_user_entities"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// RetweetsOfMeTimeline returns the most recent Tweets by the authenticated
// user that have been retweeted by others.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/statuses/retweets_of_me
func (s *TimelineService) RetweetsOfMeTimeline(params *RetweetsOfMeTimelineParams) ([]Tweet, *http.Response, error) {
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("retweets_of_me.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,81 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTimelineService_UserTimeline(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/user_timeline.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "113419064", "trim_user": "true", "include_rts": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "Gophercon talks!"}, {"text": "Why gophers are so adorable"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Timelines.UserTimeline(&UserTimelineParams{UserID: 113419064, TrimUser: Bool(true), IncludeRetweets: Bool(false)})
expected := []Tweet{Tweet{Text: "Gophercon talks!"}, Tweet{Text: "Why gophers are so adorable"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestTimelineService_HomeTimeline(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/home_timeline.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"since_id": "589147592367431680", "exclude_replies": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "Live on #Periscope"}, {"text": "Clickbait journalism"}, {"text": "Useful announcement"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Timelines.HomeTimeline(&HomeTimelineParams{SinceID: 589147592367431680, ExcludeReplies: Bool(false)})
expected := []Tweet{Tweet{Text: "Live on #Periscope"}, Tweet{Text: "Clickbait journalism"}, Tweet{Text: "Useful announcement"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestTimelineService_MentionTimeline(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/mentions_timeline.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"count": "20", "include_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "@dghubble can I get verified?"}, {"text": "@dghubble why are gophers so great?"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Timelines.MentionTimeline(&MentionTimelineParams{Count: 20, IncludeEntities: Bool(false)})
expected := []Tweet{Tweet{Text: "@dghubble can I get verified?"}, Tweet{Text: "@dghubble why are gophers so great?"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestTimelineService_RetweetsOfMeTimeline(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweets_of_me.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"trim_user": "false", "include_user_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "RT Twitter UK edition"}, {"text": "RT Triply-replicated Gophers"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Timelines.RetweetsOfMeTimeline(&RetweetsOfMeTimelineParams{TrimUser: Bool(false), IncludeUserEntities: Bool(false)})
expected := []Tweet{Tweet{Text: "RT Twitter UK edition"}, Tweet{Text: "RT Triply-replicated Gophers"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}

102
vendor/github.com/dghubble/go-twitter/twitter/trends.go generated vendored Normal file
View File

@@ -0,0 +1,102 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// TrendsService provides methods for accessing Twitter trends API endpoints.
type TrendsService struct {
sling *sling.Sling
}
// newTrendsService returns a new TrendsService.
func newTrendsService(sling *sling.Sling) *TrendsService {
return &TrendsService{
sling: sling.Path("trends/"),
}
}
// PlaceType represents a twitter trends PlaceType.
type PlaceType struct {
Code int `json:"code"`
Name string `json:"name"`
}
// Location reporesents a twitter Location.
type Location struct {
Country string `json:"country"`
CountryCode string `json:"countryCode"`
Name string `json:"name"`
ParentID int `json:"parentid"`
PlaceType PlaceType `json:"placeType"`
URL string `json:"url"`
WOEID int64 `json:"woeid"`
}
// Available returns the locations that Twitter has trending topic information for.
// https://dev.twitter.com/rest/reference/get/trends/available
func (s *TrendsService) Available() ([]Location, *http.Response, error) {
locations := new([]Location)
apiError := new(APIError)
resp, err := s.sling.New().Get("available.json").Receive(locations, apiError)
return *locations, resp, relevantError(err, *apiError)
}
// Trend represents a twitter trend.
type Trend struct {
Name string `json:"name"`
URL string `json:"url"`
PromotedContent string `json:"promoted_content"`
Query string `json:"query"`
TweetVolume int64 `json:"tweet_volume"`
}
// TrendsList represents a list of twitter trends.
type TrendsList struct {
Trends []Trend `json:"trends"`
AsOf string `json:"as_of"`
CreatedAt string `json:"created_at"`
Locations []TrendsLocation `json:"locations"`
}
// TrendsLocation represents a twitter trend location.
type TrendsLocation struct {
Name string `json:"name"`
WOEID int64 `json:"woeid"`
}
// TrendsPlaceParams are the parameters for Trends.Place.
type TrendsPlaceParams struct {
WOEID int64 `url:"id,omitempty"`
Exclude string `url:"exclude,omitempty"`
}
// Place returns the top 50 trending topics for a specific WOEID.
// https://dev.twitter.com/rest/reference/get/trends/place
func (s *TrendsService) Place(woeid int64, params *TrendsPlaceParams) ([]TrendsList, *http.Response, error) {
if params == nil {
params = &TrendsPlaceParams{}
}
trendsList := new([]TrendsList)
params.WOEID = woeid
apiError := new(APIError)
resp, err := s.sling.New().Get("place.json").QueryStruct(params).Receive(trendsList, apiError)
return *trendsList, resp, relevantError(err, *apiError)
}
// ClosestParams are the parameters for Trends.Closest.
type ClosestParams struct {
Lat float64 `url:"lat"`
Long float64 `url:"long"`
}
// Closest returns the locations that Twitter has trending topic information for, closest to a specified location.
// https://dev.twitter.com/rest/reference/get/trends/closest
func (s *TrendsService) Closest(params *ClosestParams) ([]Location, *http.Response, error) {
locations := new([]Location)
apiError := new(APIError)
resp, err := s.sling.New().Get("closest.json").QueryStruct(params).Receive(locations, apiError)
return *locations, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,87 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTrendsService_Available(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/trends/available.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"country": "Sweden","countryCode": "SE","name": "Sweden","parentid": 1,"placeType": {"code": 12,"name": "Country"},"url": "http://where.yahooapis.com/v1/place/23424954","woeid": 23424954}]`)
})
expected := []Location{
Location{
Country: "Sweden",
CountryCode: "SE",
Name: "Sweden",
ParentID: 1,
PlaceType: PlaceType{Code: 12, Name: "Country"},
URL: "http://where.yahooapis.com/v1/place/23424954",
WOEID: 23424954,
},
}
client := NewClient(httpClient)
locations, _, err := client.Trends.Available()
assert.Nil(t, err)
assert.Equal(t, expected, locations)
}
func TestTrendsService_Place(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/trends/place.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "123456"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"trends":[{"name":"#gotwitter"}], "as_of": "2017-02-08T16:18:18Z", "created_at": "2017-02-08T16:10:33Z","locations":[{"name": "Worldwide","woeid": 1}]}]`)
})
expected := []TrendsList{TrendsList{
Trends: []Trend{Trend{Name: "#gotwitter"}},
AsOf: "2017-02-08T16:18:18Z",
CreatedAt: "2017-02-08T16:10:33Z",
Locations: []TrendsLocation{TrendsLocation{Name: "Worldwide", WOEID: 1}},
}}
client := NewClient(httpClient)
places, _, err := client.Trends.Place(123456, &TrendsPlaceParams{})
assert.Nil(t, err)
assert.Equal(t, expected, places)
}
func TestTrendsService_Closest(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/trends/closest.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"lat": "37.781157", "long": "-122.400612831116"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"country": "Sweden","countryCode": "SE","name": "Sweden","parentid": 1,"placeType": {"code": 12,"name": "Country"},"url": "http://where.yahooapis.com/v1/place/23424954","woeid": 23424954}]`)
})
expected := []Location{
Location{
Country: "Sweden",
CountryCode: "SE",
Name: "Sweden",
ParentID: 1,
PlaceType: PlaceType{Code: 12, Name: "Country"},
URL: "http://where.yahooapis.com/v1/place/23424954",
WOEID: 23424954,
},
}
client := NewClient(httpClient)
locations, _, err := client.Trends.Closest(&ClosestParams{Lat: 37.781157, Long: -122.400612831116})
assert.Nil(t, err)
assert.Equal(t, expected, locations)
}

View File

@@ -0,0 +1,61 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
const twitterAPI = "https://api.twitter.com/1.1/"
// Client is a Twitter client for making Twitter API requests.
type Client struct {
sling *sling.Sling
// Twitter API Services
Accounts *AccountService
DirectMessages *DirectMessageService
Favorites *FavoriteService
Followers *FollowerService
Friends *FriendService
Friendships *FriendshipService
Search *SearchService
Statuses *StatusService
Streams *StreamService
Timelines *TimelineService
Trends *TrendsService
Users *UserService
}
// NewClient returns a new Client.
func NewClient(httpClient *http.Client) *Client {
base := sling.New().Client(httpClient).Base(twitterAPI)
return &Client{
sling: base,
Accounts: newAccountService(base.New()),
DirectMessages: newDirectMessageService(base.New()),
Favorites: newFavoriteService(base.New()),
Followers: newFollowerService(base.New()),
Friends: newFriendService(base.New()),
Friendships: newFriendshipService(base.New()),
Search: newSearchService(base.New()),
Statuses: newStatusService(base.New()),
Streams: newStreamService(httpClient, base.New()),
Timelines: newTimelineService(base.New()),
Trends: newTrendsService(base.New()),
Users: newUserService(base.New()),
}
}
// Bool returns a new pointer to the given bool value.
func Bool(v bool) *bool {
ptr := new(bool)
*ptr = v
return ptr
}
// Float returns a new pointer to the given float64 value.
func Float(v float64) *float64 {
ptr := new(float64)
*ptr = v
return ptr
}

View File

@@ -0,0 +1,93 @@
package twitter
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var defaultTestTimeout = time.Second * 1
// testServer returns an http Client, ServeMux, and Server. The client proxies
// requests to the server and handlers can be registered on the mux to handle
// requests. The caller must close the test server.
func testServer() (*http.Client, *http.ServeMux, *httptest.Server) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
transport := &RewriteTransport{&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL)
},
}}
client := &http.Client{Transport: transport}
return client, mux, server
}
// RewriteTransport rewrites https requests to http to avoid TLS cert issues
// during testing.
type RewriteTransport struct {
Transport http.RoundTripper
}
// RoundTrip rewrites the request scheme to http and calls through to the
// composed RoundTripper or if it is nil, to the http.DefaultTransport.
func (t *RewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.URL.Scheme = "http"
if t.Transport == nil {
return http.DefaultTransport.RoundTrip(req)
}
return t.Transport.RoundTrip(req)
}
func assertMethod(t *testing.T, expectedMethod string, req *http.Request) {
assert.Equal(t, expectedMethod, req.Method)
}
// assertQuery tests that the Request has the expected url query key/val pairs
func assertQuery(t *testing.T, expected map[string]string, req *http.Request) {
queryValues := req.URL.Query()
expectedValues := url.Values{}
for key, value := range expected {
expectedValues.Add(key, value)
}
assert.Equal(t, expectedValues, queryValues)
}
// assertPostForm tests that the Request has the expected key values pairs url
// encoded in its Body
func assertPostForm(t *testing.T, expected map[string]string, req *http.Request) {
req.ParseForm() // parses request Body to put url.Values in r.Form/r.PostForm
expectedValues := url.Values{}
for key, value := range expected {
expectedValues.Add(key, value)
}
assert.Equal(t, expectedValues, req.Form)
}
// assertDone asserts that the empty struct channel is closed before the given
// timeout elapses.
func assertDone(t *testing.T, ch <-chan struct{}, timeout time.Duration) {
select {
case <-ch:
_, more := <-ch
assert.False(t, more)
case <-time.After(timeout):
t.Errorf("expected channel to be closed within timeout %v", timeout)
}
}
// assertClosed asserts that the channel is closed before the given timeout
// elapses.
func assertClosed(t *testing.T, ch <-chan interface{}, timeout time.Duration) {
select {
case <-ch:
_, more := <-ch
assert.False(t, more)
case <-time.After(timeout):
t.Errorf("expected channel to be closed within timeout %v", timeout)
}
}

122
vendor/github.com/dghubble/go-twitter/twitter/users.go generated vendored Normal file
View File

@@ -0,0 +1,122 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// User represents a Twitter User.
// https://dev.twitter.com/overview/api/users
type User struct {
ContributorsEnabled bool `json:"contributors_enabled"`
CreatedAt string `json:"created_at"`
DefaultProfile bool `json:"default_profile"`
DefaultProfileImage bool `json:"default_profile_image"`
Description string `json:"description"`
Email string `json:"email"`
Entities *UserEntities `json:"entities"`
FavouritesCount int `json:"favourites_count"`
FollowRequestSent bool `json:"follow_request_sent"`
Following bool `json:"following"`
FollowersCount int `json:"followers_count"`
FriendsCount int `json:"friends_count"`
GeoEnabled bool `json:"geo_enabled"`
ID int64 `json:"id"`
IDStr string `json:"id_str"`
IsTranslator bool `json:"is_translator"`
Lang string `json:"lang"`
ListedCount int `json:"listed_count"`
Location string `json:"location"`
Name string `json:"name"`
Notifications bool `json:"notifications"`
ProfileBackgroundColor string `json:"profile_background_color"`
ProfileBackgroundImageURL string `json:"profile_background_image_url"`
ProfileBackgroundImageURLHttps string `json:"profile_background_image_url_https"`
ProfileBackgroundTile bool `json:"profile_background_tile"`
ProfileBannerURL string `json:"profile_banner_url"`
ProfileImageURL string `json:"profile_image_url"`
ProfileImageURLHttps string `json:"profile_image_url_https"`
ProfileLinkColor string `json:"profile_link_color"`
ProfileSidebarBorderColor string `json:"profile_sidebar_border_color"`
ProfileSidebarFillColor string `json:"profile_sidebar_fill_color"`
ProfileTextColor string `json:"profile_text_color"`
ProfileUseBackgroundImage bool `json:"profile_use_background_image"`
Protected bool `json:"protected"`
ScreenName string `json:"screen_name"`
ShowAllInlineMedia bool `json:"show_all_inline_media"`
Status *Tweet `json:"status"`
StatusesCount int `json:"statuses_count"`
Timezone string `json:"time_zone"`
URL string `json:"url"`
UtcOffset int `json:"utc_offset"`
Verified bool `json:"verified"`
WithheldInCountries []string `json:"withheld_in_countries"`
WithholdScope string `json:"withheld_scope"`
}
// UserService provides methods for accessing Twitter user API endpoints.
type UserService struct {
sling *sling.Sling
}
// newUserService returns a new UserService.
func newUserService(sling *sling.Sling) *UserService {
return &UserService{
sling: sling.Path("users/"),
}
}
// UserShowParams are the parameters for UserService.Show.
type UserShowParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"` // whether 'status' should include entities
}
// Show returns the requested User.
// https://dev.twitter.com/rest/reference/get/users/show
func (s *UserService) Show(params *UserShowParams) (*User, *http.Response, error) {
user := new(User)
apiError := new(APIError)
resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(user, apiError)
return user, resp, relevantError(err, *apiError)
}
// UserLookupParams are the parameters for UserService.Lookup.
type UserLookupParams struct {
UserID []int64 `url:"user_id,omitempty,comma"`
ScreenName []string `url:"screen_name,omitempty,comma"`
IncludeEntities *bool `url:"include_entities,omitempty"` // whether 'status' should include entities
}
// Lookup returns the requested Users as a slice.
// https://dev.twitter.com/rest/reference/get/users/lookup
func (s *UserService) Lookup(params *UserLookupParams) ([]User, *http.Response, error) {
users := new([]User)
apiError := new(APIError)
resp, err := s.sling.New().Get("lookup.json").QueryStruct(params).Receive(users, apiError)
return *users, resp, relevantError(err, *apiError)
}
// UserSearchParams are the parameters for UserService.Search.
type UserSearchParams struct {
Query string `url:"q,omitempty"`
Page int `url:"page,omitempty"` // 1-based page number
Count int `url:"count,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"` // whether 'status' should include entities
}
// Search queries public user accounts.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/users/search
func (s *UserService) Search(query string, params *UserSearchParams) ([]User, *http.Response, error) {
if params == nil {
params = &UserSearchParams{}
}
params.Query = query
users := new([]User)
apiError := new(APIError)
resp, err := s.sling.New().Get("search.json").QueryStruct(params).Receive(users, apiError)
return *users, resp, relevantError(err, *apiError)
}

View File

@@ -0,0 +1,92 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserService_Show(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/show.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"screen_name": "xkcdComic"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"name": "XKCD Comic", "favourites_count": 2}`)
})
client := NewClient(httpClient)
user, _, err := client.Users.Show(&UserShowParams{ScreenName: "xkcdComic"})
expected := &User{Name: "XKCD Comic", FavouritesCount: 2}
assert.Nil(t, err)
assert.Equal(t, expected, user)
}
func TestUserService_LookupWithIds(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/lookup.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "113419064,623265148"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"screen_name": "golang"}, {"screen_name": "dghubble"}]`)
})
client := NewClient(httpClient)
users, _, err := client.Users.Lookup(&UserLookupParams{UserID: []int64{113419064, 623265148}})
expected := []User{User{ScreenName: "golang"}, User{ScreenName: "dghubble"}}
assert.Nil(t, err)
assert.Equal(t, expected, users)
}
func TestUserService_LookupWithScreenNames(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/lookup.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"screen_name": "foo,bar"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"name": "Foo"}, {"name": "Bar"}]`)
})
client := NewClient(httpClient)
users, _, err := client.Users.Lookup(&UserLookupParams{ScreenName: []string{"foo", "bar"}})
expected := []User{User{Name: "Foo"}, User{Name: "Bar"}}
assert.Nil(t, err)
assert.Equal(t, expected, users)
}
func TestUserService_Search(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/search.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"count": "11", "q": "news"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"name": "BBC"}, {"name": "BBC Breaking News"}]`)
})
client := NewClient(httpClient)
users, _, err := client.Users.Search("news", &UserSearchParams{Query: "override me", Count: 11})
expected := []User{User{Name: "BBC"}, User{Name: "BBC Breaking News"}}
assert.Nil(t, err)
assert.Equal(t, expected, users)
}
func TestUserService_SearchHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/search.json", func(w http.ResponseWriter, r *http.Request) {
assertQuery(t, map[string]string{"q": "news"}, r)
})
client := NewClient(httpClient)
client.Users.Search("news", nil)
}