initial commit

This commit is contained in:
DaXcess
2022-10-18 22:59:32 +02:00
commit 6a77189343
28 changed files with 5184 additions and 0 deletions
+115
View File
@@ -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")
}
+2
View File
@@ -0,0 +1,2 @@
pub mod link;
pub mod unlink;
+105
View File
@@ -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")
}
+149
View File
@@ -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;
}
+141
View File
@@ -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")
}
+86
View File
@@ -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")
}
+2
View File
@@ -0,0 +1,2 @@
pub mod join;
pub mod leave;
+33
View File
@@ -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")
}
+42
View File
@@ -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")
}