summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/index.html45
-rw-r--r--src/main.rs158
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"))
+ }
+}