From 972adef5626706f9a192ff784ecc22d1a56b1fe4 Mon Sep 17 00:00:00 2001 From: Jokler Date: Sat, 25 Jan 2020 15:55:52 +0100 Subject: Move master and music bots into seperate files --- src/bot/music.rs | 358 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 src/bot/music.rs (limited to 'src/bot/music.rs') diff --git a/src/bot/music.rs b/src/bot/music.rs new file mode 100644 index 0000000..3677796 --- /dev/null +++ b/src/bot/music.rs @@ -0,0 +1,358 @@ +use std::future::Future; +use std::io::BufRead; +use std::sync::{Arc, Mutex}; +use std::thread; + +use log::{debug, info}; +use structopt::StructOpt; +use tokio02::sync::mpsc::UnboundedSender; +use tsclientlib::{ClientId, ConnectOptions, Identity, Invoker, MessageTarget}; + +use crate::audio_player::{AudioPlayerError, AudioPlayer, PollResult}; +use crate::command::Command; +use crate::playlist::Playlist; +use crate::teamspeak::TeamSpeakConnection; +use crate::youtube_dl::AudioMetadata; + +#[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 MusicBotMessage { + TextMessage(Message), + StateChange(State), + Quit(String), +} + +pub struct MusicBot { + name: String, + player: Arc, + teamspeak: Option>, + playlist: Arc>, + state: Arc>, +} + +#[derive(Debug)] +pub struct MusicBotArgs { + pub name: String, + pub owner: Option, + pub local: bool, + pub address: String, + pub id: Identity, + pub channel: String, + pub verbose: u8, +} + +impl MusicBot { + pub async fn new(args: MusicBotArgs) -> (Arc, impl Future) { + 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(); + + (audio_player, None) + } else { + info!("Starting in TeamSpeak mode"); + + let con_config = ConnectOptions::new(args.address) + .version(tsclientlib::Version::Linux_3_3_2) + .name(args.name.clone()) + .identity(args.id) + .log_commands(args.verbose >= 1) + .log_packets(args.verbose >= 2) + .log_udp_packets(args.verbose >= 3) + .channel(args.channel); + + 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)) + }; + + player.set_volume(0.5).unwrap(); + let player = Arc::new(player); + let playlist = Arc::new(Mutex::new(Playlist::new())); + + spawn_gstreamer_thread(player.clone(), tx.clone()); + + if args.local { + spawn_stdin_reader(tx); + } + + let bot = Arc::new(Self { + name: args.name, + player, + teamspeak: connection, + playlist, + state: Arc::new(Mutex::new(State::Stopped)), + }); + + let cbot = bot.clone(); + let msg_loop = async move { + 'outer: loop { + while let Some(msg) = rx.recv().await { + if let MusicBotMessage::Quit(reason) = msg { + cbot.with_teamspeak(|ts| ts.disconnect(&reason)); + break 'outer; + } + cbot.on_message(msg).await.unwrap(); + } + } + debug!("Left message loop"); + }; + + (bot, msg_loop) + } + + #[inline(always)] + fn with_teamspeak(&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 crate::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::>(); + + 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)?; + } + Command::Leave => { + self.quit(String::from("Leaving")); + } + } + + 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(&format!("{} - Playing", self.name)); + } + State::Paused => { + self.set_nickname(&format!("{} - Paused", self.name)); + } + State::Stopped => { + self.set_nickname(&self.name); + 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(&self.name); + self.set_description(""); + } + } + } + } + + *current_state = state; + + Ok(()) + } + + async fn on_message(&self, message: MusicBotMessage) -> Result<(), AudioPlayerError> { + match message { + MusicBotMessage::TextMessage(message) => { + if MessageTarget::Channel == message.target { + self.on_text(message).await?; + } + } + MusicBotMessage::StateChange(state) => { + self.on_state(state)?; + } + MusicBotMessage::Quit(_) => (), + } + + Ok(()) + } + + pub fn quit(&self, reason: String) { + self.player.quit(reason); + } +} + +fn spawn_stdin_reader(tx: Arc>>) { + thread::spawn(move || { + let stdin = ::std::io::stdin(); + let lock = stdin.lock(); + for line in lock.lines() { + let line = line.unwrap(); + + let message = MusicBotMessage::TextMessage(Message { + target: MessageTarget::Server, + invoker: Invoker { + name: String::from("stdin"), + id: ClientId(0), + uid: None, + }, + text: line, + }); + + let tx = tx.lock().unwrap(); + tx.send(message).unwrap(); + } + }); +} + +fn spawn_gstreamer_thread( + player: Arc, + tx: Arc>>, +) { + thread::spawn(move || loop { + if player.poll() == PollResult::Quit { + break; + } + + tx.lock() + .unwrap() + .send(MusicBotMessage::StateChange(State::EndOfStream)) + .unwrap(); + }); +} -- cgit v1.2.3-70-g09d2 From 7be0a2f10f0cfeb89b2f498cfae316b35dcb0814 Mon Sep 17 00:00:00 2001 From: Jokler Date: Sat, 25 Jan 2020 16:30:37 +0100 Subject: Fix quit method not stopping audio playback --- src/audio_player.rs | 1 + src/bot/master.rs | 20 +++++++++++++++----- src/bot/music.rs | 7 +++++-- 3 files changed, 21 insertions(+), 7 deletions(-) (limited to 'src/bot/music.rs') diff --git a/src/audio_player.rs b/src/audio_player.rs index 2ff7c11..97ecfbf 100644 --- a/src/audio_player.rs +++ b/src/audio_player.rs @@ -313,6 +313,7 @@ impl AudioPlayer { MessageView::Application(content) => { if let Some(s) = content.get_structure() { if s.get_name() == "quit" { + self.reset().unwrap(); return PollResult::Quit; } } diff --git a/src/bot/master.rs b/src/bot/master.rs index ce8b25f..641938a 100644 --- a/src/bot/master.rs +++ b/src/bot/master.rs @@ -1,5 +1,6 @@ use std::future::Future; use std::sync::{Arc, Mutex}; +use std::collections::HashMap; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Future as Future01; @@ -17,7 +18,7 @@ use crate::bot::{MusicBot, MusicBotMessage, MusicBotArgs}; pub struct MasterBot { config: MasterConfig, teamspeak: Option>, - connected_bots: Arc>>>, + connected_bots: Arc>>>, } impl MasterBot { @@ -63,7 +64,7 @@ impl MasterBot { let bot = Arc::new(Self { config, teamspeak: connection, - connected_bots: Arc::new(Mutex::new(Vec::new())), + connected_bots: Arc::new(Mutex::new(HashMap::new())), }); let cbot = bot.clone(); @@ -85,22 +86,31 @@ impl MasterBot { String::from("local") }; - info!("Connecting to {} on {}", channel, self.config.address); let preset = self.config.bots[0].clone(); + let name = format!("{}({})", preset.name, self.config.name); + + let cconnected_bots = self.connected_bots.clone(); + let disconnect_cb = Box::new(move |n| { + let mut bots = cconnected_bots.lock().expect("Mutex was not poisoned"); + bots.remove(&n); + }); + + info!("Connecting to {} on {}", channel, self.config.address); let bot_args = MusicBotArgs { - name: format!("{}({})", preset.name, self.config.name), + name: name.clone(), owner: preset.owner, local: self.config.local, address: self.config.address.clone(), id: preset.id, channel, verbose: self.config.verbose, + disconnect_cb, }; let (app, fut) = MusicBot::new(bot_args).await; tokio::spawn(fut.unit_error().boxed().compat().map(|_| ())); let mut bots = self.connected_bots.lock().expect("Mutex was not poisoned"); - bots.push(app); + bots.insert(name, app); } async fn on_message(&self, message: MusicBotMessage) -> Result<(), AudioPlayerError> { diff --git a/src/bot/music.rs b/src/bot/music.rs index 3677796..4d67f88 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -44,7 +44,6 @@ pub struct MusicBot { state: Arc>, } -#[derive(Debug)] pub struct MusicBotArgs { pub name: String, pub owner: Option, @@ -53,6 +52,7 @@ pub struct MusicBotArgs { pub id: Identity, pub channel: String, pub verbose: u8, + pub disconnect_cb: Box, } impl MusicBot { @@ -104,7 +104,7 @@ impl MusicBot { } let bot = Arc::new(Self { - name: args.name, + name: args.name.clone(), player, teamspeak: connection, playlist, @@ -112,11 +112,14 @@ impl MusicBot { }); let cbot = bot.clone(); + let mut disconnect_cb = args.disconnect_cb; + let name = args.name; let msg_loop = async move { 'outer: loop { while let Some(msg) = rx.recv().await { if let MusicBotMessage::Quit(reason) = msg { cbot.with_teamspeak(|ts| ts.disconnect(&reason)); + disconnect_cb(name); break 'outer; } cbot.on_message(msg).await.unwrap(); -- cgit v1.2.3-70-g09d2 From 32686ba4a31ecf7c9aedad65c1a6be0be7d96ea2 Mon Sep 17 00:00:00 2001 From: Jokler Date: Sat, 25 Jan 2020 19:51:15 +0100 Subject: Let the music bot leave once its channel is empty --- src/bot/master.rs | 6 ++--- src/bot/music.rs | 42 +++++++++++++++++++++++++++++++-- src/teamspeak.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 106 insertions(+), 11 deletions(-) (limited to 'src/bot/music.rs') diff --git a/src/bot/master.rs b/src/bot/master.rs index 641938a..007abea 100644 --- a/src/bot/master.rs +++ b/src/bot/master.rs @@ -1,10 +1,10 @@ +use std::collections::HashMap; use std::future::Future; use std::sync::{Arc, Mutex}; -use std::collections::HashMap; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Future as Future01; -use log::{info}; +use log::info; use serde::{Deserialize, Serialize}; use tsclientlib::{ClientId, ConnectOptions, Identity, MessageTarget}; @@ -13,7 +13,7 @@ use crate::teamspeak::TeamSpeakConnection; use crate::Args; -use crate::bot::{MusicBot, MusicBotMessage, MusicBotArgs}; +use crate::bot::{MusicBot, MusicBotArgs, MusicBotMessage}; pub struct MasterBot { config: MasterConfig, diff --git a/src/bot/music.rs b/src/bot/music.rs index 4d67f88..94e7350 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -6,9 +6,9 @@ use std::thread; use log::{debug, info}; use structopt::StructOpt; use tokio02::sync::mpsc::UnboundedSender; -use tsclientlib::{ClientId, ConnectOptions, Identity, Invoker, MessageTarget}; +use tsclientlib::{data, ChannelId, ClientId, ConnectOptions, Identity, Invoker, MessageTarget}; -use crate::audio_player::{AudioPlayerError, AudioPlayer, PollResult}; +use crate::audio_player::{AudioPlayer, AudioPlayerError, PollResult}; use crate::command::Command; use crate::playlist::Playlist; use crate::teamspeak::TeamSpeakConnection; @@ -32,6 +32,14 @@ pub enum State { #[derive(Debug)] pub enum MusicBotMessage { TextMessage(Message), + ClientChannel { + client: ClientId, + old_channel: ChannelId, + }, + ClientDisconnected { + id: ClientId, + client: data::Client, + }, StateChange(State), Quit(String), } @@ -179,6 +187,20 @@ impl MusicBot { } } + fn my_channel(&self) -> ChannelId { + self.teamspeak + .as_ref() + .map(|ts| ts.my_channel()) + .expect("my_channel needs ts") + } + + fn user_count(&self, channel: ChannelId) -> u32 { + self.teamspeak + .as_ref() + .map(|ts| ts.user_count(channel)) + .expect("user_count needs ts") + } + fn send_message(&self, text: &str) { debug!("Sending message to TeamSpeak: {}", text); @@ -307,6 +329,22 @@ impl MusicBot { self.on_text(message).await?; } } + MusicBotMessage::ClientChannel { + client: _, + old_channel, + } => { + let my_channel = self.my_channel(); + if old_channel == my_channel && self.user_count(my_channel) <= 1 { + self.quit(String::from("Channel is empty")); + } + } + MusicBotMessage::ClientDisconnected { id: _, client } => { + let old_channel = client.channel; + let my_channel = self.my_channel(); + if old_channel == my_channel && self.user_count(my_channel) <= 1 { + self.quit(String::from("Channel is empty")); + } + } MusicBotMessage::StateChange(state) => { self.on_state(state)?; } diff --git a/src/teamspeak.rs b/src/teamspeak.rs index f1abaec..f49f7a6 100644 --- a/src/teamspeak.rs +++ b/src/teamspeak.rs @@ -19,17 +19,53 @@ pub struct TeamSpeakConnection { conn: Connection, } -fn get_message<'a>(event: &Event) -> Option { +fn get_message<'a>(event: &Event) -> Option { + use tsclientlib::events::{PropertyId, PropertyValue}; + match event { Event::Message { from: target, invoker: sender, message: msg, - } => Some(Message { - target: target.clone(), + } => Some(MusicBotMessage::TextMessage(Message { + target: *target, invoker: sender.clone(), text: msg.clone(), - }), + })), + Event::PropertyChanged { + id: property, + old: from, + invoker: _, + } => match property { + PropertyId::ClientChannel(client) => { + if let PropertyValue::ChannelId(from) = from { + Some(MusicBotMessage::ClientChannel { + client: *client, + old_channel: *from, + }) + } else { + None + } + } + _ => None, + }, + Event::PropertyRemoved { + id: property, + old: client, + invoker: _, + } => match property { + PropertyId::Client(id) => { + if let PropertyValue::Client(client) = client { + Some(MusicBotMessage::ClientDisconnected { + id: *id, + client: client.clone(), + }) + } else { + None + } + } + _ => None, + }, _ => None, } } @@ -49,8 +85,9 @@ impl TeamSpeakConnection { if let ConEvents(_conn, events) = e { for event in *events { if let Some(msg) = get_message(event) { - let tx = tx.lock().unwrap(); - tx.send(MusicBotMessage::TextMessage(msg)).unwrap(); + let tx = tx.lock().expect("Mutex was not poisoned"); + // Ignore the result because the receiver might get dropped first. + let _ = tx.send(msg); } } } @@ -108,6 +145,26 @@ impl TeamSpeakConnection { path } + pub fn my_channel(&self) -> ChannelId { + let conn = self.conn.lock(); + conn.clients + .get(&conn.own_client) + .expect("can find myself") + .channel + } + + pub fn user_count(&self, channel: ChannelId) -> u32 { + let conn = self.conn.lock(); + let mut count = 0; + for (_, client) in &conn.clients { + if client.channel == channel { + count += 1; + } + } + + count + } + pub fn set_nickname(&self, name: &str) { tokio::spawn( self.conn -- cgit v1.2.3-70-g09d2 From af0a1b81707536caf4e498d86912f65981342072 Mon Sep 17 00:00:00 2001 From: Jokler Date: Sun, 26 Jan 2020 06:58:42 +0100 Subject: Randomly choose from lists of identities and names --- Cargo.lock | 19 +++++-- Cargo.toml | 1 + src/bot/master.rs | 163 ++++++++++++++++++++++++++++-------------------------- src/bot/music.rs | 19 ++++--- src/main.rs | 23 +++----- src/teamspeak.rs | 10 ++++ 6 files changed, 130 insertions(+), 105 deletions(-) (limited to 'src/bot/music.rs') diff --git a/Cargo.lock b/Cargo.lock index 90cca45..8d57a14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1786,6 +1786,7 @@ dependencies = [ "gstreamer-audio", "log", "log4rs", + "rand 0.7.3", "serde", "serde_json", "structopt", @@ -1914,22 +1915,23 @@ dependencies = [ "rand_isaac", "rand_jitter", "rand_os", - "rand_pcg", + "rand_pcg 0.1.2", "rand_xorshift", "winapi 0.3.8", ] [[package]] name = "rand" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom", "libc", "rand_chacha 0.2.1", "rand_core 0.5.1", "rand_hc 0.2.0", + "rand_pcg 0.2.1", ] [[package]] @@ -2038,6 +2040,15 @@ dependencies = [ "rand_core 0.4.2", ] +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_xorshift" version = "0.1.1" @@ -2569,7 +2580,7 @@ checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ "cfg-if", "libc", - "rand 0.7.2", + "rand 0.7.3", "redox_syscall", "remove_dir_all", "winapi 0.3.8", diff --git a/Cargo.toml b/Cargo.toml index 6622793..34726ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,4 @@ gstreamer-audio = "0.15.0" byte-slice-cast = "0.3.5" serde_json = "1.0.44" serde = "1.0.104" +rand = { version = "0.7.3", features = ["small_rng"] } diff --git a/src/bot/master.rs b/src/bot/master.rs index 007abea..1f1ddfb 100644 --- a/src/bot/master.rs +++ b/src/bot/master.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex}; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Future as Future01; use log::info; +use rand::{rngs::SmallRng, seq::SliceRandom, SeedableRng}; use serde::{Deserialize, Serialize}; use tsclientlib::{ClientId, ConnectOptions, Identity, MessageTarget}; @@ -16,8 +17,11 @@ use crate::Args; use crate::bot::{MusicBot, MusicBotArgs, MusicBotMessage}; pub struct MasterBot { - config: MasterConfig, - teamspeak: Option>, + config: Arc, + rng: Arc>, + available_names: Arc>>, + available_ids: Arc>>, + teamspeak: Arc, connected_bots: Arc>>>, } @@ -25,44 +29,42 @@ impl MasterBot { pub async fn new(args: MasterArgs) -> (Arc, impl Future) { let (tx, mut rx) = tokio02::sync::mpsc::unbounded_channel(); let tx = Arc::new(Mutex::new(tx)); - let connection = if args.local { - info!("Starting in CLI mode"); - - None - } else { - info!("Starting in TeamSpeak mode"); - - let mut con_config = ConnectOptions::new(args.address.clone()) - .version(tsclientlib::Version::Linux_3_3_2) - .name(args.name.clone()) - .identity(args.id) - .log_commands(args.verbose >= 1) - .log_packets(args.verbose >= 2) - .log_udp_packets(args.verbose >= 3); - - if let Some(channel) = args.channel { - con_config = con_config.channel(channel); - } - - let connection = Arc::new( - TeamSpeakConnection::new(tx.clone(), con_config) - .await - .unwrap(), - ); + info!("Starting in TeamSpeak mode"); + + let mut con_config = ConnectOptions::new(args.address.clone()) + .version(tsclientlib::Version::Linux_3_3_2) + .name(args.master_name.clone()) + .identity(args.id) + .log_commands(args.verbose >= 1) + .log_packets(args.verbose >= 2) + .log_udp_packets(args.verbose >= 3); + + if let Some(channel) = args.channel { + con_config = con_config.channel(channel); + } - Some(connection) - }; + let connection = Arc::new( + TeamSpeakConnection::new(tx.clone(), con_config) + .await + .unwrap(), + ); - let config = MasterConfig { - name: args.name, + let config = Arc::new(MasterConfig { + master_name: args.master_name, address: args.address, - bots: args.bots, + names: args.names, + ids: args.ids, local: args.local, verbose: args.verbose, - }; + }); + let name_count = config.names.len(); + let id_count = config.ids.len(); let bot = Arc::new(Self { config, + rng: Arc::new(Mutex::new(SmallRng::from_entropy())), + available_names: Arc::new(Mutex::new((0..name_count).collect())), + available_ids: Arc::new(Mutex::new((0..id_count).collect())), teamspeak: connection, connected_bots: Arc::new(Mutex::new(HashMap::new())), }); @@ -80,28 +82,62 @@ impl MasterBot { } async fn spawn_bot(&self, id: ClientId) { - let channel = if let Some(ts) = &self.teamspeak { - ts.channel_path_of_user(id) - } else { - String::from("local") + let channel = self.teamspeak.channel_path_of_user(id); + + let (name, name_index) = { + let mut available_names = self.available_names.lock().expect("Mutex was not poisoned"); + let mut rng = self.rng.lock().expect("Mutex was not poisoned"); + available_names.shuffle(&mut *rng); + let name_index = match available_names.pop() { + Some(v) => v, + None => { + self.teamspeak.send_message_to_user( + id, + "Out of names. Too many bots are already connected!", + ); + return; + } + }; + + (self.config.names[name_index].clone(), name_index) }; - let preset = self.config.bots[0].clone(); - let name = format!("{}({})", preset.name, self.config.name); + let (id, id_index) = { + let mut available_ids = self.available_ids.lock().expect("Mutex was not poisoned"); + let mut rng = self.rng.lock().expect("Mutex was not poisoned"); + available_ids.shuffle(&mut *rng); + let id_index = match available_ids.pop() { + Some(v) => v, + None => { + self.teamspeak.send_message_to_user( + id, + "Out of identities. Too many bots are already connected!", + ); + return; + } + }; + + (self.config.ids[id_index].clone(), id_index) + }; let cconnected_bots = self.connected_bots.clone(); - let disconnect_cb = Box::new(move |n| { + let cavailable_names = self.available_names.clone(); + let cavailable_ids = self.available_ids.clone(); + let disconnect_cb = Box::new(move |n, name_index, id_index| { let mut bots = cconnected_bots.lock().expect("Mutex was not poisoned"); bots.remove(&n); + cavailable_names.lock().expect("Mutex was not poisoned").push(name_index); + cavailable_ids.lock().expect("Mutex was not poisoned").push(id_index); }); info!("Connecting to {} on {}", channel, self.config.address); let bot_args = MusicBotArgs { name: name.clone(), - owner: preset.owner, + name_index, + id_index, local: self.config.local, address: self.config.address.clone(), - id: preset.id, + id, channel, verbose: self.config.verbose, disconnect_cb, @@ -128,15 +164,16 @@ impl MasterBot { #[derive(Debug, Serialize, Deserialize)] pub struct MasterArgs { #[serde(default = "default_name")] - pub name: String, + pub master_name: String, #[serde(default = "default_local")] pub local: bool, pub address: String, pub channel: Option, #[serde(default = "default_verbose")] pub verbose: u8, + pub names: Vec, pub id: Identity, - pub bots: Vec, + pub ids: Vec, } fn default_name() -> String { @@ -163,8 +200,9 @@ impl MasterArgs { }; Self { - name: self.name, - bots: self.bots, + master_name: self.master_name, + names: self.names, + ids: self.ids, local, address, id: self.id, @@ -175,39 +213,10 @@ impl MasterArgs { } pub struct MasterConfig { - pub name: String, + pub master_name: String, pub address: String, - pub bots: Vec, + pub names: Vec, + pub ids: Vec, pub local: bool, pub verbose: u8, } - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BotConfig { - pub name: String, - #[serde( - deserialize_with = "client_id_deserialize", - serialize_with = "client_id_serialize" - )] - pub owner: Option, - pub id: Identity, -} - -fn client_id_serialize(c: &Option, s: S) -> Result -where - S: serde::Serializer, -{ - match c { - Some(c) => s.serialize_some(&c.0), - None => s.serialize_none(), - } -} - -fn client_id_deserialize<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let id: Option = Deserialize::deserialize(deserializer)?; - - Ok(id.map(|id| ClientId(id))) -} diff --git a/src/bot/music.rs b/src/bot/music.rs index 94e7350..fd1a7da 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -54,13 +54,14 @@ pub struct MusicBot { pub struct MusicBotArgs { pub name: String, - pub owner: Option, + pub name_index: usize, + pub id_index: usize, pub local: bool, pub address: String, pub id: Identity, pub channel: String, pub verbose: u8, - pub disconnect_cb: Box, + pub disconnect_cb: Box, } impl MusicBot { @@ -77,7 +78,7 @@ impl MusicBot { let con_config = ConnectOptions::new(args.address) .version(tsclientlib::Version::Linux_3_3_2) - .name(args.name.clone()) + .name(format!("🎵 {}", args.name)) .identity(args.id) .log_commands(args.verbose >= 1) .log_packets(args.verbose >= 2) @@ -122,12 +123,14 @@ impl MusicBot { let cbot = bot.clone(); let mut disconnect_cb = args.disconnect_cb; let name = args.name; + let name_index = args.name_index; + let id_index = args.id_index; let msg_loop = async move { 'outer: loop { while let Some(msg) = rx.recv().await { if let MusicBotMessage::Quit(reason) = msg { cbot.with_teamspeak(|ts| ts.disconnect(&reason)); - disconnect_cb(name); + disconnect_cb(name, name_index, id_index); break 'outer; } cbot.on_message(msg).await.unwrap(); @@ -294,13 +297,13 @@ impl MusicBot { if *current_state != state { match state { State::Playing => { - self.set_nickname(&format!("{} - Playing", self.name)); + self.set_nickname(&format!("🎵 {} - Playing", self.name)); } State::Paused => { - self.set_nickname(&format!("{} - Paused", self.name)); + self.set_nickname(&format!("🎵 {} - Paused", self.name)); } State::Stopped => { - self.set_nickname(&self.name); + self.set_nickname(&format!("🎵 {}", self.name)); self.set_description(""); } State::EndOfStream => { @@ -310,7 +313,7 @@ impl MusicBot { self.start_playing_audio(request); } else { - self.set_nickname(&self.name); + self.set_nickname(&format!("🎵 {}", self.name)); self.set_description(""); } } diff --git a/src/main.rs b/src/main.rs index bff40f8..ae1bed7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ mod playlist; mod teamspeak; mod youtube_dl; -use bot::{BotConfig, MasterArgs, MasterBot}; +use bot::{MasterArgs, MasterBot}; #[derive(StructOpt, Debug)] #[structopt(raw(global_settings = "&[AppSettings::ColoredHelp]"))] @@ -61,22 +61,18 @@ pub struct Args { } fn main() { - //let example = BotConfig { - //name: String::from("asd"), - //id: Identity::create().unwrap(), - //owner: Some(ClientId(12)), - //}; - //let bots = vec![example]; + //let ids = vec![Identity::create().unwrap()]; //println!( //"{}", //toml::to_string(&MasterArgs { - //name: String::from("PokeBot"), + //master_name: String::from("PokeBot"), //id: Identity::create().unwrap(), + //names: vec![String::from("test")], //address: String::from("localhost"), //channel: Some(String::from("Poke If Needed")), //local: false, //verbose: 0, - //bots, + //ids, //}) //.map_err(|e| panic!(e.to_string())) //.unwrap() @@ -102,14 +98,9 @@ fn run() -> Result<(), Box> { let mut config: MasterArgs = toml::from_str(&toml)?; if let Some(count) = args.gen_id_count { - for i in 0..count { + for _ in 0..count { let id = Identity::create().expect("Failed to create id"); - let bot = BotConfig { - name: format!("{}", i), - owner: None, - id, - }; - config.bots.push(bot); + config.ids.push(id); } let toml = toml::to_string(&config)?; diff --git a/src/teamspeak.rs b/src/teamspeak.rs index f49f7a6..5dd80ba 100644 --- a/src/teamspeak.rs +++ b/src/teamspeak.rs @@ -197,6 +197,16 @@ impl TeamSpeakConnection { ); } + pub fn send_message_to_user(&self, id: ClientId, text: &str) { + tokio::spawn( + self.conn + .lock() + .to_mut() + .send_message(MessageTarget::Client(id), text) + .map_err(|e| error!("Failed to send message: {}", e)), + ); + } + pub fn disconnect(&self, reason: &str) { let opt = DisconnectOptions::new() .reason(Reason::Clientdisconnect) -- cgit v1.2.3-70-g09d2 From 09fc3800030b971da8c68217c22f5a0444d7b278 Mon Sep 17 00:00:00 2001 From: Jokler Date: Sun, 26 Jan 2020 17:53:41 +0100 Subject: Move duplicate code into on_client_left method --- src/bot/music.rs | 17 +++++++++-------- src/main.rs | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) (limited to 'src/bot/music.rs') diff --git a/src/bot/music.rs b/src/bot/music.rs index fd1a7da..0bb5274 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -336,17 +336,11 @@ impl MusicBot { client: _, old_channel, } => { - let my_channel = self.my_channel(); - if old_channel == my_channel && self.user_count(my_channel) <= 1 { - self.quit(String::from("Channel is empty")); - } + self.on_client_left_channel(old_channel); } MusicBotMessage::ClientDisconnected { id: _, client } => { let old_channel = client.channel; - let my_channel = self.my_channel(); - if old_channel == my_channel && self.user_count(my_channel) <= 1 { - self.quit(String::from("Channel is empty")); - } + self.on_client_left_channel(old_channel); } MusicBotMessage::StateChange(state) => { self.on_state(state)?; @@ -357,6 +351,13 @@ impl MusicBot { Ok(()) } + fn on_client_left_channel(&self, old_channel: ChannelId) { + let my_channel = self.my_channel(); + if old_channel == my_channel && self.user_count(my_channel) <= 1 { + self.quit(String::from("Channel is empty")); + } + } + pub fn quit(&self, reason: String) { self.player.quit(reason); } diff --git a/src/main.rs b/src/main.rs index ae1bed7..d24b6b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,7 @@ fn run() -> Result<(), Box> { let toml = toml::to_string(&config)?; let mut file = File::create(&args.config_path)?; file.write_all(toml.as_bytes())?; + return Ok(()); } -- cgit v1.2.3-70-g09d2 From 985d6bd787c07ea65804d5c079537fb8d805075f Mon Sep 17 00:00:00 2001 From: Jokler Date: Tue, 28 Jan 2020 03:04:58 +0100 Subject: Block bots from joining channels with other bots --- src/bot/master.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++----- src/bot/music.rs | 6 +++++- src/teamspeak.rs | 10 +++++++--- 3 files changed, 56 insertions(+), 9 deletions(-) (limited to 'src/bot/music.rs') diff --git a/src/bot/master.rs b/src/bot/master.rs index 1f1ddfb..3e56b1a 100644 --- a/src/bot/master.rs +++ b/src/bot/master.rs @@ -82,7 +82,40 @@ impl MasterBot { } async fn spawn_bot(&self, id: ClientId) { - let channel = self.teamspeak.channel_path_of_user(id); + let channel = self + .teamspeak + .channel_of_user(id) + .expect("Can find poke sender"); + + if channel == self.teamspeak.my_channel() { + self.teamspeak.send_message_to_user( + id, + &format!( + "Joining the channel of \"{}\" is not allowed", + self.config.master_name + ), + ); + return; + } + + for (_, bot) in &*self.connected_bots.lock().expect("Mutex was not poisoned") { + if bot.my_channel() == channel { + self.teamspeak.send_message_to_user( + id, + &format!( + "\"{}\" is already in this channel. \ + Multiple bots in one channel are not allowed.", + bot.name() + ), + ); + return; + } + } + + let channel_path = self + .teamspeak + .channel_path_of_user(id) + .expect("can find poke sender"); let (name, name_index) = { let mut available_names = self.available_names.lock().expect("Mutex was not poisoned"); @@ -126,11 +159,17 @@ impl MasterBot { let disconnect_cb = Box::new(move |n, name_index, id_index| { let mut bots = cconnected_bots.lock().expect("Mutex was not poisoned"); bots.remove(&n); - cavailable_names.lock().expect("Mutex was not poisoned").push(name_index); - cavailable_ids.lock().expect("Mutex was not poisoned").push(id_index); + cavailable_names + .lock() + .expect("Mutex was not poisoned") + .push(name_index); + cavailable_ids + .lock() + .expect("Mutex was not poisoned") + .push(id_index); }); - info!("Connecting to {} on {}", channel, self.config.address); + info!("Connecting to {} on {}", channel_path, self.config.address); let bot_args = MusicBotArgs { name: name.clone(), name_index, @@ -138,7 +177,7 @@ impl MasterBot { local: self.config.local, address: self.config.address.clone(), id, - channel, + channel: channel_path, verbose: self.config.verbose, disconnect_cb, }; diff --git a/src/bot/music.rs b/src/bot/music.rs index 0bb5274..821087c 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -190,7 +190,11 @@ impl MusicBot { } } - fn my_channel(&self) -> ChannelId { + pub fn name(&self) -> &str { + &self.name + } + + pub fn my_channel(&self) -> ChannelId { self.teamspeak .as_ref() .map(|ts| ts.my_channel()) diff --git a/src/teamspeak.rs b/src/teamspeak.rs index 5dd80ba..b429869 100644 --- a/src/teamspeak.rs +++ b/src/teamspeak.rs @@ -115,10 +115,14 @@ impl TeamSpeakConnection { tokio::run(send_packet); } - pub fn channel_path_of_user(&self, id: ClientId) -> String { + pub fn channel_of_user(&self, id: ClientId) -> Option { + Some(self.conn.lock().clients.get(&id)?.channel) + } + + pub fn channel_path_of_user(&self, id: ClientId) -> Option { let conn = self.conn.lock(); - let channel_id = conn.clients.get(&id).expect("can find poke sender").channel; + let channel_id = conn.clients.get(&id)?.channel; let mut channel = conn .channels @@ -142,7 +146,7 @@ impl TeamSpeakConnection { path.push_str(name); } - path + Some(path) } pub fn my_channel(&self) -> ChannelId { -- cgit v1.2.3-70-g09d2 From 33f9989df92deebf589b1d0384a6fe485a5492d9 Mon Sep 17 00:00:00 2001 From: Jokler Date: Tue, 28 Jan 2020 18:42:33 +0100 Subject: Make local mode spawn a music bot directly --- src/bot/music.rs | 3 ++- src/main.rs | 29 +++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) (limited to 'src/bot/music.rs') diff --git a/src/bot/music.rs b/src/bot/music.rs index 821087c..dee1514 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -368,6 +368,7 @@ impl MusicBot { } fn spawn_stdin_reader(tx: Arc>>) { + debug!("Spawning stdin reader thread"); thread::spawn(move || { let stdin = ::std::io::stdin(); let lock = stdin.lock(); @@ -375,7 +376,7 @@ fn spawn_stdin_reader(tx: Arc>>) { let line = line.unwrap(); let message = MusicBotMessage::TextMessage(Message { - target: MessageTarget::Server, + target: MessageTarget::Channel, invoker: Invoker { name: String::from("stdin"), id: ClientId(0), diff --git a/src/main.rs b/src/main.rs index 7f6fbf4..922162f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use std::io::{Read, Write}; use std::path::PathBuf; use futures::future::{FutureExt, TryFutureExt}; -use futures01::future::Future as Future01; use log::{debug, info}; use structopt::clap::AppSettings; use structopt::StructOpt; @@ -16,7 +15,7 @@ mod playlist; mod teamspeak; mod youtube_dl; -use bot::{MasterArgs, MasterBot}; +use bot::{MasterArgs, MasterBot, MusicBot, MusicBotArgs}; #[derive(StructOpt, Debug)] #[structopt(raw(global_settings = "&[AppSettings::ColoredHelp]"))] @@ -69,8 +68,6 @@ fn main() { fn run() -> Result<(), Box> { log4rs::init_file("log4rs.yml", Default::default()).unwrap(); - info!("Starting PokeBot!"); - // Parse command line options let args = Args::from_args(); @@ -95,12 +92,32 @@ fn run() -> Result<(), Box> { let bot_args = config.merge(args); + info!("Starting PokeBot!"); debug!("Received CLI arguments: {:?}", std::env::args()); tokio::run( async { - let (_, fut) = MasterBot::new(bot_args).await; - tokio::spawn(fut.unit_error().boxed().compat().map(|_| ())); + 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; + } } .unit_error() .boxed() -- cgit v1.2.3-70-g09d2