use clap::{Parser, Subcommand};
use dotenv::dotenv;
use sergeant::eventsub::start_eventsub;
use sergeant::tui::{install_hooks, restore, App};
use sergeant::twitch::api::{refresh_token, validate};
use sergeant::twitch::{
announcements::start_announcements, irc::TwitchIRC, parse::get_badges, pubsub::connect_to_pub_sub, ChannelMessages,
};
use sergeant::commands::{
add_action, add_chat_command, add_reward, authenticate_with_twitch, get_list_announcements, get_list_commands,
list_actions, list_rewards, remove_action, remove_chat_command, remove_reward, TokenStatus,
};
use sergeant::utils::get_data_directory;
use sergeant::websocket::start_websocket;
use std::{
error::Error,
fs,
process::exit,
sync::{mpsc::channel, Arc},
thread,
};
type AsyncResult<T> = Result<T, Box<dyn Error>>;
#[derive(Subcommand)]
enum SubCmds {
List,
Add {
name: String,
message: String,
timing: Option<usize>,
},
Remove {
name: String,
},
}
#[derive(Subcommand)]
enum IrcActionSubCmds {
List,
Add {
name: String,
cli: String,
},
Remove {
name: String,
},
}
#[derive(Subcommand)]
enum RewardSubCmds {
List,
Add {
name: String,
cli: String,
},
Remove {
name: String,
},
}
#[derive(Subcommand)]
enum Cmds {
Chat {
#[arg(long, short = 'n', env = "TWITCH_NAME")]
twitch_name: Option<String>,
#[arg(long, short = 't', env = "OAUTH_TOKEN")]
oauth_token: Option<String>,
#[arg(long, short, env = "CLIENT_ID")]
client_id: Option<String>,
#[arg(long, short = 's', env = "SKIP_ANNOUNCEMENTS", default_value_t = false)]
skip_announcements: bool,
},
Commands {
#[command(subcommand)]
cmd: SubCmds,
},
Rewards {
#[command(subcommand)]
cmd: RewardSubCmds,
},
IrcActions {
#[command(subcommand)]
cmd: IrcActionSubCmds,
},
Login,
}
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
commands: Cmds,
}
fn get_credentials(
twitch_name: Option<String>,
oauth_token: Option<String>,
client_id: Option<String>,
_refresh: Option<String>,
) -> Result<(String, String, String, String), Box<dyn Error>> {
match (twitch_name, oauth_token, client_id) {
(Some(twitch_name), Some(oauth_token), Some(client_id)) => {
Ok((twitch_name, oauth_token, client_id, String::new()))
}
_ => {
let error_message =
"You need to provide credentials via positional args, env vars, or by running the login command";
let mut data_dir = get_data_directory(Some("token")).expect(error_message);
data_dir.push("oath_token.txt");
let token_file = fs::read_to_string(data_dir)?;
let token_status: TokenStatus = serde_json::from_str(&token_file)?;
if token_status.success {
Ok((
token_status.username.unwrap(),
format!("oauth:{}", token_status.token.unwrap()),
token_status.client_id.unwrap(),
token_status.refresh.unwrap(),
))
} else {
panic!("{}", error_message);
}
}
}
}
fn start_chat(
twitch_name: Arc<String>,
oauth_token: Arc<String>,
client_id: Arc<String>,
refresh: Arc<String>,
skip_announcements: bool,
) -> AsyncResult<()> {
let validate_token_response = validate(&oauth_token);
let mut token_status: Option<TokenStatus> = None;
if validate_token_response.is_err() {
let token_status_result = refresh_token(&refresh);
if token_status_result.is_err() {
panic!("Token refresh failed, unable to validate Twitch API access. Please login again.");
}
token_status = Some(token_status_result.unwrap());
}
let oauth_token = if let Some(token_status) = &token_status {
let token = token_status.token.clone().unwrap();
Arc::new(token)
} else {
oauth_token
};
let client_id = if let Some(token_status) = token_status {
let client_id = token_status.client_id.unwrap();
Arc::new(client_id)
} else {
client_id
};
get_badges(&oauth_token, &client_id)?;
let (pubsub_tx, rx) = channel::<ChannelMessages>();
let announce_tx = pubsub_tx.clone();
let chat_tx = pubsub_tx.clone();
let eventsub_tx = pubsub_tx.clone();
let token = oauth_token.clone();
let id = client_id.clone();
thread::spawn(|| {
connect_to_pub_sub(token, id, pubsub_tx).unwrap();
});
let token = oauth_token.clone();
let name = twitch_name.clone();
let id = client_id.clone();
thread::spawn(move || {
let _ = start_announcements(name, token, id, announce_tx, skip_announcements);
});
let id = client_id.clone();
let token = oauth_token.clone();
thread::spawn(|| {
let mut twitch_irc = TwitchIRC::new(twitch_name, token, id, chat_tx);
twitch_irc.listen();
});
let (socket_tx, socket_rx) = channel::<ChannelMessages>();
thread::spawn(|| {
start_websocket(socket_rx);
});
let id = client_id.clone();
let token = oauth_token.clone();
let eventsub_socket_tx = socket_tx.clone();
thread::spawn(|| {
start_eventsub(token, id, eventsub_tx, eventsub_socket_tx);
});
install_hooks()?;
App::new().run(rx, socket_tx.clone())?;
restore()?;
Ok(())
}
fn list_commands() {
let result = get_list_commands();
if result.is_err() {
exit(2)
}
if let Ok(list) = &result {
if list.is_empty() {
println!("Currently no chat announcements have been added.");
return;
}
println!("Available chat commands:");
for item in list {
println!("- {}", item);
}
}
list_announcements();
}
fn add_command(command_name: &str, message: &str, timing: Option<usize>) {
let result = add_chat_command(command_name, message, timing);
if result.is_err() {
exit(1)
}
}
fn remove_command(command_name: &str) {
let result = remove_chat_command(command_name);
if result.is_err() {
exit(3)
}
}
fn list_announcements() {
let result = get_list_announcements();
if result.is_err() {
exit(4)
}
if let Ok(list) = &result {
if list.is_empty() {
println!("Currently no chat announcements have been added.");
return;
}
println!("Current chat announcements:");
for item in list {
println!("- {}", item);
}
}
}
fn start_login_flow() {
let result = authenticate_with_twitch();
if result.is_err() {
exit(5);
}
}
fn main() {
dotenv().ok();
let cli = Cli::parse();
match cli.commands {
Cmds::Chat {
twitch_name,
oauth_token,
client_id,
skip_announcements,
} => {
let (name, token, id, refresh) = get_credentials(twitch_name, oauth_token, client_id, None).unwrap();
let name = Arc::new(name);
let id = Arc::new(id);
let token = Arc::new(token);
let refresh = Arc::new(refresh);
let _ = start_chat(name, token, id, refresh, skip_announcements);
}
Cmds::Commands { cmd } => match cmd {
SubCmds::List => {
list_commands();
}
SubCmds::Add { name, message, timing } => {
add_command(&name, &message, timing);
}
SubCmds::Remove { name } => {
remove_command(&name);
}
},
Cmds::IrcActions { cmd } => match cmd {
IrcActionSubCmds::List => {
list_actions();
}
IrcActionSubCmds::Add { name, cli } => {
let _ = add_action(&name, &cli);
}
IrcActionSubCmds::Remove { name } => {
let _ = remove_action(&name);
}
},
Cmds::Rewards { cmd } => match cmd {
RewardSubCmds::List => {
list_rewards();
}
RewardSubCmds::Add { name, cli } => {
let _ = add_reward(&name, &cli);
}
RewardSubCmds::Remove { name } => {
let _ = remove_reward(&name);
}
},
Cmds::Login => {
start_login_flow();
}
};
}