From dbaaeb68bdf4143b9dca55b2baa86a2656a3249e Mon Sep 17 00:00:00 2001 From: Felix Kaaman Date: Sat, 22 Feb 2020 19:24:05 +0200 Subject: Add functions for BBCode formatting --- src/bot/music.rs | 9 +- src/teamspeak.rs | 228 ----------------------------------------------- src/teamspeak/bbcode.rs | 42 +++++++++ src/teamspeak/mod.rs | 232 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 232 deletions(-) delete mode 100644 src/teamspeak.rs create mode 100644 src/teamspeak/bbcode.rs create mode 100644 src/teamspeak/mod.rs diff --git a/src/bot/music.rs b/src/bot/music.rs index 75e61de..2539695 100644 --- a/src/bot/music.rs +++ b/src/bot/music.rs @@ -12,8 +12,9 @@ use tsclientlib::{data, ChannelId, ClientId, ConnectOptions, Identity, Invoker, use crate::audio_player::{AudioPlayer, AudioPlayerError, PollResult, Seek}; use crate::command::Command; use crate::playlist::Playlist; -use crate::teamspeak::TeamSpeakConnection; +use crate::teamspeak as ts; use crate::youtube_dl::AudioMetadata; +use ts::TeamSpeakConnection; #[derive(Debug)] pub struct Message { @@ -173,7 +174,7 @@ impl MusicBot { fn start_playing_audio(&self, metadata: AudioMetadata) { if let Some(title) = metadata.title { - self.send_message(&format!("Playing '{}'", title)); + self.send_message(&format!("Playing {}", ts::underline(&title))); self.set_description(&format!("Currently playing '{}'", title)); } else { self.send_message("Playing unknown title"); @@ -198,7 +199,7 @@ impl MusicBot { } } else { if let Some(title) = metadata.title { - self.send_message(&format!("Added '{}' to playlist", title)); + self.send_message(&format!("Added {} to playlist", ts::underline(&title))); } else { self.send_message("Added to playlist"); } @@ -293,7 +294,7 @@ impl MusicBot { Command::Seek { amount } => { if let Ok(seek) = parse_seek(&amount) { if let Ok(time) = self.player.seek(seek) { - self.send_message(&format!("New position: {}", time)); + self.send_message(&format!("New position: {}", ts::bold(&time))); } else { self.send_message("Failed to seek"); } diff --git a/src/teamspeak.rs b/src/teamspeak.rs deleted file mode 100644 index b429869..0000000 --- a/src/teamspeak.rs +++ /dev/null @@ -1,228 +0,0 @@ -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; - -use futures::compat::Future01CompatExt; -use futures01::{future::Future, sink::Sink}; -use tokio02::sync::mpsc::UnboundedSender; - -use tsclientlib::Event::ConEvents; -use tsclientlib::{ - events::Event, ChannelId, ClientId, ConnectOptions, Connection, DisconnectOptions, - MessageTarget, Reason, -}; - -use log::error; - -use crate::bot::{Message, MusicBotMessage}; - -pub struct TeamSpeakConnection { - conn: Connection, -} - -fn get_message<'a>(event: &Event) -> Option { - use tsclientlib::events::{PropertyId, PropertyValue}; - - match event { - Event::Message { - from: target, - invoker: sender, - message: msg, - } => 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, - } -} - -impl TeamSpeakConnection { - pub async fn new( - tx: Arc>>, - options: ConnectOptions, - ) -> Result { - let conn = Connection::new(options).compat().await?; - let packet = conn.lock().server.set_subscribed(true); - conn.send_packet(packet).compat().await.unwrap(); - - conn.add_event_listener( - String::from("listener"), - Box::new(move |e| { - if let ConEvents(_conn, events) = e { - for event in *events { - if let Some(msg) = get_message(event) { - let tx = tx.lock().expect("Mutex was not poisoned"); - // Ignore the result because the receiver might get dropped first. - let _ = tx.send(msg); - } - } - } - }), - ); - - Ok(TeamSpeakConnection { conn }) - } - - pub fn send_audio_packet(&self, samples: &[u8]) { - let packet = - tsproto_packets::packets::OutAudio::new(&tsproto_packets::packets::AudioData::C2S { - id: 0, - codec: tsproto_packets::packets::CodecType::OpusMusic, - data: samples, - }); - - let send_packet = self - .conn - .get_packet_sink() - .send(packet) - .map(|_| ()) - .map_err(|_| error!("Failed to send voice packet")); - - tokio::run(send_packet); - } - - 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)?.channel; - - let mut channel = conn - .channels - .get(&channel_id) - .expect("can find user channel"); - - let mut names = vec![&channel.name[..]]; - - // Channel 0 is the root channel - while channel.parent != ChannelId(0) { - names.push("/"); - channel = conn - .channels - .get(&channel.parent) - .expect("can find user channel"); - names.push(&channel.name); - } - - let mut path = String::new(); - while let Some(name) = names.pop() { - path.push_str(name); - } - - Some(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 - .lock() - .to_mut() - .set_name(name) - .map_err(|e| error!("Failed to set nickname: {}", e)), - ); - } - - pub fn set_description(&self, desc: &str) { - tokio::spawn( - self.conn - .lock() - .to_mut() - .get_client(&self.conn.lock().own_client) - .expect("Can get myself") - .set_description(desc) - .map_err(|e| error!("Failed to change description: {}", e)), - ); - } - - pub fn send_message_to_channel(&self, text: &str) { - tokio::spawn( - self.conn - .lock() - .to_mut() - .send_message(MessageTarget::Channel, text) - .map_err(|e| error!("Failed to send message: {}", e)), - ); - } - - 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) - .message(reason); - tokio::spawn( - self.conn - .disconnect(opt) - .map_err(|e| error!("Failed to send message: {}", e)), - ); - // Might or might not be required to keep tokio running while the bot disconnects - tokio::spawn( - tokio::timer::Delay::new(Instant::now() + Duration::from_secs(1)).map_err(|_| ()), - ); - } -} diff --git a/src/teamspeak/bbcode.rs b/src/teamspeak/bbcode.rs new file mode 100644 index 0000000..28be08a --- /dev/null +++ b/src/teamspeak/bbcode.rs @@ -0,0 +1,42 @@ +use std::fmt::{Formatter, Display, Error}; + +#[allow(dead_code)] +pub enum BbCode<'a> { + Bold(&'a dyn Display), + Italic(&'a dyn Display), + Underline(&'a dyn Display), + Link(&'a dyn Display, &'a str), +} + +impl<'a> Display for BbCode<'a> { + fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> { + match self { + BbCode::Bold(text) => fmt.write_fmt(format_args!("[B]{}[/B]", text))?, + BbCode::Italic(text) => fmt.write_fmt(format_args!("[I]{}[/I]", text))?, + BbCode::Underline(text) => fmt.write_fmt(format_args!("[U]{}[/U]", text))?, + BbCode::Link(text, url) => fmt.write_fmt(format_args!("[URL={}]{}[/URL]", url, text))?, + }; + + Ok(()) + } +} + +#[allow(dead_code)] +pub fn bold(text: &dyn Display) -> BbCode { + BbCode::Bold(text) +} + +#[allow(dead_code)] +pub fn italic(text: &dyn Display) -> BbCode { + BbCode::Italic(text) +} + +#[allow(dead_code)] +pub fn underline(text: &dyn Display) -> BbCode { + BbCode::Underline(text) +} + +#[allow(dead_code)] +pub fn link<'a>(text: &'a dyn Display, url: &'a str) -> BbCode<'a> { + BbCode::Link(text, url) +} diff --git a/src/teamspeak/mod.rs b/src/teamspeak/mod.rs new file mode 100644 index 0000000..5ac0d44 --- /dev/null +++ b/src/teamspeak/mod.rs @@ -0,0 +1,232 @@ +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use futures::compat::Future01CompatExt; +use futures01::{future::Future, sink::Sink}; +use tokio02::sync::mpsc::UnboundedSender; + +use tsclientlib::Event::ConEvents; +use tsclientlib::{ + events::Event, ChannelId, ClientId, ConnectOptions, Connection, DisconnectOptions, + MessageTarget, Reason, +}; + +use log::error; + +use crate::bot::{Message, MusicBotMessage}; + +mod bbcode; + +pub use bbcode::*; + +pub struct TeamSpeakConnection { + conn: Connection, +} + +fn get_message<'a>(event: &Event) -> Option { + use tsclientlib::events::{PropertyId, PropertyValue}; + + match event { + Event::Message { + from: target, + invoker: sender, + message: msg, + } => 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, + } +} + +impl TeamSpeakConnection { + pub async fn new( + tx: Arc>>, + options: ConnectOptions, + ) -> Result { + let conn = Connection::new(options).compat().await?; + let packet = conn.lock().server.set_subscribed(true); + conn.send_packet(packet).compat().await.unwrap(); + + conn.add_event_listener( + String::from("listener"), + Box::new(move |e| { + if let ConEvents(_conn, events) = e { + for event in *events { + if let Some(msg) = get_message(event) { + let tx = tx.lock().expect("Mutex was not poisoned"); + // Ignore the result because the receiver might get dropped first. + let _ = tx.send(msg); + } + } + } + }), + ); + + Ok(TeamSpeakConnection { conn }) + } + + pub fn send_audio_packet(&self, samples: &[u8]) { + let packet = + tsproto_packets::packets::OutAudio::new(&tsproto_packets::packets::AudioData::C2S { + id: 0, + codec: tsproto_packets::packets::CodecType::OpusMusic, + data: samples, + }); + + let send_packet = self + .conn + .get_packet_sink() + .send(packet) + .map(|_| ()) + .map_err(|_| error!("Failed to send voice packet")); + + tokio::run(send_packet); + } + + 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)?.channel; + + let mut channel = conn + .channels + .get(&channel_id) + .expect("can find user channel"); + + let mut names = vec![&channel.name[..]]; + + // Channel 0 is the root channel + while channel.parent != ChannelId(0) { + names.push("/"); + channel = conn + .channels + .get(&channel.parent) + .expect("can find user channel"); + names.push(&channel.name); + } + + let mut path = String::new(); + while let Some(name) = names.pop() { + path.push_str(name); + } + + Some(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 + .lock() + .to_mut() + .set_name(name) + .map_err(|e| error!("Failed to set nickname: {}", e)), + ); + } + + pub fn set_description(&self, desc: &str) { + tokio::spawn( + self.conn + .lock() + .to_mut() + .get_client(&self.conn.lock().own_client) + .expect("Can get myself") + .set_description(desc) + .map_err(|e| error!("Failed to change description: {}", e)), + ); + } + + pub fn send_message_to_channel(&self, text: &str) { + tokio::spawn( + self.conn + .lock() + .to_mut() + .send_message(MessageTarget::Channel, text) + .map_err(|e| error!("Failed to send message: {}", e)), + ); + } + + 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) + .message(reason); + tokio::spawn( + self.conn + .disconnect(opt) + .map_err(|e| error!("Failed to send message: {}", e)), + ); + // Might or might not be required to keep tokio running while the bot disconnects + tokio::spawn( + tokio::timer::Delay::new(Instant::now() + Duration::from_secs(1)).map_err(|_| ()), + ); + } +} -- cgit v1.2.3-70-g09d2