├── .github
├── dependabot.yaml
└── workflows
│ └── test.yaml
├── .gitignore
├── CHANGES.md
├── LICENSE
├── Makefile
├── README.md
├── examples
├── README.md
├── app-auth.go
├── direct_messages.go
├── go.mod
├── go.sum
├── streaming.go
└── user-auth.go
├── go.mod
├── go.sum
└── twitter
├── accounts.go
├── accounts_test.go
├── backoffs.go
├── backoffs_test.go
├── block.go
├── block_test.go
├── demux.go
├── demux_test.go
├── direct_messages.go
├── direct_messages_test.go
├── doc.go
├── entities.go
├── entities_test.go
├── errors.go
├── errors_test.go
├── favorites.go
├── favorites_test.go
├── followers.go
├── followers_test.go
├── friends.go
├── friends_test.go
├── friendships.go
├── friendships_test.go
├── lists.go
├── lists_test.go
├── premium_search.go
├── premium_search_test.go
├── rate_limits.go
├── rate_limits_test.go
├── search.go
├── search_test.go
├── statuses.go
├── statuses_test.go
├── stream_messages.go
├── stream_utils.go
├── stream_utils_test.go
├── streams.go
├── streams_test.go
├── timelines.go
├── timelines_test.go
├── trends.go
├── trends_test.go
├── twitter.go
├── twitter_test.go
├── users.go
└── users_test.go
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | pull-request-branch-name:
8 | separator: "-"
9 | open-pull-requests-limit: 3
10 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | jobs:
8 | build:
9 | name: go
10 | runs-on: ubuntu-latest
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | go: ['1.17', '1.18', '1.19']
15 | steps:
16 | - name: setup
17 | uses: actions/setup-go@v3
18 | with:
19 | go-version: ${{matrix.go}}
20 |
21 | - name: checkout
22 | uses: actions/checkout@v3
23 |
24 | - name: test
25 | run: make
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /bin
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # go-twitter Changelog
2 |
3 | Notable changes over time. Note, `go-twitter` does not follow a semver release cycle since it may change whenever the Twitter API changes (external).
4 |
5 | ## 07/2019
6 |
7 | * Add Go module support ([#143](https://github.com/dghubble/go-twitter/pull/143))
8 |
9 | ## 11/2018
10 |
11 | * Add `DirectMessageService` support for the new Twitter Direct Message Events [API](https://developer.twitter.com/en/docs/direct-messages/api-features) ([#125](https://github.com/dghubble/go-twitter/pull/125))
12 | * Add `EventsNew` method for sending a Direct Message event
13 | * Add `EventsShow` method for getting a single Direct Message event
14 | * Add `EventsList` method for listing recent Direct Message events
15 | * Add`EventsDestroy` method for destroying a Direct Message event
16 |
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Dalton Hubble
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all
2 | all: test vet fmt
3 |
4 | .PHONY: test
5 | test:
6 | @go test ./twitter -cover
7 |
8 | .PHONY: vet
9 | vet:
10 | @go vet -all ./twitter
11 |
12 | .PHONY: fmt
13 | fmt:
14 | @test -z $$(go fmt ./...)
15 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-twitter [](https://pkg.go.dev/github.com/dghubble/go-twitter) [](https://github.com/dghubble/go-twitter/actions/workflows/test.yaml?query=branch%3Amain) [](https://github.com/sponsors/dghubble) [](https://twitter.com/dghubble)
2 |
3 | **DEPRECATED** As of Nov 2022, the go-twitter API library is no longer being developed. If you fork this repo, please remove the logo since it is not covered by the license.
4 |
5 |
6 |
7 | 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.
8 |
9 | ### Features
10 |
11 | * Twitter REST API:
12 | * Accounts
13 | * Blocks
14 | * DirectMessageEvents
15 | * Favorites
16 | * Friends
17 | * Friendships
18 | * Followers
19 | * Lists
20 | * PremiumSearch
21 | * RateLimits
22 | * Search
23 | * Statuses
24 | * Timelines
25 | * Users
26 | * Twitter Streaming API
27 | * Public Streams
28 | * User Streams
29 | * Site Streams
30 | * Firehose Streams
31 |
32 | ## Install
33 |
34 | go get github.com/dghubble/go-twitter/twitter
35 |
36 | ## Documentation
37 |
38 | Read [GoDoc](https://godoc.org/github.com/dghubble/go-twitter/twitter)
39 |
40 | ## Usage
41 |
42 | ### REST API
43 |
44 | The `twitter` package provides a `Client` for accessing the Twitter API. Here are some example requests.
45 |
46 | ```go
47 | config := oauth1.NewConfig("consumerKey", "consumerSecret")
48 | token := oauth1.NewToken("accessToken", "accessSecret")
49 | httpClient := config.Client(oauth1.NoContext, token)
50 |
51 | // Twitter client
52 | client := twitter.NewClient(httpClient)
53 |
54 | // Home Timeline
55 | tweets, resp, err := client.Timelines.HomeTimeline(&twitter.HomeTimelineParams{
56 | Count: 20,
57 | })
58 |
59 | // Send a Tweet
60 | tweet, resp, err := client.Statuses.Update("just setting up my twttr", nil)
61 |
62 | // Status Show
63 | tweet, resp, err := client.Statuses.Show(585613041028431872, nil)
64 |
65 | // Search Tweets
66 | search, resp, err := client.Search.Tweets(&twitter.SearchTweetParams{
67 | Query: "gopher",
68 | })
69 |
70 | // User Show
71 | user, resp, err := client.Users.Show(&twitter.UserShowParams{
72 | ScreenName: "dghubble",
73 | })
74 |
75 | // Followers
76 | followers, resp, err := client.Followers.List(&twitter.FollowerListParams{})
77 | ```
78 |
79 | Authentication is handled by the `http.Client` passed to `NewClient` to handle user auth (OAuth1) or application auth (OAuth2). See the [Authentication](#authentication) section.
80 |
81 | Required parameters are passed as positional arguments. Optional parameters are passed typed params structs (or nil).
82 |
83 | ## Streaming API
84 |
85 | 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`.
86 |
87 | Create a `Client` with an authenticated `http.Client`. All stream endpoints require a user auth context so choose an OAuth1 `http.Client`.
88 |
89 | client := twitter.NewClient(httpClient)
90 |
91 | Next, request a managed `Stream` be started.
92 |
93 | #### Filter
94 |
95 | Filter Streams return Tweets that match one or more filtering predicates such as `Track`, `Follow`, and `Locations`.
96 |
97 | ```go
98 | params := &twitter.StreamFilterParams{
99 | Track: []string{"kitten"},
100 | StallWarnings: twitter.Bool(true),
101 | }
102 | stream, err := client.Streams.Filter(params)
103 | ```
104 |
105 | #### User
106 |
107 | User Streams provide messages specific to the authenticate User and possibly those they follow.
108 |
109 | ```go
110 | params := &twitter.StreamUserParams{
111 | With: "followings",
112 | StallWarnings: twitter.Bool(true),
113 | }
114 | stream, err := client.Streams.User(params)
115 | ```
116 |
117 | *Note* To see Direct Message events, your consumer application must ask Users for read/write/DM access to their account.
118 |
119 | #### Sample
120 |
121 | Sample Streams return a small sample of public Tweets.
122 |
123 | ```go
124 | params := &twitter.StreamSampleParams{
125 | StallWarnings: twitter.Bool(true),
126 | }
127 | stream, err := client.Streams.Sample(params)
128 | ```
129 |
130 | #### Site, Firehose
131 |
132 | Site and Firehose Streams require your application to have special permissions, but their API works the same way.
133 |
134 | ### Receiving Messages
135 |
136 | Each `Stream` maintains the connection to the Twitter Streaming API endpoint, receives messages, and sends them on the `Stream.Messages` channel.
137 |
138 | Go channels support range iterations which allow you to read the messages which are of type `interface{}`.
139 |
140 | ```go
141 | for message := range stream.Messages {
142 | fmt.Println(message)
143 | }
144 | ```
145 |
146 | 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.
147 |
148 | ### Demux
149 |
150 | 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.
151 |
152 | For this, try a `Demux`, like `SwitchDemux`, which receives messages and type switches them to call functions with typed messages.
153 |
154 | For example, say you're only interested in Tweets and Direct Messages.
155 |
156 | ```go
157 | demux := twitter.NewSwitchDemux()
158 | demux.Tweet = func(tweet *twitter.Tweet) {
159 | fmt.Println(tweet.Text)
160 | }
161 | demux.DM = func(dm *twitter.DirectMessage) {
162 | fmt.Println(dm.SenderID)
163 | }
164 | ```
165 |
166 | Pass the `Demux` each message or give it the entire `Stream.Message` channel.
167 |
168 | ```go
169 | for message := range stream.Messages {
170 | demux.Handle(message)
171 | }
172 | // or pass the channel
173 | demux.HandleChan(stream.Messages)
174 | ```
175 |
176 | ### Stopping
177 |
178 | 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.
179 |
180 | 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.
181 |
182 | ### Pitfalls
183 |
184 | **Bad**: In this example, `Stop()` is unlikely to be reached. Control stays in the message loop unless the `Stream` becomes disconnected and cannot retry.
185 |
186 | ```go
187 | // program does not terminate :(
188 | stream, _ := client.Streams.Sample(params)
189 | for message := range stream.Messages {
190 | demux.Handle(message)
191 | }
192 | stream.Stop()
193 | ```
194 |
195 | **Bad**: Here, messages are received on a non-main goroutine, but then `Stop()` is called immediately. The `Stream` is stopped and the program exits.
196 |
197 | ```go
198 | // got no messages :(
199 | stream, _ := client.Streams.Sample(params)
200 | go demux.HandleChan(stream.Messages)
201 | stream.Stop()
202 | ```
203 |
204 | **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`.
205 |
206 | ```go
207 | stream, err := client.Streams.Sample(params)
208 | go demux.HandleChan(stream.Messages)
209 |
210 | // Wait for SIGINT and SIGTERM (HIT CTRL-C)
211 | ch := make(chan os.Signal)
212 | signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
213 | log.Println(<-ch)
214 |
215 | stream.Stop()
216 | ```
217 |
218 | ## Authentication
219 |
220 | 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.
221 |
222 | 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.
223 |
224 | See the [google/go-github](https://github.com/google/go-github) client which takes the same approach.
225 |
226 | For example, make requests as a consumer application on behalf of a user who has granted access, with OAuth1.
227 |
228 | ```go
229 | // OAuth1
230 | import (
231 | "github.com/dghubble/go-twitter/twitter"
232 | "github.com/dghubble/oauth1"
233 | )
234 |
235 | config := oauth1.NewConfig("consumerKey", "consumerSecret")
236 | token := oauth1.NewToken("accessToken", "accessSecret")
237 | // http.Client will automatically authorize Requests
238 | httpClient := config.Client(oauth1.NoContext, token)
239 |
240 | // Twitter client
241 | client := twitter.NewClient(httpClient)
242 | ```
243 |
244 | If no user auth context is needed, make requests as your application with application auth.
245 |
246 | ```go
247 | // OAuth2
248 | import (
249 | "github.com/dghubble/go-twitter/twitter"
250 | "golang.org/x/oauth2"
251 | "golang.org/x/oauth2/clientcredentials"
252 | )
253 |
254 | // oauth2 configures a client that uses app credentials to keep a fresh token
255 | config := &clientcredentials.Config{
256 | ClientID: "consumerKey",
257 | ClientSecret: "consumerSecret",
258 | TokenURL: "https://api.twitter.com/oauth2/token",
259 | }
260 | // http.Client will automatically authorize Requests
261 | httpClient := config.Client(oauth2.NoContext)
262 |
263 | // Twitter client
264 | client := twitter.NewClient(httpClient)
265 | ```
266 |
267 | 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).
268 |
269 | ## Roadmap
270 |
271 | * Support gzipped streams
272 | * Auto-stop streams in the event of long stalls
273 |
274 | ## Contributing
275 |
276 | See the [Contributing Guide](https://gist.github.com/dghubble/be682c123727f70bcfe7).
277 |
278 | ## License
279 |
280 | [MIT License](LICENSE)
281 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Examples
3 |
4 | Get the dependencies and examples
5 |
6 | cd examples
7 | go get .
8 |
9 | ## User Auth (OAuth1)
10 |
11 | A user access token (OAuth1) grants a consumer application access to a user's Twitter resources.
12 |
13 | Setup an OAuth1 `http.Client` with the consumer key and secret and oauth token and secret.
14 | ```
15 | export TWITTER_CONSUMER_KEY=xxx
16 | export TWITTER_CONSUMER_SECRET=xxx
17 | export TWITTER_ACCESS_TOKEN=xxx
18 | export TWITTER_ACCESS_SECRET=xxx
19 | ```
20 |
21 | 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).
22 |
23 | ```
24 | go run user-auth.go
25 | ```
26 |
27 | ## App Auth (OAuth2)
28 |
29 | 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.
30 |
31 | Setup an OAuth2 `http.Client` with the Twitter consumer key and secret.
32 |
33 | ```
34 | export TWITTER_CONSUMER_KEY=xxx
35 | export TWITTER_CONSUMER_SECRET=xxx
36 | ```
37 |
38 | To make requests as an application, create a `twitter` `Client` and get public Tweets or timelines or other public content.
39 |
40 | ```
41 | go run app-auth.go
42 | ```
43 |
44 | ## Streaming API
45 |
46 | A user access token (OAuth1) is required for Streaming API requests. See above.
47 |
48 | ```
49 | go run streaming.go
50 | ```
51 |
52 | Hit CTRL-C to stop streaming. Uncomment different examples in code to try different streams.
53 |
--------------------------------------------------------------------------------
/examples/app-auth.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/coreos/pkg/flagutil"
9 | "github.com/dghubble/go-twitter/twitter"
10 | "golang.org/x/oauth2"
11 | "golang.org/x/oauth2/clientcredentials"
12 | )
13 |
14 | func main() {
15 | flags := struct {
16 | consumerKey string
17 | consumerSecret string
18 | }{}
19 |
20 | flag.StringVar(&flags.consumerKey, "consumer-key", "", "Twitter Consumer Key")
21 | flag.StringVar(&flags.consumerSecret, "consumer-secret", "", "Twitter Consumer Secret")
22 | flag.Parse()
23 | flagutil.SetFlagsFromEnv(flag.CommandLine, "TWITTER")
24 |
25 | if flags.consumerKey == "" || flags.consumerSecret == "" {
26 | log.Fatal("Application Access Token required")
27 | }
28 |
29 | // oauth2 configures a client that uses app credentials to keep a fresh token
30 | config := &clientcredentials.Config{
31 | ClientID: flags.consumerKey,
32 | ClientSecret: flags.consumerSecret,
33 | TokenURL: "https://api.twitter.com/oauth2/token",
34 | }
35 | // http.Client will automatically authorize Requests
36 | httpClient := config.Client(oauth2.NoContext)
37 |
38 | // Twitter client
39 | client := twitter.NewClient(httpClient)
40 |
41 | // user show
42 | userShowParams := &twitter.UserShowParams{ScreenName: "golang"}
43 | user, _, _ := client.Users.Show(userShowParams)
44 | fmt.Printf("USERS SHOW:\n%+v\n", user)
45 |
46 | // users lookup
47 | userLookupParams := &twitter.UserLookupParams{ScreenName: []string{"golang", "gophercon"}}
48 | users, _, _ := client.Users.Lookup(userLookupParams)
49 | fmt.Printf("USERS LOOKUP:\n%+v\n", users)
50 |
51 | // status show
52 | statusShowParams := &twitter.StatusShowParams{}
53 | tweet, _, _ := client.Statuses.Show(584077528026849280, statusShowParams)
54 | fmt.Printf("STATUSES SHOW:\n%+v\n", tweet)
55 |
56 | // statuses lookup
57 | statusLookupParams := &twitter.StatusLookupParams{ID: []int64{20}, TweetMode: "extended"}
58 | tweets, _, _ := client.Statuses.Lookup([]int64{573893817000140800}, statusLookupParams)
59 | fmt.Printf("STATUSES LOOKUP:\n%+v\n", tweets)
60 |
61 | // oEmbed status
62 | statusOembedParams := &twitter.StatusOEmbedParams{ID: 691076766878691329, MaxWidth: 500}
63 | oembed, _, _ := client.Statuses.OEmbed(statusOembedParams)
64 | fmt.Printf("OEMBED TWEET:\n%+v\n", oembed)
65 |
66 | // user timeline
67 | userTimelineParams := &twitter.UserTimelineParams{ScreenName: "golang", Count: 2}
68 | tweets, _, _ = client.Timelines.UserTimeline(userTimelineParams)
69 | fmt.Printf("USER TIMELINE:\n%+v\n", tweets)
70 |
71 | // search tweets
72 | searchTweetParams := &twitter.SearchTweetParams{
73 | Query: "happy birthday",
74 | TweetMode: "extended",
75 | Count: 3,
76 | }
77 | search, _, _ := client.Search.Tweets(searchTweetParams)
78 | fmt.Printf("SEARCH TWEETS:\n%+v\n", search)
79 | fmt.Printf("SEARCH METADATA:\n%+v\n", search.Metadata)
80 | }
81 |
--------------------------------------------------------------------------------
/examples/direct_messages.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 |
9 | "github.com/coreos/pkg/flagutil"
10 | "github.com/dghubble/go-twitter/twitter"
11 | "github.com/dghubble/oauth1"
12 | )
13 |
14 | func main() {
15 | flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
16 | consumerKey := flags.String("consumer-key", "", "Twitter Consumer Key")
17 | consumerSecret := flags.String("consumer-secret", "", "Twitter Consumer Secret")
18 | accessToken := flags.String("access-token", "", "Twitter Access Token")
19 | accessSecret := flags.String("access-secret", "", "Twitter Access Secret")
20 | flags.Parse(os.Args[1:])
21 | flagutil.SetFlagsFromEnv(flags, "TWITTER")
22 |
23 | if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" {
24 | log.Fatal("Consumer key/secret and Access token/secret required")
25 | }
26 |
27 | config := oauth1.NewConfig(*consumerKey, *consumerSecret)
28 | token := oauth1.NewToken(*accessToken, *accessSecret)
29 | // OAuth1 http.Client will automatically authorize Requests
30 | httpClient := config.Client(oauth1.NoContext, token)
31 |
32 | // Twitter client
33 | client := twitter.NewClient(httpClient)
34 |
35 | // List most recent 10 Direct Messages
36 | messages, _, err := client.DirectMessages.EventsList(
37 | &twitter.DirectMessageEventsListParams{Count: 10},
38 | )
39 | fmt.Println("User's DIRECT MESSAGES:")
40 | if err != nil {
41 | log.Fatal(err)
42 | }
43 | for _, event := range messages.Events {
44 | fmt.Printf("%+v\n", event)
45 | fmt.Printf(" %+v\n", event.Message)
46 | fmt.Printf(" %+v\n", event.Message.Data)
47 | }
48 |
49 | // Show Direct Message event
50 | event, _, err := client.DirectMessages.EventsShow("1066903366071017476", nil)
51 | fmt.Printf("DM Events Show:\n%+v, %v\n", event.Message.Data, err)
52 |
53 | // Create Direct Message event
54 | /*
55 | event, _, err = client.DirectMessages.EventsNew(&twitter.DirectMessageEventsNewParams{
56 | Event: &twitter.DirectMessageEvent{
57 | Type: "message_create",
58 | Message: &twitter.DirectMessageEventMessage{
59 | Target: &twitter.DirectMessageTarget{
60 | RecipientID: "2856535627",
61 | },
62 | Data: &twitter.DirectMessageData{
63 | Text: "testing",
64 | },
65 | },
66 | },
67 | })
68 | fmt.Printf("DM Event New:\n%+v, %v\n", event, err)
69 | */
70 |
71 | // Destroy Direct Message event
72 | //_, err = client.DirectMessages.EventsDestroy("1066904217049133060")
73 | //fmt.Printf("DM Events Delete:\n err: %v\n", err)
74 | }
75 |
--------------------------------------------------------------------------------
/examples/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dghubble/go-twitter/examples
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f
7 | github.com/dghubble/go-twitter v0.0.0-20220716034336-7f63262ef83a
8 | github.com/dghubble/oauth1 v0.6.0
9 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
10 | )
11 |
12 | require (
13 | github.com/cenkalti/backoff/v4 v4.1.3 // indirect
14 | github.com/dghubble/sling v1.4.0 // indirect
15 | github.com/golang/protobuf v1.2.0 // indirect
16 | github.com/google/go-querystring v1.1.0 // indirect
17 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e // indirect
18 | google.golang.org/appengine v1.4.0 // indirect
19 | )
20 |
21 | replace github.com/dghubble/go-twitter/twitter => ../
22 |
--------------------------------------------------------------------------------
/examples/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
3 | github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
4 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
5 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/dghubble/go-twitter v0.0.0-20220716034336-7f63262ef83a h1:QDqWWmsz3yf+li4eq4ydnMQUBGiwagNlgRkv0MuALxg=
10 | github.com/dghubble/go-twitter v0.0.0-20220716034336-7f63262ef83a/go.mod h1:q7VYuSasPO79IE/QBNAMYVNlzZNy4Zr7vay6is50u5I=
11 | github.com/dghubble/oauth1 v0.6.0 h1:m1yC01Ohc/eF38jwZ8JUjL1a+XHHXtGQgK+MxQbmSx0=
12 | github.com/dghubble/oauth1 v0.6.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk=
13 | github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY=
14 | github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8=
15 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
16 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
17 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
18 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
19 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
20 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
24 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
25 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
26 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
27 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
28 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
29 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
30 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
31 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
32 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
33 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
34 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
35 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
37 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
38 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
39 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
44 |
--------------------------------------------------------------------------------
/examples/streaming.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 | "os/signal"
9 | "syscall"
10 |
11 | "github.com/coreos/pkg/flagutil"
12 | "github.com/dghubble/go-twitter/twitter"
13 | "github.com/dghubble/oauth1"
14 | )
15 |
16 | func main() {
17 | flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
18 | consumerKey := flags.String("consumer-key", "", "Twitter Consumer Key")
19 | consumerSecret := flags.String("consumer-secret", "", "Twitter Consumer Secret")
20 | accessToken := flags.String("access-token", "", "Twitter Access Token")
21 | accessSecret := flags.String("access-secret", "", "Twitter Access Secret")
22 | flags.Parse(os.Args[1:])
23 | flagutil.SetFlagsFromEnv(flags, "TWITTER")
24 |
25 | if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" {
26 | log.Fatal("Consumer key/secret and Access token/secret required")
27 | }
28 |
29 | config := oauth1.NewConfig(*consumerKey, *consumerSecret)
30 | token := oauth1.NewToken(*accessToken, *accessSecret)
31 | // OAuth1 http.Client will automatically authorize Requests
32 | httpClient := config.Client(oauth1.NoContext, token)
33 |
34 | // Twitter Client
35 | client := twitter.NewClient(httpClient)
36 |
37 | // Convenience Demux demultiplexed stream messages
38 | demux := twitter.NewSwitchDemux()
39 | demux.Tweet = func(tweet *twitter.Tweet) {
40 | fmt.Println(tweet.Text)
41 | }
42 | demux.DM = func(dm *twitter.DirectMessage) {
43 | fmt.Println(dm.SenderID)
44 | }
45 | demux.Event = func(event *twitter.Event) {
46 | fmt.Printf("%#v\n", event)
47 | }
48 |
49 | fmt.Println("Starting Stream...")
50 |
51 | // FILTER
52 | filterParams := &twitter.StreamFilterParams{
53 | Track: []string{"cat"},
54 | StallWarnings: twitter.Bool(true),
55 | }
56 | stream, err := client.Streams.Filter(filterParams)
57 | if err != nil {
58 | log.Fatal(err)
59 | }
60 |
61 | // USER (quick test: auth'd user likes a tweet -> event)
62 | // userParams := &twitter.StreamUserParams{
63 | // StallWarnings: twitter.Bool(true),
64 | // With: "followings",
65 | // Language: []string{"en"},
66 | // }
67 | // stream, err := client.Streams.User(userParams)
68 | // if err != nil {
69 | // log.Fatal(err)
70 | // }
71 |
72 | // SAMPLE
73 | // sampleParams := &twitter.StreamSampleParams{
74 | // StallWarnings: twitter.Bool(true),
75 | // }
76 | // stream, err := client.Streams.Sample(sampleParams)
77 | // if err != nil {
78 | // log.Fatal(err)
79 | // }
80 |
81 | // Receive messages until stopped or stream quits
82 | go demux.HandleChan(stream.Messages)
83 |
84 | // Wait for SIGINT and SIGTERM (HIT CTRL-C)
85 | ch := make(chan os.Signal)
86 | signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
87 | log.Println(<-ch)
88 |
89 | fmt.Println("Stopping Stream...")
90 | stream.Stop()
91 | }
92 |
--------------------------------------------------------------------------------
/examples/user-auth.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 |
9 | "github.com/coreos/pkg/flagutil"
10 | "github.com/dghubble/go-twitter/twitter"
11 | "github.com/dghubble/oauth1"
12 | )
13 |
14 | func main() {
15 | flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
16 | consumerKey := flags.String("consumer-key", "", "Twitter Consumer Key")
17 | consumerSecret := flags.String("consumer-secret", "", "Twitter Consumer Secret")
18 | accessToken := flags.String("access-token", "", "Twitter Access Token")
19 | accessSecret := flags.String("access-secret", "", "Twitter Access Secret")
20 | flags.Parse(os.Args[1:])
21 | flagutil.SetFlagsFromEnv(flags, "TWITTER")
22 |
23 | if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" {
24 | log.Fatal("Consumer key/secret and Access token/secret required")
25 | }
26 |
27 | config := oauth1.NewConfig(*consumerKey, *consumerSecret)
28 | token := oauth1.NewToken(*accessToken, *accessSecret)
29 | // OAuth1 http.Client will automatically authorize Requests
30 | httpClient := config.Client(oauth1.NoContext, token)
31 |
32 | // Twitter client
33 | client := twitter.NewClient(httpClient)
34 |
35 | // Verify Credentials
36 | verifyParams := &twitter.AccountVerifyParams{
37 | SkipStatus: twitter.Bool(true),
38 | IncludeEmail: twitter.Bool(true),
39 | }
40 | user, _, _ := client.Accounts.VerifyCredentials(verifyParams)
41 | fmt.Printf("User's ACCOUNT:\n%+v\n", user)
42 |
43 | // Home Timeline
44 | homeTimelineParams := &twitter.HomeTimelineParams{
45 | Count: 2,
46 | TweetMode: "extended",
47 | }
48 | tweets, _, _ := client.Timelines.HomeTimeline(homeTimelineParams)
49 | fmt.Printf("User's HOME TIMELINE:\n%+v\n", tweets)
50 |
51 | // Mention Timeline
52 | mentionTimelineParams := &twitter.MentionTimelineParams{
53 | Count: 2,
54 | TweetMode: "extended",
55 | }
56 | tweets, _, _ = client.Timelines.MentionTimeline(mentionTimelineParams)
57 | fmt.Printf("User's MENTION TIMELINE:\n%+v\n", tweets)
58 |
59 | // Retweets of Me Timeline
60 | retweetTimelineParams := &twitter.RetweetsOfMeTimelineParams{
61 | Count: 2,
62 | TweetMode: "extended",
63 | }
64 | tweets, _, _ = client.Timelines.RetweetsOfMeTimeline(retweetTimelineParams)
65 | fmt.Printf("User's 'RETWEETS OF ME' TIMELINE:\n%+v\n", tweets)
66 |
67 | // Update (POST!) Tweet (uncomment to run)
68 | // tweet, _, _ := client.Statuses.Update("just setting up my twttr", nil)
69 | // fmt.Printf("Posted Tweet\n%v\n", tweet)
70 | }
71 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dghubble/go-twitter
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/cenkalti/backoff/v4 v4.1.3
7 | github.com/dghubble/sling v1.4.0
8 | github.com/stretchr/testify v1.8.1
9 | )
10 |
11 | require (
12 | github.com/davecgh/go-spew v1.1.1 // indirect
13 | github.com/google/go-querystring v1.1.0 // indirect
14 | github.com/pmezard/go-difflib v1.0.0 // indirect
15 | gopkg.in/yaml.v3 v3.0.1 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
2 | github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY=
7 | github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8=
8 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
9 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
10 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
11 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
15 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
16 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
17 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
18 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
19 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
20 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
21 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
27 |
--------------------------------------------------------------------------------
/twitter/accounts.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/dghubble/sling"
7 | )
8 |
9 | // AccountService provides a method for account credential verification.
10 | type AccountService struct {
11 | sling *sling.Sling
12 | }
13 |
14 | // newAccountService returns a new AccountService.
15 | func newAccountService(sling *sling.Sling) *AccountService {
16 | return &AccountService{
17 | sling: sling.Path("account/"),
18 | }
19 | }
20 |
21 | // AccountVerifyParams are the params for AccountService.VerifyCredentials.
22 | type AccountVerifyParams struct {
23 | IncludeEntities *bool `url:"include_entities,omitempty"`
24 | SkipStatus *bool `url:"skip_status,omitempty"`
25 | IncludeEmail *bool `url:"include_email,omitempty"`
26 | }
27 |
28 | // VerifyCredentials returns the authorized user if credentials are valid and
29 | // returns an error otherwise.
30 | // Requires a user auth context.
31 | // https://dev.twitter.com/rest/reference/get/account/verify_credentials
32 | func (s *AccountService) VerifyCredentials(params *AccountVerifyParams) (*User, *http.Response, error) {
33 | user := new(User)
34 | apiError := new(APIError)
35 | resp, err := s.sling.New().Get("verify_credentials.json").QueryStruct(params).Receive(user, apiError)
36 | return user, resp, relevantError(err, *apiError)
37 | }
38 |
39 | // AccountUpdateProfileParams are the params for AccountService.UpdateProfile.
40 | type AccountUpdateProfileParams struct {
41 | Name string `url:"name,omitempty"`
42 | URL string `url:"url,omitempty"`
43 | Location string `url:"location,omitempty"`
44 | Description string `url:"description,omitempty"`
45 | IncludeEntities *bool `url:"include_entities,omitempty"`
46 | SkipStatus *bool `url:"skip_status,omitempty"`
47 | }
48 |
49 | // UpdateProfile updates the account profile with specified fields and returns
50 | // the User.
51 | // Requires a user auth context.
52 | // https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile
53 | func (s *AccountService) UpdateProfile(params *AccountUpdateProfileParams) (*User, *http.Response, error) {
54 | user := new(User)
55 | apiError := new(APIError)
56 | resp, err := s.sling.New().Post("update_profile.json").QueryStruct(params).Receive(user, apiError)
57 | return user, resp, relevantError(err, *apiError)
58 | }
59 |
--------------------------------------------------------------------------------
/twitter/accounts_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestAccountService_VerifyCredentials(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | mux.HandleFunc("/1.1/account/verify_credentials.json", func(w http.ResponseWriter, r *http.Request) {
16 | assertMethod(t, "GET", r)
17 | assertQuery(t, map[string]string{"include_entities": "false", "include_email": "true"}, r)
18 | w.Header().Set("Content-Type", "application/json")
19 | fmt.Fprintf(w, `{"name": "Dalton Hubble", "id": 623265148}`)
20 | })
21 |
22 | client := NewClient(httpClient)
23 | user, _, err := client.Accounts.VerifyCredentials(&AccountVerifyParams{IncludeEntities: Bool(false), IncludeEmail: Bool(true)})
24 | expected := &User{Name: "Dalton Hubble", ID: 623265148}
25 | assert.Nil(t, err)
26 | assert.Equal(t, expected, user)
27 | }
28 | func TestAccountService_UpdateProfile(t *testing.T) {
29 | httpClient, mux, server := testServer()
30 | defer server.Close()
31 |
32 | mux.HandleFunc("/1.1/account/update_profile.json", func(w http.ResponseWriter, r *http.Request) {
33 | assertMethod(t, "POST", r)
34 | assertQuery(t, map[string]string{"location": "anywhere"}, r)
35 | w.Header().Set("Content-Type", "application/json")
36 | fmt.Fprintf(w, `{"name": "xkcdComic", "location":"anywhere"}`)
37 | })
38 |
39 | client := NewClient(httpClient)
40 | params := &AccountUpdateProfileParams{Location: "anywhere"}
41 | user, _, err := client.Accounts.UpdateProfile(params)
42 | expected := &User{Name: "xkcdComic", Location: "anywhere"}
43 | assert.Nil(t, err)
44 | assert.Equal(t, expected, user)
45 | }
46 |
--------------------------------------------------------------------------------
/twitter/backoffs.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/cenkalti/backoff/v4"
7 | )
8 |
9 | func newExponentialBackOff() *backoff.ExponentialBackOff {
10 | b := backoff.NewExponentialBackOff()
11 | b.InitialInterval = 5 * time.Second
12 | b.Multiplier = 2.0
13 | b.MaxInterval = 320 * time.Second
14 | b.Reset()
15 | return b
16 | }
17 |
18 | func newAggressiveExponentialBackOff() *backoff.ExponentialBackOff {
19 | b := backoff.NewExponentialBackOff()
20 | b.InitialInterval = 1 * time.Minute
21 | b.Multiplier = 2.0
22 | b.MaxInterval = 16 * time.Minute
23 | b.Reset()
24 | return b
25 | }
26 |
--------------------------------------------------------------------------------
/twitter/backoffs_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestNewExponentialBackOff(t *testing.T) {
11 | b := newExponentialBackOff()
12 | assert.Equal(t, 5*time.Second, b.InitialInterval)
13 | assert.Equal(t, 2.0, b.Multiplier)
14 | assert.Equal(t, 320*time.Second, b.MaxInterval)
15 | }
16 |
17 | func TestNewAggressiveExponentialBackOff(t *testing.T) {
18 | b := newAggressiveExponentialBackOff()
19 | assert.Equal(t, 1*time.Minute, b.InitialInterval)
20 | assert.Equal(t, 2.0, b.Multiplier)
21 | assert.Equal(t, 16*time.Minute, b.MaxInterval)
22 | }
23 |
24 | // BackoffRecorder is an implementation of backoff.BackOff that records
25 | // calls to NextBackOff and Reset for later inspection in tests.
26 | type BackOffRecorder struct {
27 | Count int
28 | }
29 |
30 | func (b *BackOffRecorder) NextBackOff() time.Duration {
31 | b.Count++
32 | return 1 * time.Nanosecond
33 | }
34 |
35 | func (b *BackOffRecorder) Reset() {
36 | b.Count = 0
37 | }
38 |
--------------------------------------------------------------------------------
/twitter/block.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/dghubble/sling"
7 | )
8 |
9 | // BlockService provides methods for blocking specific user.
10 | type BlockService struct {
11 | sling *sling.Sling
12 | }
13 |
14 | // newBlockService returns a new BlockService.
15 | func newBlockService(sling *sling.Sling) *BlockService {
16 | return &BlockService{
17 | sling: sling.Path("blocks/"),
18 | }
19 | }
20 |
21 | // BlockCreateParams are the parameters for BlockService.Create.
22 | type BlockCreateParams struct {
23 | ScreenName string `url:"screen_name,omitempty,comma"`
24 | UserID int64 `url:"user_id,omitempty,comma"`
25 | IncludeEntities *bool `url:"include_entities,omitempty"`
26 | SkipStatus *bool `url:"skip_status,omitempty"`
27 | }
28 |
29 | // Create blocks a specific user and returns the blocked user.
30 | // https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-blocks-create
31 | func (s *BlockService) Create(params *BlockCreateParams) (User, *http.Response, error) {
32 | users := new(User)
33 | apiError := new(APIError)
34 | resp, err := s.sling.New().Post("create.json").QueryStruct(params).Receive(users, apiError)
35 | return *users, resp, relevantError(err, *apiError)
36 | }
37 |
38 | // BlockDestroyParams are the parameters for BlockService.Destroy.
39 | type BlockDestroyParams struct {
40 | ScreenName string `url:"screen_name,omitempty,comma"`
41 | UserID int64 `url:"user_id,omitempty,comma"`
42 | IncludeEntities *bool `url:"include_entities,omitempty"`
43 | SkipStatus *bool `url:"skip_status,omitempty"`
44 | }
45 |
46 | // Destroy blocks a specific user and returns the unblocked user.
47 | // https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-blocks-destroy
48 | func (s *BlockService) Destroy(params *BlockDestroyParams) (User, *http.Response, error) {
49 | users := new(User)
50 | apiError := new(APIError)
51 | resp, err := s.sling.New().Post("destroy.json").QueryStruct(params).Receive(users, apiError)
52 | return *users, resp, relevantError(err, *apiError)
53 | }
54 |
--------------------------------------------------------------------------------
/twitter/block_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestBlockService_CreateService(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | mux.HandleFunc("/1.1/blocks/create.json", func(w http.ResponseWriter, r *http.Request) {
16 | assertMethod(t, "POST", r)
17 | assertQuery(t, map[string]string{"screen_name": "golang"}, r)
18 | w.Header().Set("Content-Type", "application/json")
19 | fmt.Fprintf(w, `{"screen_name": "golang"}`)
20 | })
21 |
22 | client := NewClient(httpClient)
23 | users, _, err := client.Blocks.Create(&BlockCreateParams{ScreenName: "golang"})
24 | expected := User{ScreenName: "golang"}
25 | assert.Nil(t, err)
26 | assert.Equal(t, expected, users)
27 | }
28 |
29 | func TestBlockService_DestroyService(t *testing.T) {
30 | httpClient, mux, server := testServer()
31 | defer server.Close()
32 |
33 | mux.HandleFunc("/1.1/blocks/destroy.json", func(w http.ResponseWriter, r *http.Request) {
34 | assertMethod(t, "POST", r)
35 | assertQuery(t, map[string]string{"screen_name": "golang"}, r)
36 | w.Header().Set("Content-Type", "application/json")
37 | fmt.Fprintf(w, `{"screen_name": "golang"}`)
38 | })
39 |
40 | client := NewClient(httpClient)
41 | users, _, err := client.Blocks.Destroy(&BlockDestroyParams{ScreenName: "golang"})
42 | expected := User{ScreenName: "golang"}
43 | assert.Nil(t, err)
44 | assert.Equal(t, expected, users)
45 | }
46 |
--------------------------------------------------------------------------------
/twitter/demux.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | // A Demux receives interface{} messages individually or from a channel and
4 | // sends those messages to one or more outputs determined by the
5 | // implementation.
6 | type Demux interface {
7 | Handle(message interface{})
8 | HandleChan(messages <-chan interface{})
9 | }
10 |
11 | // SwitchDemux receives messages and uses a type switch to send each typed
12 | // message to a handler function.
13 | type SwitchDemux struct {
14 | All func(message interface{})
15 | Tweet func(tweet *Tweet)
16 | DM func(dm *DirectMessage)
17 | StatusDeletion func(deletion *StatusDeletion)
18 | LocationDeletion func(LocationDeletion *LocationDeletion)
19 | StreamLimit func(limit *StreamLimit)
20 | StatusWithheld func(statusWithheld *StatusWithheld)
21 | UserWithheld func(userWithheld *UserWithheld)
22 | StreamDisconnect func(disconnect *StreamDisconnect)
23 | Warning func(warning *StallWarning)
24 | FriendsList func(friendsList *FriendsList)
25 | Event func(event *Event)
26 | Other func(message interface{})
27 | }
28 |
29 | // NewSwitchDemux returns a new SwitchMux which has NoOp handler functions.
30 | func NewSwitchDemux() SwitchDemux {
31 | return SwitchDemux{
32 | All: func(message interface{}) {},
33 | Tweet: func(tweet *Tweet) {},
34 | DM: func(dm *DirectMessage) {},
35 | StatusDeletion: func(deletion *StatusDeletion) {},
36 | LocationDeletion: func(LocationDeletion *LocationDeletion) {},
37 | StreamLimit: func(limit *StreamLimit) {},
38 | StatusWithheld: func(statusWithheld *StatusWithheld) {},
39 | UserWithheld: func(userWithheld *UserWithheld) {},
40 | StreamDisconnect: func(disconnect *StreamDisconnect) {},
41 | Warning: func(warning *StallWarning) {},
42 | FriendsList: func(friendsList *FriendsList) {},
43 | Event: func(event *Event) {},
44 | Other: func(message interface{}) {},
45 | }
46 | }
47 |
48 | // Handle determines the type of a message and calls the corresponding receiver
49 | // function with the typed message. All messages are passed to the All func.
50 | // Messages with unmatched types are passed to the Other func.
51 | func (d SwitchDemux) Handle(message interface{}) {
52 | d.All(message)
53 | switch msg := message.(type) {
54 | case *Tweet:
55 | d.Tweet(msg)
56 | case *DirectMessage:
57 | d.DM(msg)
58 | case *StatusDeletion:
59 | d.StatusDeletion(msg)
60 | case *LocationDeletion:
61 | d.LocationDeletion(msg)
62 | case *StreamLimit:
63 | d.StreamLimit(msg)
64 | case *StatusWithheld:
65 | d.StatusWithheld(msg)
66 | case *UserWithheld:
67 | d.UserWithheld(msg)
68 | case *StreamDisconnect:
69 | d.StreamDisconnect(msg)
70 | case *StallWarning:
71 | d.Warning(msg)
72 | case *FriendsList:
73 | d.FriendsList(msg)
74 | case *Event:
75 | d.Event(msg)
76 | default:
77 | d.Other(msg)
78 | }
79 | }
80 |
81 | // HandleChan receives messages and calls the corresponding receiver function
82 | // with the typed message. All messages are passed to the All func. Messages
83 | // with unmatched type are passed to the Other func.
84 | func (d SwitchDemux) HandleChan(messages <-chan interface{}) {
85 | for message := range messages {
86 | d.Handle(message)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/twitter/demux_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestDemux_Handle(t *testing.T) {
10 | messages, expectedCounts := exampleMessages()
11 | counts := &counter{}
12 | demux := newCounterDemux(counts)
13 | for _, message := range messages {
14 | demux.Handle(message)
15 | }
16 | assert.Equal(t, expectedCounts, counts)
17 | }
18 |
19 | func TestDemux_HandleChan(t *testing.T) {
20 | messages, expectedCounts := exampleMessages()
21 | counts := &counter{}
22 | demux := newCounterDemux(counts)
23 | ch := make(chan interface{})
24 | // stream messages into channel
25 | go func() {
26 | for _, msg := range messages {
27 | ch <- msg
28 | }
29 | close(ch)
30 | }()
31 | // handle channel messages until exhausted
32 | demux.HandleChan(ch)
33 | assert.Equal(t, expectedCounts, counts)
34 | }
35 |
36 | // counter counts stream messages by type for testing.
37 | type counter struct {
38 | all int
39 | tweet int
40 | dm int
41 | statusDeletion int
42 | locationDeletion int
43 | streamLimit int
44 | statusWithheld int
45 | userWithheld int
46 | streamDisconnect int
47 | stallWarning int
48 | friendsList int
49 | event int
50 | other int
51 | }
52 |
53 | // newCounterDemux returns a Demux which counts message types.
54 | func newCounterDemux(counter *counter) Demux {
55 | demux := NewSwitchDemux()
56 | demux.All = func(interface{}) {
57 | counter.all++
58 | }
59 | demux.Tweet = func(*Tweet) {
60 | counter.tweet++
61 | }
62 | demux.DM = func(*DirectMessage) {
63 | counter.dm++
64 | }
65 | demux.StatusDeletion = func(*StatusDeletion) {
66 | counter.statusDeletion++
67 | }
68 | demux.LocationDeletion = func(*LocationDeletion) {
69 | counter.locationDeletion++
70 | }
71 | demux.StreamLimit = func(*StreamLimit) {
72 | counter.streamLimit++
73 | }
74 | demux.StatusWithheld = func(*StatusWithheld) {
75 | counter.statusWithheld++
76 | }
77 | demux.UserWithheld = func(*UserWithheld) {
78 | counter.userWithheld++
79 | }
80 | demux.StreamDisconnect = func(*StreamDisconnect) {
81 | counter.streamDisconnect++
82 | }
83 | demux.Warning = func(*StallWarning) {
84 | counter.stallWarning++
85 | }
86 | demux.FriendsList = func(*FriendsList) {
87 | counter.friendsList++
88 | }
89 | demux.Event = func(*Event) {
90 | counter.event++
91 | }
92 | demux.Other = func(interface{}) {
93 | counter.other++
94 | }
95 | return demux
96 | }
97 |
98 | // examples messages returns a test stream of messages and the expected
99 | // counts of each message type.
100 | func exampleMessages() (messages []interface{}, expectedCounts *counter) {
101 | var (
102 | tweet = &Tweet{}
103 | dm = &DirectMessage{}
104 | statusDeletion = &StatusDeletion{}
105 | locationDeletion = &LocationDeletion{}
106 | streamLimit = &StreamLimit{}
107 | statusWithheld = &StatusWithheld{}
108 | userWithheld = &UserWithheld{}
109 | streamDisconnect = &StreamDisconnect{}
110 | stallWarning = &StallWarning{}
111 | friendsList = &FriendsList{}
112 | event = &Event{}
113 | otherA = func() {}
114 | otherB = struct{}{}
115 | )
116 | messages = []interface{}{tweet, dm, statusDeletion, locationDeletion,
117 | streamLimit, statusWithheld, userWithheld, streamDisconnect,
118 | stallWarning, friendsList, event, otherA, otherB}
119 | expectedCounts = &counter{
120 | all: len(messages),
121 | tweet: 1,
122 | dm: 1,
123 | statusDeletion: 1,
124 | locationDeletion: 1,
125 | streamLimit: 1,
126 | statusWithheld: 1,
127 | userWithheld: 1,
128 | streamDisconnect: 1,
129 | stallWarning: 1,
130 | friendsList: 1,
131 | event: 1,
132 | other: 2,
133 | }
134 | return messages, expectedCounts
135 | }
136 |
--------------------------------------------------------------------------------
/twitter/direct_messages.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/dghubble/sling"
8 | )
9 |
10 | // DirectMessageEvents lists Direct Message events.
11 | type DirectMessageEvents struct {
12 | Events []DirectMessageEvent `json:"events"`
13 | NextCursor string `json:"next_cursor"`
14 | }
15 |
16 | // DirectMessageEvent is a single Direct Message sent or received.
17 | type DirectMessageEvent struct {
18 | CreatedAt string `json:"created_timestamp,omitempty"`
19 | ID string `json:"id,omitempty"`
20 | Type string `json:"type"`
21 | Message *DirectMessageEventMessage `json:"message_create"`
22 | }
23 |
24 | // DirectMessageEventMessage contains message contents, along with sender and
25 | // target recipient.
26 | type DirectMessageEventMessage struct {
27 | SenderID string `json:"sender_id,omitempty"`
28 | Target *DirectMessageTarget `json:"target"`
29 | Data *DirectMessageData `json:"message_data"`
30 | }
31 |
32 | // DirectMessageTarget specifies the recipient of a Direct Message event.
33 | type DirectMessageTarget struct {
34 | RecipientID string `json:"recipient_id"`
35 | }
36 |
37 | // DirectMessageData is the message data contained in a Direct Message event.
38 | type DirectMessageData struct {
39 | Text string `json:"text"`
40 | Entities *Entities `json:"entities,omitempty"`
41 | Attachment *DirectMessageDataAttachment `json:"attachment,omitempty"`
42 | QuickReply *DirectMessageQuickReply `json:"quick_reply,omitempty"`
43 | CTAs []DirectMessageCTA `json:"ctas,omitempty"`
44 | }
45 |
46 | // DirectMessageDataAttachment contains message data attachments for a Direct
47 | // Message event.
48 | type DirectMessageDataAttachment struct {
49 | Type string `json:"type"`
50 | Media MediaEntity `json:"media"`
51 | }
52 |
53 | // DirectMessageQuickReply contains quick reply data for a Direct Message
54 | // event.
55 | type DirectMessageQuickReply struct {
56 | Type string `json:"type"`
57 | Options []DirectMessageQuickReplyOption `json:"options"`
58 | }
59 |
60 | // DirectMessageQuickReplyOption represents Option object for
61 | // a Direct Message's Quick Reply.
62 | type DirectMessageQuickReplyOption struct {
63 | Label string `json:"label"`
64 | Description string `json:"description,omitempty"`
65 | Metadata string `json:"metadata,omitempty"`
66 | }
67 |
68 | // DirectMessageCTA contains CTA data for a Direct Message event.
69 | type DirectMessageCTA struct {
70 | Type string `json:"type"`
71 | Label string `json:"label"`
72 | URL string `json:"url"`
73 | }
74 |
75 | // DirectMessageService provides methods for accessing Twitter direct message
76 | // API endpoints.
77 | type DirectMessageService struct {
78 | baseSling *sling.Sling
79 | sling *sling.Sling
80 | }
81 |
82 | // newDirectMessageService returns a new DirectMessageService.
83 | func newDirectMessageService(sling *sling.Sling) *DirectMessageService {
84 | return &DirectMessageService{
85 | baseSling: sling.New(),
86 | sling: sling.Path("direct_messages/"),
87 | }
88 | }
89 |
90 | // DirectMessageEventsNewParams are the parameters for
91 | // DirectMessageService.EventsNew
92 | type DirectMessageEventsNewParams struct {
93 | Event *DirectMessageEvent `json:"event"`
94 | }
95 |
96 | // EventsNew publishes a new Direct Message event and returns the event.
97 | // Requires a user auth context with DM scope.
98 | // https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event
99 | func (s *DirectMessageService) EventsNew(params *DirectMessageEventsNewParams) (*DirectMessageEvent, *http.Response, error) {
100 | // Twitter API wraps the event response
101 | wrap := &struct {
102 | Event *DirectMessageEvent `json:"event"`
103 | }{}
104 | apiError := new(APIError)
105 | resp, err := s.sling.New().Post("events/new.json").BodyJSON(params).Receive(wrap, apiError)
106 | return wrap.Event, resp, relevantError(err, *apiError)
107 | }
108 |
109 | // DirectMessageEventsShowParams are the parameters for
110 | // DirectMessageService.EventsShow
111 | type DirectMessageEventsShowParams struct {
112 | ID string `url:"id,omitempty"`
113 | }
114 |
115 | // EventsShow returns a single Direct Message event by id.
116 | // Requires a user auth context with DM scope.
117 | // https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-event
118 | func (s *DirectMessageService) EventsShow(id string, params *DirectMessageEventsShowParams) (*DirectMessageEvent, *http.Response, error) {
119 | if params == nil {
120 | params = &DirectMessageEventsShowParams{}
121 | }
122 | params.ID = id
123 | // Twitter API wraps the event response
124 | wrap := &struct {
125 | Event *DirectMessageEvent `json:"event"`
126 | }{}
127 | apiError := new(APIError)
128 | resp, err := s.sling.New().Get("events/show.json").QueryStruct(params).Receive(wrap, apiError)
129 | return wrap.Event, resp, relevantError(err, *apiError)
130 | }
131 |
132 | // DirectMessageEventsListParams are the parameters for
133 | // DirectMessageService.EventsList
134 | type DirectMessageEventsListParams struct {
135 | Cursor string `url:"cursor,omitempty"`
136 | Count int `url:"count,omitempty"`
137 | }
138 |
139 | // EventsList returns Direct Message events (both sent and received) within
140 | // the last 30 days in reverse chronological order.
141 | // Requires a user auth context with DM scope.
142 | // https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events
143 | func (s *DirectMessageService) EventsList(params *DirectMessageEventsListParams) (*DirectMessageEvents, *http.Response, error) {
144 | events := new(DirectMessageEvents)
145 | apiError := new(APIError)
146 | resp, err := s.sling.New().Get("events/list.json").QueryStruct(params).Receive(events, apiError)
147 | return events, resp, relevantError(err, *apiError)
148 | }
149 |
150 | // EventsDestroy deletes the Direct Message event by id.
151 | // Requires a user auth context with DM scope.
152 | // https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message-event
153 | func (s *DirectMessageService) EventsDestroy(id string) (*http.Response, error) {
154 | params := struct {
155 | ID string `url:"id,omitempty"`
156 | }{id}
157 | apiError := new(APIError)
158 | resp, err := s.sling.New().Delete("events/destroy.json").QueryStruct(params).Receive(nil, apiError)
159 | return resp, relevantError(err, *apiError)
160 | }
161 |
162 | // DEPRECATED
163 |
164 | // DirectMessage is a direct message to a single recipient (DEPRECATED).
165 | type DirectMessage struct {
166 | CreatedAt string `json:"created_at"`
167 | Entities *Entities `json:"entities"`
168 | ID int64 `json:"id"`
169 | IDStr string `json:"id_str"`
170 | Recipient *User `json:"recipient"`
171 | RecipientID int64 `json:"recipient_id"`
172 | RecipientScreenName string `json:"recipient_screen_name"`
173 | Sender *User `json:"sender"`
174 | SenderID int64 `json:"sender_id"`
175 | SenderScreenName string `json:"sender_screen_name"`
176 | Text string `json:"text"`
177 | }
178 |
179 | // CreatedAtTime returns the time a Direct Message was created (DEPRECATED).
180 | func (d DirectMessage) CreatedAtTime() (time.Time, error) {
181 | return time.Parse(time.RubyDate, d.CreatedAt)
182 | }
183 |
184 | // directMessageShowParams are the parameters for DirectMessageService.Show
185 | type directMessageShowParams struct {
186 | ID int64 `url:"id,omitempty"`
187 | }
188 |
189 | // Show returns the requested Direct Message (DEPRECATED).
190 | // Requires a user auth context with DM scope.
191 | // https://dev.twitter.com/rest/reference/get/direct_messages/show
192 | func (s *DirectMessageService) Show(id int64) (*DirectMessage, *http.Response, error) {
193 | params := &directMessageShowParams{ID: id}
194 | dm := new(DirectMessage)
195 | apiError := new(APIError)
196 | resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(dm, apiError)
197 | return dm, resp, relevantError(err, *apiError)
198 | }
199 |
200 | // DirectMessageGetParams are the parameters for DirectMessageService.Get
201 | // (DEPRECATED).
202 | type DirectMessageGetParams struct {
203 | SinceID int64 `url:"since_id,omitempty"`
204 | MaxID int64 `url:"max_id,omitempty"`
205 | Count int `url:"count,omitempty"`
206 | IncludeEntities *bool `url:"include_entities,omitempty"`
207 | SkipStatus *bool `url:"skip_status,omitempty"`
208 | }
209 |
210 | // Get returns recent Direct Messages received by the authenticated user
211 | // (DEPRECATED).
212 | // Requires a user auth context with DM scope.
213 | // https://dev.twitter.com/rest/reference/get/direct_messages
214 | func (s *DirectMessageService) Get(params *DirectMessageGetParams) ([]DirectMessage, *http.Response, error) {
215 | dms := new([]DirectMessage)
216 | apiError := new(APIError)
217 | resp, err := s.baseSling.New().Get("direct_messages.json").QueryStruct(params).Receive(dms, apiError)
218 | return *dms, resp, relevantError(err, *apiError)
219 | }
220 |
221 | // DirectMessageSentParams are the parameters for DirectMessageService.Sent
222 | // (DEPRECATED).
223 | type DirectMessageSentParams struct {
224 | SinceID int64 `url:"since_id,omitempty"`
225 | MaxID int64 `url:"max_id,omitempty"`
226 | Count int `url:"count,omitempty"`
227 | Page int `url:"page,omitempty"`
228 | IncludeEntities *bool `url:"include_entities,omitempty"`
229 | }
230 |
231 | // Sent returns recent Direct Messages sent by the authenticated user
232 | // (DEPRECATED).
233 | // Requires a user auth context with DM scope.
234 | // https://dev.twitter.com/rest/reference/get/direct_messages/sent
235 | func (s *DirectMessageService) Sent(params *DirectMessageSentParams) ([]DirectMessage, *http.Response, error) {
236 | dms := new([]DirectMessage)
237 | apiError := new(APIError)
238 | resp, err := s.sling.New().Get("sent.json").QueryStruct(params).Receive(dms, apiError)
239 | return *dms, resp, relevantError(err, *apiError)
240 | }
241 |
242 | // DirectMessageNewParams are the parameters for DirectMessageService.New
243 | // (DEPRECATED).
244 | type DirectMessageNewParams struct {
245 | UserID int64 `url:"user_id,omitempty"`
246 | ScreenName string `url:"screen_name,omitempty"`
247 | Text string `url:"text"`
248 | }
249 |
250 | // New sends a new Direct Message to a specified user as the authenticated
251 | // user (DEPRECATED).
252 | // Requires a user auth context with DM scope.
253 | // https://dev.twitter.com/rest/reference/post/direct_messages/new
254 | func (s *DirectMessageService) New(params *DirectMessageNewParams) (*DirectMessage, *http.Response, error) {
255 | dm := new(DirectMessage)
256 | apiError := new(APIError)
257 | resp, err := s.sling.New().Post("new.json").BodyForm(params).Receive(dm, apiError)
258 | return dm, resp, relevantError(err, *apiError)
259 | }
260 |
261 | // DirectMessageDestroyParams are the parameters for DirectMessageService.Destroy
262 | // (DEPRECATED).
263 | type DirectMessageDestroyParams struct {
264 | ID int64 `url:"id,omitempty"`
265 | IncludeEntities *bool `url:"include_entities,omitempty"`
266 | }
267 |
268 | // Destroy deletes the Direct Message with the given id and returns it if
269 | // successful (DEPRECATED).
270 | // Requires a user auth context with DM scope.
271 | // https://dev.twitter.com/rest/reference/post/direct_messages/destroy
272 | func (s *DirectMessageService) Destroy(id int64, params *DirectMessageDestroyParams) (*DirectMessage, *http.Response, error) {
273 | if params == nil {
274 | params = &DirectMessageDestroyParams{}
275 | }
276 | params.ID = id
277 | dm := new(DirectMessage)
278 | apiError := new(APIError)
279 | resp, err := s.sling.New().Post("destroy.json").BodyForm(params).Receive(dm, apiError)
280 | return dm, resp, relevantError(err, *apiError)
281 | }
282 |
--------------------------------------------------------------------------------
/twitter/direct_messages_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | var (
12 | testDMEvent = DirectMessageEvent{
13 | CreatedAt: "1542410751275",
14 | ID: "1063573894173323269",
15 | Type: "message_create",
16 | Message: &DirectMessageEventMessage{
17 | SenderID: "623265148",
18 | Target: &DirectMessageTarget{
19 | RecipientID: "3694959333",
20 | },
21 | Data: &DirectMessageData{
22 | Text: "example",
23 | Entities: &Entities{
24 | Hashtags: []HashtagEntity{},
25 | Urls: []URLEntity{},
26 | UserMentions: []MentionEntity{},
27 | Symbols: []SymbolEntity{
28 | {Indices: Indices{38, 43}, Text: "TWTR"},
29 | },
30 | },
31 | },
32 | },
33 | }
34 | testDMEventID = "1063573894173323269"
35 | testDMEventJSON = `
36 | {
37 | "type": "message_create",
38 | "id": "1063573894173323269",
39 | "created_timestamp": "1542410751275",
40 | "message_create": {
41 | "target": {
42 | "recipient_id": "3694959333"
43 | },
44 | "sender_id": "623265148",
45 | "message_data": {
46 | "text": "example",
47 | "entities": {
48 | "hashtags": [],
49 | "symbols": [{"indices": [38,43], "text":"TWTR"}],
50 | "user_mentions": [],
51 | "urls": []
52 | }
53 | }
54 | }
55 | }`
56 | testDMEventShowJSON = `{"event": ` + testDMEventJSON + `}`
57 | testDMEventListJSON = `{"events": [` + testDMEventJSON + `], "next_cursor": "AB345dkfC"}`
58 | testDMEventNewInputJSON = `{"event":{"type":"message_create","message_create":{"target":{"recipient_id":"3694959333"},"message_data":{"text":"example"}}}}
59 | `
60 | // DEPRECATED
61 | testDM = DirectMessage{
62 | ID: 240136858829479936,
63 | Recipient: &User{ScreenName: "theSeanCook"},
64 | Sender: &User{ScreenName: "s0c1alm3dia"},
65 | Text: "hello world",
66 | }
67 | testDMIDStr = "240136858829479936"
68 | testDMJSON = `{"id": 240136858829479936,"recipient": {"screen_name": "theSeanCook"},"sender": {"screen_name": "s0c1alm3dia"},"text": "hello world"}`
69 | )
70 |
71 | func TestDirectMessageService_EventsNew(t *testing.T) {
72 | httpClient, mux, server := testServer()
73 | defer server.Close()
74 |
75 | mux.HandleFunc("/1.1/direct_messages/events/new.json", func(w http.ResponseWriter, r *http.Request) {
76 | assertMethod(t, "POST", r)
77 | assertPostJSON(t, testDMEventNewInputJSON, r)
78 | w.Header().Set("Content-Type", "application/json")
79 | fmt.Fprint(w, testDMEventShowJSON)
80 | })
81 |
82 | client := NewClient(httpClient)
83 | event, _, err := client.DirectMessages.EventsNew(&DirectMessageEventsNewParams{
84 | Event: &DirectMessageEvent{
85 | Type: "message_create",
86 | Message: &DirectMessageEventMessage{
87 | Target: &DirectMessageTarget{
88 | RecipientID: "3694959333",
89 | },
90 | Data: &DirectMessageData{
91 | Text: "example",
92 | },
93 | },
94 | },
95 | })
96 | assert.Nil(t, err)
97 | assert.Equal(t, &testDMEvent, event)
98 | }
99 |
100 | func TestDirectMessageService_EventsShow(t *testing.T) {
101 | httpClient, mux, server := testServer()
102 | defer server.Close()
103 |
104 | mux.HandleFunc("/1.1/direct_messages/events/show.json", func(w http.ResponseWriter, r *http.Request) {
105 | assertMethod(t, "GET", r)
106 | assertQuery(t, map[string]string{"id": testDMEventID}, r)
107 | w.Header().Set("Content-Type", "application/json")
108 | fmt.Fprint(w, testDMEventShowJSON)
109 | })
110 |
111 | client := NewClient(httpClient)
112 | event, _, err := client.DirectMessages.EventsShow(testDMEventID, nil)
113 | assert.Nil(t, err)
114 | assert.Equal(t, &testDMEvent, event)
115 | }
116 |
117 | func TestDirectMessageService_EventsList(t *testing.T) {
118 | httpClient, mux, server := testServer()
119 | defer server.Close()
120 |
121 | mux.HandleFunc("/1.1/direct_messages/events/list.json", func(w http.ResponseWriter, r *http.Request) {
122 | assertMethod(t, "GET", r)
123 | assertQuery(t, map[string]string{"count": "10"}, r)
124 | w.Header().Set("Content-Type", "application/json")
125 | fmt.Fprint(w, testDMEventListJSON)
126 | })
127 | expected := &DirectMessageEvents{
128 | Events: []DirectMessageEvent{testDMEvent},
129 | NextCursor: "AB345dkfC",
130 | }
131 |
132 | client := NewClient(httpClient)
133 | events, _, err := client.DirectMessages.EventsList(&DirectMessageEventsListParams{Count: 10})
134 | assert.Equal(t, expected, events)
135 | assert.Nil(t, err)
136 | }
137 |
138 | func TestDirectMessageService_EventsDestroy(t *testing.T) {
139 | httpClient, mux, server := testServer()
140 | defer server.Close()
141 |
142 | mux.HandleFunc("/1.1/direct_messages/events/destroy.json", func(w http.ResponseWriter, r *http.Request) {
143 | assertMethod(t, "DELETE", r)
144 | assertQuery(t, map[string]string{"id": testDMEventID}, r)
145 | w.Header().Set("Content-Type", "application/json")
146 | // successful delete returns 204 No Content
147 | w.WriteHeader(204)
148 | })
149 |
150 | client := NewClient(httpClient)
151 | resp, err := client.DirectMessages.EventsDestroy(testDMEventID)
152 | assert.Nil(t, err)
153 | assert.NotNil(t, resp)
154 | }
155 |
156 | func TestDirectMessageService_EventsDestroyError(t *testing.T) {
157 | httpClient, mux, server := testServer()
158 | defer server.Close()
159 |
160 | mux.HandleFunc("/1.1/direct_messages/events/destroy.json", func(w http.ResponseWriter, r *http.Request) {
161 | assertMethod(t, "DELETE", r)
162 | assertQuery(t, map[string]string{"id": testDMEventID}, r)
163 | w.Header().Set("Content-Type", "application/json")
164 | // failure to delete event that doesn't exist
165 | w.WriteHeader(404)
166 | fmt.Fprintf(w, `{"errors":[{"code": 34, "message": "Sorry, that page does not exist"}]}`)
167 | })
168 | expected := APIError{
169 | Errors: []ErrorDetail{
170 | {Code: 34, Message: "Sorry, that page does not exist"},
171 | },
172 | }
173 |
174 | client := NewClient(httpClient)
175 | resp, err := client.DirectMessages.EventsDestroy(testDMEventID)
176 | assert.NotNil(t, resp)
177 | if assert.Error(t, err) {
178 | assert.Equal(t, expected, err)
179 | }
180 | }
181 |
182 | // DEPRECATED
183 |
184 | func TestDirectMessageService_Show(t *testing.T) {
185 | httpClient, mux, server := testServer()
186 | defer server.Close()
187 |
188 | mux.HandleFunc("/1.1/direct_messages/show.json", func(w http.ResponseWriter, r *http.Request) {
189 | assertMethod(t, "GET", r)
190 | assertQuery(t, map[string]string{"id": testDMIDStr}, r)
191 | w.Header().Set("Content-Type", "application/json")
192 | fmt.Fprint(w, testDMJSON)
193 | })
194 |
195 | client := NewClient(httpClient)
196 | dms, _, err := client.DirectMessages.Show(testDM.ID)
197 | assert.Nil(t, err)
198 | assert.Equal(t, &testDM, dms)
199 | }
200 |
201 | func TestDirectMessageService_Get(t *testing.T) {
202 | httpClient, mux, server := testServer()
203 | defer server.Close()
204 |
205 | mux.HandleFunc("/1.1/direct_messages.json", func(w http.ResponseWriter, r *http.Request) {
206 | assertMethod(t, "GET", r)
207 | assertQuery(t, map[string]string{"since_id": "589147592367431680", "count": "1"}, r)
208 | w.Header().Set("Content-Type", "application/json")
209 | fmt.Fprintf(w, `[`+testDMJSON+`]`)
210 | })
211 |
212 | client := NewClient(httpClient)
213 | params := &DirectMessageGetParams{SinceID: 589147592367431680, Count: 1}
214 | dms, _, err := client.DirectMessages.Get(params)
215 | expected := []DirectMessage{testDM}
216 | assert.Nil(t, err)
217 | assert.Equal(t, expected, dms)
218 | }
219 |
220 | func TestDirectMessageService_Sent(t *testing.T) {
221 | httpClient, mux, server := testServer()
222 | defer server.Close()
223 |
224 | mux.HandleFunc("/1.1/direct_messages/sent.json", func(w http.ResponseWriter, r *http.Request) {
225 | assertMethod(t, "GET", r)
226 | assertQuery(t, map[string]string{"since_id": "589147592367431680", "count": "1"}, r)
227 | w.Header().Set("Content-Type", "application/json")
228 | fmt.Fprintf(w, `[`+testDMJSON+`]`)
229 | })
230 |
231 | client := NewClient(httpClient)
232 | params := &DirectMessageSentParams{SinceID: 589147592367431680, Count: 1}
233 | dms, _, err := client.DirectMessages.Sent(params)
234 | expected := []DirectMessage{testDM}
235 | assert.Nil(t, err)
236 | assert.Equal(t, expected, dms)
237 | }
238 |
239 | func TestDirectMessageService_New(t *testing.T) {
240 | httpClient, mux, server := testServer()
241 | defer server.Close()
242 |
243 | mux.HandleFunc("/1.1/direct_messages/new.json", func(w http.ResponseWriter, r *http.Request) {
244 | assertMethod(t, "POST", r)
245 | assertPostForm(t, map[string]string{"screen_name": "theseancook", "text": "hello world"}, r)
246 | w.Header().Set("Content-Type", "application/json")
247 | fmt.Fprint(w, testDMJSON)
248 | })
249 |
250 | client := NewClient(httpClient)
251 | params := &DirectMessageNewParams{ScreenName: "theseancook", Text: "hello world"}
252 | dm, _, err := client.DirectMessages.New(params)
253 | assert.Nil(t, err)
254 | assert.Equal(t, &testDM, dm)
255 | }
256 |
257 | func TestDirectMessageService_Destroy(t *testing.T) {
258 | httpClient, mux, server := testServer()
259 | defer server.Close()
260 |
261 | mux.HandleFunc("/1.1/direct_messages/destroy.json", func(w http.ResponseWriter, r *http.Request) {
262 | assertMethod(t, "POST", r)
263 | assertPostForm(t, map[string]string{"id": testDMIDStr}, r)
264 | w.Header().Set("Content-Type", "application/json")
265 | fmt.Fprint(w, testDMJSON)
266 | })
267 |
268 | client := NewClient(httpClient)
269 | dm, _, err := client.DirectMessages.Destroy(testDM.ID, nil)
270 | assert.Nil(t, err)
271 | assert.Equal(t, &testDM, dm)
272 | }
273 |
--------------------------------------------------------------------------------
/twitter/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package twitter provides a Client for the Twitter API.
3 |
4 | Deprecated: This package will no longer be developed.
5 |
6 | The twitter package provides a Client for accessing the Twitter API. Here are
7 | some example requests.
8 |
9 | // Twitter client
10 | client := twitter.NewClient(httpClient)
11 | // Home Timeline
12 | tweets, resp, err := client.Timelines.HomeTimeline(&HomeTimelineParams{})
13 | // Send a Tweet
14 | tweet, resp, err := client.Statuses.Update("just setting up my twttr", nil)
15 | // Status Show
16 | tweet, resp, err := client.Statuses.Show(585613041028431872, nil)
17 | // User Show
18 | params := &twitter.UserShowParams{ScreenName: "dghubble"}
19 | user, resp, err := client.Users.Show(params)
20 | // Followers
21 | followers, resp, err := client.Followers.List(&FollowerListParams{})
22 |
23 | Required parameters are passed as positional arguments. Optional parameters
24 | are passed in a typed params struct (or pass nil).
25 |
26 | # Authentication
27 |
28 | By design, the Twitter Client accepts any http.Client so user auth (OAuth1) or
29 | application auth (OAuth2) requests can be made by using the appropriate
30 | authenticated client. Use the https://github.com/dghubble/oauth1 and
31 | https://github.com/golang/oauth2 packages to obtain an http.Client which
32 | transparently authorizes requests.
33 |
34 | For example, make requests as a consumer application on behalf of a user who
35 | has granted access, with OAuth1.
36 |
37 | // OAuth1
38 | import (
39 | "github.com/dghubble/go-twitter/twitter"
40 | "github.com/dghubble/oauth1"
41 | )
42 |
43 | config := oauth1.NewConfig("consumerKey", "consumerSecret")
44 | token := oauth1.NewToken("accessToken", "accessSecret")
45 | // http.Client will automatically authorize Requests
46 | httpClient := config.Client(oauth1.NoContext, token)
47 |
48 | // twitter client
49 | client := twitter.NewClient(httpClient)
50 |
51 | If no user auth context is needed, make requests as your application with
52 | application auth.
53 |
54 | // OAuth2
55 | import (
56 | "github.com/dghubble/go-twitter/twitter"
57 | "golang.org/x/oauth2"
58 | "golang.org/x/oauth2/clientcredentials"
59 | )
60 |
61 | // oauth2 configures a client that uses app credentials to keep a fresh token
62 | config := &clientcredentials.Config{
63 | ClientID: flags.consumerKey,
64 | ClientSecret: flags.consumerSecret,
65 | TokenURL: "https://api.twitter.com/oauth2/token",
66 | }
67 | // http.Client will automatically authorize Requests
68 | httpClient := config.Client(oauth2.NoContext)
69 |
70 | // Twitter client
71 | client := twitter.NewClient(httpClient)
72 |
73 | To implement Login with Twitter, see https://github.com/dghubble/gologin.
74 | */
75 | package twitter
76 |
--------------------------------------------------------------------------------
/twitter/entities.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | // Entities represent metadata and context info parsed from Twitter components.
4 | // https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/entities-object
5 | type Entities struct {
6 | Hashtags []HashtagEntity `json:"hashtags"`
7 | Media []MediaEntity `json:"media"`
8 | Urls []URLEntity `json:"urls"`
9 | UserMentions []MentionEntity `json:"user_mentions"`
10 | Symbols []SymbolEntity `json:"symbols"`
11 | Polls []PollEntity `json:"polls"`
12 | }
13 |
14 | // HashtagEntity represents a hashtag which has been parsed from text.
15 | type HashtagEntity struct {
16 | Indices Indices `json:"indices"`
17 | Text string `json:"text"`
18 | }
19 |
20 | // URLEntity represents a URL which has been parsed from text.
21 | type URLEntity struct {
22 | URL string `json:"url"`
23 | DisplayURL string `json:"display_url"`
24 | ExpandedURL string `json:"expanded_url"`
25 | Unwound UnwoundURL `string:"unwound"`
26 | Indices Indices `json:"indices"`
27 | }
28 |
29 | // UnwoundURL represents an enhanced URL
30 | // https://developer.twitter.com/en/docs/twitter-api/enterprise/enrichments/overview/expanded-and-enhanced-urls
31 | type UnwoundURL struct {
32 | URL string `json:"url"`
33 | Status int `json:"status"`
34 | Title string `json:"title"`
35 | Description string `json:"description"`
36 | }
37 |
38 | // MediaEntity represents media elements associated with a Tweet.
39 | type MediaEntity struct {
40 | URLEntity
41 | ID int64 `json:"id"`
42 | IDStr string `json:"id_str"`
43 | MediaURL string `json:"media_url"`
44 | MediaURLHttps string `json:"media_url_https"`
45 | SourceStatusID int64 `json:"source_status_id"`
46 | SourceStatusIDStr string `json:"source_status_id_str"`
47 | Type string `json:"type"`
48 | Sizes MediaSizes `json:"sizes"`
49 | VideoInfo VideoInfo `json:"video_info"`
50 | }
51 |
52 | // MentionEntity represents Twitter user mentions parsed from text.
53 | type MentionEntity struct {
54 | Indices Indices `json:"indices"`
55 | ID int64 `json:"id"`
56 | IDStr string `json:"id_str"`
57 | Name string `json:"name"`
58 | ScreenName string `json:"screen_name"`
59 | }
60 |
61 | // SymbolEntity represents a symbol (e.g. $twtr) which has been parsed from text.
62 | // https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/entities#symbols
63 | type SymbolEntity struct {
64 | Indices Indices `json:"indices"`
65 | Text string `json:"text"`
66 | }
67 |
68 | // PollEntity represents a Twitter Poll from a Tweet.
69 | // Note that poll metadata is only available with enterprise.
70 | // https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/entities#polls
71 | type PollEntity struct {
72 | Options []PollOption `json:"options"`
73 | EndDateTime string `json:"end_datetime"`
74 | DurationMinutes string `json:"duration_minutes"`
75 | }
76 |
77 | // PollOption represents a position option in a PollEntity.
78 | type PollOption struct {
79 | Position int `json:"position"`
80 | Text string `json:"text"`
81 | }
82 |
83 | // UserEntities contain Entities parsed from User url and description fields.
84 | // https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/entities-object#mentions
85 | type UserEntities struct {
86 | URL Entities `json:"url"`
87 | Description Entities `json:"description"`
88 | }
89 |
90 | // ExtendedEntity contains media information.
91 | // https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/extended-entities-object
92 | type ExtendedEntity struct {
93 | Media []MediaEntity `json:"media"`
94 | }
95 |
96 | // Indices represent the start and end offsets within text.
97 | type Indices [2]int
98 |
99 | // Start returns the index at which an entity starts, inclusive.
100 | func (i Indices) Start() int {
101 | return i[0]
102 | }
103 |
104 | // End returns the index at which an entity ends, exclusive.
105 | func (i Indices) End() int {
106 | return i[1]
107 | }
108 |
109 | // MediaSizes contain the different size media that are available.
110 | // https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/entities-object#media-size
111 | type MediaSizes struct {
112 | Thumb MediaSize `json:"thumb"`
113 | Large MediaSize `json:"large"`
114 | Medium MediaSize `json:"medium"`
115 | Small MediaSize `json:"small"`
116 | }
117 |
118 | // MediaSize describes the height, width, and resizing method used.
119 | type MediaSize struct {
120 | Width int `json:"w"`
121 | Height int `json:"h"`
122 | Resize string `json:"resize"`
123 | }
124 |
125 | // VideoInfo is available on video media objects.
126 | type VideoInfo struct {
127 | AspectRatio [2]int `json:"aspect_ratio"`
128 | DurationMillis int `json:"duration_millis"`
129 | Variants []VideoVariant `json:"variants"`
130 | }
131 |
132 | // VideoVariant describes one of the available video formats.
133 | type VideoVariant struct {
134 | ContentType string `json:"content_type"`
135 | Bitrate int `json:"bitrate"`
136 | URL string `json:"url"`
137 | }
138 |
--------------------------------------------------------------------------------
/twitter/entities_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestIndices(t *testing.T) {
10 | cases := []struct {
11 | pair Indices
12 | expectedStart int
13 | expectedEnd int
14 | }{
15 | {Indices{}, 0, 0},
16 | {Indices{25, 47}, 25, 47},
17 | }
18 | for _, c := range cases {
19 | assert.Equal(t, c.expectedStart, c.pair.Start())
20 | assert.Equal(t, c.expectedEnd, c.pair.End())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/twitter/errors.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // APIError represents a Twitter API Error response
8 | // https://dev.twitter.com/overview/api/response-codes
9 | type APIError struct {
10 | Errors []ErrorDetail `json:"errors"`
11 | }
12 |
13 | // ErrorDetail represents an individual item in an APIError.
14 | type ErrorDetail struct {
15 | Message string `json:"message"`
16 | Code int `json:"code"`
17 | }
18 |
19 | func (e APIError) Error() string {
20 | if len(e.Errors) > 0 {
21 | err := e.Errors[0]
22 | return fmt.Sprintf("twitter: %d %v", err.Code, err.Message)
23 | }
24 | return ""
25 | }
26 |
27 | // Empty returns true if empty. Otherwise, at least 1 error message/code is
28 | // present and false is returned.
29 | func (e APIError) Empty() bool {
30 | return len(e.Errors) == 0
31 | }
32 |
33 | // relevantError returns any non-nil http-related error (creating the request,
34 | // getting the response, decoding) if any. If the decoded apiError is non-zero
35 | // the apiError is returned. Otherwise, no errors occurred, returns nil.
36 | func relevantError(httpError error, apiError APIError) error {
37 | if httpError != nil {
38 | return httpError
39 | }
40 | if apiError.Empty() {
41 | return nil
42 | }
43 | return apiError
44 | }
45 |
--------------------------------------------------------------------------------
/twitter/errors_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | var errAPI = APIError{
11 | Errors: []ErrorDetail{
12 | {Message: "Status is a duplicate", Code: 187},
13 | },
14 | }
15 | var errHTTP = fmt.Errorf("unknown host")
16 |
17 | func TestAPIError_Error(t *testing.T) {
18 | err := APIError{}
19 | if assert.Error(t, err) {
20 | assert.Equal(t, "", err.Error())
21 | }
22 | if assert.Error(t, errAPI) {
23 | assert.Equal(t, "twitter: 187 Status is a duplicate", errAPI.Error())
24 | }
25 | }
26 |
27 | func TestAPIError_Empty(t *testing.T) {
28 | err := APIError{}
29 | assert.True(t, err.Empty())
30 | assert.False(t, errAPI.Empty())
31 | }
32 |
33 | func TestRelevantError(t *testing.T) {
34 | cases := []struct {
35 | httpError error
36 | apiError APIError
37 | expected error
38 | }{
39 | {nil, APIError{}, nil},
40 | {nil, errAPI, errAPI},
41 | {errHTTP, APIError{}, errHTTP},
42 | {errHTTP, errAPI, errHTTP},
43 | }
44 | for _, c := range cases {
45 | err := relevantError(c.httpError, c.apiError)
46 | assert.Equal(t, c.expected, err)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/twitter/favorites.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/dghubble/sling"
7 | )
8 |
9 | // FavoriteService provides methods for accessing Twitter favorite API endpoints.
10 | //
11 | // Note: the like action was known as favorite before November 3, 2015; the
12 | // historical naming remains in API methods and object properties.
13 | type FavoriteService struct {
14 | sling *sling.Sling
15 | }
16 |
17 | // newFavoriteService returns a new FavoriteService.
18 | func newFavoriteService(sling *sling.Sling) *FavoriteService {
19 | return &FavoriteService{
20 | sling: sling.Path("favorites/"),
21 | }
22 | }
23 |
24 | // FavoriteListParams are the parameters for FavoriteService.List.
25 | type FavoriteListParams struct {
26 | UserID int64 `url:"user_id,omitempty"`
27 | ScreenName string `url:"screen_name,omitempty"`
28 | Count int `url:"count,omitempty"`
29 | SinceID int64 `url:"since_id,omitempty"`
30 | MaxID int64 `url:"max_id,omitempty"`
31 | IncludeEntities *bool `url:"include_entities,omitempty"`
32 | TweetMode string `url:"tweet_mode,omitempty"`
33 | }
34 |
35 | // List returns liked Tweets from the specified user.
36 | // https://dev.twitter.com/rest/reference/get/favorites/list
37 | func (s *FavoriteService) List(params *FavoriteListParams) ([]Tweet, *http.Response, error) {
38 | favorites := new([]Tweet)
39 | apiError := new(APIError)
40 | resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(favorites, apiError)
41 | return *favorites, resp, relevantError(err, *apiError)
42 | }
43 |
44 | // FavoriteCreateParams are the parameters for FavoriteService.Create.
45 | type FavoriteCreateParams struct {
46 | ID int64 `url:"id,omitempty"`
47 | }
48 |
49 | // Create favorites the specified tweet.
50 | // Requires a user auth context.
51 | // https://dev.twitter.com/rest/reference/post/favorites/create
52 | func (s *FavoriteService) Create(params *FavoriteCreateParams) (*Tweet, *http.Response, error) {
53 | tweet := new(Tweet)
54 | apiError := new(APIError)
55 | resp, err := s.sling.New().Post("create.json").QueryStruct(params).Receive(tweet, apiError)
56 | return tweet, resp, relevantError(err, *apiError)
57 | }
58 |
59 | // FavoriteDestroyParams are the parameters for FavoriteService.Destroy.
60 | type FavoriteDestroyParams struct {
61 | ID int64 `url:"id,omitempty"`
62 | }
63 |
64 | // Destroy un-favorites the specified tweet.
65 | // Requires a user auth context.
66 | // https://dev.twitter.com/rest/reference/post/favorites/destroy
67 | func (s *FavoriteService) Destroy(params *FavoriteDestroyParams) (*Tweet, *http.Response, error) {
68 | tweet := new(Tweet)
69 | apiError := new(APIError)
70 | resp, err := s.sling.New().Post("destroy.json").QueryStruct(params).Receive(tweet, apiError)
71 | return tweet, resp, relevantError(err, *apiError)
72 | }
73 |
--------------------------------------------------------------------------------
/twitter/favorites_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestFavoriteService_List(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | mux.HandleFunc("/1.1/favorites/list.json", func(w http.ResponseWriter, r *http.Request) {
16 | assertMethod(t, "GET", r)
17 | assertQuery(t, map[string]string{"user_id": "113419064", "since_id": "101492475", "include_entities": "false"}, r)
18 | w.Header().Set("Content-Type", "application/json")
19 | fmt.Fprintf(w, `[{"text": "Gophercon talks!"}, {"text": "Why gophers are so adorable"}]`)
20 | })
21 |
22 | client := NewClient(httpClient)
23 | tweets, _, err := client.Favorites.List(&FavoriteListParams{UserID: 113419064, SinceID: 101492475, IncludeEntities: Bool(false)})
24 | expected := []Tweet{
25 | {Text: "Gophercon talks!"},
26 | {Text: "Why gophers are so adorable"},
27 | }
28 | assert.Nil(t, err)
29 | assert.Equal(t, expected, tweets)
30 | }
31 |
32 | func TestFavoriteService_Create(t *testing.T) {
33 | httpClient, mux, server := testServer()
34 | defer server.Close()
35 |
36 | mux.HandleFunc("/1.1/favorites/create.json", func(w http.ResponseWriter, r *http.Request) {
37 | assertMethod(t, "POST", r)
38 | assertPostForm(t, map[string]string{"id": "12345"}, r)
39 | w.Header().Set("Content-Type", "application/json")
40 | fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very informative tweet"}`)
41 | })
42 |
43 | client := NewClient(httpClient)
44 | params := &FavoriteCreateParams{ID: 12345}
45 | tweet, _, err := client.Favorites.Create(params)
46 | assert.Nil(t, err)
47 | expected := &Tweet{ID: 581980947630845953, Text: "very informative tweet"}
48 | assert.Equal(t, expected, tweet)
49 | }
50 |
51 | func TestFavoriteService_Destroy(t *testing.T) {
52 | httpClient, mux, server := testServer()
53 | defer server.Close()
54 |
55 | mux.HandleFunc("/1.1/favorites/destroy.json", func(w http.ResponseWriter, r *http.Request) {
56 | assertMethod(t, "POST", r)
57 | assertPostForm(t, map[string]string{"id": "12345"}, r)
58 | w.Header().Set("Content-Type", "application/json")
59 | fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very unhappy tweet"}`)
60 | })
61 |
62 | client := NewClient(httpClient)
63 | params := &FavoriteDestroyParams{ID: 12345}
64 | tweet, _, err := client.Favorites.Destroy(params)
65 | assert.Nil(t, err)
66 | expected := &Tweet{ID: 581980947630845953, Text: "very unhappy tweet"}
67 | assert.Equal(t, expected, tweet)
68 | }
69 |
--------------------------------------------------------------------------------
/twitter/followers.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/dghubble/sling"
7 | )
8 |
9 | // FollowerIDs is a cursored collection of follower ids.
10 | type FollowerIDs struct {
11 | IDs []int64 `json:"ids"`
12 | NextCursor int64 `json:"next_cursor"`
13 | NextCursorStr string `json:"next_cursor_str"`
14 | PreviousCursor int64 `json:"previous_cursor"`
15 | PreviousCursorStr string `json:"previous_cursor_str"`
16 | }
17 |
18 | // Followers is a cursored collection of followers.
19 | type Followers struct {
20 | Users []User `json:"users"`
21 | NextCursor int64 `json:"next_cursor"`
22 | NextCursorStr string `json:"next_cursor_str"`
23 | PreviousCursor int64 `json:"previous_cursor"`
24 | PreviousCursorStr string `json:"previous_cursor_str"`
25 | }
26 |
27 | // FollowerService provides methods for accessing Twitter followers endpoints.
28 | type FollowerService struct {
29 | sling *sling.Sling
30 | }
31 |
32 | // newFollowerService returns a new FollowerService.
33 | func newFollowerService(sling *sling.Sling) *FollowerService {
34 | return &FollowerService{
35 | sling: sling.Path("followers/"),
36 | }
37 | }
38 |
39 | // FollowerIDParams are the parameters for FollowerService.Ids
40 | type FollowerIDParams struct {
41 | UserID int64 `url:"user_id,omitempty"`
42 | ScreenName string `url:"screen_name,omitempty"`
43 | Cursor int64 `url:"cursor,omitempty"`
44 | Count int `url:"count,omitempty"`
45 | }
46 |
47 | // IDs returns a cursored collection of user ids following the specified user.
48 | // https://dev.twitter.com/rest/reference/get/followers/ids
49 | func (s *FollowerService) IDs(params *FollowerIDParams) (*FollowerIDs, *http.Response, error) {
50 | ids := new(FollowerIDs)
51 | apiError := new(APIError)
52 | resp, err := s.sling.New().Get("ids.json").QueryStruct(params).Receive(ids, apiError)
53 | return ids, resp, relevantError(err, *apiError)
54 | }
55 |
56 | // FollowerListParams are the parameters for FollowerService.List
57 | type FollowerListParams struct {
58 | UserID int64 `url:"user_id,omitempty"`
59 | ScreenName string `url:"screen_name,omitempty"`
60 | Cursor int64 `url:"cursor,omitempty"`
61 | Count int `url:"count,omitempty"`
62 | SkipStatus *bool `url:"skip_status,omitempty"`
63 | IncludeUserEntities *bool `url:"include_user_entities,omitempty"`
64 | }
65 |
66 | // List returns a cursored collection of Users following the specified user.
67 | // https://dev.twitter.com/rest/reference/get/followers/list
68 | func (s *FollowerService) List(params *FollowerListParams) (*Followers, *http.Response, error) {
69 | followers := new(Followers)
70 | apiError := new(APIError)
71 | resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(followers, apiError)
72 | return followers, resp, relevantError(err, *apiError)
73 | }
74 |
--------------------------------------------------------------------------------
/twitter/followers_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestFollowerService_Ids(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | mux.HandleFunc("/1.1/followers/ids.json", func(w http.ResponseWriter, r *http.Request) {
16 | assertMethod(t, "GET", r)
17 | assertQuery(t, map[string]string{"user_id": "623265148", "count": "5", "cursor": "1516933260114270762"}, r)
18 | w.Header().Set("Content-Type", "application/json")
19 | fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
20 | })
21 | expected := &FollowerIDs{
22 | IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
23 | NextCursor: 1516837838944119498,
24 | NextCursorStr: "1516837838944119498",
25 | PreviousCursor: -1516924983503961435,
26 | PreviousCursorStr: "-1516924983503961435",
27 | }
28 |
29 | client := NewClient(httpClient)
30 | params := &FollowerIDParams{
31 | UserID: 623265148,
32 | Count: 5,
33 | Cursor: 1516933260114270762,
34 | }
35 | followerIDs, _, err := client.Followers.IDs(params)
36 | assert.Nil(t, err)
37 | assert.Equal(t, expected, followerIDs)
38 | }
39 |
40 | func TestFollowerService_List(t *testing.T) {
41 | httpClient, mux, server := testServer()
42 | defer server.Close()
43 |
44 | mux.HandleFunc("/1.1/followers/list.json", func(w http.ResponseWriter, r *http.Request) {
45 | assertMethod(t, "GET", r)
46 | assertQuery(t, map[string]string{"screen_name": "dghubble", "count": "5", "cursor": "1516933260114270762", "skip_status": "true", "include_user_entities": "false"}, r)
47 | w.Header().Set("Content-Type", "application/json")
48 | fmt.Fprintf(w, `{"users": [{"id": 123}], "next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
49 | })
50 | expected := &Followers{
51 | Users: []User{
52 | {ID: 123},
53 | },
54 | NextCursor: 1516837838944119498,
55 | NextCursorStr: "1516837838944119498",
56 | PreviousCursor: -1516924983503961435,
57 | PreviousCursorStr: "-1516924983503961435",
58 | }
59 |
60 | client := NewClient(httpClient)
61 | params := &FollowerListParams{
62 | ScreenName: "dghubble",
63 | Count: 5,
64 | Cursor: 1516933260114270762,
65 | SkipStatus: Bool(true),
66 | IncludeUserEntities: Bool(false),
67 | }
68 | followers, _, err := client.Followers.List(params)
69 | assert.Nil(t, err)
70 | assert.Equal(t, expected, followers)
71 | }
72 |
--------------------------------------------------------------------------------
/twitter/friends.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/dghubble/sling"
7 | )
8 |
9 | // FriendIDs is a cursored collection of friend ids.
10 | type FriendIDs struct {
11 | IDs []int64 `json:"ids"`
12 | NextCursor int64 `json:"next_cursor"`
13 | NextCursorStr string `json:"next_cursor_str"`
14 | PreviousCursor int64 `json:"previous_cursor"`
15 | PreviousCursorStr string `json:"previous_cursor_str"`
16 | }
17 |
18 | // Friends is a cursored collection of friends.
19 | type Friends struct {
20 | Users []User `json:"users"`
21 | NextCursor int64 `json:"next_cursor"`
22 | NextCursorStr string `json:"next_cursor_str"`
23 | PreviousCursor int64 `json:"previous_cursor"`
24 | PreviousCursorStr string `json:"previous_cursor_str"`
25 | }
26 |
27 | // FriendService provides methods for accessing Twitter friends endpoints.
28 | type FriendService struct {
29 | sling *sling.Sling
30 | }
31 |
32 | // newFriendService returns a new FriendService.
33 | func newFriendService(sling *sling.Sling) *FriendService {
34 | return &FriendService{
35 | sling: sling.Path("friends/"),
36 | }
37 | }
38 |
39 | // FriendIDParams are the parameters for FriendService.Ids
40 | type FriendIDParams struct {
41 | UserID int64 `url:"user_id,omitempty"`
42 | ScreenName string `url:"screen_name,omitempty"`
43 | Cursor int64 `url:"cursor,omitempty"`
44 | Count int `url:"count,omitempty"`
45 | }
46 |
47 | // IDs returns a cursored collection of user ids that the specified user is following.
48 | // https://dev.twitter.com/rest/reference/get/friends/ids
49 | func (s *FriendService) IDs(params *FriendIDParams) (*FriendIDs, *http.Response, error) {
50 | ids := new(FriendIDs)
51 | apiError := new(APIError)
52 | resp, err := s.sling.New().Get("ids.json").QueryStruct(params).Receive(ids, apiError)
53 | return ids, resp, relevantError(err, *apiError)
54 | }
55 |
56 | // FriendListParams are the parameters for FriendService.List
57 | type FriendListParams struct {
58 | UserID int64 `url:"user_id,omitempty"`
59 | ScreenName string `url:"screen_name,omitempty"`
60 | Cursor int64 `url:"cursor,omitempty"`
61 | Count int `url:"count,omitempty"`
62 | SkipStatus *bool `url:"skip_status,omitempty"`
63 | IncludeUserEntities *bool `url:"include_user_entities,omitempty"`
64 | }
65 |
66 | // List returns a cursored collection of Users that the specified user is following.
67 | // https://dev.twitter.com/rest/reference/get/friends/list
68 | func (s *FriendService) List(params *FriendListParams) (*Friends, *http.Response, error) {
69 | friends := new(Friends)
70 | apiError := new(APIError)
71 | resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(friends, apiError)
72 | return friends, resp, relevantError(err, *apiError)
73 | }
74 |
--------------------------------------------------------------------------------
/twitter/friends_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestFriendService_Ids(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | mux.HandleFunc("/1.1/friends/ids.json", func(w http.ResponseWriter, r *http.Request) {
16 | assertMethod(t, "GET", r)
17 | assertQuery(t, map[string]string{"user_id": "623265148", "count": "5", "cursor": "1516933260114270762"}, r)
18 | w.Header().Set("Content-Type", "application/json")
19 | fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
20 | })
21 | expected := &FriendIDs{
22 | IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
23 | NextCursor: 1516837838944119498,
24 | NextCursorStr: "1516837838944119498",
25 | PreviousCursor: -1516924983503961435,
26 | PreviousCursorStr: "-1516924983503961435",
27 | }
28 |
29 | client := NewClient(httpClient)
30 | params := &FriendIDParams{
31 | UserID: 623265148,
32 | Count: 5,
33 | Cursor: 1516933260114270762,
34 | }
35 | friendIDs, _, err := client.Friends.IDs(params)
36 | assert.Nil(t, err)
37 | assert.Equal(t, expected, friendIDs)
38 | }
39 |
40 | func TestFriendService_List(t *testing.T) {
41 | httpClient, mux, server := testServer()
42 | defer server.Close()
43 |
44 | mux.HandleFunc("/1.1/friends/list.json", func(w http.ResponseWriter, r *http.Request) {
45 | assertMethod(t, "GET", r)
46 | assertQuery(t, map[string]string{"screen_name": "dghubble", "count": "5", "cursor": "1516933260114270762", "skip_status": "true", "include_user_entities": "false"}, r)
47 | w.Header().Set("Content-Type", "application/json")
48 | fmt.Fprintf(w, `{"users": [{"id": 123}], "next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
49 | })
50 | expected := &Friends{
51 | Users: []User{
52 | {ID: 123},
53 | },
54 | NextCursor: 1516837838944119498,
55 | NextCursorStr: "1516837838944119498",
56 | PreviousCursor: -1516924983503961435,
57 | PreviousCursorStr: "-1516924983503961435",
58 | }
59 |
60 | client := NewClient(httpClient)
61 | params := &FriendListParams{
62 | ScreenName: "dghubble",
63 | Count: 5,
64 | Cursor: 1516933260114270762,
65 | SkipStatus: Bool(true),
66 | IncludeUserEntities: Bool(false),
67 | }
68 | friends, _, err := client.Friends.List(params)
69 | assert.Nil(t, err)
70 | assert.Equal(t, expected, friends)
71 | }
72 |
--------------------------------------------------------------------------------
/twitter/friendships.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/dghubble/sling"
7 | )
8 |
9 | // FriendshipService provides methods for accessing Twitter friendship API
10 | // endpoints.
11 | type FriendshipService struct {
12 | sling *sling.Sling
13 | }
14 |
15 | // newFriendshipService returns a new FriendshipService.
16 | func newFriendshipService(sling *sling.Sling) *FriendshipService {
17 | return &FriendshipService{
18 | sling: sling.Path("friendships/"),
19 | }
20 | }
21 |
22 | // FriendshipCreateParams are parameters for FriendshipService.Create
23 | type FriendshipCreateParams struct {
24 | ScreenName string `url:"screen_name,omitempty"`
25 | UserID int64 `url:"user_id,omitempty"`
26 | Follow *bool `url:"follow,omitempty"`
27 | }
28 |
29 | // Create creates a friendship to (i.e. follows) the specified user and
30 | // returns the followed user.
31 | // Requires a user auth context.
32 | // https://dev.twitter.com/rest/reference/post/friendships/create
33 | func (s *FriendshipService) Create(params *FriendshipCreateParams) (*User, *http.Response, error) {
34 | user := new(User)
35 | apiError := new(APIError)
36 | resp, err := s.sling.New().Post("create.json").QueryStruct(params).Receive(user, apiError)
37 | return user, resp, relevantError(err, *apiError)
38 | }
39 |
40 | // FriendshipShowParams are paramenters for FriendshipService.Show
41 | type FriendshipShowParams struct {
42 | SourceID int64 `url:"source_id,omitempty"`
43 | SourceScreenName string `url:"source_screen_name,omitempty"`
44 | TargetID int64 `url:"target_id,omitempty"`
45 | TargetScreenName string `url:"target_screen_name,omitempty"`
46 | }
47 |
48 | // Show returns the relationship between two arbitrary users.
49 | // Requires a user auth or an app context.
50 | // https://dev.twitter.com/rest/reference/get/friendships/show
51 | func (s *FriendshipService) Show(params *FriendshipShowParams) (*Relationship, *http.Response, error) {
52 | response := new(RelationshipResponse)
53 | apiError := new(APIError)
54 | resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(response, apiError)
55 | return response.Relationship, resp, relevantError(err, *apiError)
56 | }
57 |
58 | // RelationshipResponse contains a relationship.
59 | type RelationshipResponse struct {
60 | Relationship *Relationship `json:"relationship"`
61 | }
62 |
63 | // Relationship represents the relation between a source user and target user.
64 | type Relationship struct {
65 | Source RelationshipSource `json:"source"`
66 | Target RelationshipTarget `json:"target"`
67 | }
68 |
69 | // RelationshipSource represents the source user.
70 | type RelationshipSource struct {
71 | ID int64 `json:"id"`
72 | IDStr string `json:"id_str"`
73 | ScreenName string `json:"screen_name"`
74 | Following bool `json:"following"`
75 | FollowedBy bool `json:"followed_by"`
76 | CanDM bool `json:"can_dm"`
77 | Blocking bool `json:"blocking"`
78 | Muting bool `json:"muting"`
79 | AllReplies bool `json:"all_replies"`
80 | WantRetweets bool `json:"want_retweets"`
81 | MarkedSpam bool `json:"marked_spam"`
82 | NotificationsEnabled bool `json:"notifications_enabled"`
83 | }
84 |
85 | // RelationshipTarget represents the target user.
86 | type RelationshipTarget struct {
87 | ID int64 `json:"id"`
88 | IDStr string `json:"id_str"`
89 | ScreenName string `json:"screen_name"`
90 | Following bool `json:"following"`
91 | FollowedBy bool `json:"followed_by"`
92 | }
93 |
94 | // FriendshipDestroyParams are paramenters for FriendshipService.Destroy
95 | type FriendshipDestroyParams struct {
96 | ScreenName string `url:"screen_name,omitempty"`
97 | UserID int64 `url:"user_id,omitempty"`
98 | }
99 |
100 | // Destroy destroys a friendship to (i.e. unfollows) the specified user and
101 | // returns the unfollowed user.
102 | // Requires a user auth context.
103 | // https://dev.twitter.com/rest/reference/post/friendships/destroy
104 | func (s *FriendshipService) Destroy(params *FriendshipDestroyParams) (*User, *http.Response, error) {
105 | user := new(User)
106 | apiError := new(APIError)
107 | resp, err := s.sling.New().Post("destroy.json").QueryStruct(params).Receive(user, apiError)
108 | return user, resp, relevantError(err, *apiError)
109 | }
110 |
111 | // FriendshipPendingParams are paramenters for FriendshipService.Outgoing
112 | type FriendshipPendingParams struct {
113 | Cursor int64 `url:"cursor,omitempty"`
114 | }
115 |
116 | // Outgoing returns a collection of numeric IDs for every protected user for whom the authenticating
117 | // user has a pending follow request.
118 | // https://dev.twitter.com/rest/reference/get/friendships/outgoing
119 | func (s *FriendshipService) Outgoing(params *FriendshipPendingParams) (*FriendIDs, *http.Response, error) {
120 | ids := new(FriendIDs)
121 | apiError := new(APIError)
122 | resp, err := s.sling.New().Get("outgoing.json").QueryStruct(params).Receive(ids, apiError)
123 | return ids, resp, relevantError(err, *apiError)
124 | }
125 |
126 | // Incoming returns a collection of numeric IDs for every user who has a pending request to
127 | // follow the authenticating user.
128 | // https://dev.twitter.com/rest/reference/get/friendships/incoming
129 | func (s *FriendshipService) Incoming(params *FriendshipPendingParams) (*FriendIDs, *http.Response, error) {
130 | ids := new(FriendIDs)
131 | apiError := new(APIError)
132 | resp, err := s.sling.New().Get("incoming.json").QueryStruct(params).Receive(ids, apiError)
133 | return ids, resp, relevantError(err, *apiError)
134 | }
135 |
136 | // FriendshipLookupParams are parameters for FriendshipService.Lookup
137 | type FriendshipLookupParams struct {
138 | UserID []int64 `url:"user_id,omitempty,comma"`
139 | ScreenName []string `url:"screen_name,omitempty,comma"`
140 | }
141 |
142 | // FriendshipResponse represents the target user.
143 | type FriendshipResponse struct {
144 | ID int64 `json:"id"`
145 | IDStr string `json:"id_str"`
146 | ScreenName string `json:"screen_name"`
147 | Name string `json:"name"`
148 | Connections []string `json:"connections"`
149 | }
150 |
151 | // Lookup returns the relationships of the authenticating user to the comma-separated list of up to
152 | // 100 screen_names or user_ids provided.
153 | // https://dev.twitter.com/rest/reference/get/friendships/lookup
154 | func (s *FriendshipService) Lookup(params *FriendshipLookupParams) (*[]FriendshipResponse, *http.Response, error) {
155 | ids := new([]FriendshipResponse)
156 | apiError := new(APIError)
157 | resp, err := s.sling.New().Get("lookup.json").QueryStruct(params).Receive(ids, apiError)
158 | return ids, resp, relevantError(err, *apiError)
159 | }
160 |
--------------------------------------------------------------------------------
/twitter/friendships_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestFriendshipService_Create(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | mux.HandleFunc("/1.1/friendships/create.json", func(w http.ResponseWriter, r *http.Request) {
16 | assertMethod(t, "POST", r)
17 | assertPostForm(t, map[string]string{"user_id": "12345"}, r)
18 | w.Header().Set("Content-Type", "application/json")
19 | fmt.Fprintf(w, `{"id": 12345, "name": "Doug Williams"}`)
20 | })
21 |
22 | client := NewClient(httpClient)
23 | params := &FriendshipCreateParams{UserID: 12345}
24 | user, _, err := client.Friendships.Create(params)
25 | assert.Nil(t, err)
26 | expected := &User{ID: 12345, Name: "Doug Williams"}
27 | assert.Equal(t, expected, user)
28 | }
29 |
30 | func TestFriendshipService_Show(t *testing.T) {
31 | httpClient, mux, server := testServer()
32 | defer server.Close()
33 |
34 | mux.HandleFunc("/1.1/friendships/show.json", func(w http.ResponseWriter, r *http.Request) {
35 | assertMethod(t, "GET", r)
36 | assertQuery(t, map[string]string{"source_screen_name": "foo", "target_screen_name": "bar"}, r)
37 | w.Header().Set("Content-Type", "application/json")
38 | 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 } } }`)
39 | })
40 |
41 | client := NewClient(httpClient)
42 | params := &FriendshipShowParams{SourceScreenName: "foo", TargetScreenName: "bar"}
43 | relationship, _, err := client.Friendships.Show(params)
44 | assert.Nil(t, err)
45 | expected := &Relationship{
46 | Source: RelationshipSource{ID: 8649302, ScreenName: "foo", IDStr: "8649302", CanDM: false, Muting: true, WantRetweets: false},
47 | Target: RelationshipTarget{ID: 12148, ScreenName: "bar", IDStr: "12148", Following: true, FollowedBy: false},
48 | }
49 | assert.Equal(t, expected, relationship)
50 | }
51 |
52 | func TestFriendshipService_Destroy(t *testing.T) {
53 | httpClient, mux, server := testServer()
54 | defer server.Close()
55 |
56 | mux.HandleFunc("/1.1/friendships/destroy.json", func(w http.ResponseWriter, r *http.Request) {
57 | assertMethod(t, "POST", r)
58 | assertPostForm(t, map[string]string{"user_id": "12345"}, r)
59 | w.Header().Set("Content-Type", "application/json")
60 | fmt.Fprintf(w, `{"id": 12345, "name": "Doug Williams"}`)
61 | })
62 |
63 | client := NewClient(httpClient)
64 | params := &FriendshipDestroyParams{UserID: 12345}
65 | user, _, err := client.Friendships.Destroy(params)
66 | assert.Nil(t, err)
67 | expected := &User{ID: 12345, Name: "Doug Williams"}
68 | assert.Equal(t, expected, user)
69 | }
70 |
71 | func TestFriendshipService_Outgoing(t *testing.T) {
72 | httpClient, mux, server := testServer()
73 | defer server.Close()
74 |
75 | mux.HandleFunc("/1.1/friendships/outgoing.json", func(w http.ResponseWriter, r *http.Request) {
76 | assertMethod(t, "GET", r)
77 | assertQuery(t, map[string]string{"cursor": "1516933260114270762"}, r)
78 | w.Header().Set("Content-Type", "application/json")
79 | fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
80 | })
81 | expected := &FriendIDs{
82 | IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
83 | NextCursor: 1516837838944119498,
84 | NextCursorStr: "1516837838944119498",
85 | PreviousCursor: -1516924983503961435,
86 | PreviousCursorStr: "-1516924983503961435",
87 | }
88 |
89 | client := NewClient(httpClient)
90 | params := &FriendshipPendingParams{
91 | Cursor: 1516933260114270762,
92 | }
93 | friendIDs, _, err := client.Friendships.Outgoing(params)
94 | assert.Nil(t, err)
95 | assert.Equal(t, expected, friendIDs)
96 | }
97 |
98 | func TestFriendshipService_Incoming(t *testing.T) {
99 | httpClient, mux, server := testServer()
100 | defer server.Close()
101 |
102 | mux.HandleFunc("/1.1/friendships/incoming.json", func(w http.ResponseWriter, r *http.Request) {
103 | assertMethod(t, "GET", r)
104 | assertQuery(t, map[string]string{"cursor": "1516933260114270762"}, r)
105 | w.Header().Set("Content-Type", "application/json")
106 | fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
107 | })
108 | expected := &FriendIDs{
109 | IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
110 | NextCursor: 1516837838944119498,
111 | NextCursorStr: "1516837838944119498",
112 | PreviousCursor: -1516924983503961435,
113 | PreviousCursorStr: "-1516924983503961435",
114 | }
115 |
116 | client := NewClient(httpClient)
117 | params := &FriendshipPendingParams{
118 | Cursor: 1516933260114270762,
119 | }
120 | friendIDs, _, err := client.Friendships.Incoming(params)
121 | assert.Nil(t, err)
122 | assert.Equal(t, expected, friendIDs)
123 | }
124 |
125 | func TestFriendshipService_Lookup(t *testing.T) {
126 | httpClient, mux, server := testServer()
127 | defer server.Close()
128 |
129 | mux.HandleFunc("/1.1/friendships/lookup.json", func(w http.ResponseWriter, r *http.Request) {
130 | assertMethod(t, "GET", r)
131 | assertQuery(t, map[string]string{"user_id": "123,214"}, r)
132 | w.Header().Set("Content-Type", "application/json")
133 | fmt.Fprintf(w, `[{"name": "andy piper (pipes)","screen_name": "andypiper","id": 786491,"id_str": "786491","connections": ["following"]}]`)
134 | })
135 |
136 | expected := &[]FriendshipResponse{
137 | {Name: "andy piper (pipes)", ScreenName: "andypiper", ID: 786491, IDStr: "786491", Connections: []string{"following"}},
138 | }
139 |
140 | client := NewClient(httpClient)
141 | params := &FriendshipLookupParams{
142 | UserID: []int64{123, 214},
143 | }
144 | friendIDs, _, err := client.Friendships.Lookup(params)
145 | assert.Nil(t, err)
146 | assert.Equal(t, expected, friendIDs)
147 | }
148 |
--------------------------------------------------------------------------------
/twitter/premium_search.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/dghubble/sling"
8 | )
9 |
10 | // PremiumSearch represents the result of a Tweet search.
11 | // https://developer.twitter.com/en/docs/tweets/search/api-reference/premium-search
12 | type PremiumSearch struct {
13 | Results []Tweet `json:"results"`
14 | Next string `json:"next"`
15 | RequestParameters *RequestParameters `json:"requestParameters"`
16 | }
17 |
18 | // RequestParameters describes a request parameter that was passed to a Premium search API.
19 | type RequestParameters struct {
20 | MaxResults int `json:"maxResults"`
21 | FromDate string `json:"fromDate"`
22 | ToDate string `json:"toDate"`
23 | }
24 |
25 | // PremiumSearchCount describes a response of Premium search API's count endpoint.
26 | // https://developer.twitter.com/en/docs/tweets/search/api-reference/premium-search#CountsEndpoint
27 | type PremiumSearchCount struct {
28 | Results []TweetCount `json:"results"`
29 | TotalCount int64 `json:"totalCount"`
30 | RequestParameters *RequestCountParameters `json:"requestParameters"`
31 | }
32 |
33 | // RequestCountParameters describes a request parameter that was passed to a Premium search API.
34 | type RequestCountParameters struct {
35 | Bucket string `json:"bucket"`
36 | FromDate string `json:"fromDate"`
37 | ToDate string `json:"toDate"`
38 | }
39 |
40 | // TweetCount represents a count of Tweets in the TimePeriod matching a search query.
41 | type TweetCount struct {
42 | TimePeriod string `json:"timePeriod"`
43 | Count int64 `json:"count"`
44 | }
45 |
46 | // PremiumSearchService provides methods for accessing Twitter premium search API endpoints.
47 | type PremiumSearchService struct {
48 | sling *sling.Sling
49 | }
50 |
51 | // newSearchService returns a new SearchService.
52 | func newPremiumSearchService(sling *sling.Sling) *PremiumSearchService {
53 | return &PremiumSearchService{
54 | sling: sling.Path("tweets/search/"),
55 | }
56 | }
57 |
58 | // PremiumSearchTweetParams are the parameters for PremiumSearchService.SearchFullArchive and Search30Days
59 | type PremiumSearchTweetParams struct {
60 | Query string `url:"query,omitempty"`
61 | Tag string `url:"tag,omitempty"`
62 | FromDate string `url:"fromDate,omitempty"`
63 | ToDate string `url:"toDate,omitempty"`
64 | MaxResults int `url:"maxResults,omitempty"`
65 | Next string `url:"next,omitempty"`
66 | }
67 |
68 | // PremiumSearchCountTweetParams are the parameters for PremiumSearchService.CountFullArchive and Count30Days
69 | type PremiumSearchCountTweetParams struct {
70 | Query string `url:"query,omitempty"`
71 | Tag string `url:"tag,omitempty"`
72 | FromDate string `url:"fromDate,omitempty"`
73 | ToDate string `url:"toDate,omitempty"`
74 | Bucket string `url:"bucket,omitempty"`
75 | Next string `url:"next,omitempty"`
76 | }
77 |
78 | // SearchFullArchive returns a collection of Tweets matching a search query from tweets back to the very first tweet.
79 | // https://developer.twitter.com/en/docs/tweets/search/api-reference/premium-search
80 | func (s *PremiumSearchService) SearchFullArchive(params *PremiumSearchTweetParams, label string) (*PremiumSearch, *http.Response, error) {
81 | search := new(PremiumSearch)
82 | apiError := new(APIError)
83 | path := fmt.Sprintf("fullarchive/%s.json", label)
84 | resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(search, apiError)
85 | return search, resp, relevantError(err, *apiError)
86 | }
87 |
88 | // Search30Days returns a collection of Tweets matching a search query from Tweets posted within the last 30 days.
89 | // https://developer.twitter.com/en/docs/tweets/search/api-reference/premium-search
90 | func (s *PremiumSearchService) Search30Days(params *PremiumSearchTweetParams, label string) (*PremiumSearch, *http.Response, error) {
91 | search := new(PremiumSearch)
92 | apiError := new(APIError)
93 | path := fmt.Sprintf("30day/%s.json", label)
94 | resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(search, apiError)
95 | return search, resp, relevantError(err, *apiError)
96 | }
97 |
98 | // CountFullArchive returns a counts of Tweets matching a search query from tweets back to the very first tweet.
99 | // https://developer.twitter.com/en/docs/tweets/search/api-reference/premium-search#CountsEndpoint
100 | func (s *PremiumSearchService) CountFullArchive(params *PremiumSearchCountTweetParams, label string) (*PremiumSearchCount, *http.Response, error) {
101 | counts := new(PremiumSearchCount)
102 | apiError := new(APIError)
103 | path := fmt.Sprintf("fullarchive/%s/counts.json", label)
104 | resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(counts, apiError)
105 | return counts, resp, relevantError(err, *apiError)
106 | }
107 |
108 | // Count30Days returns a counts of Tweets matching a search query from Tweets posted within the last 30 days.
109 | // https://developer.twitter.com/en/docs/tweets/search/api-reference/premium-search#CountsEndpoint
110 | func (s *PremiumSearchService) Count30Days(params *PremiumSearchCountTweetParams, label string) (*PremiumSearchCount, *http.Response, error) {
111 | counts := new(PremiumSearchCount)
112 | apiError := new(APIError)
113 | path := fmt.Sprintf("30day/%s/counts.json", label)
114 | resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(counts, apiError)
115 | return counts, resp, relevantError(err, *apiError)
116 | }
117 |
--------------------------------------------------------------------------------
/twitter/premium_search_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestPremiumSearchService_Tweets(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | assertSearchBody := func(t *testing.T, r *http.Request) {
16 | assertMethod(t, "GET", r)
17 | assertQuery(t, map[string]string{"query": "url:\"http://example.com\"", "tag": "8HYG54ZGTU", "fromDate": "201512220000", "toDate": "201712220000", "maxResults": "500", "next": "NTcxODIyMDMyODMwMjU1MTA0"}, r)
18 | }
19 | setResponse := func(w http.ResponseWriter) {
20 | w.Header().Set("Content-Type", "application/json")
21 | fmt.Fprintf(w, `{"results":[{"id":781760642139250689}],"next":"NTcxODIyMDMyODMwMjU1MTA0","requestParameters":{"maxResults":500,"fromDate":"201512200000","toDate":"201712200000"}}`)
22 | }
23 |
24 | mux.HandleFunc("/1.1/tweets/search/fullarchive/test.json", func(w http.ResponseWriter, r *http.Request) {
25 | assertSearchBody(t, r)
26 | setResponse(w)
27 | })
28 | mux.HandleFunc("/1.1/tweets/search/30day/test.json", func(w http.ResponseWriter, r *http.Request) {
29 | assertSearchBody(t, r)
30 | setResponse(w)
31 | })
32 |
33 | params := &PremiumSearchTweetParams{
34 | Query: "url:\"http://example.com\"",
35 | Tag: "8HYG54ZGTU",
36 | FromDate: "201512220000",
37 | ToDate: "201712220000",
38 | MaxResults: 500,
39 | Next: "NTcxODIyMDMyODMwMjU1MTA0",
40 | }
41 | expected := &PremiumSearch{
42 | Results: []Tweet{
43 | {ID: 781760642139250689},
44 | },
45 | Next: "NTcxODIyMDMyODMwMjU1MTA0",
46 | RequestParameters: &RequestParameters{
47 | MaxResults: 500,
48 | FromDate: "201512200000",
49 | ToDate: "201712200000",
50 | },
51 | }
52 |
53 | {
54 | client := NewClient(httpClient)
55 | search, _, err := client.PremiumSearch.SearchFullArchive(
56 | params,
57 | "test",
58 | )
59 | assert.Nil(t, err)
60 | assert.Equal(t, expected, search)
61 | }
62 | {
63 | client := NewClient(httpClient)
64 | search, _, err := client.PremiumSearch.Search30Days(
65 | params,
66 | "test",
67 | )
68 | assert.Nil(t, err)
69 | assert.Equal(t, expected, search)
70 | }
71 | }
72 |
73 | func TestPremiumSearchService_Counts(t *testing.T) {
74 | httpClient, mux, server := testServer()
75 | defer server.Close()
76 |
77 | assertCountBody := func(t *testing.T, r *http.Request) {
78 | assertMethod(t, "GET", r)
79 | assertQuery(t, map[string]string{"query": "url:\"http://example.com\"", "tag": "8HYG54ZGTU", "fromDate": "201512220000", "toDate": "201712220000", "bucket": "day", "next": "NTcxODIyMDMyODMwMjU1MTA0"}, r)
80 | }
81 | setResponse := func(w http.ResponseWriter) {
82 | w.Header().Set("Content-Type", "application/json")
83 | fmt.Fprintf(w, `{"results":[{"timePeriod":"201701010000","count":32},{"timePeriod":"201701020000","count":45}],"totalCount":2027,"requestParameters":{"bucket":"day","fromDate":"201512200000","toDate":"201712200000"}}`)
84 | }
85 |
86 | mux.HandleFunc("/1.1/tweets/search/fullarchive/test/counts.json", func(w http.ResponseWriter, r *http.Request) {
87 | assertCountBody(t, r)
88 | setResponse(w)
89 | })
90 | mux.HandleFunc("/1.1/tweets/search/30day/test/counts.json", func(w http.ResponseWriter, r *http.Request) {
91 | assertCountBody(t, r)
92 | setResponse(w)
93 | })
94 |
95 | params := &PremiumSearchCountTweetParams{
96 | Query: "url:\"http://example.com\"",
97 | Tag: "8HYG54ZGTU",
98 | FromDate: "201512220000",
99 | ToDate: "201712220000",
100 | Bucket: "day",
101 | Next: "NTcxODIyMDMyODMwMjU1MTA0",
102 | }
103 | expected := &PremiumSearchCount{
104 | Results: []TweetCount{
105 | {
106 | TimePeriod: "201701010000",
107 | Count: 32,
108 | },
109 | {
110 | TimePeriod: "201701020000",
111 | Count: 45,
112 | },
113 | },
114 | TotalCount: 2027,
115 | RequestParameters: &RequestCountParameters{
116 | Bucket: "day",
117 | FromDate: "201512200000",
118 | ToDate: "201712200000",
119 | },
120 | }
121 |
122 | {
123 | client := NewClient(httpClient)
124 | search, _, err := client.PremiumSearch.CountFullArchive(
125 | params,
126 | "test",
127 | )
128 | assert.Nil(t, err)
129 | assert.Equal(t, expected, search)
130 | }
131 | {
132 | client := NewClient(httpClient)
133 | search, _, err := client.PremiumSearch.Count30Days(
134 | params,
135 | "test",
136 | )
137 | assert.Nil(t, err)
138 | assert.Equal(t, expected, search)
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/twitter/rate_limits.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/dghubble/sling"
7 | )
8 |
9 | // RateLimitService provides methods for accessing Twitter rate limits
10 | // API endpoints.
11 | type RateLimitService struct {
12 | sling *sling.Sling
13 | }
14 |
15 | // newRateLimitService returns a new RateLimitService.
16 | func newRateLimitService(sling *sling.Sling) *RateLimitService {
17 | return &RateLimitService{
18 | sling: sling.Path("application/"),
19 | }
20 | }
21 |
22 | // RateLimit summarizes current rate limits of resource families.
23 | type RateLimit struct {
24 | RateLimitContext *RateLimitContext `json:"rate_limit_context"`
25 | Resources *RateLimitResources `json:"resources"`
26 | }
27 |
28 | // RateLimitContext contains auth context
29 | type RateLimitContext struct {
30 | AccessToken string `json:"access_token"`
31 | }
32 |
33 | // RateLimitResources contains all limit status data for endpoints group by resources
34 | type RateLimitResources struct {
35 | Application map[string]*RateLimitResource `json:"application"`
36 | Favorites map[string]*RateLimitResource `json:"favorites"`
37 | Followers map[string]*RateLimitResource `json:"followers"`
38 | Friends map[string]*RateLimitResource `json:"friends"`
39 | Friendships map[string]*RateLimitResource `json:"friendships"`
40 | Geo map[string]*RateLimitResource `json:"geo"`
41 | Help map[string]*RateLimitResource `json:"help"`
42 | Lists map[string]*RateLimitResource `json:"lists"`
43 | Search map[string]*RateLimitResource `json:"search"`
44 | Statuses map[string]*RateLimitResource `json:"statuses"`
45 | Trends map[string]*RateLimitResource `json:"trends"`
46 | Users map[string]*RateLimitResource `json:"users"`
47 | }
48 |
49 | // RateLimitResource contains limit status data for a single endpoint
50 | type RateLimitResource struct {
51 | Limit int `json:"limit"`
52 | Remaining int `json:"remaining"`
53 | Reset int `json:"reset"`
54 | }
55 |
56 | // RateLimitParams are the parameters for RateLimitService.Status.
57 | type RateLimitParams struct {
58 | Resources []string `url:"resources,omitempty,comma"`
59 | }
60 |
61 | // Status summarizes the current rate limits of specified resource families.
62 | // https://developer.twitter.com/en/docs/developer-utilities/rate-limit-status/api-reference/get-application-rate_limit_status
63 | func (s *RateLimitService) Status(params *RateLimitParams) (*RateLimit, *http.Response, error) {
64 | rateLimit := new(RateLimit)
65 | apiError := new(APIError)
66 | resp, err := s.sling.New().Get("rate_limit_status.json").QueryStruct(params).Receive(rateLimit, apiError)
67 | return rateLimit, resp, relevantError(err, *apiError)
68 | }
69 |
--------------------------------------------------------------------------------
/twitter/rate_limits_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestRateLimitService_Status(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | mux.HandleFunc("/1.1/application/rate_limit_status.json", func(w http.ResponseWriter, r *http.Request) {
16 | assertMethod(t, "GET", r)
17 | assertQuery(t, map[string]string{"resources": "statuses,users"}, r)
18 | w.Header().Set("Content-Type", "application/json")
19 | fmt.Fprintf(w, `{"rate_limit_context":{"access_token":"a_fake_access_token"},"resources":{"statuses":{"/statuses/mentions_timeline":{"limit":75,"remaining":75,"reset":1403602426},"/statuses/lookup":{"limit":900,"remaining":900,"reset":1403602426}}}}`)
20 | })
21 |
22 | client := NewClient(httpClient)
23 | rateLimits, _, err := client.RateLimits.Status(&RateLimitParams{Resources: []string{"statuses", "users"}})
24 | expected := &RateLimit{
25 | RateLimitContext: &RateLimitContext{AccessToken: "a_fake_access_token"},
26 | Resources: &RateLimitResources{
27 | Statuses: map[string]*RateLimitResource{
28 | "/statuses/mentions_timeline": {
29 | Limit: 75,
30 | Remaining: 75,
31 | Reset: 1403602426,
32 | },
33 | "/statuses/lookup": {
34 | Limit: 900,
35 | Remaining: 900,
36 | Reset: 1403602426,
37 | },
38 | }}}
39 |
40 | assert.Nil(t, err)
41 | assert.Equal(t, expected, rateLimits)
42 | }
43 |
--------------------------------------------------------------------------------
/twitter/search.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/dghubble/sling"
7 | )
8 |
9 | // Search represents the result of a Tweet search.
10 | type Search struct {
11 | Statuses []Tweet `json:"statuses"`
12 | Metadata *SearchMetadata `json:"search_metadata"`
13 | }
14 |
15 | // SearchMetadata describes a Search result.
16 | type SearchMetadata struct {
17 | Count int `json:"count"`
18 | SinceID int64 `json:"since_id"`
19 | SinceIDStr string `json:"since_id_str"`
20 | MaxID int64 `json:"max_id"`
21 | MaxIDStr string `json:"max_id_str"`
22 | RefreshURL string `json:"refresh_url"`
23 | NextResults string `json:"next_results"`
24 | CompletedIn float64 `json:"completed_in"`
25 | Query string `json:"query"`
26 | }
27 |
28 | // SearchService provides methods for accessing Twitter search API endpoints.
29 | type SearchService struct {
30 | sling *sling.Sling
31 | }
32 |
33 | // newSearchService returns a new SearchService.
34 | func newSearchService(sling *sling.Sling) *SearchService {
35 | return &SearchService{
36 | sling: sling.Path("search/"),
37 | }
38 | }
39 |
40 | // SearchTweetParams are the parameters for SearchService.Tweets
41 | type SearchTweetParams struct {
42 | Query string `url:"q,omitempty"`
43 | Geocode string `url:"geocode,omitempty"`
44 | Lang string `url:"lang,omitempty"`
45 | Locale string `url:"locale,omitempty"`
46 | ResultType string `url:"result_type,omitempty"`
47 | Count int `url:"count,omitempty"`
48 | SinceID int64 `url:"since_id,omitempty"`
49 | MaxID int64 `url:"max_id,omitempty"`
50 | Until string `url:"until,omitempty"`
51 | Since string `url:"since,omitempty"`
52 | Filter string `url:"filter,omitempty"`
53 | IncludeEntities *bool `url:"include_entities,omitempty"`
54 | TweetMode string `url:"tweet_mode,omitempty"`
55 | }
56 |
57 | // Tweets returns a collection of Tweets matching a search query.
58 | // https://dev.twitter.com/rest/reference/get/search/tweets
59 | func (s *SearchService) Tweets(params *SearchTweetParams) (*Search, *http.Response, error) {
60 | search := new(Search)
61 | apiError := new(APIError)
62 | resp, err := s.sling.New().Get("tweets.json").QueryStruct(params).Receive(search, apiError)
63 | return search, resp, relevantError(err, *apiError)
64 | }
65 |
--------------------------------------------------------------------------------
/twitter/search_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestSearchService_Tweets(t *testing.T) {
12 | httpClient, mux, server := testServer()
13 | defer server.Close()
14 |
15 | mux.HandleFunc("/1.1/search/tweets.json", func(w http.ResponseWriter, r *http.Request) {
16 | assertMethod(t, "GET", r)
17 | assertQuery(t, map[string]string{"q": "happy birthday", "result_type": "popular", "count": "1", "since": "2012-01-01", "filter": "safe"}, r)
18 | w.Header().Set("Content-Type", "application/json")
19 | 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", "since":"2012-01-01", "filter":"safe"}}`)
20 | })
21 |
22 | client := NewClient(httpClient)
23 | search, _, err := client.Search.Tweets(&SearchTweetParams{
24 | Query: "happy birthday",
25 | Count: 1,
26 | ResultType: "popular",
27 | Since: "2012-01-01",
28 | Filter: "safe"})
29 | expected := &Search{
30 | Statuses: []Tweet{
31 | {ID: 781760642139250689},
32 | },
33 | Metadata: &SearchMetadata{
34 | Count: 1,
35 | SinceID: 0,
36 | SinceIDStr: "0",
37 | MaxID: 781760642139250689,
38 | MaxIDStr: "781760642139250689",
39 | RefreshURL: "?since_id=781760642139250689&q=happy+birthday&include_entities=1",
40 | NextResults: "?max_id=781760640104828927&q=happy+birthday&count=1&include_entities=1",
41 | CompletedIn: 0.043,
42 | Query: "happy birthday",
43 | },
44 | }
45 | assert.Nil(t, err)
46 | assert.Equal(t, expected, search)
47 | }
48 |
--------------------------------------------------------------------------------
/twitter/statuses.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/dghubble/sling"
9 | )
10 |
11 | // Tweet represents a Twitter Tweet, previously called a status.
12 | // https://dev.twitter.com/overview/api/tweets
13 | type Tweet struct {
14 | Coordinates *Coordinates `json:"coordinates"`
15 | CreatedAt string `json:"created_at"`
16 | CurrentUserRetweet *TweetIdentifier `json:"current_user_retweet"`
17 | Entities *Entities `json:"entities"`
18 | FavoriteCount int `json:"favorite_count"`
19 | Favorited bool `json:"favorited"`
20 | FilterLevel string `json:"filter_level"`
21 | ID int64 `json:"id"`
22 | IDStr string `json:"id_str"`
23 | InReplyToScreenName string `json:"in_reply_to_screen_name"`
24 | InReplyToStatusID int64 `json:"in_reply_to_status_id"`
25 | InReplyToStatusIDStr string `json:"in_reply_to_status_id_str"`
26 | InReplyToUserID int64 `json:"in_reply_to_user_id"`
27 | InReplyToUserIDStr string `json:"in_reply_to_user_id_str"`
28 | Lang string `json:"lang"`
29 | PossiblySensitive bool `json:"possibly_sensitive"`
30 | QuoteCount int `json:"quote_count"`
31 | ReplyCount int `json:"reply_count"`
32 | RetweetCount int `json:"retweet_count"`
33 | Retweeted bool `json:"retweeted"`
34 | RetweetedStatus *Tweet `json:"retweeted_status"`
35 | Source string `json:"source"`
36 | Scopes map[string]interface{} `json:"scopes"`
37 | Text string `json:"text"`
38 | FullText string `json:"full_text"`
39 | DisplayTextRange Indices `json:"display_text_range"`
40 | Place *Place `json:"place"`
41 | Truncated bool `json:"truncated"`
42 | User *User `json:"user"`
43 | WithheldCopyright bool `json:"withheld_copyright"`
44 | WithheldInCountries []string `json:"withheld_in_countries"`
45 | WithheldScope string `json:"withheld_scope"`
46 | ExtendedEntities *ExtendedEntity `json:"extended_entities"`
47 | ExtendedTweet *ExtendedTweet `json:"extended_tweet"`
48 | QuotedStatusID int64 `json:"quoted_status_id"`
49 | QuotedStatusIDStr string `json:"quoted_status_id_str"`
50 | QuotedStatus *Tweet `json:"quoted_status"`
51 | }
52 |
53 | // CreatedAtTime returns the time a tweet was created.
54 | func (t Tweet) CreatedAtTime() (time.Time, error) {
55 | return time.Parse(time.RubyDate, t.CreatedAt)
56 | }
57 |
58 | // ExtendedTweet represents fields embedded in extended Tweets when served in
59 | // compatibility mode (default).
60 | // https://dev.twitter.com/overview/api/upcoming-changes-to-tweets
61 | type ExtendedTweet struct {
62 | FullText string `json:"full_text"`
63 | DisplayTextRange Indices `json:"display_text_range"`
64 | Entities *Entities `json:"entities"`
65 | ExtendedEntities *ExtendedEntity `json:"extended_entities"`
66 | }
67 |
68 | // Place represents a Twitter Place / Location
69 | // https://dev.twitter.com/overview/api/places
70 | type Place struct {
71 | Attributes map[string]string `json:"attributes"`
72 | BoundingBox *BoundingBox `json:"bounding_box"`
73 | Country string `json:"country"`
74 | CountryCode string `json:"country_code"`
75 | FullName string `json:"full_name"`
76 | Geometry *BoundingBox `json:"geometry"`
77 | ID string `json:"id"`
78 | Name string `json:"name"`
79 | PlaceType string `json:"place_type"`
80 | Polylines []string `json:"polylines"`
81 | URL string `json:"url"`
82 | }
83 |
84 | // BoundingBox represents the bounding coordinates (longitude, latitutde)
85 | // defining the bounds of a box containing a Place entity.
86 | type BoundingBox struct {
87 | Coordinates [][][2]float64 `json:"coordinates"`
88 | Type string `json:"type"`
89 | }
90 |
91 | // Coordinates are pairs of longitude and latitude locations.
92 | type Coordinates struct {
93 | Coordinates [2]float64 `json:"coordinates"`
94 | Type string `json:"type"`
95 | }
96 |
97 | // TweetIdentifier represents the id by which a Tweet can be identified.
98 | type TweetIdentifier struct {
99 | ID int64 `json:"id"`
100 | IDStr string `json:"id_str"`
101 | }
102 |
103 | // StatusService provides methods for accessing Twitter status API endpoints.
104 | type StatusService struct {
105 | sling *sling.Sling
106 | }
107 |
108 | // newStatusService returns a new StatusService.
109 | func newStatusService(sling *sling.Sling) *StatusService {
110 | return &StatusService{
111 | sling: sling.Path("statuses/"),
112 | }
113 | }
114 |
115 | // StatusShowParams are the parameters for StatusService.Show
116 | type StatusShowParams struct {
117 | ID int64 `url:"id,omitempty"`
118 | TrimUser *bool `url:"trim_user,omitempty"`
119 | IncludeMyRetweet *bool `url:"include_my_retweet,omitempty"`
120 | IncludeEntities *bool `url:"include_entities,omitempty"`
121 | TweetMode string `url:"tweet_mode,omitempty"`
122 | }
123 |
124 | // Show returns the requested Tweet.
125 | // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-statuses-show-id
126 | func (s *StatusService) Show(id int64, params *StatusShowParams) (*Tweet, *http.Response, error) {
127 | if params == nil {
128 | params = &StatusShowParams{}
129 | }
130 | params.ID = id
131 | tweet := new(Tweet)
132 | apiError := new(APIError)
133 | resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(tweet, apiError)
134 | return tweet, resp, relevantError(err, *apiError)
135 | }
136 |
137 | // StatusLookupParams are the parameters for StatusService.Lookup
138 | type StatusLookupParams struct {
139 | ID []int64 `url:"id,omitempty,comma"`
140 | TrimUser *bool `url:"trim_user,omitempty"`
141 | IncludeEntities *bool `url:"include_entities,omitempty"`
142 | Map *bool `url:"map,omitempty"`
143 | TweetMode string `url:"tweet_mode,omitempty"`
144 | }
145 |
146 | // Lookup returns the requested Tweets as a slice. Combines ids from the
147 | // required ids argument and from params.Id.
148 | // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-statuses-lookup
149 | func (s *StatusService) Lookup(ids []int64, params *StatusLookupParams) ([]Tweet, *http.Response, error) {
150 | if params == nil {
151 | params = &StatusLookupParams{}
152 | }
153 | params.ID = append(params.ID, ids...)
154 | tweets := new([]Tweet)
155 | apiError := new(APIError)
156 | resp, err := s.sling.New().Get("lookup.json").QueryStruct(params).Receive(tweets, apiError)
157 | return *tweets, resp, relevantError(err, *apiError)
158 | }
159 |
160 | // StatusUpdateParams are the parameters for StatusService.Update
161 | type StatusUpdateParams struct {
162 | Status string `url:"status,omitempty"`
163 | InReplyToStatusID int64 `url:"in_reply_to_status_id,omitempty"`
164 | AutoPopulateReplyMetadata *bool `url:"auto_populate_reply_metadata,omitempty"`
165 | ExcludeReplyUserIds []int64 `url:"exclude_reply_user_ids,comma,omitempty"`
166 | AttachmentURL string `url:"attachment_url,omitempty"`
167 | MediaIds []int64 `url:"media_ids,omitempty,comma"`
168 | PossiblySensitive *bool `url:"possibly_sensitive,omitempty"`
169 | Lat *float64 `url:"lat,omitempty"`
170 | Long *float64 `url:"long,omitempty"`
171 | PlaceID string `url:"place_id,omitempty"`
172 | DisplayCoordinates *bool `url:"display_coordinates,omitempty"`
173 | TrimUser *bool `url:"trim_user,omitempty"`
174 | CardURI string `url:"card_uri,omitempty"`
175 | // Deprecated
176 | TweetMode string `url:"tweet_mode,omitempty"`
177 | }
178 |
179 | // Update updates the user's status, also known as Tweeting.
180 | // Requires a user auth context.
181 | // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update
182 | func (s *StatusService) Update(status string, params *StatusUpdateParams) (*Tweet, *http.Response, error) {
183 | if params == nil {
184 | params = &StatusUpdateParams{}
185 | }
186 | params.Status = status
187 | tweet := new(Tweet)
188 | apiError := new(APIError)
189 | resp, err := s.sling.New().Post("update.json").BodyForm(params).Receive(tweet, apiError)
190 | return tweet, resp, relevantError(err, *apiError)
191 | }
192 |
193 | // StatusRetweetParams are the parameters for StatusService.Retweet
194 | type StatusRetweetParams struct {
195 | ID int64 `url:"id,omitempty"`
196 | TrimUser *bool `url:"trim_user,omitempty"`
197 | TweetMode string `url:"tweet_mode,omitempty"`
198 | }
199 |
200 | // Retweet retweets the Tweet with the given id and returns the original Tweet
201 | // with embedded retweet details.
202 | // Requires a user auth context.
203 | // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-retweet-id
204 | func (s *StatusService) Retweet(id int64, params *StatusRetweetParams) (*Tweet, *http.Response, error) {
205 | if params == nil {
206 | params = &StatusRetweetParams{}
207 | }
208 | params.ID = id
209 | tweet := new(Tweet)
210 | apiError := new(APIError)
211 | path := fmt.Sprintf("retweet/%d.json", params.ID)
212 | resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
213 | return tweet, resp, relevantError(err, *apiError)
214 | }
215 |
216 | // StatusUnretweetParams are the parameters for StatusService.Unretweet
217 | type StatusUnretweetParams struct {
218 | ID int64 `url:"id,omitempty"`
219 | TrimUser *bool `url:"trim_user,omitempty"`
220 | TweetMode string `url:"tweet_mode,omitempty"`
221 | }
222 |
223 | // Unretweet unretweets the Tweet with the given id and returns the original Tweet.
224 | // Requires a user auth context.
225 | // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-unretweet-id
226 | func (s *StatusService) Unretweet(id int64, params *StatusUnretweetParams) (*Tweet, *http.Response, error) {
227 | if params == nil {
228 | params = &StatusUnretweetParams{}
229 | }
230 | params.ID = id
231 | tweet := new(Tweet)
232 | apiError := new(APIError)
233 | path := fmt.Sprintf("unretweet/%d.json", params.ID)
234 | resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
235 | return tweet, resp, relevantError(err, *apiError)
236 | }
237 |
238 | // StatusRetweetsParams are the parameters for StatusService.Retweets
239 | type StatusRetweetsParams struct {
240 | ID int64 `url:"id,omitempty"`
241 | Count int `url:"count,omitempty"`
242 | TrimUser *bool `url:"trim_user,omitempty"`
243 | TweetMode string `url:"tweet_mode,omitempty"`
244 | }
245 |
246 | // Retweets returns the most recent retweets of the Tweet with the given id.
247 | // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-statuses-retweets-id
248 | func (s *StatusService) Retweets(id int64, params *StatusRetweetsParams) ([]Tweet, *http.Response, error) {
249 | if params == nil {
250 | params = &StatusRetweetsParams{}
251 | }
252 | params.ID = id
253 | tweets := new([]Tweet)
254 | apiError := new(APIError)
255 | path := fmt.Sprintf("retweets/%d.json", params.ID)
256 | resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(tweets, apiError)
257 | return *tweets, resp, relevantError(err, *apiError)
258 | }
259 |
260 | // StatusRetweeterParams are the parameters for StatusService.Retweeters.
261 | type StatusRetweeterParams struct {
262 | ID int64 `url:"id,omitempty"`
263 | Count int `url:"count,omitempty"`
264 | Cursor int64 `url:"cursor,omitempty"`
265 | }
266 |
267 | // RetweeterIDs is a cursored collection of User IDs that retweeted a Tweet.
268 | type RetweeterIDs struct {
269 | IDs []int64 `json:"ids"`
270 | PreviousCursor int64 `json:"previous_cursor"`
271 | PreviousCursorStr string `json:"previous_cursor_str"`
272 | NextCursor int64 `json:"next_cursor"`
273 | NextCursorStr string `json:"next_cursor_str"`
274 | }
275 |
276 | // Retweeters return the retweeters of a specific tweet.
277 | // https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-retweeters-ids
278 | func (s *StatusService) Retweeters(params *StatusRetweeterParams) (*RetweeterIDs, *http.Response, error) {
279 | retweeters := new(RetweeterIDs)
280 | apiError := new(APIError)
281 | resp, err := s.sling.Get("retweeters/ids.json").QueryStruct(params).Receive(retweeters, apiError)
282 | return retweeters, resp, relevantError(err, *apiError)
283 | }
284 |
285 | // StatusDestroyParams are the parameters for StatusService.Destroy
286 | type StatusDestroyParams struct {
287 | ID int64 `url:"id,omitempty"`
288 | TrimUser *bool `url:"trim_user,omitempty"`
289 | TweetMode string `url:"tweet_mode,omitempty"`
290 | }
291 |
292 | // Destroy deletes the Tweet with the given id and returns it if successful.
293 | // Requires a user auth context.
294 | // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-favorites-destroy
295 | func (s *StatusService) Destroy(id int64, params *StatusDestroyParams) (*Tweet, *http.Response, error) {
296 | if params == nil {
297 | params = &StatusDestroyParams{}
298 | }
299 | params.ID = id
300 | tweet := new(Tweet)
301 | apiError := new(APIError)
302 | path := fmt.Sprintf("destroy/%d.json", params.ID)
303 | resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
304 | return tweet, resp, relevantError(err, *apiError)
305 | }
306 |
307 | // OEmbedTweet represents a Tweet in oEmbed format.
308 | type OEmbedTweet struct {
309 | URL string `json:"url"`
310 | ProviderURL string `json:"provider_url"`
311 | ProviderName string `json:"provider_name"`
312 | AuthorName string `json:"author_name"`
313 | Version string `json:"version"`
314 | AuthorURL string `json:"author_url"`
315 | Type string `json:"type"`
316 | HTML string `json:"html"`
317 | Height int64 `json:"height"`
318 | Width int64 `json:"width"`
319 | CacheAge string `json:"cache_age"`
320 | }
321 |
322 | // StatusOEmbedParams are the parameters for StatusService.OEmbed
323 | type StatusOEmbedParams struct {
324 | ID int64 `url:"id,omitempty"`
325 | URL string `url:"url,omitempty"`
326 | Align string `url:"align,omitempty"`
327 | MaxWidth int64 `url:"maxwidth,omitempty"`
328 | HideMedia *bool `url:"hide_media,omitempty"`
329 | HideThread *bool `url:"hide_media,omitempty"`
330 | OmitScript *bool `url:"hide_media,omitempty"`
331 | WidgetType string `url:"widget_type,omitempty"`
332 | HideTweet *bool `url:"hide_tweet,omitempty"`
333 | }
334 |
335 | // OEmbed returns the requested Tweet in oEmbed format.
336 | // https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-statuses-oembed
337 | func (s *StatusService) OEmbed(params *StatusOEmbedParams) (*OEmbedTweet, *http.Response, error) {
338 | oEmbedTweet := new(OEmbedTweet)
339 | apiError := new(APIError)
340 | resp, err := s.sling.New().Get("oembed.json").QueryStruct(params).Receive(oEmbedTweet, apiError)
341 | return oEmbedTweet, resp, relevantError(err, *apiError)
342 | }
343 |
--------------------------------------------------------------------------------
/twitter/statuses_test.go:
--------------------------------------------------------------------------------
1 | package twitter
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestStatusService_Show(t *testing.T) {
13 | httpClient, mux, server := testServer()
14 | defer server.Close()
15 |
16 | mux.HandleFunc("/1.1/statuses/show.json", func(w http.ResponseWriter, r *http.Request) {
17 | assertMethod(t, "GET", r)
18 | assertQuery(t, map[string]string{"id": "589488862814076930", "include_entities": "false"}, r)
19 | w.Header().Set("Content-Type", "application/json")
20 | fmt.Fprintf(w, `{"user": {"screen_name": "dghubble"}, "text": ".@audreyr use a DONTREADME file if you really want people to read it :P"}`)
21 | })
22 |
23 | client := NewClient(httpClient)
24 | params := &StatusShowParams{ID: 5441, IncludeEntities: Bool(false)}
25 | tweet, _, err := client.Statuses.Show(589488862814076930, params)
26 | expected := &Tweet{User: &User{ScreenName: "dghubble"}, Text: ".@audreyr use a DONTREADME file if you really want people to read it :P"}
27 | assert.Nil(t, err)
28 | assert.Equal(t, expected, tweet)
29 | }
30 |
31 | func TestStatusService_ShowHandlesNilParams(t *testing.T) {
32 | httpClient, mux, server := testServer()
33 | defer server.Close()
34 |
35 | mux.HandleFunc("/1.1/statuses/show.json", func(w http.ResponseWriter, r *http.Request) {
36 | assertQuery(t, map[string]string{"id": "589488862814076930"}, r)
37 | })
38 | client := NewClient(httpClient)
39 | _, _, err := client.Statuses.Show(589488862814076930, nil)
40 | assert.Nil(t, err)
41 | }
42 |
43 | func TestStatusService_Lookup(t *testing.T) {
44 | httpClient, mux, server := testServer()
45 | defer server.Close()
46 |
47 | mux.HandleFunc("/1.1/statuses/lookup.json", func(w http.ResponseWriter, r *http.Request) {
48 | assertMethod(t, "GET", r)
49 | assertQuery(t, map[string]string{"id": "20,573893817000140800", "trim_user": "true"}, r)
50 | w.Header().Set("Content-Type", "application/json")
51 | fmt.Fprintf(w, `[{"id": 20, "text": "just setting up my twttr"}, {"id": 573893817000140800, "text": "Don't get lost #PaxEast2015"}]`)
52 | })
53 |
54 | client := NewClient(httpClient)
55 | params := &StatusLookupParams{ID: []int64{20}, TrimUser: Bool(true)}
56 | tweets, _, err := client.Statuses.Lookup([]int64{573893817000140800}, params)
57 | expected := []Tweet{{ID: 20, Text: "just setting up my twttr"}, {ID: 573893817000140800, Text: "Don't get lost #PaxEast2015"}}
58 | assert.Nil(t, err)
59 | assert.Equal(t, expected, tweets)
60 | }
61 |
62 | func TestStatusService_LookupHandlesNilParams(t *testing.T) {
63 | httpClient, mux, server := testServer()
64 | defer server.Close()
65 | mux.HandleFunc("/1.1/statuses/lookup.json", func(w http.ResponseWriter, r *http.Request) {
66 | assertQuery(t, map[string]string{"id": "20,573893817000140800"}, r)
67 | })
68 | client := NewClient(httpClient)
69 | _, _, err := client.Statuses.Lookup([]int64{20, 573893817000140800}, nil)
70 | assert.Nil(t, err)
71 | }
72 |
73 | func TestStatusService_Update(t *testing.T) {
74 | httpClient, mux, server := testServer()
75 | defer server.Close()
76 |
77 | mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
78 | assertMethod(t, "POST", r)
79 | assertQuery(t, map[string]string{}, r)
80 | assertPostForm(t, map[string]string{"status": "very informative tweet", "media_ids": "123456789,987654321", "lat": "37.826706", "long": "-122.42219"}, r)
81 | w.Header().Set("Content-Type", "application/json")
82 | fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very informative tweet"}`)
83 | })
84 |
85 | client := NewClient(httpClient)
86 | params := &StatusUpdateParams{MediaIds: []int64{123456789, 987654321}, Lat: Float(37.826706), Long: Float(-122.422190)}
87 | tweet, _, err := client.Statuses.Update("very informative tweet", params)
88 | expected := &Tweet{ID: 581980947630845953, Text: "very informative tweet"}
89 | assert.Nil(t, err)
90 | assert.Equal(t, expected, tweet)
91 | }
92 |
93 | func TestStatusService_UpdateHandlesNilParams(t *testing.T) {
94 | httpClient, mux, server := testServer()
95 | defer server.Close()
96 | mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
97 | assertPostForm(t, map[string]string{"status": "very informative tweet"}, r)
98 | })
99 | client := NewClient(httpClient)
100 | _, _, err := client.Statuses.Update("very informative tweet", nil)
101 | assert.Nil(t, err)
102 | }
103 |
104 | func TestStatusService_APIError(t *testing.T) {
105 | httpClient, mux, server := testServer()
106 | defer server.Close()
107 | mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
108 | assertPostForm(t, map[string]string{"status": "very informative tweet"}, r)
109 | w.Header().Set("Content-Type", "application/json")
110 | w.WriteHeader(403)
111 | fmt.Fprintf(w, `{"errors": [{"message": "Status is a duplicate", "code": 187}]}`)
112 | })
113 |
114 | client := NewClient(httpClient)
115 | _, _, err := client.Statuses.Update("very informative tweet", nil)
116 | expected := APIError{
117 | Errors: []ErrorDetail{
118 | {Message: "Status is a duplicate", Code: 187},
119 | },
120 | }
121 | if assert.Error(t, err) {
122 | assert.Equal(t, expected, err)
123 | }
124 | }
125 |
126 | func TestStatusService_HTTPError(t *testing.T) {
127 | httpClient, _, server := testServer()
128 | server.Close()
129 | client := NewClient(httpClient)
130 | _, _, err := client.Statuses.Update("very informative tweet", nil)
131 | if err == nil || !strings.Contains(err.Error(), "connection refused") {
132 | t.Errorf("Statuses.Update error expected connection refused, got: \n %+v", err)
133 | }
134 | }
135 |
136 | func TestStatusService_Retweet(t *testing.T) {
137 | httpClient, mux, server := testServer()
138 | defer server.Close()
139 |
140 | mux.HandleFunc("/1.1/statuses/retweet/20.json", func(w http.ResponseWriter, r *http.Request) {
141 | assertMethod(t, "POST", r)
142 | assertQuery(t, map[string]string{}, r)
143 | assertPostForm(t, map[string]string{"id": "20", "trim_user": "true"}, r)
144 | w.Header().Set("Content-Type", "application/json")
145 | fmt.Fprintf(w, `{"id": 581980947630202020, "text": "RT @jack: just setting up my twttr", "retweeted_status": {"id": 20, "text": "just setting up my twttr"}}`)
146 | })
147 |
148 | client := NewClient(httpClient)
149 | params := &StatusRetweetParams{TrimUser: Bool(true)}
150 | tweet, _, err := client.Statuses.Retweet(20, params)
151 | expected := &Tweet{ID: 581980947630202020, Text: "RT @jack: just setting up my twttr", RetweetedStatus: &Tweet{ID: 20, Text: "just setting up my twttr"}}
152 | assert.Nil(t, err)
153 | assert.Equal(t, expected, tweet)
154 | }
155 |
156 | func TestStatusService_RetweetHandlesNilParams(t *testing.T) {
157 | httpClient, mux, server := testServer()
158 | defer server.Close()
159 |
160 | mux.HandleFunc("/1.1/statuses/retweet/20.json", func(w http.ResponseWriter, r *http.Request) {
161 | assertPostForm(t, map[string]string{"id": "20"}, r)
162 | })
163 |
164 | client := NewClient(httpClient)
165 | _, _, err := client.Statuses.Retweet(20, nil)
166 | assert.Nil(t, err)
167 | }
168 |
169 | func TestStatusService_Unretweet(t *testing.T) {
170 | httpClient, mux, server := testServer()
171 | defer server.Close()
172 |
173 | mux.HandleFunc("/1.1/statuses/unretweet/20.json", func(w http.ResponseWriter, r *http.Request) {
174 | assertMethod(t, "POST", r)
175 | assertQuery(t, map[string]string{}, r)
176 | assertPostForm(t, map[string]string{"id": "20", "trim_user": "true"}, r)
177 | w.Header().Set("Content-Type", "application/json")
178 | fmt.Fprintf(w, `{"id": 581980947630202020, "text":"RT @jack: just setting up my twttr", "retweeted_status": {"id": 20, "text": "just setting up my twttr"}}`)
179 | })
180 |
181 | client := NewClient(httpClient)
182 | params := &StatusUnretweetParams{TrimUser: Bool(true)}
183 | tweet, _, err := client.Statuses.Unretweet(20, params)
184 | expected := &Tweet{ID: 581980947630202020, Text: "RT @jack: just setting up my twttr", RetweetedStatus: &Tweet{ID: 20, Text: "just setting up my twttr"}}
185 | assert.Nil(t, err)
186 | assert.Equal(t, expected, tweet)
187 | }
188 |
189 | func TestStatusService_Retweets(t *testing.T) {
190 | httpClient, mux, server := testServer()
191 | defer server.Close()
192 |
193 | mux.HandleFunc("/1.1/statuses/retweets/20.json", func(w http.ResponseWriter, r *http.Request) {
194 | assertMethod(t, "GET", r)
195 | assertQuery(t, map[string]string{"id": "20", "count": "2"}, r)
196 | w.Header().Set("Content-Type", "application/json")
197 | fmt.Fprintf(w, `[{"text": "RT @jack: just setting up my twttr"}, {"text": "RT @jack: just setting up my twttr"}]`)
198 | })
199 |
200 | client := NewClient(httpClient)
201 | params := &StatusRetweetsParams{Count: 2}
202 | retweets, _, err := client.Statuses.Retweets(20, params)
203 | expected := []Tweet{{Text: "RT @jack: just setting up my twttr"}, {Text: "RT @jack: just setting up my twttr"}}
204 | assert.Nil(t, err)
205 | assert.Equal(t, expected, retweets)
206 | }
207 |
208 | func TestStatusService_RetweetsHandlesNilParams(t *testing.T) {
209 | httpClient, mux, server := testServer()
210 | defer server.Close()
211 |
212 | mux.HandleFunc("/1.1/statuses/retweets/20.json", func(w http.ResponseWriter, r *http.Request) {
213 | assertQuery(t, map[string]string{"id": "20"}, r)
214 | })
215 |
216 | client := NewClient(httpClient)
217 | _, _, err := client.Statuses.Retweets(20, nil)
218 | assert.Nil(t, err)
219 | }
220 |
221 | func TestStatusService_Retweeters(t *testing.T) {
222 | httpClient, mux, server := testServer()
223 | defer server.Close()
224 |
225 | mux.HandleFunc("/1.1/statuses/retweeters/ids.json", func(w http.ResponseWriter, r *http.Request) {
226 | assertMethod(t, "GET", r)
227 | assertQuery(t, map[string]string{"id": "1554515292390408192", "count": "10"}, r)
228 | w.Header().Set("Content-Type", "application/json")
229 | fmt.Fprintf(w, `{
230 | "ids": [316736642],
231 | "previous_cursor": 0,
232 | "previous_cursor_str": "0",
233 | "next_cursor": 0,
234 | "next_cursor_str": "0"
235 | }`)
236 | })
237 |
238 | client := NewClient(httpClient)
239 | params := &StatusRetweeterParams{
240 | ID: 1554515292390408192,
241 | Count: 10,
242 | }
243 | tweets, _, err := client.Statuses.Retweeters(params)
244 | expected := &RetweeterIDs{
245 | IDs: []int64{316736642},
246 | PreviousCursor: 0,
247 | PreviousCursorStr: "0",
248 | NextCursor: 0,
249 | NextCursorStr: "0",
250 | }
251 | assert.Nil(t, err)
252 | assert.Equal(t, expected, tweets)
253 | }
254 |
255 | func TestStatusService_Destroy(t *testing.T) {
256 | httpClient, mux, server := testServer()
257 | defer server.Close()
258 |
259 | mux.HandleFunc("/1.1/statuses/destroy/40.json", func(w http.ResponseWriter, r *http.Request) {
260 | assertMethod(t, "POST", r)
261 | assertQuery(t, map[string]string{}, r)
262 | assertPostForm(t, map[string]string{"id": "40", "trim_user": "true"}, r)
263 | w.Header().Set("Content-Type", "application/json")
264 | fmt.Fprintf(w, `{"id": 40, "text": "wishing I had another sammich"}`)
265 | })
266 |
267 | client := NewClient(httpClient)
268 | params := &StatusDestroyParams{TrimUser: Bool(true)}
269 | tweet, _, err := client.Statuses.Destroy(40, params)
270 | // feed Biz Stone a sammich, he deletes sammich Tweet
271 | expected := &Tweet{ID: 40, Text: "wishing I had another sammich"}
272 | assert.Nil(t, err)
273 | assert.Equal(t, expected, tweet)
274 | }
275 |
276 | func TestStatusService_DestroyHandlesNilParams(t *testing.T) {
277 | httpClient, mux, server := testServer()
278 | defer server.Close()
279 |
280 | mux.HandleFunc("/1.1/statuses/destroy/40.json", func(w http.ResponseWriter, r *http.Request) {
281 | assertPostForm(t, map[string]string{"id": "40"}, r)
282 | })
283 |
284 | client := NewClient(httpClient)
285 | _, _, err := client.Statuses.Destroy(40, nil)
286 | assert.Nil(t, err)
287 | }
288 |
289 | func TestStatusService_OEmbed(t *testing.T) {
290 | httpClient, mux, server := testServer()
291 | defer server.Close()
292 |
293 | mux.HandleFunc("/1.1/statuses/oembed.json", func(w http.ResponseWriter, r *http.Request) {
294 | assertMethod(t, "GET", r)
295 | assertQuery(t, map[string]string{"id": "691076766878691329", "maxwidth": "400", "hide_media": "true"}, r)
296 | w.Header().Set("Content-Type", "application/json")
297 | // abbreviated oEmbed response
298 | fmt.Fprintf(w, `{"url": "https://twitter.com/dghubble/statuses/691076766878691329", "width": 400, "html": "