diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/index.html | 45 | ||||
-rw-r--r-- | src/main.rs | 158 |
2 files changed, 203 insertions, 0 deletions
diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..ccc1b22 --- /dev/null +++ b/src/index.html @@ -0,0 +1,45 @@ +<!doctype html> +<html> + <head> + <title>Authorization</title> + <meta name="ROBOTS" content="NOFOLLOW" /> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <script type="text/javascript"> + <!-- + function initiate() { + var hash = document.location.hash.substr(1); + document.getElementById("javascript").className = ""; + if (hash != null) { + document.location.replace("/token?" + hash); + } else { + document.getElementById("javascript").innerHTML = + "Error: Access Token not found"; + } + } + --> + </script> + <style type="text/css"> + body { + text-align: center; + background-color: #fff; + max-width: 500px; + margin: auto; + } + noscript { + color: red; + } + .hide { + display: none; + } + </style> + </head> + <body onload="initiate()"> + <h1>Authorization</h1> + <noscript> + <p> + This page requires <strong>JavaScript</strong> to get your token. + </p></noscript + > + <p id="javascript" class="hide">You should be redirected..</p> + </body> +</html> diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..da8aa22 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,158 @@ +use std::{ + collections::HashMap, + io::{BufRead, BufReader, Write}, + net::{TcpListener, TcpStream}, + ops::ControlFlow, + path::{Path, PathBuf}, +}; + +use futures::TryStreamExt; +use main_error::MainError; +use reqwest::Url; +use twitch_api::HelixClient; +use twitch_oauth2::{client::Client, AccessToken, ImplicitUserTokenBuilder, Scope, UserToken}; + +const CLIENT_ID: &str = "phiay4sq36lfv9zu7cbqwz2ndnesfd8"; +const PORT: u16 = 65432; +const HTML: &str = include_str!("index.html"); + +#[tokio::main] +async fn main() -> Result<(), MainError> { + let reqwest = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build()?; + + let token = get_access_token(&reqwest).await?; + + let client = HelixClient::<reqwest::Client>::with_client(reqwest); + + let streams = client + .get_followed_streams(&token) + .try_collect::<Vec<_>>() + .await?; + let games = client + .get_games_by_id( + &streams + .iter() + .map(|s| &s.game_id) + .collect::<Vec<_>>() + .into(), + &token, + ) + .map_ok(|g| (g.id.clone(), g)) + .try_collect::<std::collections::HashMap<_, _>>() + .await?; + + println!( + "{}", + streams + .iter() + .map(|s| format!( + "{user_name}: [{game}] | {title}", + user_name = s.user_name, + game = games.get(&s.game_id).map(|c| c.name.as_str()).unwrap_or(""), + title = s.title + )) + .collect::<Vec<_>>() + .join("\n") + ); + + Ok(()) +} + +fn read_token_file<P: AsRef<Path>>(p: P) -> Result<AccessToken, MainError> { + std::fs::read_to_string(&p)?.parse().map_err(Into::into) +} + +async fn get_access_token<C>(client: &C) -> Result<UserToken, MainError> +where + C: Client, +{ + let file_path = std::env::var("XDG_STATE_HOME") + .map(PathBuf::from) + .or_else(|_| std::env::var("HOME").map(|p| PathBuf::from(p).join(".local/state")))? + .join("twitch-live.toml"); + + if let Ok(token) = read_token_file(&file_path) { + if let Ok(token) = UserToken::from_token(client, token).await { + return Ok(token); + } + } + + let mut builder = ImplicitUserTokenBuilder::new( + CLIENT_ID.parse()?, + format!("http://localhost:{PORT}/redirect").parse()?, + ); + + builder.add_scope(Scope::UserReadFollows); + + let (url, _) = builder.generate_url(); + + std::process::Command::new("xdg-open") + .arg(url.to_string()) + .status()?; + + let listener = TcpListener::bind(format!("localhost:{PORT}")).unwrap(); + + let url = loop { + let stream = listener + .incoming() + .next() + .ok_or_else(|| MainError::from("Invalid incoming request"))??; + + match handle_connection(stream)? { + ControlFlow::Continue(()) => continue, + ControlFlow::Break(url) => { + break url; + } + } + }; + + let map: HashMap<_, _> = url.query_pairs().collect(); + + let token = builder + .get_user_token( + client, + map.get("state").map(|s| s.as_ref()), + map.get("access_token").map(|s| s.as_ref()), + map.get("error").map(|s| s.as_ref()), + map.get("error_description").map(|s| s.as_ref()), + ) + .await?; + + let mut file = std::fs::File::create(file_path)?; + file.write_all(token.access_token.as_str().as_bytes())?; + + Ok(token) +} + +fn handle_connection(mut stream: TcpStream) -> Result<ControlFlow<Url>, MainError> { + let buf_reader = BufReader::new(&mut stream); + let request_line = buf_reader + .lines() + .next() + .ok_or_else(|| MainError::from("Recieved invalid request"))??; + + let target = request_line + .split(' ') + .nth(1) + .ok_or_else(|| MainError::from("Recieved invalid request"))?; + + if target == "/redirect" { + stream.write_all( + format!( + "HTTP/1.1 200\r\nContent-Length: {}\r\n\r\n{HTML}", + HTML.len() + ) + .as_bytes(), + )?; + Ok(ControlFlow::Continue(())) + } else if target.starts_with("/token?") { + stream.write_all(b"HTTP/1.1 200\r\nContent-Length: 0")?; + Ok(ControlFlow::Break( + Url::parse(&format!("http://localhost:{PORT}/"))?.join(target)?, + )) + } else { + Err(MainError::from("Recieved invalid request")) + } +} |