1145 lines
32 KiB
Rust
1145 lines
32 KiB
Rust
//! Example demonstrating how to make use of individual track audio events,
|
|
//! and how to use the `TrackQueue` system.
|
|
//!
|
|
//! Requires the "cache", "standard_framework", and "voice" features be enabled in your
|
|
//! Cargo.toml, like so:
|
|
//!
|
|
//! ```toml
|
|
//! [dependencies.serenity]
|
|
//! git = "https://github.com/serenity-rs/serenity.git"
|
|
//! features = ["cache", "framework", "standard_framework", "voice"]
|
|
//! ```
|
|
use std::{
|
|
env,
|
|
sync::{
|
|
Arc,
|
|
},
|
|
};
|
|
|
|
use reqwest::Client as HttpClient;
|
|
|
|
use serenity::{
|
|
async_trait,
|
|
client::{Client, Context, EventHandler},
|
|
framework::{
|
|
standard::{
|
|
macros::{command, group},
|
|
Args,
|
|
CommandResult,
|
|
Configuration,
|
|
},
|
|
StandardFramework,
|
|
},
|
|
http::Http,
|
|
model::{channel::Message, gateway::Ready, prelude::ChannelId},
|
|
prelude::{GatewayIntents, Mentionable, TypeMapKey},
|
|
Result as SerenityResult, builder::{CreateEmbed, CreateMessage}, gateway::ShardManager,
|
|
};
|
|
|
|
use serenity::all::standard::HelpOptions;
|
|
use serenity::all::standard::CommandGroup;
|
|
use serenity::all::GuildId;
|
|
use serenity::all::UserId;
|
|
use serenity::all::ClientBuilder;
|
|
use serenity::all::Colour;
|
|
use serenity::all::standard::help_commands;
|
|
use serenity::all::standard::macros::help;
|
|
|
|
use songbird::{
|
|
input::YoutubeDl,
|
|
Event,
|
|
EventContext,
|
|
EventHandler as VoiceEventHandler,
|
|
SerenityInit,
|
|
TrackEvent, tracks::Track,
|
|
};
|
|
|
|
struct HttpKey;
|
|
|
|
impl TypeMapKey for HttpKey {
|
|
type Value = HttpClient;
|
|
}
|
|
|
|
struct ShardManagerContainer;
|
|
|
|
impl TypeMapKey for ShardManagerContainer {
|
|
type Value = Arc<ShardManager>;
|
|
}
|
|
|
|
struct Handler;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use tracing::error;
|
|
|
|
use std::fs;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::collections::HashSet;
|
|
use std::collections::HashMap;
|
|
|
|
use base64_stream::FromBase64Writer;
|
|
|
|
use tokio::{sync::Mutex, process::Command};
|
|
use uuid::Uuid;
|
|
|
|
#[async_trait]
|
|
impl EventHandler for Handler {
|
|
async fn ready(&self, _: Context, ready: Ready) {
|
|
println!("{} is connected!", ready.user.name);
|
|
}
|
|
}
|
|
|
|
#[group]
|
|
#[commands(
|
|
leave, mute, play, skip, stop, ping, unmute, volume, vox, chaos, restart, seek, tts, tts_list
|
|
)]
|
|
|
|
struct General;
|
|
|
|
#[help]
|
|
async fn my_help(
|
|
context: &Context,
|
|
msg: &Message,
|
|
args: Args,
|
|
help_options: &'static HelpOptions,
|
|
groups: &[&'static CommandGroup],
|
|
owners: HashSet<UserId>,
|
|
) -> CommandResult {
|
|
let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await;
|
|
Ok(())
|
|
}
|
|
|
|
|
|
/////////
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct WinterGuildOptions {
|
|
volume : f32,
|
|
}
|
|
|
|
impl Default for WinterGuildOptions {
|
|
fn default() -> WinterGuildOptions {
|
|
WinterGuildOptions {
|
|
volume: 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Default, Debug)]
|
|
struct WinterOptions {
|
|
guild_options : HashMap<GuildId, WinterGuildOptions>,
|
|
}
|
|
|
|
impl WinterOptions {
|
|
fn set_volume(&mut self, id: GuildId, volume: f32) {
|
|
self.guild_options.entry(id).or_default().volume = volume;
|
|
self.save_to_file();
|
|
}
|
|
|
|
fn get_volume(&self, id: GuildId) -> f32 {
|
|
if let Some(options) = self.guild_options.get(&id) {
|
|
options.volume
|
|
} else {
|
|
WinterGuildOptions::default().volume
|
|
}
|
|
}
|
|
|
|
fn load_from_file() -> WinterOptions {
|
|
if let Ok(data) = fs::read_to_string("./data/winter_options.json") {
|
|
let options : WinterOptions = serde_json::from_str(&data)
|
|
.expect("winter_options is not valid JSON.");
|
|
options
|
|
} else {
|
|
println!("Using default options.");
|
|
WinterOptions::default()
|
|
}
|
|
}
|
|
|
|
fn save_to_file(&self) {
|
|
let data = serde_json::to_string(&self);
|
|
if data.is_err() {
|
|
println!("Failed to serialize options file.");
|
|
return;
|
|
}
|
|
|
|
if fs::write("./data/winter_options.json", data.unwrap()).is_err() {
|
|
println!("Failed to write options file.");
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Winter {
|
|
options : WinterOptions,
|
|
}
|
|
|
|
impl Winter {
|
|
pub fn serenity() -> Arc<Mutex<Self>> {
|
|
Arc::new(Mutex::new(Self {
|
|
options: WinterOptions::load_from_file()
|
|
}))
|
|
}
|
|
}
|
|
|
|
struct WinterKey;
|
|
|
|
impl TypeMapKey for WinterKey {
|
|
type Value = Arc<Mutex<Winter>>;
|
|
}
|
|
pub trait WinterSerenityInit {
|
|
fn register_winter(self) -> Self;
|
|
fn register_winter_with(self, voice: Arc<Mutex<Winter>>) -> Self;
|
|
}
|
|
|
|
impl WinterSerenityInit for ClientBuilder {
|
|
fn register_winter(self) -> Self {
|
|
register(self)
|
|
}
|
|
|
|
fn register_winter_with(self, voice: Arc<Mutex<Winter>>) -> Self {
|
|
register_with(self, voice)
|
|
}
|
|
}
|
|
|
|
pub fn register(client_builder: ClientBuilder) -> ClientBuilder {
|
|
let voice = Winter::serenity();
|
|
register_with(client_builder, voice)
|
|
}
|
|
|
|
pub fn register_with(client_builder: ClientBuilder, voice: Arc<Mutex<Winter>>) -> ClientBuilder {
|
|
client_builder
|
|
.type_map_insert::<WinterKey>(voice)
|
|
}
|
|
|
|
pub async fn winter_get(ctx: &Context) -> Option<Arc<Mutex<Winter>>> {
|
|
let data = ctx.data.read().await;
|
|
|
|
return data.get::<WinterKey>().cloned();
|
|
}
|
|
|
|
/////////
|
|
|
|
async fn get_http_client(ctx: &Context) -> HttpClient {
|
|
let data = ctx.data.read().await;
|
|
data.get::<HttpKey>()
|
|
.cloned()
|
|
.expect("Guaranteed to exist in the typemap.")
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
// Configure the client with your Discord bot token in the environment.
|
|
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
|
|
|
let framework = StandardFramework::new().group(&GENERAL_GROUP);
|
|
framework.configure(Configuration::new().prefix("~"));
|
|
|
|
let intents = GatewayIntents::non_privileged()
|
|
| GatewayIntents::MESSAGE_CONTENT;
|
|
|
|
let mut client = Client::builder(&token, intents)
|
|
.event_handler(Handler)
|
|
.framework(framework)
|
|
.register_songbird()
|
|
.register_winter()
|
|
.type_map_insert::<HttpKey>(HttpClient::new())
|
|
.await
|
|
.expect("Err creating client");
|
|
|
|
{
|
|
let mut data = client.data.write().await;
|
|
data.insert::<ShardManagerContainer>(client.shard_manager.clone());
|
|
}
|
|
|
|
let shard_manager = client.shard_manager.clone();
|
|
|
|
tokio::spawn(async move {
|
|
tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler");
|
|
shard_manager.shutdown_all().await;
|
|
});
|
|
|
|
if let Err(why) = client.start().await {
|
|
error!("Client error: {:?}", why);
|
|
}
|
|
}
|
|
|
|
async fn ensure_joined(ctx: &Context, msg: &Message) -> bool {
|
|
let (guild_id, channel_id) = {
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let channel_id = guild
|
|
.voice_states
|
|
.get(&msg.author.id)
|
|
.and_then(|voice_state| voice_state.channel_id);
|
|
|
|
(guild.id, channel_id)
|
|
};
|
|
|
|
let connect_to = match channel_id {
|
|
Some(channel) => channel,
|
|
None => {
|
|
reply(&ctx, &msg, "No channel", "You are not in any voice channel!", None, true).await;
|
|
return false;
|
|
},
|
|
};
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let winter = winter_get(ctx)
|
|
.await
|
|
.expect("Winter placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let handler = handler_lock.lock().await;
|
|
|
|
if handler.current_channel().is_some() {
|
|
if handler.current_channel().unwrap() == songbird::id::ChannelId::from(connect_to) {
|
|
return true;
|
|
} else {
|
|
reply(&ctx, &msg, "Wrong channel, silly!", &format!("You must to be in my channel, {}, to play music!", connect_to.mention()), None, true).await;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, &format!("Joined {}", connect_to.mention()))
|
|
.await,
|
|
);
|
|
|
|
let chan_id = msg.channel_id;
|
|
|
|
let send_http = ctx.http.clone();
|
|
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
if !handler.is_deaf() {
|
|
if let Err(e) = handler.deafen(true).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
}
|
|
|
|
handler.add_global_event(
|
|
Event::Track(TrackEvent::End),
|
|
TrackEndNotifier {
|
|
chan_id,
|
|
http: send_http,
|
|
},
|
|
);
|
|
|
|
handler.add_global_event(
|
|
Event::Track(TrackEvent::End),
|
|
TrackVolumeUpdater {
|
|
winter,
|
|
guild_id,
|
|
},
|
|
);
|
|
|
|
return true;
|
|
} else {
|
|
reply(&ctx, &msg, "I'm not allowed here!", "Failed to join the channel.", None, true).await;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
struct TrackVolumeUpdater {
|
|
winter: Arc<Mutex<Winter>>,
|
|
guild_id: GuildId,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for TrackVolumeUpdater {
|
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
|
// Update the volume for the track we are about to play.
|
|
if let EventContext::Track(track_list) = ctx {
|
|
if track_list.len() > 1 {
|
|
let winter = self.winter.lock().await;
|
|
let volume = winter.options.get_volume(self.guild_id);
|
|
let _ = track_list[1].1.set_volume(volume);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
struct TrackEndNotifier {
|
|
chan_id: ChannelId,
|
|
http: Arc<Http>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for TrackEndNotifier {
|
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
|
if let EventContext::Track(track_list) = ctx {
|
|
check_msg(
|
|
self.chan_id
|
|
.say(&self.http, &format!("Tracks ended: {}.", track_list.len()))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
let has_handler = manager.get(guild_id).is_some();
|
|
|
|
if has_handler {
|
|
if let Err(e) = manager.remove(guild_id).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
reply(&ctx, &msg, "Left Voice Channel", "Goodbye! <3", None, false).await;
|
|
} else {
|
|
reply(&ctx, &msg, "Oops!", "I wasn't in a voice channel, so I did nothing", None, true).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let handler_lock = match manager.get(guild_id) {
|
|
Some(handler) => handler,
|
|
None => {
|
|
reply(&ctx, &msg, "No channel", "You are not in any voice channel!", None, true).await;
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
if handler.is_mute() {
|
|
reply(&ctx, &msg, "Aaaaaa!", "I am already being quiet! Why are you being mean to me :<", None, true).await;
|
|
} else {
|
|
if let Err(e) = handler.mute(true).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
reply(&ctx, &msg, "Muted!", "I'll be quiet for now...", None, false).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
|
|
reply(&ctx, &msg, "Ping!", "Hello there! :3", None, false).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct SongEndNotifier {
|
|
chan_id: ChannelId,
|
|
http: Arc<Http>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for SongEndNotifier {
|
|
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
|
|
check_msg(
|
|
self.chan_id
|
|
.say(&self.http, "Song faded out completely!")
|
|
.await,
|
|
);
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
pub struct YtDlpOutput {
|
|
pub artist: Option<String>,
|
|
pub album: Option<String>,
|
|
pub channel: Option<String>,
|
|
pub duration: Option<f64>,
|
|
pub filesize: Option<u64>,
|
|
pub http_headers: Option<HashMap<String, String>>,
|
|
pub release_date: Option<String>,
|
|
pub thumbnail: Option<String>,
|
|
pub title: Option<String>,
|
|
pub track: Option<String>,
|
|
pub upload_date: Option<String>,
|
|
pub uploader: Option<String>,
|
|
pub url: String,
|
|
pub webpage_url: Option<String>,
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let url = args.rest().to_string();
|
|
if url.is_empty() {
|
|
reply(&ctx, &msg, "Tell me what you want!", "You must provide a URL or search term for me to play video or audio!", None, false).await;
|
|
return Ok(());
|
|
}
|
|
|
|
let http_client = get_http_client(ctx).await;
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let winter_lock = winter_get(ctx)
|
|
.await
|
|
.expect("Winter placed in at initialisation.")
|
|
.clone();
|
|
|
|
let winter = winter_lock.lock().await;
|
|
|
|
if !ensure_joined(ctx, msg).await {
|
|
return Ok(());
|
|
}
|
|
|
|
let do_search = !url.starts_with("http");
|
|
|
|
let sources = if do_search {
|
|
let mut vec = Vec::new();
|
|
vec.push(YoutubeDl::new_search(http_client, url));
|
|
vec
|
|
} else {
|
|
let ytdl_args = [
|
|
"-j",
|
|
url.as_str(),
|
|
"--flat-playlist",
|
|
];
|
|
|
|
let mut output = Command::new("yt-dlp")
|
|
.args(ytdl_args)
|
|
.output()
|
|
.await
|
|
.unwrap();
|
|
|
|
if !output.status.success() {
|
|
println!("Fuck! Couldn't run yt-dlp");
|
|
return Ok(());
|
|
}
|
|
|
|
let out = output
|
|
.stdout
|
|
.split_mut(|&b| b == b'\n')
|
|
.filter_map(|x| (!x.is_empty()).then(|| serde_json::from_slice(x)))
|
|
.collect::<Result<Vec<YtDlpOutput>, _>>()
|
|
.unwrap();
|
|
|
|
let mut vec = Vec::new();
|
|
for playlist_src in out {
|
|
vec.push(YoutubeDl::new(http_client.clone(), playlist_src.url));
|
|
}
|
|
vec
|
|
};
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
for source in sources {
|
|
let mut input = songbird::input::Input::from(source);
|
|
|
|
let aux_metadata = input.aux_metadata().await.unwrap();
|
|
|
|
let title = aux_metadata.title.clone().unwrap_or("Unknown Title".to_string());
|
|
let artist = aux_metadata.artist.clone().unwrap_or("Unknown Artist".to_string());
|
|
let thumbnail = aux_metadata.thumbnail.clone();
|
|
let mut duration = "Unknown duration".to_string();
|
|
if aux_metadata.duration.is_some() {
|
|
let meta_duration = aux_metadata.duration.unwrap();
|
|
|
|
let seconds = meta_duration.as_secs() % 60;
|
|
let minutes = (meta_duration.as_secs() / 60) % 60;
|
|
let hours = (meta_duration.as_secs() / 60) / 60;
|
|
duration = format!("{:02}:{:02}:{:02}", hours, minutes, seconds).to_string();
|
|
}
|
|
|
|
let track = songbird::tracks::Track::new(input)
|
|
.volume(winter.options.get_volume(guild_id));
|
|
handler.enqueue(track).await;
|
|
|
|
let mut embed = CreateEmbed::new()
|
|
.title(title.clone())
|
|
.field("Arist", artist, true)
|
|
.field("Duration", duration, true)
|
|
.field("Queue Position", &format!("{:02}", handler.queue().len()), true)
|
|
.color(Colour::from_rgb(202,255,239));
|
|
|
|
if thumbnail.is_some() {
|
|
embed = embed.thumbnail(thumbnail.unwrap());
|
|
}
|
|
|
|
let builder = CreateMessage::new().embed(embed);
|
|
|
|
let msg = msg.channel_id.send_message(&ctx, builder).await;
|
|
|
|
if let Err(why) = msg {
|
|
println!("Error sending message: {:?}", why);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn chaos(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let url = match args.single::<String>() {
|
|
Ok(url) => url,
|
|
Err(_) => {
|
|
reply(&ctx, &msg, "Tell me what you want!", "You must provide a URL or search term for me to play video or audio!", None, false).await;
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
if !url.starts_with("http") {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Must provide a valid URL")
|
|
.await,
|
|
);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
let http_client = get_http_client(ctx).await;
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
|
|
let winter_lock = winter_get(ctx)
|
|
.await
|
|
.expect("Winter placed in at initialisation.")
|
|
.clone();
|
|
|
|
let winter = winter_lock.lock().await;
|
|
|
|
if !ensure_joined(ctx, msg).await {
|
|
return Ok(());
|
|
}
|
|
|
|
let do_search = !url.starts_with("http");
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
let src = if do_search {
|
|
YoutubeDl::new_search(http_client, url)
|
|
} else {
|
|
YoutubeDl::new(http_client, url)
|
|
};
|
|
|
|
check_msg(msg.reply(&ctx.http, "Added song to chaos mode.").await);
|
|
|
|
let track = songbird::tracks::Track::new(src.into())
|
|
.volume(winter.options.get_volume(guild_id));
|
|
handler.play(track);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn vox(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let line = args.rest();
|
|
if line.is_empty() {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Must provide a VOX line")
|
|
.await,
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
let words = line.split(" ");
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
|
|
let winter_lock = winter_get(ctx)
|
|
.await
|
|
.expect("Winter placed in at initialisation.")
|
|
.clone();
|
|
|
|
let winter = winter_lock.lock().await;
|
|
|
|
if !ensure_joined(ctx, msg).await {
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
for (_, word) in words.enumerate() {
|
|
// basic sanity check
|
|
if word.chars().all(|x| x.is_alphanumeric()) {
|
|
let vox_path = format!("./assets/vox/{}.wav", word);
|
|
|
|
let source = songbird::input::File::new(vox_path);
|
|
let track = Track::new(source.into())
|
|
.volume(winter.options.get_volume(guild_id));
|
|
handler.enqueue(track).await;
|
|
}
|
|
}
|
|
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(
|
|
&ctx.http,
|
|
format!("Added vox line: {} to queue! (position {})", line, handler.queue().len()),
|
|
)
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct TikTokPOSTBody {
|
|
text : String,
|
|
voice : String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct TikTokResponse {
|
|
data : String,
|
|
}
|
|
|
|
const TIKTOK_VOICES: &'static [&'static str] = &[
|
|
// DISNEY VOICES
|
|
"en_us_ghostface", // Ghost Face
|
|
"en_us_chewbacca", // Chewbacca
|
|
"en_us_c3po", // C3PO
|
|
"en_us_stitch", // Stitch
|
|
"en_us_stormtrooper", // Stormtrooper
|
|
"en_us_rocket", // Rocket
|
|
|
|
// ENGLISH VOICES
|
|
"en_au_001", // English AU - Female
|
|
"en_au_002", // English AU - Male
|
|
"en_uk_001", // English UK - Male 1
|
|
"en_uk_003", // English UK - Male 2
|
|
"en_us_001", // English US - Female (Int. 1)
|
|
"en_us_002", // English US - Female (Int. 2)
|
|
"en_us_006", // English US - Male 1
|
|
"en_us_007", // English US - Male 2
|
|
"en_us_009", // English US - Male 3
|
|
"en_us_010", // English US - Male 4
|
|
|
|
// EUROPE VOICES
|
|
"fr_001", // French - Male 1
|
|
"fr_002", // French - Male 2
|
|
"de_001", // German - Female
|
|
"de_002", // German - Male
|
|
"es_002", // Spanish - Male
|
|
|
|
// AMERICA VOICES
|
|
"es_mx_002", // Spanish MX - Male
|
|
"br_001", // Portuguese BR - Female 1
|
|
"br_003", // Portuguese BR - Female 2
|
|
"br_004", // Portuguese BR - Female 3
|
|
"br_005", // Portuguese BR - Male
|
|
|
|
// ASIA VOICES
|
|
"id_001", // Indonesian - Female
|
|
"jp_001", // Japanese - Female 1
|
|
"jp_003", // Japanese - Female 2
|
|
"jp_005", // Japanese - Female 3
|
|
"jp_006", // Japanese - Male
|
|
"kr_002", // Korean - Male 1
|
|
"kr_003", // Korean - Female
|
|
"kr_004", // Korean - Male 2
|
|
|
|
// SINGING VOICES
|
|
"en_female_f08_salut_damour", // Alto
|
|
"en_male_m03_lobby", // Tenor
|
|
"en_female_f08_warmy_breeze", // Warmy Breeze
|
|
"en_male_m03_sunshine_soon", // Sunshine Soon
|
|
|
|
// OTHER
|
|
"en_male_narration", // narrator
|
|
"en_male_funny", // wacky
|
|
"en_female_emotional" // peaceful
|
|
];
|
|
|
|
use rand::seq::SliceRandom;
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn tts_list(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|
let mut voice_list : String = "".to_string();
|
|
for voice in TIKTOK_VOICES {
|
|
voice_list += voice;
|
|
voice_list += "\n";
|
|
}
|
|
reply(&ctx, &msg, "TikTok TTS Voices", voice_list.as_str(), None, false).await;
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn tts(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let mut line = args.rest();
|
|
if line.is_empty() {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Must provide a TTS line")
|
|
.await,
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
line = line.trim();
|
|
|
|
let mut voice = TIKTOK_VOICES.choose(&mut rand::thread_rng()).unwrap().to_string();
|
|
|
|
if line.starts_with(".") {
|
|
match line.split_once(' ') {
|
|
Some((key, value)) => {
|
|
voice = key[1..key.len()].to_string();
|
|
line = value;
|
|
}
|
|
None => {
|
|
reply(&ctx, &msg, "Oh no!", "No text for TTS!", None, true).await;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let winter_lock = winter_get(ctx)
|
|
.await
|
|
.expect("Winter placed in at initialisation.")
|
|
.clone();
|
|
|
|
let winter = winter_lock.lock().await;
|
|
|
|
if !ensure_joined(ctx, msg).await {
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
let args = TikTokPOSTBody {
|
|
text: line.to_string(),
|
|
voice: voice.clone(),
|
|
};
|
|
|
|
let json = serde_json::to_string(&args).expect("Failed to serialize TikTokPOST Body");
|
|
|
|
let client = reqwest::Client::new();
|
|
let res = client
|
|
.post("https://tiktok-tts.weilnet.workers.dev/api/generation")
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
.body(json)
|
|
.send()
|
|
.await?;
|
|
|
|
let response : TikTokResponse = match serde_json::from_str(res.text().await?.as_str()) {
|
|
Ok(response) => response,
|
|
Err(why) => {
|
|
reply(&ctx, &msg, "Oh no!", &format!("Error querying tts. Reason: {:?}", why), None, true).await;
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let file_path = format!("/tmp/tts{}.wav", Uuid::new_v4().to_string());
|
|
let tts_data = File::create(file_path.clone()).unwrap();
|
|
|
|
let mut writer = FromBase64Writer::new(tts_data);
|
|
writer.write_all(response.data.as_bytes()).unwrap();
|
|
writer.flush().unwrap();
|
|
|
|
let source = songbird::input::File::new(file_path);
|
|
let track = Track::new(source.into())
|
|
.volume(winter.options.get_volume(guild_id));
|
|
|
|
handler.play(track);
|
|
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(
|
|
&ctx.http,
|
|
format!("Playing TTS line: {} with voice: {}", line, voice),
|
|
)
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let handler = handler_lock.lock().await;
|
|
let queue = handler.queue();
|
|
let _ = queue.skip();
|
|
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(
|
|
&ctx.http,
|
|
format!("Song skipped: {} in queue.", queue.len()),
|
|
)
|
|
.await,
|
|
);
|
|
} else {
|
|
reply(&ctx, &msg, "No channel", "You are not in any voice channel!", None, true).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn stop(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
let queue = handler.queue();
|
|
let _ = queue.stop();
|
|
let _ = handler.stop();
|
|
|
|
reply(&ctx, &msg, "Empty inside", "Queue + chaos cleared.", None, false).await;
|
|
} else {
|
|
reply(&ctx, &msg, "No channel", "You are not in any voice channel!", None, true).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
if let Err(e) = handler.mute(false).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
reply(&ctx, &msg, "I am free!", "I am unmuted now! :3", None, false).await;
|
|
} else {
|
|
reply(&ctx, &msg, "No channel", "You are not in any voice channel!", None, true).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn volume(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let volume_string = args.rest();
|
|
if volume_string.is_empty() {
|
|
reply(&ctx, &msg, "Tell me what you want!", "You need to tell me a volume expression in percentage.", None, true).await;
|
|
return Ok(());
|
|
}
|
|
let volume_human_eval = meval::eval_str(volume_string);
|
|
if !volume_human_eval.is_ok() {
|
|
reply(&ctx, &msg, "/shrug", "Couldn't evaluate volume expression.", None, true).await;
|
|
return Ok(());
|
|
}
|
|
let volume_human = volume_human_eval.unwrap() as f32;
|
|
let volume = volume_human / 100.0;
|
|
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let winter_lock = winter_get(ctx)
|
|
.await
|
|
.expect("Winter placed in at initialisation.")
|
|
.clone();
|
|
|
|
let mut winter = winter_lock.lock().await;
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let handler = handler_lock.lock().await;
|
|
|
|
for (_, track) in handler.queue().current_queue().iter().enumerate() {
|
|
if !track.set_volume(volume).is_ok() {
|
|
reply(&ctx, &msg, "IDK!", "Failed to set current volume.", None, true).await;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
winter.options.set_volume(guild_id, volume);
|
|
reply(&ctx, &msg, "Am I being too loud?", &format!("Set volume to {}%.", volume_human), None, false).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn seek(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let seek_string = args.rest();
|
|
if seek_string.is_empty() {
|
|
reply(&ctx, &msg, "Tell me what you want!", "You need to tell me a position expression in seconds.", None, true).await;
|
|
return Ok(());
|
|
}
|
|
let seek_human_eval = meval::eval_str(seek_string);
|
|
if !seek_human_eval.is_ok() {
|
|
reply(&ctx, &msg, "/shrug", "Couldn't evaluate seek expression.", None, true).await;
|
|
return Ok(());
|
|
}
|
|
let seek_human = seek_human_eval.unwrap() as f32;
|
|
|
|
let seek = std::time::Duration::from_secs_f32(seek_human);
|
|
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let handler = handler_lock.lock().await;
|
|
|
|
for (_, track) in handler.queue().current_queue().iter().enumerate() {
|
|
let _success = track.seek(seek);
|
|
}
|
|
}
|
|
|
|
let seconds = seek.as_secs() % 60;
|
|
let minutes = (seek.as_secs() / 60) % 60;
|
|
let hours = (seek.as_secs() / 60) / 60;
|
|
reply(&ctx, &msg, "I'm moving!", &format!("Seeked to to {:02}:{:02}:{:02}.", hours, minutes, seconds), None, false).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn restart(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let data = ctx.data.read().await;
|
|
|
|
if let Some(manager) = data.get::<ShardManagerContainer>() {
|
|
msg.reply(ctx, "Shutting down!").await?;
|
|
manager.shutdown_all().await;
|
|
} else {
|
|
msg.reply(ctx, "There was a problem getting the shard manager").await?;
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks that a message successfully sent; if not, then logs why to stdout.
|
|
fn check_msg(result: SerenityResult<Message>) {
|
|
if let Err(why) = result {
|
|
println!("Error sending message: {:?}", why);
|
|
}
|
|
}
|
|
|
|
async fn reply<S :Into<String>>(ctx: &Context, context_msg: &Message, title: S, desc: S, image: Option<S>, error: bool) {
|
|
let mut embed = CreateEmbed::new()
|
|
.title(title.into())
|
|
.description(desc.into());
|
|
|
|
if image.is_some() {
|
|
embed = embed.image(image.unwrap().into());
|
|
}
|
|
|
|
if error {
|
|
embed = embed.color(Colour::from_rgb(255,218,218));
|
|
} else {
|
|
embed = embed.color(Colour::from_rgb(223,255,198));
|
|
}
|
|
|
|
let builder = CreateMessage::new().embed(embed);
|
|
|
|
let msg = context_msg.channel_id.send_message(&ctx, builder).await;
|
|
|
|
if let Err(why) = msg {
|
|
println!("Error sending message: {:?}", why);
|
|
}
|
|
} |