summaryrefslogtreecommitdiffstats
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs419
1 files changed, 68 insertions, 351 deletions
diff --git a/src/main.rs b/src/main.rs
index f4f7559..922162f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,52 +1,51 @@
-use std::io::{BufRead, Read};
+use std::fs::File;
+use std::io::{Read, Write};
use std::path::PathBuf;
-use std::sync::{Arc, Mutex};
-use std::thread;
use futures::future::{FutureExt, TryFutureExt};
use log::{debug, info};
use structopt::clap::AppSettings;
use structopt::StructOpt;
-use tokio02::sync::mpsc::UnboundedSender;
-use tsclientlib::{ClientId, ConnectOptions, Identity, Invoker, MessageTarget};
+use tsclientlib::Identity;
mod audio_player;
+mod bot;
mod command;
mod playlist;
mod teamspeak;
mod youtube_dl;
-use audio_player::*;
-use playlist::*;
-use teamspeak::*;
-use youtube_dl::AudioMetadata;
-use command::Command;
+use bot::{MasterArgs, MasterBot, MusicBot, MusicBotArgs};
#[derive(StructOpt, Debug)]
#[structopt(raw(global_settings = "&[AppSettings::ColoredHelp]"))]
-struct Args {
+pub struct Args {
#[structopt(short = "l", long = "local", help = "Run locally in text mode")]
local: bool,
#[structopt(
+ short = "g",
+ long = "generate-identities",
+ help = "Generate 'count' identities"
+ )]
+ gen_id_count: Option<u8>,
+ #[structopt(
short = "a",
long = "address",
- default_value = "localhost",
help = "The address of the server to connect to"
)]
- address: String,
+ address: Option<String>,
#[structopt(
- short = "i",
- long = "id",
- help = "Identity file - good luck creating one",
- parse(from_os_str)
+ help = "Configuration file",
+ parse(from_os_str),
+ default_value = "config.toml"
)]
- id_path: Option<PathBuf>,
+ config_path: PathBuf,
#[structopt(
- short = "c",
- long = "channel",
- help = "The channel the bot should connect to"
+ short = "d",
+ long = "master_channel",
+ help = "The channel the master bot should connect to"
)]
- default_channel: Option<String>,
+ master_channel: Option<String>,
#[structopt(
short = "v",
long = "verbose",
@@ -60,352 +59,70 @@ struct Args {
// 3. Print udp packets
}
-#[derive(Debug)]
-pub struct Message {
- pub target: MessageTarget,
- pub invoker: Invoker,
- pub text: String,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub enum State {
- Playing,
- Paused,
- Stopped,
- EndOfStream,
-}
-
-#[derive(Debug)]
-pub enum ApplicationMessage {
- TextMessage(Message),
- StateChange(State),
-}
-
-struct Application {
- player: Arc<AudioPlayer>,
- teamspeak: Option<Arc<TeamSpeakConnection>>,
- playlist: Arc<Mutex<Playlist>>,
- state: Arc<Mutex<State>>,
-}
-
-impl Application {
- pub fn new(
- player: Arc<AudioPlayer>,
- playlist: Arc<Mutex<Playlist>>,
- teamspeak: Option<Arc<TeamSpeakConnection>>,
- ) -> Self {
- Self {
- player,
- teamspeak,
- playlist,
- state: Arc::new(Mutex::new(State::Stopped)),
- }
- }
-
- #[inline(always)]
- fn with_teamspeak<F: Fn(&TeamSpeakConnection)>(&self, func: F) {
- if let Some(ts) = &self.teamspeak {
- func(&ts);
- }
- }
-
- fn start_playing_audio(&self, metadata: AudioMetadata) {
- if let Some(title) = metadata.title {
- self.send_message(&format!("Playing '{}'", title));
- self.set_description(&format!("Currently playing '{}'", title));
- } else {
- self.send_message("Playing unknown title");
- self.set_description("Currently playing");
- }
- self.player.reset().unwrap();
- self.player.set_source_url(metadata.url).unwrap();
- self.player.play().unwrap();
- }
-
- pub async fn add_audio(&self, url: String) {
- match youtube_dl::get_audio_download_url(url).await {
- Ok(metadata) => {
- info!("Found audio url: {}", metadata.url);
-
- let mut playlist = self.playlist.lock().expect("Mutex was not poisoned");
- playlist.push(metadata.clone());
-
- if !self.player.is_started() {
- if let Some(request) = playlist.pop() {
- self.start_playing_audio(request);
- }
- } else {
- if let Some(title) = metadata.title {
- self.send_message(&format!("Added '{}' to playlist", title));
- } else {
- self.send_message("Added to playlist");
- }
- }
- }
- Err(e) => {
- info!("Failed to find audio url: {}", e);
-
- self.send_message(&format!("Failed to find url: {}", e));
- }
- }
- }
-
- fn send_message(&self, text: &str) {
- debug!("Sending message to TeamSpeak: {}", text);
-
- self.with_teamspeak(|ts| ts.send_message_to_channel(text));
- }
-
- fn set_nickname(&self, name: &str) {
- info!("Setting TeamsSpeak nickname to {}", name);
-
- self.with_teamspeak(|ts| ts.set_nickname(name));
- }
-
- fn set_description(&self, desc: &str) {
- info!("Setting TeamsSpeak description to {}", desc);
-
- self.with_teamspeak(|ts| ts.set_description(desc));
- }
-
- async fn on_text(&self, message: Message) -> Result<(), AudioPlayerError> {
- let msg = message.text;
- if msg.starts_with("!") {
- let tokens = msg[1..].split_whitespace().collect::<Vec<_>>();
-
- match Command::from_iter_safe(&tokens) {
- Ok(args) => self.on_command(args).await?,
- Err(e) if e.kind == structopt::clap::ErrorKind::HelpDisplayed => {
- self.send_message(&format!("\n{}", e.message));
- }
- _ => (),
- }
- }
-
- Ok(())
- }
-
- async fn on_command(&self, command: Command) -> Result<(), AudioPlayerError> {
- match command {
- Command::Play => {
- let playlist = self.playlist.lock().expect("Mutex was not poisoned");
-
- if !self.player.is_started() {
- if !playlist.is_empty() {
- self.player.stop_current()?;
- }
- } else {
- self.player.play()?;
- }
- }
- Command::Add { url } => {
- // strip bbcode tags from url
- let url = url.replace("[URL]", "").replace("[/URL]", "");
-
- self.add_audio(url.to_string()).await;
- }
- Command::Pause => {
- self.player.pause()?;
- }
- Command::Stop => {
- self.player.reset()?;
- }
- Command::Next => {
- let playlist = self.playlist.lock().expect("Mutex was not poisoned");
- if !playlist.is_empty() {
- info!("Skipping to next track");
- self.player.stop_current()?;
- } else {
- info!("Playlist empty, cannot skip");
- self.player.reset()?;
- }
- }
- Command::Clear => {
- self.playlist
- .lock()
- .expect("Mutex was not poisoned")
- .clear();
- }
- Command::Volume { percent: volume } => {
- let volume = volume.max(0.0).min(100.0) * 0.01;
- self.player.set_volume(volume)?;
- }
- }
-
- Ok(())
- }
-
- fn on_state(&self, state: State) -> Result<(), AudioPlayerError> {
- let mut current_state = self.state.lock().unwrap();
- if *current_state != state {
- match state {
- State::Playing => {
- self.set_nickname("PokeBot - Playing");
- }
- State::Paused => {
- self.set_nickname("PokeBot - Paused");
- }
- State::Stopped => {
- self.set_nickname("PokeBot");
- self.set_description("");
- }
- State::EndOfStream => {
- let next_track = self.playlist.lock().expect("Mutex was not poisoned").pop();
- if let Some(request) = next_track {
- info!("Advancing playlist");
-
- self.start_playing_audio(request);
- } else {
- self.set_nickname("PokeBot");
- self.set_description("");
- }
- }
- }
- }
-
- *current_state = state;
-
- Ok(())
- }
-
- pub async fn on_message(&self, message: ApplicationMessage) -> Result<(), AudioPlayerError> {
- match message {
- ApplicationMessage::TextMessage(message) => {
- if let MessageTarget::Poke(who) = message.target {
- info!("Poked by {}, joining their channel", who);
- self.with_teamspeak(|ts| ts.join_channel_of_user(who));
- } else {
- self.on_text(message).await?;
- }
- }
- ApplicationMessage::StateChange(state) => {
- self.on_state(state)?;
- }
- }
-
- Ok(())
+fn main() {
+ if let Err(e) = run() {
+ println!("Error: {}", e);
}
}
-fn main() {
+fn run() -> Result<(), Box<dyn std::error::Error>> {
log4rs::init_file("log4rs.yml", Default::default()).unwrap();
- tokio::run(async_main().unit_error().boxed().compat());
-}
-
-async fn async_main() {
- info!("Starting PokeBot!");
-
// Parse command line options
let args = Args::from_args();
- debug!("Received CLI arguments: {:?}", std::env::args());
+ let mut file = File::open(&args.config_path)?;
+ let mut toml = String::new();
+ file.read_to_string(&mut toml)?;
- let (tx, mut rx) = tokio02::sync::mpsc::unbounded_channel();
- let tx = Arc::new(Mutex::new(tx));
- let (player, connection) = if args.local {
- info!("Starting in CLI mode");
- let audio_player = AudioPlayer::new(tx.clone(), None).unwrap();
+ let mut config: MasterArgs = toml::from_str(&toml)?;
- (audio_player, None)
- } else {
- info!("Starting in TeamSpeak mode");
-
- let id = if let Some(path) = args.id_path {
- let mut file = std::fs::File::open(path).expect("Failed to open id file");
- let mut content = String::new();
- file.read_to_string(&mut content)
- .expect("Failed to read id file");
-
- toml::from_str(&content).expect("Failed to parse id file")
- } else {
- Identity::create().expect("Failed to create id")
- };
-
- let mut con_config = ConnectOptions::new(args.address)
- .version(tsclientlib::Version::Linux_3_3_2)
- .name(String::from("PokeBot"))
- .identity(id)
- .log_commands(args.verbose >= 1)
- .log_packets(args.verbose >= 2)
- .log_udp_packets(args.verbose >= 3);
-
- if let Some(channel) = args.default_channel {
- con_config = con_config.channel(channel);
+ if let Some(count) = args.gen_id_count {
+ for _ in 0..count {
+ let id = Identity::create().expect("Failed to create id");
+ config.ids.push(id);
}
- let connection = Arc::new(
- TeamSpeakConnection::new(tx.clone(), con_config)
- .await
- .unwrap(),
- );
- let cconnection = connection.clone();
- let audio_player = AudioPlayer::new(
- tx.clone(),
- Some(Box::new(move |samples| {
- cconnection.send_audio_packet(samples);
- })),
- )
- .unwrap();
-
- (audio_player, Some(connection))
- };
+ let toml = toml::to_string(&config)?;
+ let mut file = File::create(&args.config_path)?;
+ file.write_all(toml.as_bytes())?;
- player.set_volume(0.5).unwrap();
- let player = Arc::new(player);
- let playlist = Arc::new(Mutex::new(Playlist::new()));
- let application = Arc::new(Application::new(
- player.clone(),
- playlist.clone(),
- connection,
- ));
-
- spawn_gstreamer_thread(player, tx.clone());
-
- if args.local {
- spawn_stdin_reader(tx);
- }
-
- loop {
- while let Some(msg) = rx.recv().await {
- application.on_message(msg).await.unwrap();
- }
+ return Ok(());
}
-}
-fn spawn_stdin_reader(tx: Arc<Mutex<UnboundedSender<ApplicationMessage>>>) {
- thread::spawn(move || {
- let stdin = ::std::io::stdin();
- let lock = stdin.lock();
- for line in lock.lines() {
- let line = line.unwrap();
+ let bot_args = config.merge(args);
- let message = ApplicationMessage::TextMessage(Message {
- target: MessageTarget::Server,
- invoker: Invoker {
- name: String::from("stdin"),
- id: ClientId(0),
- uid: None,
- },
- text: line,
- });
+ info!("Starting PokeBot!");
+ debug!("Received CLI arguments: {:?}", std::env::args());
- let tx = tx.lock().unwrap();
- tx.send(message).unwrap();
+ tokio::run(
+ async {
+ if bot_args.local {
+ let name = bot_args.names[0].clone();
+ let id = bot_args.ids[0].clone();
+
+ let disconnect_cb = Box::new(move |_, _, _| {});
+
+ let bot_args = MusicBotArgs {
+ name: name,
+ name_index: 0,
+ id_index: 0,
+ local: true,
+ address: bot_args.address.clone(),
+ id,
+ channel: String::from("local"),
+ verbose: bot_args.verbose,
+ disconnect_cb,
+ };
+ MusicBot::new(bot_args).await.1.await;
+ } else {
+ MasterBot::new(bot_args).await.1.await;
+ }
}
- });
-}
-
-fn spawn_gstreamer_thread(
- player: Arc<AudioPlayer>,
- tx: Arc<Mutex<UnboundedSender<ApplicationMessage>>>,
-) {
- thread::spawn(move || loop {
- player.poll();
+ .unit_error()
+ .boxed()
+ .compat(),
+ );
- tx.lock()
- .unwrap()
- .send(ApplicationMessage::StateChange(State::EndOfStream))
- .unwrap();
- });
+ Ok(())
}