Twitch oauth enroll endpoint

This commit is contained in:
Brandon Thomas committed 2021-11-15 02:28:25 -05:00
1 parent c7989106b7
commit 9b2c0edcc5
7 files changed
+232 -171

No files matched your search

Generated
+1
View File
@@ -1865,6 +1865,7 @@ dependencies = [
"anyhow",
"base64 0.13.0",
"chrono",
"container_common",
"dotenv",
"log",
"serde",
@@ -2,16 +2,32 @@ use actix_http::{StatusCode, header};
use actix_web::{HttpRequest, web, HttpResponse, ResponseError, HttpResponseBuilder};
use crate::ObsGatewayServerState;
use http_server_common::response::response_error_helpers::to_simple_json_error;
use http_server_common::response::response_success_helpers::simple_json_success;
use log::info;
use std::fmt;
use std::sync::Arc;
use http_server_common::response::response_success_helpers::simple_json_success;
use twitch_oauth2::tokens::UserTokenBuilder;
use twitch_oauth2::{Scope, ClientSecret};
use twitch_api2::twitch_oauth2::ClientId;
use http_server_common::response::to_json_success_response::to_json_success_response;
#[derive(Serialize)]
pub struct OauthBeginEnrollResult {
redirect_url: String,
}
#[derive(Debug)]
pub enum OauthBeginEnrollError {
ServerError,
}
// NB: Not using DeriveMore since Clion doesn't understand it.
impl fmt::Display for OauthBeginEnrollError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "OauthBeginEnrollError")
}
}
impl ResponseError for OauthBeginEnrollError {
fn status_code(&self) -> StatusCode {
match *self {
@@ -24,18 +40,11 @@ impl ResponseError for OauthBeginEnrollError {
Self::ServerError => "server error".to_string(),
};
//to_simple_json_error(&error_reason, self.status_code())
to_simple_json_error(&error_reason, self.status_code())
HttpResponseBuilder::new(self.status_code())
.set_header(header::CONTENT_TYPE, "application/json")
.body("TODO")
}
}
// NB: Not using DeriveMore since Clion doesn't understand it.
impl fmt::Display for OauthBeginEnrollError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
//HttpResponseBuilder::new(self.status_code())
// .set_header(header::CONTENT_TYPE, "application/json")
// .body("TODO")
}
}
@@ -44,7 +53,25 @@ pub async fn oauth_begin_enroll(
server_state: web::Data<Arc<ObsGatewayServerState>>
) -> Result<HttpResponse, OauthBeginEnrollError> {
let redirect_url =
twitch_oauth2::url::Url::parse(&server_state.twitch_oauth_secrets.redirect_url).unwrap();
info!("oauth enrollment begin");
Ok(simple_json_success())
let client_id = ClientId::new(&server_state.twitch_oauth_secrets.client_id);
let client_secret = ClientSecret::new(&server_state.twitch_oauth_secrets.client_secret);
// TODO: Not all scopes are needed!
let mut builder = UserTokenBuilder::new(client_id, client_secret, redirect_url)
.set_scopes(Scope::all())
.force_verify(true);
let (url, _csrf_token) = builder.generate_url();
let response = OauthBeginEnrollResult {
redirect_url: url.to_string(),
};
let response = to_json_success_response(&response)
.map_err(|_| OauthBeginEnrollError::ServerError)?;
Ok(response)
}
+144 -157
View File
@@ -14,6 +14,7 @@
pub mod endpoints;
pub mod pubsub_gateway;
pub mod server_state;
pub mod twitch;
pub mod util;
@@ -47,31 +48,12 @@ use twitch_api2::pubsub::Topic;
use twitch_api2::pubsub;
use twitch_oauth2::tokens::UserTokenBuilder;
use twitch_oauth2::{AppAccessToken, Scope, TwitchToken, tokens::errors::AppAccessTokenError, ClientId, ClientSecret};
use crate::server_state::{ObsGatewayServerState, EnvConfig, TwitchOauthSecrets};
const DEFAULT_BIND_ADDRESS : &'static str = "0.0.0.0:12345";
const DEFAULT_BIND_ADDRESS : &'static str = "0.0.0.0:54321";
pub type AnyhowResult<T> = anyhow::Result<T>;
/// State that is injected into every endpoint.
#[derive(Clone)]
pub struct ObsGatewayServerState {
/// Configuration from ENV vars.
/// Some of this might not be used.
pub env_config: EnvConfig,
pub hostname: String,
}
#[derive(Clone)]
pub struct EnvConfig {
// Number of thread workers.
pub num_workers: usize,
pub bind_address: String,
pub cookie_domain: String,
pub cookie_secure: bool,
pub cookie_http_only: bool,
pub website_homepage_redirect: String,
}
#[actix_web::main]
//#[tokio::main]
async fn main() -> AnyhowResult<()> {
@@ -122,126 +104,129 @@ async fn main() -> AnyhowResult<()> {
//std::thread::sleep(Duration::from_secs(5000));
// ==================== OAUTH FLOW ====================
println!("Oauth flow...");
let redirect_url = twitch_oauth2::url::Url::parse("http://localhost/test")?;
let mut builder = UserTokenBuilder::new(client_id, client_secret, redirect_url)
.set_scopes(Scope::all())
.force_verify(true);
//builder.add_scope(Scope::BitsRead);
let (url, _csrf_token) = builder.generate_url();
println!("Go to this page: {}", url);
let input = rpassword::prompt_password_stdout(
"Paste in the resulting adress after authenticating (input hidden): ",
)?;
let u = twitch_oauth2::url::Url::parse(&input)?;
let map: std::collections::HashMap<_, _> = u.query_pairs().collect();
let user_token = match (map.get("state"), map.get("code")) {
(Some(state), Some(code)) => {
let token = builder
.get_user_token(
&reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?,
state,
code,
)
.await?;
println!("Got token: {:?}", token);
token
}
_ => match (map.get("error"), map.get("error_description")) {
(std::option::Option::Some(error), std::option::Option::Some(error_description)) => {
anyhow::bail!(
"twitch errored with error: {} - {}",
error,
error_description
);
}
_ => anyhow::bail!("invalid url passed"),
},
};
let auth_token = user_token.access_token.as_str();
// ==================== PUBSUB SUBSCRIPTION AND MAIN LOOP ====================
let mut client = PollingTwitchWebsocketClient::new()?;
println!("Connecting PubSub...");
client.connect().await?;
println!("Connected");
//println!("Starting polling thread...");
//client.start_ping_thread().await;
println!("Sending PING...");
client.send_ping().await?;
println!("Try read next...");
let r = client.try_next().await?;
println!("Result: {:?}", r);
println!("Begin LISTEN...");
let bit_topic = pubsub::channel_bits::ChannelBitsEventsV2 {
channel_id: user_id,
}.into_topic();
let topics = [bit_topic];
client.listen(&auth_token, &topics).await?;
println!("Try read next...");
let r = client.try_next().await?;
println!("Result: {:?}", r);
/*
Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 {
channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement:
Some(BadgeEntitlement { new_version: 100, previous_version: 1 }), bits_used: 100,
channel_id: "652567283", channel_name: "vocodes", chat_message: "Cheer100 testing the cheer",
context: Cheer, is_anonymous: false, time: "2021-09-27T04:30:53.627717085Z",
total_bits_used: 101, user_id: "650154491", user_name: "testytest512" },
message_id: "793f745e-8f3e-5f71-bc41-4808c4b49a53", version: "1.0", is_anonymous: false } } })
Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 {
channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement: None,
bits_used: 1, channel_id: "652567283", channel_name: "vocodes",
chat_message: "mario: hello everybody Cheer1", context: Cheer, is_anonymous: false,
time: "2021-09-27T04:47:32.943872952Z", total_bits_used: 102, user_id: "650154491",
user_name: "testytest512" }, message_id: "5844328d-495b-5585-9d6c-3c99949f9a0a", version: "1.0",
is_anonymous: false } } })
*/
println!("Try read next...");
let r = client.try_next().await?;
println!("Result: {:?}", r);
println!("Sleep...");
std::thread::sleep(Duration::from_millis(30000));
//let text = msg.into_text()?;
//println!("Text: {}", text);
// ========================================================
// // ==================== OAUTH FLOW ====================
//
// println!("Oauth flow...");
//
// let redirect_url = twitch_oauth2::url::Url::parse("http://localhost/test")?;
// let mut builder = UserTokenBuilder::new(
// client_id.clone(),
// client_secret.clone(),
// redirect_url)
// .set_scopes(Scope::all())
// .force_verify(true);
//
// //builder.add_scope(Scope::BitsRead);
//
// let (url, _csrf_token) = builder.generate_url();
//
// println!("Go to this page: {}", url);
//
//
//
// let input = rpassword::prompt_password_stdout(
// "Paste in the resulting adress after authenticating (input hidden): ",
// )?;
//
// let u = twitch_oauth2::url::Url::parse(&input)?;
//
// let map: std::collections::HashMap<_, _> = u.query_pairs().collect();
//
// let user_token = match (map.get("state"), map.get("code")) {
// (Some(state), Some(code)) => {
// let token = builder
// .get_user_token(
// &reqwest::Client::builder()
// .redirect(reqwest::redirect::Policy::none())
// .build()?,
// state,
// code,
// )
// .await?;
// println!("Got token: {:?}", token);
//
// token
// }
// _ => match (map.get("error"), map.get("error_description")) {
// (std::option::Option::Some(error), std::option::Option::Some(error_description)) => {
// anyhow::bail!(
// "twitch errored with error: {} - {}",
// error,
// error_description
// );
// }
// _ => anyhow::bail!("invalid url passed"),
// },
// };
//
// let auth_token = user_token.access_token.as_str();
//
// // ==================== PUBSUB SUBSCRIPTION AND MAIN LOOP ====================
//
// let mut client = PollingTwitchWebsocketClient::new()?;
//
// println!("Connecting PubSub...");
// client.connect().await?;
//
// println!("Connected");
//
// //println!("Starting polling thread...");
// //client.start_ping_thread().await;
//
// println!("Sending PING...");
//
// client.send_ping().await?;
//
// println!("Try read next...");
// let r = client.try_next().await?;
// println!("Result: {:?}", r);
//
//
// println!("Begin LISTEN...");
//
// let bit_topic = pubsub::channel_bits::ChannelBitsEventsV2 {
// channel_id: user_id,
// }.into_topic();
//
// let topics = [bit_topic];
//
// client.listen(&auth_token, &topics).await?;
//
// println!("Try read next...");
// let r = client.try_next().await?;
// println!("Result: {:?}", r);
//
// /*
// Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 {
// channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement:
// Some(BadgeEntitlement { new_version: 100, previous_version: 1 }), bits_used: 100,
// channel_id: "652567283", channel_name: "vocodes", chat_message: "Cheer100 testing the cheer",
// context: Cheer, is_anonymous: false, time: "2021-09-27T04:30:53.627717085Z",
// total_bits_used: 101, user_id: "650154491", user_name: "testytest512" },
// message_id: "793f745e-8f3e-5f71-bc41-4808c4b49a53", version: "1.0", is_anonymous: false } } })
//
// Result: Some(Message { data: ChannelBitsEventsV2 { topic: ChannelBitsEventsV2 {
// channel_id: 652567283 }, reply: BitsEvent { data: BitsEventData { badge_entitlement: None,
// bits_used: 1, channel_id: "652567283", channel_name: "vocodes",
// chat_message: "mario: hello everybody Cheer1", context: Cheer, is_anonymous: false,
// time: "2021-09-27T04:47:32.943872952Z", total_bits_used: 102, user_id: "650154491",
// user_name: "testytest512" }, message_id: "5844328d-495b-5585-9d6c-3c99949f9a0a", version: "1.0",
// is_anonymous: false } } })
// */
//
//
//
// println!("Try read next...");
// let r = client.try_next().await?;
// println!("Result: {:?}", r);
//
// println!("Sleep...");
// std::thread::sleep(Duration::from_millis(30000));
//
// //let text = msg.into_text()?;
// //println!("Text: {}", text);
//
// // ========================================================
// info!("Connecting to database...");
//
@@ -285,6 +270,11 @@ async fn main() -> AnyhowResult<()> {
cookie_http_only,
website_homepage_redirect,
},
twitch_oauth_secrets: TwitchOauthSecrets {
client_id: secrets.app_client_id.clone(),
client_secret: secrets.app_client_secret.clone(),
redirect_url: "http://localhost:54321/twitch/oauth_redirect".to_string()
},
hostname: server_hostname,
};
@@ -319,22 +309,19 @@ pub async fn serve(server_state: ObsGatewayServerState) -> AnyhowResult<()>
.route(web::get().to(get_root_index))
)
// Twitch
.service(
web::resource("/twitch")
.route(web::get().to(ws_index))
.route(web::head().to(|| HttpResponse::Ok()))
)
// /twitch_oauth_enroll
// /twitch_oauth_redirect
.service(
web::resource("/twitch_oauth_enroll")
.route(web::get().to(oauth_begin_enroll))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
web::resource("/twitch_oauth_redirect")
.route(web::get().to(oauth_begin_enroll))
.route(web::head().to(|| HttpResponse::Ok()))
.service(web::scope("/twitch")
.service(web::resource("/oauth_enroll")
.route(web::get().to(oauth_begin_enroll))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/oauth_redirect")
.route(web::get().to(oauth_begin_enroll))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/websocket")
.route(web::get().to(ws_index))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
//.default_service( web::route().to(default_route_404))
})
@@ -0,0 +1,31 @@
use twitch_oauth2::{ClientId, ClientSecret};
/// State that is injected into every endpoint.
#[derive(Clone)]
pub struct ObsGatewayServerState {
/// Configuration from ENV vars.
/// Some of this might not be used.
pub env_config: EnvConfig,
pub hostname: String,
pub twitch_oauth_secrets: TwitchOauthSecrets,
}
// TODO: Many of these do not need to be passed around past server init.
#[derive(Clone)]
pub struct EnvConfig {
// Number of thread workers.
pub num_workers: usize,
pub bind_address: String,
pub cookie_domain: String,
pub cookie_secure: bool,
pub cookie_http_only: bool,
pub website_homepage_redirect: String,
}
/// Necessary to run the OAuth flow.
#[derive(Clone)]
pub struct TwitchOauthSecrets {
pub client_id: String,
pub client_secret: String,
pub redirect_url: String,
}
+3
View File
@@ -14,6 +14,9 @@ path = "src/lib.rs"
[dependencies]
# Internal
container_common = { path = "../container_common" }
# External
anyhow = "1.0.40"
base64 = "0.13.0"
@@ -1,3 +1,4 @@
pub mod response_error_helpers;
pub mod response_success_helpers;
pub mod serialize_as_json_error;
pub mod to_json_success_response;
@@ -0,0 +1,11 @@
use serde::Serialize;
use actix_web::HttpResponse;
use container_common::anyhow_result::AnyhowResult;
pub fn to_json_success_response(response: &impl Serialize) -> AnyhowResult<HttpResponse> {
let body = serde_json::to_string(&response)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}