grindr.rs
Unofficial async Rust client for the Grindr API, powering Open Grind client.
[!Important] This is an unofficial library, not affiliated with or endorsed by Grindr. It is provided for research and interoperability. Automating access may violate Grindr's Terms of Service. You are responsible for how you use it.
Features
- Async, clonable client built on
tokioandwreq - Fingerprint matching Grindr's official Android APK's network lib: TLS (JA3/JA4), HTTP/2 (frames, pseudoheaders), required headers
- Session handling — tokens are refreshed automatically
- Background WebSocket with automatic reconnect and states callback
- Device identities spoofing — store DeviceInfo along session token to decrease the chance of triggering Cloudflare block pages
This crate is a transport: it handles authentication, fingerprinting, connection, but does not ship typed models for every endpoint. You choose the path and deserialize the body yourself.
Installation
[]
= "0.1"
= { = "1", = ["macros", "rt-multi-thread", "sync"] }
= "1"
Versioning
This crate's version is <lib version>+<Grindr APK version>. For example, 0.1.0+26.9.1.163471 is library 0.1.0 targeting APK 26.9.1.163471. The +<apk> suffix is SemVer build metadata: informational only, and ignored by Cargo when resolving versions. Retargeting the APK is treated as a breaking change, so it bumps the minor, requiring manual upgrade. The targeted version is also exposed as grindr::APP_VERSION.
Quick start
use ;
async
Sessions & device identity
// Load `device` and `saved` from disk
let client = new?;
// Persist session whenever it changes
let mut sessions = client.session_receiver;
spawn;
WebSocket
The realtime WebSocket is opt-in, REST works without it. Call client.connect().await once to start the shared background socket, which then connects as soon as a session exists. Subscribe to events and send commands:
use WsCommand;
// Subscribe to events
let mut events = client.ws_receiver;
spawn;
// Send a command
client
.ws_sender
.send
.await
.ok;
The background task is never started for you. client.connect().await is the only thing that starts it, and it's idempotent and shared across clones. Until you call it, no socket is opened and ws_receiver() produces nothing. Watch the connection with GrindrClient::connection_state, and observe failed background token refreshes via GrindrClient::auth_event_receiver.
API reference
Full generated docs: https://docs.rs/grindr.
GrindrClient
| Method | Description |
|---|---|
new(device, session) -> Result<Self> |
Create a client, optionally resuming a stored Session |
login(email, password) -> Result<LoginResult> |
Email + password login |
google_sign_in(access_token) -> Result<LoginResult> |
Google OAuth sign-in |
refresh_token() -> Result<LoginResult> |
Force token refresh |
logout() |
Clear the session and disconnect the websocket |
request_authenticated_raw(method, path, body) -> Result<RawResponse> |
Authenticated API call returning the raw status + body |
request_authenticated_bytes(method, path, content_type, body) -> Result<RawResponse> |
Like request_authenticated_raw, but with a raw binary body (e.g. media uploads) |
rotate_device(device) -> Result<DeviceInfo> |
Set the device identity in place, keeping the session; returns old device info |
current_device() -> DeviceInfo |
Get the device identity currently in use |
recaptcha_first_party_enabled() -> Result<bool> |
Check whether first-party reCAPTCHA is enabled |
session_receiver() -> watch::Receiver<Option<Session>> |
Watch the current session (updates on login/refresh/logout) |
connection_state() -> watch::Receiver<WsConnectionState> |
Watch the websocket connection state |
ws_receiver() -> broadcast::Receiver<WsEvent> |
Subscribe to websocket events |
ws_sender() -> mpsc::Sender<WsCommand> |
Get websocket sender for commands |
connect() |
Opt in to the realtime websocket and start the shared background task |
auth_event_receiver() -> broadcast::Receiver<AuthEvent> |
Subscribe to background token refresh failures |
Types
DeviceInfo— device identity, build withDeviceInfo::generate()orDeviceInfo::default()Session— session token and other secretsSessionKind—EmailorGoogleLoginResult—{ profile_id }, returned by the auth methodsRawResponse—{ status: u16, body: Vec<u8> }WsCommand— websocket command{ type, ref_id, payload }WsEvent— websocket event{ event_type, payload }WsConnectionState—Connected/DisconnectedAuthEvent—{ message, unauthorized }from background refreshesGrindrError— the crate error type (Http,Auth,Api,Unauthorized,InvalidRequest);GrindrError::from_response(status, body)maps a non-successRawResponsethe same way the typed methods doMethod— re-exportedwreq::Methodforrequest_authenticated_rawBytes— re-exportedbytes::Bytesforrequest_authenticated_bytes
Low-level helpers
For building your own wreq::Client with an identical fingerprint:
probe_emulation() -> wreq::EmulationProvider— tls/http2 emulation profilebuild_user_agent(device, tier) -> String—User-Agentvaluebuild_device_info_header(device) -> String—L-Device-InfovalueGrindrHeaders::build(device, ua, authorization, roles)— full and correctly ordered headers list
Examples
Observe TLS session resumption:
Assert the emulated fingerprint:
The fingerprint_check example verifies JA3/JA4, the Akamai http/2 fingerprint and header ordering against tls.peet.ws. Pass --all flag to check both http/2 and http/1.1 (websocket) clients.
Minimum supported Rust version
Rust 1.80.