Initialize Tinder API Wrapper with server configuration and Docker setup
This commit is contained in:
94
README.md
Normal file
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Tinder API Wrapper
|
||||||
|
|
||||||
|
A Go wrapper for the Tinder API that provides easy access to basic Tinder functionality such as liking/passing users and getting recommendations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Simple, idiomatic Go API
|
||||||
|
- Support for Tinder's core endpoints:
|
||||||
|
- Like a user
|
||||||
|
- Pass on a user
|
||||||
|
- Get user recommendations
|
||||||
|
- Configurable API endpoint
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get tinder-api-wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
tinder "tinder-api-wrapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create a new client with the desired API endpoint
|
||||||
|
client, err := tinder.NewClient("https://tinder.cloonar.com")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recommendations
|
||||||
|
recs, err := client.GetRecommendations("en")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get recommendations: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like a user
|
||||||
|
likeResp, err := client.LikeUser("user123")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to like user: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Match: %v, Likes remaining: %d\n", likeResp.Match, likeResp.LikesRemaining)
|
||||||
|
|
||||||
|
// Pass on a user
|
||||||
|
passResp, err := client.PassUser("user456")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to pass on user: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Status: %s\n", passResp.Status)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See the `example` directory for more detailed usage examples.
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### NewClient(endpoint string) (*Client, error)
|
||||||
|
|
||||||
|
Creates a new Tinder API client with the specified API endpoint.
|
||||||
|
|
||||||
|
### (c *Client) LikeUser(userID string) (*LikeResponse, error)
|
||||||
|
|
||||||
|
Likes a user with the given ID.
|
||||||
|
|
||||||
|
### (c *Client) PassUser(userID string) (*PassResponse, error)
|
||||||
|
|
||||||
|
Passes on (dislikes) a user with the given ID.
|
||||||
|
|
||||||
|
### (c *Client) GetRecommendations(locale string) (*RecsResponse, error)
|
||||||
|
|
||||||
|
Retrieves a list of recommended user profiles. The `locale` parameter is optional and can be an empty string.
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
The wrapper includes the following main types for API responses:
|
||||||
|
|
||||||
|
- `LikeResponse`: Response from liking a user
|
||||||
|
- `PassResponse`: Response from passing on a user
|
||||||
|
- `RecsResponse`: Recommendations response containing user profiles
|
||||||
|
- `UserRecommendation`: Detailed user profile information
|
||||||
|
- `Photo`: User photo information
|
||||||
|
- `InstagramPhoto`: Instagram photo information
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is available under the MIT License.
|
||||||
127
auth.go
Normal file
127
auth.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package tinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthRequest represents the request body for authentication
|
||||||
|
type AuthRequest struct {
|
||||||
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
FacebookID string `json:"facebook_id,omitempty"`
|
||||||
|
FacebookToken string `json:"facebook_token,omitempty"`
|
||||||
|
OTP string `json:"otp,omitempty"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResponse represents the response from authentication endpoints
|
||||||
|
type AuthResponse struct {
|
||||||
|
Meta struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
} `json:"meta"`
|
||||||
|
Data struct {
|
||||||
|
APIToken string `json:"api_token,omitempty"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
IsNewUser bool `json:"is_new_user,omitempty"`
|
||||||
|
SMSSent bool `json:"sms_sent,omitempty"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileResponse represents the response from the profile endpoint
|
||||||
|
type ProfileResponse struct {
|
||||||
|
Meta struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"meta"`
|
||||||
|
Data struct {
|
||||||
|
User struct {
|
||||||
|
ID string `json:"_id"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"user"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendPhoneLogin initiates a phone login by requesting an OTP code
|
||||||
|
func (c *Client) SendPhoneLogin(phoneNumber string) (*AuthResponse, error) {
|
||||||
|
authReq := AuthRequest{
|
||||||
|
PhoneNumber: phoneNumber,
|
||||||
|
}
|
||||||
|
return c.doAuthRequest("/v2/auth/sms/send", authReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginWithOTP completes the phone login using the provided OTP code
|
||||||
|
func (c *Client) LoginWithOTP(phoneNumber, otp string) (*AuthResponse, error) {
|
||||||
|
authReq := AuthRequest{
|
||||||
|
PhoneNumber: phoneNumber,
|
||||||
|
OTP: otp,
|
||||||
|
}
|
||||||
|
return c.doAuthRequest("/v2/auth/sms/validate", authReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginWithFacebook authenticates using a Facebook ID and token
|
||||||
|
func (c *Client) LoginWithFacebook(facebookID, facebookToken string) (*AuthResponse, error) {
|
||||||
|
authReq := AuthRequest{
|
||||||
|
FacebookID: facebookID,
|
||||||
|
FacebookToken: facebookToken,
|
||||||
|
}
|
||||||
|
return c.doAuthRequest("/v2/auth/facebook", authReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshAuth refreshes the authentication token
|
||||||
|
func (c *Client) RefreshAuth(refreshToken string) (*AuthResponse, error) {
|
||||||
|
authReq := AuthRequest{
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
}
|
||||||
|
return c.doAuthRequest("/v2/auth/login/refresh", authReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProfile retrieves the current user's profile
|
||||||
|
func (c *Client) GetProfile() (*ProfileResponse, error) {
|
||||||
|
resp, err := c.doRequest(http.MethodGet, "/v2/profile", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("API error: %s (status code: %d)", body, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var profileResp ProfileResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&profileResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &profileResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// doAuthRequest performs an authentication request and handles common auth response processing
|
||||||
|
func (c *Client) doAuthRequest(path string, authReq AuthRequest) (*AuthResponse, error) {
|
||||||
|
resp, err := c.doRequest(http.MethodPost, path, authReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("API error: %s (status code: %d)", body, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var authResp AuthResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the auth token if it was returned
|
||||||
|
if authResp.Data.APIToken != "" {
|
||||||
|
c.authToken = authResp.Data.APIToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return &authResp, nil
|
||||||
|
}
|
||||||
143
client.go
Normal file
143
client.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package tinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client represents a Tinder API client
|
||||||
|
type Client struct {
|
||||||
|
baseURL *url.URL
|
||||||
|
httpClient *http.Client
|
||||||
|
authToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Tinder API client with the specified endpoint
|
||||||
|
func NewClient(endpoint string) (*Client, error) {
|
||||||
|
baseURL, err := url.Parse(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid API endpoint: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
baseURL: baseURL,
|
||||||
|
httpClient: &http.Client{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthToken sets the authentication token for the client
|
||||||
|
func (c *Client) WithAuthToken(token string) *Client {
|
||||||
|
c.authToken = token
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRequest performs an HTTP request with proper authentication headers
|
||||||
|
func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) {
|
||||||
|
endpoint := c.baseURL.ResolveReference(&url.URL{Path: path})
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if body != nil {
|
||||||
|
jsonData, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request body: %v", err)
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest(method, endpoint.String(), bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
} else {
|
||||||
|
req, err = http.NewRequest(method, endpoint.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.authToken != "" {
|
||||||
|
req.Header.Set("X-Auth-Token", c.authToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("API request failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LikeUser sends a like action to a specific user
|
||||||
|
func (c *Client) LikeUser(userID string) (*LikeResponse, error) {
|
||||||
|
resp, err := c.doRequest(http.MethodGet, fmt.Sprintf("/like/%s", userID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("API error: %s (status code: %d)", body, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var likeResp LikeResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&likeResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &likeResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassUser sends a pass action for a specific user
|
||||||
|
func (c *Client) PassUser(userID string) (*PassResponse, error) {
|
||||||
|
resp, err := c.doRequest(http.MethodGet, fmt.Sprintf("/pass/%s", userID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("API error: %s (status code: %d)", body, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var passResp PassResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&passResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &passResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecommendations retrieves a list of recommended user profiles
|
||||||
|
func (c *Client) GetRecommendations(locale string) (*RecsResponse, error) {
|
||||||
|
endpoint := c.baseURL.ResolveReference(&url.URL{Path: "/v2/recs/core"})
|
||||||
|
|
||||||
|
if locale != "" {
|
||||||
|
query := endpoint.Query()
|
||||||
|
query.Set("locale", locale)
|
||||||
|
endpoint.RawQuery = query.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.doRequest(http.MethodGet, endpoint.Path+"?"+endpoint.RawQuery, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("API error: %s (status code: %d)", body, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var recsResp RecsResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&recsResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &recsResp, nil
|
||||||
|
}
|
||||||
25
cmd/server/Dockerfile
Normal file
25
cmd/server/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM golang:1.18-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go mod files
|
||||||
|
COPY go.mod ./
|
||||||
|
COPY go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN go build -o /tinder-proxy ./cmd/server
|
||||||
|
|
||||||
|
# Use a smaller image for the final container
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /tinder-proxy /tinder-proxy
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/tinder-proxy"]
|
||||||
15
cmd/server/config/config.go
Normal file
15
cmd/server/config/config.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// Config holds the server configuration
|
||||||
|
type Config struct {
|
||||||
|
ListenAddr string
|
||||||
|
TargetAPI string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Config with default values
|
||||||
|
func New() *Config {
|
||||||
|
return &Config{
|
||||||
|
ListenAddr: ":8080",
|
||||||
|
TargetAPI: "https://tinder.cloonar.com",
|
||||||
|
}
|
||||||
|
}
|
||||||
139
cmd/server/handlers/auth.go
Normal file
139
cmd/server/handlers/auth.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
tinder "tinder-api-wrapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleSendPhoneAuth handles the phone authentication request
|
||||||
|
func HandleSendPhoneAuth(client *tinder.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var authReq struct {
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &authReq); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.SendPhoneLogin(authReq.PhoneNumber)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleValidateOTP handles the OTP validation request
|
||||||
|
func HandleValidateOTP(client *tinder.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var authReq struct {
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
OTP string `json:"otp"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &authReq); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.LoginWithOTP(authReq.PhoneNumber, authReq.OTP)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFacebookAuth handles Facebook authentication
|
||||||
|
func HandleFacebookAuth(client *tinder.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var authReq struct {
|
||||||
|
FacebookID string `json:"facebook_id"`
|
||||||
|
FacebookToken string `json:"facebook_token"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &authReq); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.LoginWithFacebook(authReq.FacebookID, authReq.FacebookToken)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRefreshAuth handles authentication token refresh
|
||||||
|
func HandleRefreshAuth(client *tinder.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var authReq struct {
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
if err := readJSON(r, &authReq); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.RefreshAuth(authReq.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetProfile handles profile retrieval
|
||||||
|
func HandleGetProfile(client *tinder.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authToken := r.Header.Get("X-Auth-Token")
|
||||||
|
if authToken == "" {
|
||||||
|
http.Error(w, "Missing authentication token", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientWithAuth := client.WithAuthToken(authToken)
|
||||||
|
resp, err := clientWithAuth.GetProfile()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
cmd/server/handlers/core.go
Normal file
76
cmd/server/handlers/core.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tinder "tinder-api-wrapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleLike handles the /like/{userId} endpoint
|
||||||
|
func HandleLike(client *tinder.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, "/like/")
|
||||||
|
if path == "" {
|
||||||
|
http.Error(w, "User ID is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.LikeUser(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePass handles the /pass/{userId} endpoint
|
||||||
|
func HandlePass(client *tinder.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, "/pass/")
|
||||||
|
if path == "" {
|
||||||
|
http.Error(w, "User ID is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.PassUser(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetRecs handles the /v2/recs/core endpoint
|
||||||
|
func HandleGetRecs(client *tinder.Client) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
locale := r.URL.Query().Get("locale")
|
||||||
|
resp, err := client.GetRecommendations(locale)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("API error: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
37
cmd/server/handlers/utils.go
Normal file
37
cmd/server/handlers/utils.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readJSON reads and decodes JSON from request body
|
||||||
|
func readJSON(r *http.Request, v interface{}) error {
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read request body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, v); err != nil {
|
||||||
|
return fmt.Errorf("invalid request format: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSON writes JSON response with proper headers
|
||||||
|
func writeJSON(w http.ResponseWriter, v interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogMiddleware logs all HTTP requests
|
||||||
|
func LogMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("%s %s", r.Method, r.URL.Path)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
35
cmd/server/main.go
Normal file
35
cmd/server/main.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
tinder "tinder-api-wrapper"
|
||||||
|
"tinder-api-wrapper/cmd/server/config"
|
||||||
|
"tinder-api-wrapper/cmd/server/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize configuration
|
||||||
|
cfg := config.New()
|
||||||
|
|
||||||
|
// Parse command line flags
|
||||||
|
flag.StringVar(&cfg.ListenAddr, "listen", cfg.ListenAddr, "Address to listen on (e.g., :8080)")
|
||||||
|
flag.StringVar(&cfg.TargetAPI, "target", cfg.TargetAPI, "Target Tinder API endpoint")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Create Tinder client
|
||||||
|
client, err := tinder.NewClient(cfg.TargetAPI)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create Tinder client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup router
|
||||||
|
handler := router.Setup(client)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
fmt.Printf("Starting Tinder API proxy server on %s -> %s\n", cfg.ListenAddr, cfg.TargetAPI)
|
||||||
|
log.Fatal(http.ListenAndServe(cfg.ListenAddr, handler))
|
||||||
|
}
|
||||||
28
cmd/server/router/router.go
Normal file
28
cmd/server/router/router.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
tinder "tinder-api-wrapper"
|
||||||
|
"tinder-api-wrapper/cmd/server/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup creates and configures the HTTP router
|
||||||
|
func Setup(client *tinder.Client) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Core endpoints
|
||||||
|
mux.HandleFunc("/like/", handlers.HandleLike(client))
|
||||||
|
mux.HandleFunc("/pass/", handlers.HandlePass(client))
|
||||||
|
mux.HandleFunc("/v2/recs/core", handlers.HandleGetRecs(client))
|
||||||
|
|
||||||
|
// Auth endpoints
|
||||||
|
mux.HandleFunc("/v2/auth/sms/send", handlers.HandleSendPhoneAuth(client))
|
||||||
|
mux.HandleFunc("/v2/auth/sms/validate", handlers.HandleValidateOTP(client))
|
||||||
|
mux.HandleFunc("/v2/auth/facebook", handlers.HandleFacebookAuth(client))
|
||||||
|
mux.HandleFunc("/v2/auth/login/refresh", handlers.HandleRefreshAuth(client))
|
||||||
|
mux.HandleFunc("/v2/profile", handlers.HandleGetProfile(client))
|
||||||
|
|
||||||
|
// Wrap with logging middleware
|
||||||
|
return handlers.LogMiddleware(mux)
|
||||||
|
}
|
||||||
21
default.nix
Normal file
21
default.nix
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{ lib, buildGoModule, fetchGit }:
|
||||||
|
|
||||||
|
buildGoModule rec {
|
||||||
|
pname = "tinder-api-wrapper";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
src = fetchGit {
|
||||||
|
url = "ssh://git@your-gitea-server.com/owner/tinder-api-wrapper.git";
|
||||||
|
ref = "main"; # Or specific branch/tag
|
||||||
|
# rev = "specific-commit-hash"; # Optionally pin to specific commit
|
||||||
|
};
|
||||||
|
|
||||||
|
vendorSha256 = null; # Will be replaced with actual hash on first build
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Tinder API Wrapper Service";
|
||||||
|
homepage = "https://your-gitea-server.com/owner/tinder-api-wrapper";
|
||||||
|
license = licenses.mit;
|
||||||
|
maintainers = with maintainers; [ /* add maintainers */ ];
|
||||||
|
};
|
||||||
|
}
|
||||||
472
docs/api.yaml
Normal file
472
docs/api.yaml
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Tinder API
|
||||||
|
description: >
|
||||||
|
This is an OpenAPI specification for a subset of the Tinder API endpoints.
|
||||||
|
It covers basic functionality including authentication, user actions (like/pass),
|
||||||
|
and recommendations.
|
||||||
|
version: "1.0.0"
|
||||||
|
servers:
|
||||||
|
- url: https://tinder.cloonar.com
|
||||||
|
paths:
|
||||||
|
/like/{user_id}:
|
||||||
|
get:
|
||||||
|
operationId: likeUser
|
||||||
|
summary: Like a user
|
||||||
|
description: >
|
||||||
|
Call this endpoint to "like" a user. In the Tinder app the like action is performed
|
||||||
|
when you swipe right or tap the heart.
|
||||||
|
parameters:
|
||||||
|
- name: user_id
|
||||||
|
in: path
|
||||||
|
description: The Tinder user ID of the profile to like.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful like response.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
match:
|
||||||
|
type: boolean
|
||||||
|
likes_remaining:
|
||||||
|
type: integer
|
||||||
|
X-Padding:
|
||||||
|
type: string
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
/pass/{user_id}:
|
||||||
|
get:
|
||||||
|
operationId: passUser
|
||||||
|
summary: Pass on a user
|
||||||
|
description: >
|
||||||
|
Call this endpoint to “pass” (dislike) a user. In the Tinder app this action happens
|
||||||
|
when you swipe left or tap the red X.
|
||||||
|
parameters:
|
||||||
|
- name: user_id
|
||||||
|
in: path
|
||||||
|
description: The Tinder user ID of the profile to pass.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful pass response.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: A status message or code.
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
/v2/recs/core:
|
||||||
|
get:
|
||||||
|
operationId: getRecsCore
|
||||||
|
summary: Get Recommendations
|
||||||
|
description: >
|
||||||
|
Retrieves a list of recommended Tinder profiles. An optional locale query parameter can be provided.
|
||||||
|
parameters:
|
||||||
|
- name: locale
|
||||||
|
in: query
|
||||||
|
description: Locale for recommendations (e.g. "en").
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A list of recommended profiles.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
results:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/UserRecommendation'
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
/v2/auth/sms/send:
|
||||||
|
post:
|
||||||
|
operationId: sendPhoneLogin
|
||||||
|
summary: Send OTP code
|
||||||
|
description: Initiate phone authentication by requesting an OTP code
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
phone_number:
|
||||||
|
type: string
|
||||||
|
description: The phone number to send the OTP to
|
||||||
|
required:
|
||||||
|
- phone_number
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: SMS sent successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthResponse'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/Error'
|
||||||
|
|
||||||
|
/v2/auth/sms/validate:
|
||||||
|
post:
|
||||||
|
operationId: validateOTP
|
||||||
|
summary: Validate OTP
|
||||||
|
description: Complete phone authentication by validating the OTP code
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
phone_number:
|
||||||
|
type: string
|
||||||
|
description: The phone number the OTP was sent to
|
||||||
|
otp:
|
||||||
|
type: string
|
||||||
|
description: The OTP code received via SMS
|
||||||
|
required:
|
||||||
|
- phone_number
|
||||||
|
- otp
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OTP validated successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthResponse'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/Error'
|
||||||
|
|
||||||
|
/v2/auth/facebook:
|
||||||
|
post:
|
||||||
|
operationId: facebookAuth
|
||||||
|
summary: Facebook Authentication
|
||||||
|
description: Authenticate using Facebook credentials
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
facebook_id:
|
||||||
|
type: string
|
||||||
|
description: Facebook user ID
|
||||||
|
facebook_token:
|
||||||
|
type: string
|
||||||
|
description: Facebook access token
|
||||||
|
required:
|
||||||
|
- facebook_id
|
||||||
|
- facebook_token
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Facebook authentication successful
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthResponse'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/Error'
|
||||||
|
|
||||||
|
/v2/auth/login/refresh:
|
||||||
|
post:
|
||||||
|
operationId: refreshAuth
|
||||||
|
summary: Refresh Authentication
|
||||||
|
description: Refresh the authentication token using a refresh token
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
refresh_token:
|
||||||
|
type: string
|
||||||
|
description: The refresh token obtained from a previous authentication
|
||||||
|
required:
|
||||||
|
- refresh_token
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Token refreshed successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthResponse'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/Error'
|
||||||
|
|
||||||
|
/v2/profile:
|
||||||
|
get:
|
||||||
|
operationId: getProfile
|
||||||
|
summary: Get User Profile
|
||||||
|
description: Retrieve the authenticated user's profile information
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Profile retrieved successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ProfileResponse'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/Error'
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
UserRecommendation:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
user:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
_id:
|
||||||
|
type: string
|
||||||
|
bio:
|
||||||
|
type: string
|
||||||
|
birth_date:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
photos:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Photo'
|
||||||
|
gender:
|
||||||
|
type: integer
|
||||||
|
jobs:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
schools:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
instagram:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
last_fetch_time:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
completed_initial_fetch:
|
||||||
|
type: boolean
|
||||||
|
photos:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/InstagramPhoto'
|
||||||
|
media_count:
|
||||||
|
type: integer
|
||||||
|
profile_picture:
|
||||||
|
type: string
|
||||||
|
facebook:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
common_connections:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
connection_count:
|
||||||
|
type: integer
|
||||||
|
common_interests:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
spotify:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
spotify_connected:
|
||||||
|
type: boolean
|
||||||
|
spotify_theme_track:
|
||||||
|
type: object
|
||||||
|
description: Track info (partial schema – extend as needed)
|
||||||
|
distance_mi:
|
||||||
|
type: integer
|
||||||
|
content_hash:
|
||||||
|
type: string
|
||||||
|
s_number:
|
||||||
|
type: integer
|
||||||
|
teaser:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
string:
|
||||||
|
type: string
|
||||||
|
teasers:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
string:
|
||||||
|
type: string
|
||||||
|
Photo:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
crop_info:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
width_pct:
|
||||||
|
type: number
|
||||||
|
x_offset_pct:
|
||||||
|
type: number
|
||||||
|
height_pct:
|
||||||
|
type: number
|
||||||
|
y_offset_pct:
|
||||||
|
type: number
|
||||||
|
algo:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
width_pct:
|
||||||
|
type: number
|
||||||
|
x_offset_pct:
|
||||||
|
type: number
|
||||||
|
height_pct:
|
||||||
|
type: number
|
||||||
|
y_offset_pct:
|
||||||
|
type: number
|
||||||
|
processed_by_bullseye:
|
||||||
|
type: boolean
|
||||||
|
user_customized:
|
||||||
|
type: boolean
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
processedFiles:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
height:
|
||||||
|
type: integer
|
||||||
|
width:
|
||||||
|
type: integer
|
||||||
|
fileName:
|
||||||
|
type: string
|
||||||
|
extension:
|
||||||
|
type: string
|
||||||
|
main:
|
||||||
|
type: boolean
|
||||||
|
InstagramPhoto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
image:
|
||||||
|
type: string
|
||||||
|
thumbnail:
|
||||||
|
type: string
|
||||||
|
ts:
|
||||||
|
type: string
|
||||||
|
link:
|
||||||
|
type: string
|
||||||
|
AuthResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
api_token:
|
||||||
|
type: string
|
||||||
|
description: The authentication token to use for subsequent requests
|
||||||
|
refresh_token:
|
||||||
|
type: string
|
||||||
|
description: Token that can be used to refresh the authentication
|
||||||
|
is_new_user:
|
||||||
|
type: boolean
|
||||||
|
description: Whether this is a new user account
|
||||||
|
sms_sent:
|
||||||
|
type: boolean
|
||||||
|
description: Whether an SMS was sent (for phone authentication)
|
||||||
|
|
||||||
|
ProfileResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
_id:
|
||||||
|
type: string
|
||||||
|
bio:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
responses:
|
||||||
|
Error:
|
||||||
|
description: Error response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
securitySchemes:
|
||||||
|
BearerAuth:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-Auth-Token
|
||||||
19
example-configuration.nix
Normal file
19
example-configuration.nix
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Example NixOS configuration
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./nixos-module.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
services.tinder-api-wrapper = {
|
||||||
|
enable = true;
|
||||||
|
port = 8080; # default port
|
||||||
|
# Optionally override user/group if needed
|
||||||
|
# user = "custom-user";
|
||||||
|
# group = "custom-group";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Open firewall port
|
||||||
|
networking.firewall.allowedTCPPorts = [ 8080 ];
|
||||||
|
}
|
||||||
56
example/example.go
Normal file
56
example/example.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
tinder "tinder-api-wrapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create a new Tinder client with the API endpoint
|
||||||
|
client, err := tinder.NewClient("https://tinder.cloonar.com")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create Tinder client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Get recommendations
|
||||||
|
fmt.Println("Getting recommendations...")
|
||||||
|
recs, err := client.GetRecommendations("en")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get recommendations: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print out the recommendations
|
||||||
|
fmt.Printf("Found %d recommendations\n", len(recs.Data.Results))
|
||||||
|
for i, rec := range recs.Data.Results {
|
||||||
|
fmt.Printf("%d. %s, %s\n", i+1, rec.User.Name, rec.User.Bio)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Like a user
|
||||||
|
if len(recs.Data.Results) > 0 {
|
||||||
|
userID := recs.Data.Results[0].User.ID
|
||||||
|
fmt.Printf("Liking user %s...\n", userID)
|
||||||
|
|
||||||
|
likeResp, err := client.LikeUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to like user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Like response: Match=%v, LikesRemaining=%d\n",
|
||||||
|
likeResp.Match, likeResp.LikesRemaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Pass on a user
|
||||||
|
if len(recs.Data.Results) > 1 {
|
||||||
|
userID := recs.Data.Results[1].User.ID
|
||||||
|
fmt.Printf("Passing on user %s...\n", userID)
|
||||||
|
|
||||||
|
passResp, err := client.PassUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to pass on user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Pass response: Status=%s\n", passResp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
78
nixos-module.nix
Normal file
78
nixos-module.nix
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.tinder-api-wrapper;
|
||||||
|
in {
|
||||||
|
options.services.tinder-api-wrapper = with lib; {
|
||||||
|
enable = mkEnableOption "Tinder API wrapper service";
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8080;
|
||||||
|
description = "Port to listen on";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "tinder-api";
|
||||||
|
description = "User account to run service.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "tinder-api";
|
||||||
|
description = "Group account to run service.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
nixpkgs.overlays = [
|
||||||
|
(self: super: {
|
||||||
|
tinder-api-wrapper = self.callPackage ./default.nix {};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
description = "Tinder API wrapper service user";
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
|
||||||
|
systemd.services.tinder-api-wrapper = {
|
||||||
|
description = "Tinder API Wrapper Service";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
ExecStart = "${pkgs.tinder-api-wrapper}/bin/server -port ${toString cfg.port}";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "10";
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RemoveIPC = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
91
types.go
Normal file
91
types.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package tinder
|
||||||
|
|
||||||
|
// LikeResponse contains the response data from a like action
|
||||||
|
type LikeResponse struct {
|
||||||
|
Match bool `json:"match"`
|
||||||
|
LikesRemaining int `json:"likes_remaining"`
|
||||||
|
XPadding string `json:"X-Padding,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassResponse contains the response data from a pass action
|
||||||
|
type PassResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecsResponse contains recommendation data
|
||||||
|
type RecsResponse struct {
|
||||||
|
Meta struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"meta"`
|
||||||
|
Data struct {
|
||||||
|
Results []UserRecommendation `json:"results"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserRecommendation represents a recommended user profile
|
||||||
|
type UserRecommendation struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
DistanceMi int `json:"distance_mi"`
|
||||||
|
ContentHash string `json:"content_hash"`
|
||||||
|
SNumber int `json:"s_number"`
|
||||||
|
User struct {
|
||||||
|
ID string `json:"_id"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
|
BirthDate string `json:"birth_date"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Photos []Photo `json:"photos"`
|
||||||
|
Gender int `json:"gender"`
|
||||||
|
Jobs []any `json:"jobs"`
|
||||||
|
Schools []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"schools"`
|
||||||
|
} `json:"user"`
|
||||||
|
Instagram struct {
|
||||||
|
LastFetchTime string `json:"last_fetch_time"`
|
||||||
|
CompletedInitialFetch bool `json:"completed_initial_fetch"`
|
||||||
|
Photos []InstagramPhoto `json:"photos"`
|
||||||
|
MediaCount int `json:"media_count"`
|
||||||
|
ProfilePicture string `json:"profile_picture"`
|
||||||
|
} `json:"instagram"`
|
||||||
|
Spotify struct {
|
||||||
|
SpotifyConnected bool `json:"spotify_connected"`
|
||||||
|
SpotifyThemeTrack any `json:"spotify_theme_track"`
|
||||||
|
} `json:"spotify"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photo represents a user photo
|
||||||
|
type Photo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
FileName string `json:"fileName"`
|
||||||
|
Extension string `json:"extension"`
|
||||||
|
Main bool `json:"main"`
|
||||||
|
ProcessedFiles []struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
} `json:"processedFiles"`
|
||||||
|
CropInfo struct {
|
||||||
|
User struct {
|
||||||
|
WidthPct float64 `json:"width_pct"`
|
||||||
|
XOffsetPct float64 `json:"x_offset_pct"`
|
||||||
|
HeightPct float64 `json:"height_pct"`
|
||||||
|
YOffsetPct float64 `json:"y_offset_pct"`
|
||||||
|
} `json:"user"`
|
||||||
|
ProcessedByBullseye bool `json:"processed_by_bullseye"`
|
||||||
|
UserCustomized bool `json:"user_customized"`
|
||||||
|
} `json:"crop_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstagramPhoto represents a photo from Instagram
|
||||||
|
type InstagramPhoto struct {
|
||||||
|
Image string `json:"image"`
|
||||||
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
Ts string `json:"ts"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error represents an API error
|
||||||
|
type Error struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user