initial commit
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
use log::error;
|
||||
use serenity::{
|
||||
builder::CreateApplicationCommand,
|
||||
model::prelude::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
},
|
||||
prelude::Context,
|
||||
Result as SerenityResult,
|
||||
};
|
||||
|
||||
use crate::{bot::commands::CommandOutput, database::Database};
|
||||
|
||||
pub const NAME: &str = "link";
|
||||
|
||||
async fn respond_message(
|
||||
ctx: &Context,
|
||||
command: &ApplicationCommandInteraction,
|
||||
msg: impl Into<String>,
|
||||
ephemeral: bool,
|
||||
) -> SerenityResult<()> {
|
||||
command
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(msg.into()).ephemeral(ephemeral))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn check_msg(result: SerenityResult<()>) {
|
||||
if let Err(why) = result {
|
||||
error!("Error sending message: {:?}", why);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput {
|
||||
Box::pin(async move {
|
||||
let data = ctx.data.read().await;
|
||||
let database = data.get::<Database>().unwrap();
|
||||
|
||||
if let Ok(_) = database.get_user_account(command.user.id.to_string()).await {
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"You have already linked your Spotify account.",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(request) = database.get_user_request(command.user.id.to_string()).await {
|
||||
let base = std::env::var("SPOTICORD_ACCOUNTS_URL").unwrap();
|
||||
let link = format!("{}/spotify/{}", base, request.token);
|
||||
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
format!("Go to the following URL to link your account:\n{}", link),
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
match database
|
||||
.create_user_request(command.user.id.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(request) => {
|
||||
let base = std::env::var("SPOTICORD_ACCOUNTS_URL").unwrap();
|
||||
let link = format!("{}/spotify/{}", base, request.token);
|
||||
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
format!("Go to the following URL to link your account:\n{}", link),
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
Err(why) => {
|
||||
error!("Error creating user request: {:?}", why);
|
||||
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"An error occurred while serving your request. Please try again later.",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||
command
|
||||
.name(NAME)
|
||||
.description("Link your Spotify account to Spoticord")
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod link;
|
||||
pub mod unlink;
|
||||
@@ -0,0 +1,105 @@
|
||||
use log::error;
|
||||
use serenity::{
|
||||
builder::CreateApplicationCommand,
|
||||
model::prelude::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
},
|
||||
prelude::Context,
|
||||
Result as SerenityResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
bot::commands::CommandOutput,
|
||||
database::{Database, DatabaseError},
|
||||
session::manager::SessionManager,
|
||||
};
|
||||
|
||||
pub const NAME: &str = "unlink";
|
||||
|
||||
async fn respond_message(
|
||||
ctx: &Context,
|
||||
command: &ApplicationCommandInteraction,
|
||||
msg: impl Into<String>,
|
||||
ephemeral: bool,
|
||||
) -> SerenityResult<()> {
|
||||
command
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(msg.into()).ephemeral(ephemeral))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn check_msg(result: SerenityResult<()>) {
|
||||
if let Err(why) = result {
|
||||
error!("Error sending message: {:?}", why);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput {
|
||||
Box::pin(async move {
|
||||
let data = ctx.data.read().await;
|
||||
let database = data.get::<Database>().unwrap();
|
||||
let session_manager = data.get::<SessionManager>().unwrap();
|
||||
|
||||
// Disconnect session if user has any
|
||||
if let Some(session) = session_manager.find(command.user.id).await {
|
||||
if let Err(why) = session.disconnect().await {
|
||||
error!("Error disconnecting session: {:?}", why);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user exists in the first place
|
||||
if let Err(why) = database
|
||||
.delete_user_account(command.user.id.to_string())
|
||||
.await
|
||||
{
|
||||
if let DatabaseError::InvalidStatusCode(status) = why {
|
||||
if status == 404 {
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"You cannot unlink your Spotify account if you currently don't have a linked Spotify account.",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
error!("Error deleting user account: {:?}", why);
|
||||
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"An unexpected error has occured while trying to unlink your account. Please try again later.",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"Successfully unlinked your Spotify account from Spoticord",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||
command
|
||||
.name(NAME)
|
||||
.description("Unlink your Spotify account from Spoticord")
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use std::{collections::HashMap, future::Future, pin::Pin};
|
||||
|
||||
use log::{debug, error};
|
||||
use serenity::{
|
||||
builder::{CreateApplicationCommand, CreateApplicationCommands},
|
||||
model::application::command::Command,
|
||||
model::prelude::{
|
||||
interaction::{application_command::ApplicationCommandInteraction, InteractionResponseType},
|
||||
GuildId,
|
||||
},
|
||||
prelude::{Context, TypeMapKey},
|
||||
};
|
||||
|
||||
mod core;
|
||||
mod music;
|
||||
|
||||
mod ping;
|
||||
mod token;
|
||||
|
||||
pub type CommandOutput = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
pub type CommandExecutor = fn(Context, ApplicationCommandInteraction) -> CommandOutput;
|
||||
|
||||
pub struct CommandManager {
|
||||
commands: HashMap<String, CommandInfo>,
|
||||
}
|
||||
|
||||
pub struct CommandInfo {
|
||||
pub name: String,
|
||||
pub executor: CommandExecutor,
|
||||
pub register: fn(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand,
|
||||
}
|
||||
|
||||
impl CommandManager {
|
||||
pub fn new() -> Self {
|
||||
let mut instance = Self {
|
||||
commands: HashMap::new(),
|
||||
};
|
||||
|
||||
// Debug-only commands
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
instance.insert_command(ping::NAME, ping::register, ping::run);
|
||||
instance.insert_command(token::NAME, token::register, token::run);
|
||||
}
|
||||
|
||||
// Core commands
|
||||
instance.insert_command(core::link::NAME, core::link::register, core::link::run);
|
||||
instance.insert_command(
|
||||
core::unlink::NAME,
|
||||
core::unlink::register,
|
||||
core::unlink::run,
|
||||
);
|
||||
|
||||
// Music commands
|
||||
instance.insert_command(music::join::NAME, music::join::register, music::join::run);
|
||||
instance.insert_command(
|
||||
music::leave::NAME,
|
||||
music::leave::register,
|
||||
music::leave::run,
|
||||
);
|
||||
|
||||
instance
|
||||
}
|
||||
|
||||
pub fn insert_command(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
register: fn(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand,
|
||||
executor: CommandExecutor,
|
||||
) {
|
||||
let name = name.into();
|
||||
|
||||
self.commands.insert(
|
||||
name.clone(),
|
||||
CommandInfo {
|
||||
name,
|
||||
register,
|
||||
executor,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn register_commands(&self, ctx: &Context) {
|
||||
let cmds = &self.commands;
|
||||
|
||||
debug!(
|
||||
"Registering {} command{}",
|
||||
cmds.len(),
|
||||
if cmds.len() == 1 { "" } else { "s" }
|
||||
);
|
||||
|
||||
fn _register_commands<'a>(
|
||||
cmds: &HashMap<String, CommandInfo>,
|
||||
mut commands: &'a mut CreateApplicationCommands,
|
||||
) -> &'a mut CreateApplicationCommands {
|
||||
for cmd in cmds {
|
||||
commands = commands.create_application_command(|command| (cmd.1.register)(command));
|
||||
}
|
||||
|
||||
commands
|
||||
}
|
||||
|
||||
if let Ok(guild_id) = std::env::var("GUILD_ID") {
|
||||
if let Ok(guild_id) = guild_id.parse::<u64>() {
|
||||
let guild_id = GuildId(guild_id);
|
||||
guild_id
|
||||
.set_application_commands(&ctx.http, |command| _register_commands(cmds, command))
|
||||
.await
|
||||
.expect("Failed to create guild commands");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Command::set_global_application_commands(&ctx.http, |command| {
|
||||
_register_commands(cmds, command)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create global commands");
|
||||
}
|
||||
|
||||
pub async fn execute_command(&self, ctx: &Context, interaction: ApplicationCommandInteraction) {
|
||||
let command = self.commands.get(&interaction.data.name);
|
||||
|
||||
if let Some(command) = command {
|
||||
(command.executor)(ctx.clone(), interaction.clone()).await;
|
||||
} else {
|
||||
// Command does not exist
|
||||
if let Err(why) = interaction
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message
|
||||
.content("Woops, that command doesn't exist")
|
||||
.ephemeral(true)
|
||||
})
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("Failed to respond to command: {}", why);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeMapKey for CommandManager {
|
||||
type Value = CommandManager;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use log::error;
|
||||
use serenity::{
|
||||
builder::CreateApplicationCommand,
|
||||
model::prelude::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
},
|
||||
prelude::Context,
|
||||
Result as SerenityResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
bot::commands::CommandOutput,
|
||||
session::manager::{SessionCreateError, SessionManager},
|
||||
};
|
||||
|
||||
pub const NAME: &str = "join";
|
||||
|
||||
async fn respond_message(
|
||||
ctx: &Context,
|
||||
command: &ApplicationCommandInteraction,
|
||||
msg: impl Into<String>,
|
||||
ephemeral: bool,
|
||||
) -> SerenityResult<()> {
|
||||
command
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(msg.into()).ephemeral(ephemeral))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn check_msg(result: SerenityResult<()>) {
|
||||
if let Err(why) = result {
|
||||
error!("Error sending message: {:?}", why);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput {
|
||||
Box::pin(async move {
|
||||
let guild = ctx.cache.guild(command.guild_id.unwrap()).unwrap();
|
||||
|
||||
// Get the voice channel id of the calling user
|
||||
let channel_id = match guild
|
||||
.voice_states
|
||||
.get(&command.user.id)
|
||||
.and_then(|state| state.channel_id)
|
||||
{
|
||||
Some(channel_id) => channel_id,
|
||||
None => {
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"You need to connect to a voice channel",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let data = ctx.data.read().await;
|
||||
let mut session_manager = data.get::<SessionManager>().unwrap().clone();
|
||||
|
||||
// Check if another session is already active in this server
|
||||
if let Some(session) = session_manager.get_session(guild.id).await {
|
||||
let msg = if session.get_owner() == command.user.id {
|
||||
"You are already playing music in this server"
|
||||
} else {
|
||||
"Someone else is already playing music in this server"
|
||||
};
|
||||
|
||||
check_msg(respond_message(&ctx, &command, msg, true).await);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
// Prevent duplicate Spotify sessions
|
||||
if let Some(session) = session_manager.find(command.user.id).await {
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
format!(
|
||||
"You are already playing music in another server ({}).\nStop playing in that server first before joining this one.",
|
||||
ctx.cache.guild(session.get_guild_id()).unwrap().name
|
||||
),
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the session, and handle potential errors
|
||||
if let Err(why) = session_manager
|
||||
.create_session(&ctx, guild.id, channel_id, command.user.id)
|
||||
.await
|
||||
{
|
||||
// Need to link first
|
||||
if let SessionCreateError::NoSpotifyError = why {
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"You need to link your Spotify account. Use `/link` or go to https://account.spoticord.com/ to get started.",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Any other error
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"An error occurred while joining the channel. Please try again later.",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
check_msg(respond_message(&ctx, &command, "Joined the voice channel.", false).await);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||
command
|
||||
.name(NAME)
|
||||
.description("Request the bot to join the current voice channel")
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use log::error;
|
||||
use serenity::{
|
||||
builder::CreateApplicationCommand,
|
||||
model::prelude::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
},
|
||||
prelude::Context,
|
||||
Result as SerenityResult,
|
||||
};
|
||||
|
||||
use crate::{bot::commands::CommandOutput, session::manager::SessionManager};
|
||||
|
||||
pub const NAME: &str = "leave";
|
||||
|
||||
async fn respond_message(
|
||||
ctx: &Context,
|
||||
command: &ApplicationCommandInteraction,
|
||||
msg: &str,
|
||||
ephemeral: bool,
|
||||
) -> SerenityResult<()> {
|
||||
command
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(msg).ephemeral(ephemeral))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn check_msg(result: SerenityResult<()>) {
|
||||
if let Err(why) = result {
|
||||
error!("Error sending message: {:?}", why);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput {
|
||||
Box::pin(async move {
|
||||
let data = ctx.data.read().await;
|
||||
let session_manager = data.get::<SessionManager>().unwrap().clone();
|
||||
|
||||
let session = match session_manager.get_session(command.guild_id.unwrap()).await {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"I'm currently not connected to any voice channel",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if session.get_owner() != command.user.id {
|
||||
// This message was generated by AI, and I love it.
|
||||
check_msg(respond_message(&ctx, &command, "You are not the one who summoned me", true).await);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(why) = session.disconnect().await {
|
||||
error!("Error disconnecting from voice channel: {:?}", why);
|
||||
|
||||
check_msg(
|
||||
respond_message(
|
||||
&ctx,
|
||||
&command,
|
||||
"An error occurred while disconnecting from the voice channel",
|
||||
true,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
check_msg(respond_message(&ctx, &command, "Successfully left the voice channel", false).await);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||
command
|
||||
.name(NAME)
|
||||
.description("Request the bot to leave the current voice channel")
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod join;
|
||||
pub mod leave;
|
||||
@@ -0,0 +1,33 @@
|
||||
use log::info;
|
||||
use serenity::{
|
||||
builder::CreateApplicationCommand,
|
||||
model::prelude::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
},
|
||||
prelude::Context,
|
||||
};
|
||||
|
||||
use super::CommandOutput;
|
||||
|
||||
pub const NAME: &str = "ping";
|
||||
|
||||
pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput {
|
||||
Box::pin(async move {
|
||||
info!("Pong!");
|
||||
|
||||
command
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content("Pong!"))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||
command
|
||||
.name("ping")
|
||||
.description("Check if the bot is alive")
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use serenity::{
|
||||
builder::CreateApplicationCommand,
|
||||
model::prelude::interaction::{
|
||||
application_command::ApplicationCommandInteraction, InteractionResponseType,
|
||||
},
|
||||
prelude::Context,
|
||||
};
|
||||
|
||||
use crate::database::Database;
|
||||
|
||||
use super::CommandOutput;
|
||||
|
||||
pub const NAME: &str = "token";
|
||||
|
||||
pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput {
|
||||
Box::pin(async move {
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<Database>().unwrap();
|
||||
|
||||
let token = db.get_access_token(command.user.id.to_string()).await;
|
||||
|
||||
let content = match token {
|
||||
Ok(token) => format!("Your token is: {}", token),
|
||||
Err(why) => format!("You don't have a token yet. (Real: {})", why),
|
||||
};
|
||||
|
||||
command
|
||||
.create_interaction_response(&ctx.http, |response| {
|
||||
response
|
||||
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| message.content(content).ephemeral(true))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
|
||||
command
|
||||
.name("token")
|
||||
.description("Get your Spotify access token")
|
||||
}
|
||||
Reference in New Issue
Block a user