Skip to content

Commit

Permalink
Replace brittle subscription token heuristic with explicit args (#18)
Browse files Browse the repository at this point in the history
We support subscription tokens in two formats:
* A string pulled from the NYT-S cookie. This is the recommended path.
* A string pulled from the nyt-s HTTP header. While it appears that this
header is no longer used, it still works with the NYT API and my old
header-based token still works, so this is still supported.

Previously, we used a brittle heuristic based on string length to
determine whether a passed in token was a header or a cookie. This is
now broken, as cookies are not a set length.

Instead, force the user to specify via CLI arg what kind of token
they're passing in. Since most people are probably using a cookie at
this point, the old `-t` short arg is mapped to the cookie-based token.

Fixes #17
  • Loading branch information
kesyog authored Nov 13, 2024
1 parent af1b6f5 commit 66478c3
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "crossword"
version = "1.2.0"
version = "2.0.0"
authors = ["Kesavan Yogeswaran <[email protected]>"]
license = "Apache-2.0"
edition = "2021"
Expand Down
26 changes: 16 additions & 10 deletions src/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ pub struct SolvedPuzzleStats {
pub cheated: bool,
}

/// NYT subscription token
#[derive(Debug, Clone)]
pub enum SubscriptionToken {
/// Token extract from nyt-s HTTP header
Header(String),
/// Token extracted from NYT-S cookie
Cookie(String),
}

/// An HTTP client with a rate-limiting wrapper
#[derive(Debug, Clone)]
pub struct RateLimitedClient {
Expand All @@ -117,20 +126,17 @@ impl RateLimitedClient {
///
/// * `nyt_s` - NYT subscription token extracted from web browser
/// * `quota` - Outgoing request quota in requests per second
pub fn new(nyt_s: &str, quota: NonZeroU32) -> Self {
pub fn new(nyt_token: SubscriptionToken, quota: NonZeroU32) -> Self {
let mut headers = HeaderMap::new();
headers.insert(header::ACCEPT, "application/json".parse().unwrap());
headers.insert(header::DNT, "1".parse().unwrap());
if nyt_s.len() == 162 {
headers.insert(
match nyt_token {
SubscriptionToken::Cookie(cookie) => headers.insert(
header::COOKIE,
HeaderValue::from_str(&format!("NYT-S={}", nyt_s)).unwrap(),
);
} else if nyt_s.len() == 142 {
headers.insert("nyt-s", nyt_s.parse().unwrap());
} else {
panic!("NYT-S must be either 162 characters (for NYT-S cookie) or 142 characters (for nyt-s header)");
}
HeaderValue::from_str(&format!("NYT-S={}", cookie)).unwrap(),
),
SubscriptionToken::Header(header) => headers.insert("nyt-s", header.parse().unwrap()),
};

let client = reqwest::ClientBuilder::new()
.user_agent("Scraping personal stats")
Expand Down
30 changes: 24 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

use anyhow::Result;
use chrono::{naive::NaiveDate, Duration};
use clap::Parser;
use clap::{Args, Parser};
use core::num::NonZeroU32;
use crossword::api_client::RateLimitedClient;
use crossword::api_client::{RateLimitedClient, SubscriptionToken};
use crossword::database::Database;
use crossword::{logger, DAY_STEP};
use indicatif::{ProgressBar, ProgressStyle};
Expand All @@ -26,9 +26,8 @@ use tokio::sync::mpsc;

#[derive(Debug, Parser)]
struct Opt {
/// NYT subscription token extracted from web browser
#[arg(short = 't', long = "token", env = "NYT_S")]
nyt_token: String,
#[command(flatten)]
subscription_token: NytToken,

/// Earliest puzzle date to pull results from in YYYY-MM-DD format
#[arg(short, long, env = "NYT_XWORD_START")]
Expand All @@ -49,6 +48,18 @@ struct Opt {
db_path: PathBuf,
}

/// NYT subscription token extracted from web browser
#[derive(Args, Debug)]
#[group(required = true, multiple = false)]
struct NytToken {
/// NYT subscription token from nyt-s HTTP header
#[arg(long, env = "NYT_S_HEADER")]
nyt_header: Option<String>,
/// NYT subscription token from NYT-S cookie
#[arg(long, short = 't', env = "NYT_S_COOKIE")]
nyt_cookie: Option<String>,
}

#[tokio::main]
async fn main() -> Result<()> {
dotenv::dotenv().ok();
Expand Down Expand Up @@ -88,7 +99,14 @@ async fn main() -> Result<()> {
let (tx, rx) = mpsc::unbounded_channel();
let logger_handle = tokio::spawn(logger::task_fn(rx, stats_db, progress));

let client = RateLimitedClient::new(&opt.nyt_token, opt.request_quota);
let token = if let Some(header) = opt.subscription_token.nyt_header {
SubscriptionToken::Header(header)
} else if let Some(cookie) = opt.subscription_token.nyt_cookie {
SubscriptionToken::Cookie(cookie)
} else {
anyhow::bail!("No NYT subscription token provided");
};
let client = RateLimitedClient::new(token, opt.request_quota);

let ids_task = tokio::spawn(crossword::search::fetch_ids_and_stats(
client.clone(),
Expand Down

0 comments on commit 66478c3

Please sign in to comment.