Skip to content

Commit

Permalink
Fetch crumb for quote data
Browse files Browse the repository at this point in the history
  • Loading branch information
tarkah committed Jul 25, 2023
1 parent ee71125 commit 50f6149
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 29 deletions.
75 changes: 64 additions & 11 deletions api/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ use std::collections::HashMap;

use anyhow::{bail, Context, Result};
use futures::AsyncReadExt;
use http::Uri;
use isahc::HttpClient;
use http::{header, Request, Uri};
use isahc::{AsyncReadResponseExt, HttpClient};
use serde::de::DeserializeOwned;

use crate::model::{Chart, ChartData, Company, CompanyData, Options, OptionsHeader};
use crate::model::{Chart, ChartData, Company, CompanyData, CrumbData, Options, OptionsHeader};
use crate::{Interval, Range};

#[derive(Debug)]
Expand Down Expand Up @@ -36,10 +36,16 @@ impl Client {
}
}

async fn get<T: DeserializeOwned>(&self, url: Uri) -> Result<T> {
async fn get<T: DeserializeOwned>(&self, url: Uri, cookie: Option<String>) -> Result<T> {
let mut req = Request::builder().method(http::Method::GET).uri(url);

if let Some(cookie) = cookie {
req = req.header(header::COOKIE, cookie);
}

let res = self
.client
.get_async(url)
.send_async(req.body(())?)
.await
.context("Failed to get request")?;

Expand Down Expand Up @@ -73,7 +79,7 @@ impl Client {
Some(params),
)?;

let response: Chart = self.get(url).await?;
let response: Chart = self.get(url, None).await?;

if let Some(err) = response.chart.error {
bail!(
Expand All @@ -92,17 +98,22 @@ impl Client {
bail!("Failed to get chart data for {}", symbol);
}

pub async fn get_company_data(&self, symbol: &str) -> Result<CompanyData> {
pub async fn get_company_data(
&self,
symbol: &str,
crumb_data: CrumbData,
) -> Result<CompanyData> {
let mut params = HashMap::new();
params.insert("modules", "price,assetProfile".to_string());
params.insert("crumb", crumb_data.crumb);

let url = self.get_url(
Version::V10,
&format!("finance/quoteSummary/{}", symbol),
Some(params),
)?;

let response: Company = self.get(url).await?;
let response: Company = self.get(url, Some(crumb_data.cookie)).await?;

if let Some(err) = response.company.error {
bail!(
Expand All @@ -124,7 +135,7 @@ impl Client {
pub async fn get_options_expiration_dates(&self, symbol: &str) -> Result<Vec<i64>> {
let url = self.get_url(Version::V7, &format!("finance/options/{}", symbol), None)?;

let response: Options = self.get(url).await?;
let response: Options = self.get(url, None).await?;

if let Some(err) = response.option_chain.error {
bail!(
Expand Down Expand Up @@ -158,7 +169,7 @@ impl Client {
Some(params),
)?;

let response: Options = self.get(url).await?;
let response: Options = self.get(url, None).await?;

if let Some(err) = response.option_chain.error {
bail!(
Expand All @@ -178,6 +189,38 @@ impl Client {

bail!("Failed to get options data for {}", symbol);
}

pub async fn get_crumb(&self) -> Result<CrumbData> {
let res = self
.client
.get_async("https://fc.yahoo.com")
.await
.context("Failed to get request")?;

let Some(cookie) = res
.headers()
.get(header::SET_COOKIE)
.and_then(|header| header.to_str().ok())
.and_then(|s| s.split_once(';').map(|(value, _)| value))
else {
bail!("Couldn't fetch cookie");
};

let request = Request::builder()
.uri(self.get_url(Version::V1, "test/getcrumb", None)?)
.header(header::USER_AGENT, "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36")
.header(header::COOKIE, cookie)
.method(http::Method::GET)
.body(())?;
let mut res = self.client.send_async(request).await?;

let crumb = res.text().await?;

Ok(CrumbData {
cookie: cookie.to_string(),
crumb,
})
}
}

impl Default for Client {
Expand All @@ -202,6 +245,7 @@ impl Default for Client {

#[derive(Debug, Clone)]
pub enum Version {
V1,
V7,
V8,
V10,
Expand All @@ -210,6 +254,7 @@ pub enum Version {
impl Version {
fn as_str(&self) -> &'static str {
match self {
Version::V1 => "v1",
Version::V7 => "v7",
Version::V8 => "v8",
Version::V10 => "v10",
Expand All @@ -228,7 +273,15 @@ mod tests {
let symbols = vec!["SPY", "AAPL", "AMD", "TSLA", "ES=F", "BTC-USD", "DX-Y.NYB"];

for symbol in symbols {
let data = client.get_company_data(symbol).await;
let data = client
.get_company_data(
symbol,
CrumbData {
cookie: "".into(),
crumb: "".into(),
},
)
.await;

if let Err(e) = data {
println!("{}", e);
Expand Down
6 changes: 6 additions & 0 deletions api/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ use anyhow::Result;
use serde::de::{SeqAccess, Visitor};
use serde::{Deserialize, Deserializer};

#[derive(Debug, Clone)]
pub struct CrumbData {
pub cookie: String,
pub crumb: String,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Chart {
Expand Down
16 changes: 6 additions & 10 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::common::ChartType;
use crate::widget::options;
use crate::{cleanup_terminal, ENABLE_PRE_POST, SHOW_VOLUMES, SHOW_X_LABELS};

fn handle_keys_add_stock(keycode: KeyCode, mut app: &mut app::App) {
fn handle_keys_add_stock(keycode: KeyCode, app: &mut app::App) {
match keycode {
KeyCode::Enter => {
let mut stock = app.add_stock.enter(app.chart_type);
Expand Down Expand Up @@ -38,7 +38,7 @@ fn handle_keys_add_stock(keycode: KeyCode, mut app: &mut app::App) {
}
}

fn handle_keys_display_stock(keycode: KeyCode, modifiers: KeyModifiers, mut app: &mut app::App) {
fn handle_keys_display_stock(keycode: KeyCode, modifiers: KeyModifiers, app: &mut app::App) {
match (keycode, modifiers) {
(KeyCode::Left, KeyModifiers::CONTROL) => {
let new_idx = if app.current_tab == 0 {
Expand Down Expand Up @@ -113,7 +113,7 @@ fn handle_keys_display_stock(keycode: KeyCode, modifiers: KeyModifiers, mut app:
}
}

fn handle_keys_display_summary(keycode: KeyCode, mut app: &mut app::App) {
fn handle_keys_display_summary(keycode: KeyCode, app: &mut app::App) {
match keycode {
KeyCode::Left => {
app.summary_time_frame = app.summary_time_frame.down();
Expand Down Expand Up @@ -146,7 +146,7 @@ fn handle_keys_display_summary(keycode: KeyCode, mut app: &mut app::App) {
}
}

fn handle_keys_display_options(keycode: KeyCode, mut app: &mut app::App) {
fn handle_keys_display_options(keycode: KeyCode, app: &mut app::App) {
match keycode {
KeyCode::Esc | KeyCode::Char('o') | KeyCode::Char('q') => {
app.stocks[app.current_tab].toggle_options();
Expand Down Expand Up @@ -231,11 +231,7 @@ fn handle_keys_display_options(keycode: KeyCode, mut app: &mut app::App) {
}
}

pub fn handle_keys_configure_chart(
keycode: KeyCode,
modifiers: KeyModifiers,
mut app: &mut app::App,
) {
pub fn handle_keys_configure_chart(keycode: KeyCode, modifiers: KeyModifiers, app: &mut app::App) {
match (keycode, modifiers) {
(keycode, _)
if matches!(
Expand Down Expand Up @@ -284,7 +280,7 @@ pub fn handle_keys_configure_chart(
pub fn handle_key_bindings(
mode: Mode,
key_event: KeyEvent,
mut app: &mut app::App,
app: &mut app::App,
request_redraw: &Sender<()>,
) {
match (mode, key_event.modifiers, key_event.code) {
Expand Down
12 changes: 12 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::time::Duration;
use std::{io, panic, thread};

use api::model::CrumbData;
use crossbeam_channel::{bounded, select, unbounded, Receiver, Sender};
use crossterm::event::{Event, MouseEvent, MouseEventKind};
use crossterm::{cursor, execute, terminal};
Expand Down Expand Up @@ -42,6 +43,7 @@ lazy_static! {
pub static ref SHOW_VOLUMES: RwLock<bool> = RwLock::new(OPTS.show_volumes);
pub static ref DEFAULT_TIMESTAMPS: RwLock<HashMap<TimeFrame, Vec<i64>>> = Default::default();
pub static ref THEME: theme::Theme = OPTS.theme.unwrap_or_default();
pub static ref YAHOO_CRUMB: RwLock<Option<CrumbData>> = RwLock::default();
}

fn main() {
Expand Down Expand Up @@ -78,6 +80,8 @@ fn main() {

let default_timestamp_service = DefaultTimestampService::new();

set_crumb();

let app = Arc::new(Mutex::new(app::App {
mode: starting_mode,
stocks: starting_stocks,
Expand Down Expand Up @@ -229,3 +233,11 @@ fn setup_panic_hook() {
better_panic::Settings::auto().create_panic_handler()(panic_info);
}));
}

fn set_crumb() {
async_std::task::spawn(async move {
if let Ok(crumb) = CLIENT.get_crumb().await {
*YAHOO_CRUMB.write() = Some(crumb);
}
});
}
9 changes: 8 additions & 1 deletion src/task/company.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use futures::future::BoxFuture;

use super::*;
use crate::api::model::CompanyData;
use crate::YAHOO_CRUMB;

/// Returns a companies profile information. Only needs to be returned once.
pub struct Company {
Expand Down Expand Up @@ -31,7 +32,13 @@ impl AsyncTask for Company {
Box::pin(async move {
let symbol = input.as_ref();

crate::CLIENT.get_company_data(symbol).await.ok()
let crumb = YAHOO_CRUMB.read().clone();

if let Some(crumb) = crumb {
crate::CLIENT.get_company_data(symbol, crumb).await.ok()
} else {
None
}
})
}
}
19 changes: 12 additions & 7 deletions src/task/current_price.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use async_std::sync::Arc;
use futures::future::BoxFuture;

use super::*;
use crate::YAHOO_CRUMB;

/// Returns the current price, only if it has changed
pub struct CurrentPrice {
Expand Down Expand Up @@ -30,17 +31,21 @@ impl AsyncTask for CurrentPrice {
Box::pin(async move {
let symbol = input.as_ref();

if let Ok(response) = crate::CLIENT.get_company_data(symbol).await {
let regular_price = response.price.regular_market_price.price;
let crumb = YAHOO_CRUMB.read().clone();

let post_price = response.price.post_market_price.price;
if let Some(crumb) = crumb {
if let Ok(response) = crate::CLIENT.get_company_data(symbol, crumb).await {
let regular_price = response.price.regular_market_price.price;

let volume = response.price.regular_market_volume.fmt.unwrap_or_default();
let post_price = response.price.post_market_price.price;

Some((regular_price, post_price, volume))
} else {
None
let volume = response.price.regular_market_volume.fmt.unwrap_or_default();

return Some((regular_price, post_price, volume));
}
}

None
})
}
}

0 comments on commit 50f6149

Please sign in to comment.