Added youtube playlist support
This commit is contained in:
parent
1abb492dff
commit
e6f60efbe7
|
@ -7,7 +7,7 @@ use serenity::{
|
||||||
model::{channel::Embed, guild::Member, Permissions},
|
model::{channel::Embed, guild::Member, Permissions},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::data::GuildOptionsKey;
|
use crate::{data::GuildOptionsKey, utils::message::embed_author};
|
||||||
|
|
||||||
pub(crate) async fn handle_interaction(ctx: &Context, object: Value) -> CommandResult {
|
pub(crate) async fn handle_interaction(ctx: &Context, object: Value) -> CommandResult {
|
||||||
let data = object.get("data").ok_or("Failed to get data")?;
|
let data = object.get("data").ok_or("Failed to get data")?;
|
||||||
|
@ -64,7 +64,7 @@ pub(crate) async fn handle_interaction(ctx: &Context, object: Value) -> CommandR
|
||||||
goulag(ctx, data, guild_id, &author_member, id, token).await?
|
goulag(ctx, data, guild_id, &author_member, id, token).await?
|
||||||
} else {
|
} else {
|
||||||
Response::new_with_embed(4, |e| {
|
Response::new_with_embed(4, |e| {
|
||||||
super::embed_author(e, Some(&author_member))
|
embed_author(e, Some(&author_member))
|
||||||
.title("Error")
|
.title("Error")
|
||||||
.description("You don't have the right to do that")
|
.description("You don't have the right to do that")
|
||||||
.colour((247, 76, 0))
|
.colour((247, 76, 0))
|
||||||
|
@ -129,7 +129,7 @@ async fn goulag(
|
||||||
member.add_role(&ctx.http, mute_role).await?;
|
member.add_role(&ctx.http, mute_role).await?;
|
||||||
|
|
||||||
Response::new_with_embed(4, |e| {
|
Response::new_with_embed(4, |e| {
|
||||||
super::embed_author(e, Some(author))
|
embed_author(e, Some(author))
|
||||||
.title("Mute")
|
.title("Mute")
|
||||||
.description(format!("{} was muted", member.display_name()))
|
.description(format!("{} was muted", member.display_name()))
|
||||||
.colour((247, 76, 0))
|
.colour((247, 76, 0))
|
||||||
|
@ -142,7 +142,7 @@ async fn goulag(
|
||||||
}
|
}
|
||||||
|
|
||||||
Response::new_with_embed(4, |e| {
|
Response::new_with_embed(4, |e| {
|
||||||
super::embed_author(e, Some(author))
|
embed_author(e, Some(author))
|
||||||
.title("Error")
|
.title("Error")
|
||||||
.description("Unknown mute role")
|
.description("Unknown mute role")
|
||||||
.colour((247, 76, 0))
|
.colour((247, 76, 0))
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use serenity::{builder::CreateEmbed, model::guild::Member};
|
|
||||||
|
|
||||||
pub(crate) mod admin;
|
pub(crate) mod admin;
|
||||||
pub(crate) mod general;
|
pub(crate) mod general;
|
||||||
pub(crate) mod interaction;
|
pub(crate) mod interaction;
|
||||||
|
@ -11,22 +9,3 @@ pub(crate) mod settings;
|
||||||
pub(crate) mod music;
|
pub(crate) mod music;
|
||||||
|
|
||||||
pub(crate) type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
pub(crate) type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
fn embed_author<'a>(e: &'a mut CreateEmbed, author: Option<&Member>) -> &'a mut CreateEmbed {
|
|
||||||
if let Some(author) = author {
|
|
||||||
e.footer(|f| {
|
|
||||||
f.text(if let Some(nick) = &author.nick {
|
|
||||||
nick
|
|
||||||
} else {
|
|
||||||
&author.user.name
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(url) = &author.user.avatar_url() {
|
|
||||||
f.icon_url(url);
|
|
||||||
}
|
|
||||||
f
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
use serenity::{
|
||||||
|
client::Context,
|
||||||
|
framework::standard::{CommandError, CommandResult},
|
||||||
|
model::{guild::Member, id::ChannelId},
|
||||||
|
};
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
use crate::utils::message::embed_response;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum UseVoiceError {
|
||||||
|
NotInGuild,
|
||||||
|
NotInVoiceChannel,
|
||||||
|
NotInSameVoiceChannel,
|
||||||
|
NotEnoughPermission,
|
||||||
|
CommandError(CommandError),
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CommandError> for UseVoiceError {
|
||||||
|
fn from(err: CommandError) -> Self {
|
||||||
|
Self::CommandError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for UseVoiceError {}
|
||||||
|
|
||||||
|
impl Display for UseVoiceError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::NotInGuild => write!(f, "DM are not supported"),
|
||||||
|
Self::NotInVoiceChannel => write!(f, "Not in a voice channel"),
|
||||||
|
Self::NotEnoughPermission => write!(f, "You don't have the right to do that"),
|
||||||
|
Self::NotInSameVoiceChannel => {
|
||||||
|
write!(f, "You must on the same voice channel than the bot")
|
||||||
|
}
|
||||||
|
Self::CommandError(e) => write!(f, "Something went wrong : {}", e),
|
||||||
|
Self::Unknown => write!(f, "Something went wrong"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UseVoiceError {
|
||||||
|
pub(crate) async fn send_error_message(
|
||||||
|
&self,
|
||||||
|
ctx: &Context,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
member: Option<&Member>,
|
||||||
|
) -> CommandResult<()> {
|
||||||
|
channel_id
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
embed_response(m, "Error", &format!("{}", self), member)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,27 +2,26 @@ use std::{sync::Arc, usize};
|
||||||
|
|
||||||
use tokio::sync::Mutex as TokioMutex;
|
use tokio::sync::Mutex as TokioMutex;
|
||||||
|
|
||||||
use crate::data::GuildOptionsKey;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
builder::CreateMessage,
|
|
||||||
client::Context,
|
client::Context,
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
macros::{command, group},
|
macros::{command, group},
|
||||||
Args, CommandError, CommandResult,
|
Args, CommandResult,
|
||||||
},
|
},
|
||||||
http::Http,
|
http::Http,
|
||||||
model::{
|
model::{channel::Message, id::ChannelId, misc::Mentionable, permissions::Permissions},
|
||||||
channel::Message,
|
|
||||||
guild::{Member, PartialMember},
|
|
||||||
id::{ChannelId, GuildId},
|
|
||||||
misc::Mentionable,
|
|
||||||
permissions::Permissions,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use songbird::{input::Metadata, Call, Event, EventContext, TrackEvent};
|
use songbird::{Call, Event, EventContext, TrackEvent};
|
||||||
|
|
||||||
use super::embed_author;
|
mod error;
|
||||||
|
mod utils;
|
||||||
|
mod youtube;
|
||||||
|
|
||||||
|
use crate::utils::{message::embed_response, permissions::has_permission};
|
||||||
|
use utils::embed_song;
|
||||||
|
|
||||||
|
use self::utils::{can_use_voice_command, is_mute};
|
||||||
|
|
||||||
struct TrackStartNotifier {
|
struct TrackStartNotifier {
|
||||||
chan_id: ChannelId,
|
chan_id: ChannelId,
|
||||||
|
@ -62,94 +61,6 @@ impl songbird::EventHandler for TrackStartNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn embed_song(msg: &mut CreateMessage, title: &str, metadata: &Metadata, author: Option<&Member>) {
|
|
||||||
msg.embed(|e| {
|
|
||||||
e.title(title);
|
|
||||||
|
|
||||||
if let Some(title) = &metadata.title {
|
|
||||||
e.field("Title", title, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(url) = &metadata.source_url {
|
|
||||||
e.url(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(duration) = &metadata.duration {
|
|
||||||
let seconds = duration.as_secs();
|
|
||||||
|
|
||||||
let hours = seconds / (60 * 60);
|
|
||||||
let min = (seconds - (hours * 60 * 60)) / 60;
|
|
||||||
let seconds = seconds - (min * 60);
|
|
||||||
|
|
||||||
e.field(
|
|
||||||
"Duration",
|
|
||||||
if hours > 0 {
|
|
||||||
format!("{}:{:02}:{:02}", hours, min, seconds)
|
|
||||||
} else {
|
|
||||||
format!("{}:{:02}", min, seconds)
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(img) = &metadata.thumbnail {
|
|
||||||
e.image(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
embed_author(e, author).colour((247, 76, 0))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn embed_queued(msg: &mut CreateMessage, metadata: &Metadata, author: Option<&Member>) {
|
|
||||||
msg.embed(|e| {
|
|
||||||
e.title("Queued");
|
|
||||||
|
|
||||||
if let Some(title) = &metadata.title {
|
|
||||||
e.field("Title", title, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(url) = &metadata.source_url {
|
|
||||||
e.url(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(duration) = &metadata.duration {
|
|
||||||
let seconds = duration.as_secs();
|
|
||||||
|
|
||||||
let hours = seconds / (60 * 60);
|
|
||||||
let min = (seconds - (hours * 60 * 60)) / 60;
|
|
||||||
let seconds = seconds - (min * 60);
|
|
||||||
|
|
||||||
e.field(
|
|
||||||
"Duration",
|
|
||||||
if hours > 0 {
|
|
||||||
format!("{}:{:02}:{:02}", hours, min, seconds)
|
|
||||||
} else {
|
|
||||||
format!("{}:{:02}", min, seconds)
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(img) = &metadata.thumbnail {
|
|
||||||
e.image(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
embed_author(e, author).colour((247, 76, 0))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn embed_response<'a, 'b>(
|
|
||||||
msg: &'a mut CreateMessage<'b>,
|
|
||||||
title: &str,
|
|
||||||
content: &str,
|
|
||||||
author: Option<&Member>,
|
|
||||||
) -> &'a mut CreateMessage<'b> {
|
|
||||||
msg.embed(|e| {
|
|
||||||
e.title(title).description(content);
|
|
||||||
embed_author(e, author).colour((247, 76, 0))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[group]
|
#[group]
|
||||||
#[commands(join, leave, play, stop, next, pause, resume, remove, queue)]
|
#[commands(join, leave, play, stop, next, pause, resume, remove, queue)]
|
||||||
struct Music;
|
struct Music;
|
||||||
|
@ -285,7 +196,7 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
|
||||||
let handler = handler.lock().await;
|
let handler = handler.lock().await;
|
||||||
match can_use_voice_command(ctx, &handler, &msg, &member).await {
|
match can_use_voice_command(ctx, &handler, &msg, &member).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
std::mem::drop(handler);
|
std::mem::drop(handler); // Drop mutex
|
||||||
if let Err(e) = manager.remove(guild_id).await {
|
if let Err(e) = manager.remove(guild_id).await {
|
||||||
log::error!("Failed to leave : {}", e);
|
log::error!("Failed to leave : {}", e);
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
|
@ -371,23 +282,106 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
match can_use_voice_command(ctx, &handler, &msg, &member).await {
|
match can_use_voice_command(ctx, &handler, &msg, &member).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let source = if url.starts_with("http") {
|
let source = if url.starts_with("http") {
|
||||||
match songbird::ytdl(&url).await {
|
if url.contains("youtube.com") && url.contains("list=") {
|
||||||
Ok(source) => source,
|
msg.channel_id
|
||||||
Err(why) => {
|
.send_message(&ctx.http, |m| {
|
||||||
error!("Err starting source: {:?}", why);
|
embed_response(
|
||||||
|
m,
|
||||||
|
"Loading the playlist",
|
||||||
|
"This could take some time",
|
||||||
|
Some(&member),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let list = match youtube::get_list_of_urls(&url).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to load playlist : {}", e);
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
embed_response(
|
||||||
|
m,
|
||||||
|
"Error",
|
||||||
|
"Failed to load the playlist",
|
||||||
|
Some(&member),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if clean {
|
||||||
|
handler.queue().stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
for music in list {
|
||||||
|
let s = match songbird::ytdl(&music).await {
|
||||||
|
Ok(source) => source,
|
||||||
|
Err(why) => {
|
||||||
|
error!("Err starting source: {:?}", why);
|
||||||
|
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
embed_response(
|
||||||
|
m,
|
||||||
|
"Error",
|
||||||
|
"Failed to load the song",
|
||||||
|
Some(&member),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handler.enqueue_source(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
let track = handler.queue().current();
|
||||||
|
|
||||||
|
if let Some(track) = &track {
|
||||||
|
let author = msg.member(&ctx).await?;
|
||||||
|
msg.channel_id
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
embed_song(m, "Now playing", track.metadata(), Some(&author))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(&ctx.http, |m| {
|
.send_message(&ctx.http, |m| {
|
||||||
embed_response(
|
embed_response(
|
||||||
m,
|
m,
|
||||||
"Error",
|
"Error",
|
||||||
"Failed to load the song",
|
"Failed to load the playlist",
|
||||||
Some(&member),
|
Some(&member),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
match songbird::ytdl(&url).await {
|
||||||
|
Ok(source) => source,
|
||||||
|
Err(why) => {
|
||||||
|
error!("Err starting source: {:?}", why);
|
||||||
|
|
||||||
return Ok(());
|
msg.channel_id
|
||||||
|
.send_message(&ctx.http, |m| {
|
||||||
|
embed_response(
|
||||||
|
m,
|
||||||
|
"Error",
|
||||||
|
"Failed to load the song",
|
||||||
|
Some(&member),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -422,12 +416,16 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let author = msg.member(&ctx).await?;
|
let author = msg.member(&ctx).await?;
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(&ctx.http, |m| {
|
.send_message(&ctx.http, |m| {
|
||||||
if handler.queue().len() == 1 {
|
embed_song(
|
||||||
embed_song(m, "Now playing", meta, Some(&author));
|
m,
|
||||||
} else {
|
if handler.queue().len() == 1 {
|
||||||
embed_queued(m, meta, Some(&author));
|
"Now playing"
|
||||||
}
|
} else {
|
||||||
m
|
"Queued"
|
||||||
|
},
|
||||||
|
meta,
|
||||||
|
Some(&author),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
@ -692,8 +690,7 @@ async fn remove(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
handler.queue().skip()?;
|
handler.queue().skip()?;
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(&ctx.http, |m| {
|
.send_message(&ctx.http, |m| {
|
||||||
embed_song(m, "Removed", track.metadata(), Some(&member));
|
embed_song(m, "Removed", track.metadata(), Some(&member))
|
||||||
m
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
@ -710,8 +707,7 @@ async fn remove(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
Some(q) => {
|
Some(q) => {
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(&ctx.http, |m| {
|
.send_message(&ctx.http, |m| {
|
||||||
embed_song(m, "Removed", q.metadata(), Some(&member));
|
embed_song(m, "Removed", q.metadata(), Some(&member))
|
||||||
m
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
@ -811,124 +807,3 @@ async fn queue(ctx: &Context, msg: &Message) -> CommandResult<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_mute(ctx: &Context, member: &PartialMember, guild_id: GuildId) -> CommandResult<bool> {
|
|
||||||
let data = ctx.data.read().await;
|
|
||||||
|
|
||||||
let data = data
|
|
||||||
.get::<GuildOptionsKey>()
|
|
||||||
.expect("Failed to get guild cache");
|
|
||||||
|
|
||||||
if let Some(mute_role) = data.get(&guild_id).and_then(|o| o.get_mute_role()) {
|
|
||||||
Ok(member.roles.contains(&mute_role))
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum UseVoiceError {
|
|
||||||
NotInGuild,
|
|
||||||
NotInVoiceChannel,
|
|
||||||
NotInSameVoiceChannel,
|
|
||||||
NotEnoughPermission,
|
|
||||||
CommandError(CommandError),
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CommandError> for UseVoiceError {
|
|
||||||
fn from(err: CommandError) -> Self {
|
|
||||||
Self::CommandError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UseVoiceError {
|
|
||||||
async fn send_error_message(
|
|
||||||
&self,
|
|
||||||
ctx: &Context,
|
|
||||||
channel_id: &ChannelId,
|
|
||||||
member: Option<&Member>,
|
|
||||||
) -> CommandResult<()> {
|
|
||||||
let msg = match self {
|
|
||||||
Self::NotInGuild => "DM are not supported",
|
|
||||||
Self::NotInVoiceChannel => "Not in a voice channel",
|
|
||||||
Self::NotEnoughPermission => "You don't have the right to do that",
|
|
||||||
Self::NotInSameVoiceChannel => "You must on the same voice channel than the bot",
|
|
||||||
Self::CommandError(_) => "Something went wrong",
|
|
||||||
Self::Unknown => "Something went wrong",
|
|
||||||
};
|
|
||||||
|
|
||||||
channel_id
|
|
||||||
.send_message(&ctx.http, |m| embed_response(m, "Error", msg, member))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn can_use_voice_command(
|
|
||||||
ctx: &Context,
|
|
||||||
call: &Call,
|
|
||||||
msg: &Message,
|
|
||||||
member: &Member,
|
|
||||||
) -> Result<(), UseVoiceError> {
|
|
||||||
if has_permission(ctx, member, &[Permissions::MANAGE_GUILD]).await? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let guild = match msg.guild(&ctx.cache).await {
|
|
||||||
Some(guild) => guild,
|
|
||||||
None => return Err(UseVoiceError::NotInGuild),
|
|
||||||
};
|
|
||||||
|
|
||||||
let channel_id = match guild.voice_states.get(&msg.author.id) {
|
|
||||||
Some(voice_state) => match voice_state.channel_id {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return Err(UseVoiceError::Unknown),
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
return Err(UseVoiceError::NotInVoiceChannel);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let bot_channel_id = match call.current_channel() {
|
|
||||||
Some(c) => c,
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if bot_channel_id.0 != channel_id.0 {
|
|
||||||
Err(UseVoiceError::NotInSameVoiceChannel)
|
|
||||||
} else if let Some(member) = &msg.member {
|
|
||||||
if is_mute(ctx, member, guild.id).await? {
|
|
||||||
Err(UseVoiceError::NotEnoughPermission)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(UseVoiceError::Unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn has_permission(
|
|
||||||
ctx: &Context,
|
|
||||||
member: &Member,
|
|
||||||
permissions: &[Permissions],
|
|
||||||
) -> CommandResult<bool> {
|
|
||||||
let p = member.permissions(ctx).await?;
|
|
||||||
for perm in permissions {
|
|
||||||
if p.contains(*perm) {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let roles = member.roles(ctx).await.unwrap();
|
|
||||||
for role in roles {
|
|
||||||
for perm in permissions {
|
|
||||||
if role.has_permissions(*perm, false) {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
use serenity::{
|
||||||
|
builder::CreateMessage,
|
||||||
|
client::Context,
|
||||||
|
framework::standard::CommandResult,
|
||||||
|
model::{
|
||||||
|
channel::Message,
|
||||||
|
guild::{Member, PartialMember},
|
||||||
|
id::GuildId,
|
||||||
|
Permissions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use songbird::{input::Metadata, Call};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data::GuildOptionsKey,
|
||||||
|
utils::{message::embed_author, permissions::has_permission},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::error::UseVoiceError;
|
||||||
|
|
||||||
|
pub(crate) fn embed_song<'a, 'b>(
|
||||||
|
msg: &'a mut CreateMessage<'b>,
|
||||||
|
title: &str,
|
||||||
|
metadata: &Metadata,
|
||||||
|
author: Option<&Member>,
|
||||||
|
) -> &'a mut CreateMessage<'b> {
|
||||||
|
msg.embed(|e| {
|
||||||
|
e.title(title);
|
||||||
|
|
||||||
|
if let Some(title) = &metadata.title {
|
||||||
|
e.field("Title", title, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(url) = &metadata.source_url {
|
||||||
|
e.url(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(duration) = &metadata.duration {
|
||||||
|
let seconds = duration.as_secs();
|
||||||
|
|
||||||
|
let hours = seconds / (60 * 60);
|
||||||
|
let min = (seconds - (hours * 60 * 60)) / 60;
|
||||||
|
let seconds = seconds - (min * 60);
|
||||||
|
|
||||||
|
e.field(
|
||||||
|
"Duration",
|
||||||
|
if hours > 0 {
|
||||||
|
format!("{}:{:02}:{:02}", hours, min, seconds)
|
||||||
|
} else {
|
||||||
|
format!("{}:{:02}", min, seconds)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(img) = &metadata.thumbnail {
|
||||||
|
e.image(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_author(e, author).colour((247, 76, 0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn is_mute(
|
||||||
|
ctx: &Context,
|
||||||
|
member: &PartialMember,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> CommandResult<bool> {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
|
||||||
|
let data = data
|
||||||
|
.get::<GuildOptionsKey>()
|
||||||
|
.expect("Failed to get guild cache");
|
||||||
|
|
||||||
|
if let Some(mute_role) = data.get(&guild_id).and_then(|o| o.get_mute_role()) {
|
||||||
|
Ok(member.roles.contains(&mute_role))
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn can_use_voice_command(
|
||||||
|
ctx: &Context,
|
||||||
|
call: &Call,
|
||||||
|
msg: &Message,
|
||||||
|
member: &Member,
|
||||||
|
) -> Result<(), UseVoiceError> {
|
||||||
|
if has_permission(ctx, member, &[Permissions::MANAGE_GUILD]).await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild = match msg.guild(&ctx.cache).await {
|
||||||
|
Some(guild) => guild,
|
||||||
|
None => return Err(UseVoiceError::NotInGuild),
|
||||||
|
};
|
||||||
|
|
||||||
|
let channel_id = match guild.voice_states.get(&msg.author.id) {
|
||||||
|
Some(voice_state) => match voice_state.channel_id {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Err(UseVoiceError::Unknown),
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
return Err(UseVoiceError::NotInVoiceChannel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let bot_channel_id = match call.current_channel() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if bot_channel_id.0 != channel_id.0 {
|
||||||
|
Err(UseVoiceError::NotInSameVoiceChannel)
|
||||||
|
} else if let Some(member) = &msg.member {
|
||||||
|
if is_mute(ctx, member, guild.id).await? {
|
||||||
|
Err(UseVoiceError::NotEnoughPermission)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(UseVoiceError::Unknown)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::{
|
||||||
|
io::{Error, ErrorKind, Result},
|
||||||
|
process::Command,
|
||||||
|
};
|
||||||
|
pub(crate) async fn get_list_of_urls(url: &str) -> Result<Vec<String>> {
|
||||||
|
let output = Command::new("youtube-dl")
|
||||||
|
.args(&["-j", "--flat-playlist", &url])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let output = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let mut json_output = vec![];
|
||||||
|
for line in output.lines() {
|
||||||
|
let json: YtdlResponse = serde_json::from_str(line)?;
|
||||||
|
json_output.push(String::from("https://youtube.com/watch?v=") + &json.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json_output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct YtdlResponse {
|
||||||
|
url: String,
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod data;
|
mod data;
|
||||||
mod presence;
|
mod presence;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
const MINIMUM_MENTIONS: usize = 10;
|
const MINIMUM_MENTIONS: usize = 10;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
use serenity::{
|
||||||
|
builder::{CreateEmbed, CreateMessage},
|
||||||
|
model::guild::Member,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn embed_response<'a, 'b>(
|
||||||
|
msg: &'a mut CreateMessage<'b>,
|
||||||
|
title: &str,
|
||||||
|
content: &str,
|
||||||
|
author: Option<&Member>,
|
||||||
|
) -> &'a mut CreateMessage<'b> {
|
||||||
|
msg.embed(|e| {
|
||||||
|
e.title(title).description(content);
|
||||||
|
embed_author(e, author).colour((247, 76, 0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn embed_author<'a>(
|
||||||
|
e: &'a mut CreateEmbed,
|
||||||
|
author: Option<&Member>,
|
||||||
|
) -> &'a mut CreateEmbed {
|
||||||
|
if let Some(author) = author {
|
||||||
|
e.footer(|f| {
|
||||||
|
f.text(if let Some(nick) = &author.nick {
|
||||||
|
nick
|
||||||
|
} else {
|
||||||
|
&author.user.name
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(url) = &author.user.avatar_url() {
|
||||||
|
f.icon_url(url);
|
||||||
|
}
|
||||||
|
f
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
e
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub(crate) mod message;
|
||||||
|
pub(crate) mod permissions;
|
|
@ -0,0 +1,29 @@
|
||||||
|
use serenity::{
|
||||||
|
client::Context,
|
||||||
|
framework::standard::CommandResult,
|
||||||
|
model::{guild::Member, Permissions},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) async fn has_permission(
|
||||||
|
ctx: &Context,
|
||||||
|
member: &Member,
|
||||||
|
permissions: &[Permissions],
|
||||||
|
) -> CommandResult<bool> {
|
||||||
|
let p = member.permissions(ctx).await?;
|
||||||
|
for perm in permissions {
|
||||||
|
if p.contains(*perm) {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let roles = member.roles(ctx).await.unwrap();
|
||||||
|
for role in roles {
|
||||||
|
for perm in permissions {
|
||||||
|
if role.has_permissions(*perm, false) {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
Loading…
Reference in New Issue