Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 51 additions & 10 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"

"github.com/imroc/req/v3"
"github.com/k3a/html2text"
)

type Client struct {
client *req.Client
apiKey string
client *req.Client
apiKey string
proxies *[]Proxy
nextProxy int
mutex *sync.Mutex
}

func (c *Client) Client() *http.Client {
Expand Down Expand Up @@ -101,12 +106,14 @@ func (c *Client) FetchTicketListings(
input FetchTicketListingsInput,
) (TicketListings, error) {
input.applyDefaults()
err := input.Validate()
if err != nil {
if err := input.Validate(); err != nil {
return nil, fmt.Errorf("invalid input: %w", err)
}

// Iterate through feeds until have equal to or more ticket listings than desired
if err := c.setupProxy(); err != nil {
return nil, err
}

ticketListings := make(TicketListings, 0, input.MaxNumber)
earliestTicketTime := input.CreatedBefore
for (input.MaxNumber < 0 || len(ticketListings) < input.MaxNumber) &&
Expand Down Expand Up @@ -135,13 +142,24 @@ func (c *Client) FetchTicketListings(
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
}

// setupProxy sets up the proxy for the client if proxies are configured.
func (c *Client) setupProxy() error {
if c.proxies != nil && len(*c.proxies) > 0 {
proxyUrl, err := c.nextProxyUrl()
if err != nil {
return fmt.Errorf("failed to get next proxy URL: %w", err)
}
c.client.SetProxyURL(proxyUrl.String())
}
return nil
}

// FetchTicketListings gets ticket listings using the specified feel url.
func (c *Client) FetchTicketListingsByFeedUrl(
ctx context.Context,
Expand Down Expand Up @@ -175,10 +193,33 @@ func NewClient(apiKey string) (*Client, error) {
}

client := req.C().ImpersonateChrome()
return &Client{
client: client,
apiKey: apiKey,
}, nil

c := &Client{
client: client,
apiKey: apiKey,
proxies: nil,
mutex: &sync.Mutex{},
}

return c, nil
}

// NewClientWithProxies creates a new Twickets client with a list of proxies.
func NewClientWithProxies(apiKey string, proxies *[]Proxy) (*Client, error) {
c, err := NewClient(apiKey)
if err != nil {
return nil, err
}
c.proxies = proxies
return c, nil
}

func (c *Client) nextProxyUrl() (*url.URL, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
proxy := (*c.proxies)[c.nextProxy]
c.nextProxy = (c.nextProxy + 1) % len(*c.proxies)
return proxy.URL()
}

func sliceToMaxNumTicketListings(listings TicketListings, maxNumTicketListings int) TicketListings {
Expand Down
37 changes: 37 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"testing"

"github.com/ahobsonsayers/twigots"
Expand All @@ -14,6 +15,42 @@ import (
"github.com/stretchr/testify/require"
)

func TestClientWithProxy(t *testing.T) {
testutils.SkipIfCI(t)

projectDirectory := projectDirectory(t)
_ = godotenv.Load(filepath.Join(projectDirectory, ".env"))

twicketsAPIKey := os.Getenv("TWICKETS_API_KEY")
require.NotEmpty(t, twicketsAPIKey, "TWICKETS_API_KEY is not set")
proxyUser := os.Getenv("PROXY_USER")
require.NotEmpty(t, proxyUser, "PROXY_USER is not set")
proxyPassword := os.Getenv("PROXY_PASSWORD")
require.NotEmpty(t, proxyPassword, "PROXY_PASSWORD is not set")
proxyHost := os.Getenv("PROXY_HOST")
require.NotEmpty(t, proxyHost, "PROXY_HOST is not set")
proxyPort := os.Getenv("PROXY_PORT")
require.NotEmpty(t, proxyPort, "PROXY_PORT is not set")
proxyPortInt, err := strconv.Atoi(proxyPort)
require.NoError(t, err, "failed to convert PROXY_PORT to int")
proxy, err := twigots.NewProxy(proxyHost, proxyPortInt, proxyUser, proxyPassword)
require.NoError(t, err)
require.NoError(t, err)

twicketsClient, err := twigots.NewClientWithProxies(twicketsAPIKey, &[]twigots.Proxy{*proxy})
require.NoError(t, err)

listings, err := twicketsClient.FetchTicketListings(
context.Background(),
twigots.FetchTicketListingsInput{
Country: twigots.CountryUnitedKingdom,
MaxNumber: 10,
},
)
require.NoError(t, err)
spew.Dump(listings)
}

// TODO: Use httptest client

func TestGetLatestTicketListings(t *testing.T) {
Expand Down
100 changes: 100 additions & 0 deletions proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package twigots

import (
"errors"
"fmt"
"net"
"net/url"
"strconv"
"strings"
)

type Proxy struct {
Host string
Port int
User string
Password string
}

// NewProxy creates a new Proxy instance with the given host, port, user, and password.
func NewProxy(host string, port int, user, password string) (*Proxy, error) {
if host == "" {
return nil, errors.New("host is required")
}
if port <= 0 {
return nil, errors.New("port must be positive")
}
return &Proxy{
Host: host,
Port: port,
User: user,
Password: password,
}, nil
}

// GenerateProxyList creates a list of Proxy instances from a list of proxy URLs.
func GenerateProxyList(proxyHosts []string, user, password string) ([]Proxy, error) {
var proxyList []Proxy
for _, p := range proxyHosts {
if !strings.Contains(p, "://") {
p = "socks5://" + p
}
parsedUrl, err := url.Parse(p)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy URL '%s': %w", p, err)
}
if parsedUrl.Scheme != "socks5" {
return nil, fmt.Errorf("unsupported proxy scheme for '%s': %s", p, parsedUrl.Scheme)
}
host, port, err := net.SplitHostPort(parsedUrl.Host)
if err != nil {
return nil, fmt.Errorf("failed to split host and port for '%s': %w", p, err)
}
portNum, err := strconv.Atoi(port)
if err != nil {
return nil, fmt.Errorf("invalid port number for '%s': %w", p, err)
}
proxyList = append(proxyList, Proxy{
Host: host,
Port: portNum,
User: user,
Password: password,
})
}
return proxyList, nil
}

// String returns the proxy URL as a string.
func (p *Proxy) String() (string, error) {
parsedUrl, err := p.URL()
if err != nil {
return "", fmt.Errorf("failed to get proxy URL: %w", err)
}
return parsedUrl.String(), nil
}

// URL returns the proxy URL as a *url.URL instance.
func (p *Proxy) URL() (*url.URL, error) {
if p.Host == "" || p.Port <= 0 {
return nil, errors.New("host and port must be set for proxy URL")
}

var proxyUrl string

if strings.TrimSpace(p.User) == "" || strings.TrimSpace(p.Password) == "" {
proxyUrl = fmt.Sprintf("socks5://%s:%d", p.Host, p.Port)
} else {
proxyUrl = fmt.Sprintf(
"socks5://%s:%s@%s:%d",
p.User,
p.Password,
p.Host,
p.Port,
)
}
parsedUrl, err := url.Parse(proxyUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy URL: %w", err)
}
return parsedUrl, nil
}
Loading