Initialize Tinder API Wrapper with server configuration and Docker setup

This commit is contained in:
2025-03-20 22:19:48 +01:00
commit e99b56e434
17 changed files with 1459 additions and 0 deletions

94
README.md Normal file
View 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
View 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
View 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
View 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"]

View 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
View 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)
}
}

View 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)
}
}

View 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
View 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))
}

View 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
View 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
View 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
View 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
View 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)
}
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module tinder-api-wrapper
go 1.23.6

78
nixos-module.nix Normal file
View 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
View 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"`
}