-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient.go
207 lines (178 loc) · 6.32 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package twigots
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
client *http.Client
}
var DefaultClient = NewClient(nil)
// FetchTicketListingsInput defines parameters when getting ticket listings.
//
// Ticket listings can either be fetched by maximum number or by time period.
// The default is to get a maximum number of ticket listings.
//
// If both a maximum number and a time period are set, whichever condition
// is met first will stop the fetching of ticket listings.
type FetchTicketListingsInput struct {
// Required fields
APIKey string
Country Country
// Regions for which to fetch ticket listings from.
// Leave this unset or empty to fetch listings from any region.
// Defaults to any region (unset).
Regions []Region
// MaxNumber is the maximum number of ticket listings to fetch.
// If getting ticket listings within in a time period using `CreatedAfter`, set this to an arbitrarily
// large number (e.g. 250) to ensure all listings in the period are fetched, while preventing
// accidentally fetching too many listings and possibly being rate limited or blocked.
// Defaults to 10.
// Set to -1 if no limit is desired. This is dangerous and should only be used with well constrained time periods.
MaxNumber int
// CreatedAfter is the time which ticket listings must have been created after to be fetched.
// Set this to fetch listings within a time period.
// Set `MaxNumber` to an arbitrarily large number (e.g. 250) to ensure all listings in the period are fetched,
// while preventing accidentally fetching too many listings and possibly being rate limited or blocked.
CreatedAfter time.Time
// CreatedBefore is the time which ticket listings must have been created before to be fetched.
// Set this to fetch listings within a time period.
// Defaults to current time.
CreatedBefore time.Time
// NumPerRequest is the number of ticket listings to fetch in each request.
// Not all requested listings are fetched at once - instead a series of requests are made,
// each fetching the number of listings specified here. In theory this can be arbitrarily
// large to prevent having to make too many requests, however it has been known that any
// other number than 10 can sometimes not work.
// Defaults to 10. Usually can be ignored.
NumPerRequest int
}
func (f *FetchTicketListingsInput) applyDefaults() {
if f.MaxNumber == 0 {
f.MaxNumber = 10
}
if f.CreatedBefore.IsZero() {
f.CreatedBefore = time.Now()
}
if f.NumPerRequest <= 0 {
f.NumPerRequest = 10
}
}
// Validate the input struct used to get ticket listings.
// This is used internally to check the input, but can also be used externally.
func (f FetchTicketListingsInput) Validate() error {
if f.APIKey == "" {
return errors.New("api key must be set")
}
if f.Country.Value == "" {
return errors.New("country must be set")
}
if !Countries.Contains(f.Country) {
return fmt.Errorf("country '%s' is not valid", f.Country)
}
if f.CreatedBefore.Before(f.CreatedAfter) {
return errors.New("created after time must be after the created before time")
}
if f.MaxNumber < 0 && f.CreatedAfter.IsZero() {
return errors.New("if not limiting number of ticket listings, created after must be set")
}
return nil
}
// FetchTicketListings gets ticket listings using the specified input.
func (c *Client) FetchTicketListings(
ctx context.Context,
input FetchTicketListingsInput,
) (TicketListings, error) {
input.applyDefaults()
err := input.Validate()
if err != nil {
return nil, fmt.Errorf("invalid input: %w", err)
}
// Iterate through feeds until have equal to or more ticket listings than desired
ticketListings := make(TicketListings, 0, input.MaxNumber)
earliestTicketTime := input.CreatedBefore
for (input.MaxNumber < 0 || len(ticketListings) < input.MaxNumber) &&
earliestTicketTime.After(input.CreatedAfter) {
feedUrl, err := FeedUrl(FeedUrlInput{
APIKey: input.APIKey,
Country: input.Country,
Regions: input.Regions,
NumListings: input.NumPerRequest,
BeforeTime: earliestTicketTime,
})
if err != nil {
return nil, fmt.Errorf("failed to get feed url: %w", err)
}
feedTicketListings, err := c.FetchTicketListingsByFeedUrl(ctx, feedUrl)
if err != nil {
return nil, err
}
ticketListings = append(ticketListings, feedTicketListings...)
earliestTicketTime = feedTicketListings[len(feedTicketListings)-1].CreatedAt.Time
}
// Only return ticket listings requested
ticketListings = sliceToMaxNumTicketListings(ticketListings, input.MaxNumber)
ticketListings = filterToCreatedAfter(ticketListings, input.CreatedAfter)
return ticketListings, nil
}
// FetchTicketListings gets ticket listings using the specified feel url.
func (c *Client) FetchTicketListingsByFeedUrl(
ctx context.Context,
feedUrl string,
) (TicketListings, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, feedUrl, http.NoBody)
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", "") // Twickets blocks some user agents
response, err := c.client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode >= 300 {
err := fmt.Errorf("error response %s", response.Status)
if response.StatusCode == http.StatusForbidden {
err = fmt.Errorf("%s: possibly due to tls misconfiguration", err)
}
return nil, err
}
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
return UnmarshalTwicketsFeedJson(bodyBytes)
}
// NewClient creates a new Twickets client
func NewClient(httpClient *http.Client) *Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
if httpClient.Transport == nil {
httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
}
return &Client{client: httpClient}
}
func sliceToMaxNumTicketListings(listings TicketListings, maxNumTicketListings int) TicketListings {
if len(listings) > maxNumTicketListings {
listings = listings[:maxNumTicketListings]
}
return listings
}
func filterToCreatedAfter(listings TicketListings, createdAfter time.Time) TicketListings {
filteredListings := make(TicketListings, 0, len(listings))
for _, listing := range listings {
if matchesCreatedAfter(listing, createdAfter) {
filteredListings = append(filteredListings, listing)
}
}
return filteredListings
}