- Rust 100%
| .vscode | ||
| examples | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
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
[dependencies]
grindr = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] }
serde_json = "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 grindr::{DeviceInfo, GrindrClient, Method};
#[tokio::main]
async fn main() -> Result<(), grindr::GrindrError> {
let device = DeviceInfo::generate();
let client = GrindrClient::new(device, None)?;
let me = client.login("m@example.com", "yourpassword").await?;
println!("logged in as profile {}", me.profile_id);
// URL must start with `/`
// Session token is added automatically
// API reference: <https://opengrind.org/grindr-api/>
// Dev tool: <https://git.opengrind.org/open-grind/grindr-api-dev-tool>
let resp = client
.request_authenticated_raw(Method::GET, "/v3/me/profile", None)
.await?;
println!("status {}", resp.status);
let profile: serde_json::Value = serde_json::from_slice(&resp.body).unwrap();
println!("{profile:#?}");
Ok(())
}
Sessions & device identity
// Load `device` and `saved` from disk
let client = GrindrClient::new(device, saved)?;
// Persist session whenever it changes
let mut sessions = client.session_receiver();
tokio::spawn(async move {
while sessions.changed().await.is_ok() {
// Option<Session>
let current = sessions.borrow().clone();
// Serialize to disk and store securely
save_session(¤t);
}
});
WebSocket
The client maintains a background WebSocket once a session exists. Subscribe to events and send commands:
use grindr::WsCommand;
// Subscribe to events
let mut events = client.ws_receiver();
tokio::spawn(async move {
while let Ok(event) = events.recv().await {
println!("event {}: {}", event.event_type, event.payload);
}
});
// Send a command
client
.ws_sender()
.send(WsCommand {
r#type: "chat.v1.typing".to_owned(),
ref_id: "1".to_owned(),
payload: serde_json::json!({ "conversationId": "abc" }),
})
.await
.ok();
The async background task starts on the first authenticated call. When you resume a stored session and want the socket up before issuing any request, call client.connect().await explicitly. 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 |
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() |
Start the background websocket now (e.g. on a resumed session) |
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)Method— re-exportedwreq::Methodforrequest_authenticated_raw
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:
cargo run --example warm_probe
Assert the emulated fingerprint:
cargo run --example fingerprint_check
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.