forked from DiscordBots/Rustendo
Compare commits
47 commits
Author | SHA1 | Date | |
---|---|---|---|
23ff902e01 | |||
84048b1b67 | |||
e56e83213f | |||
b368a23adf | |||
389f61682f | |||
0c686e5517 | |||
80dc6d05ba | |||
242fa52170 | |||
078586dd3f | |||
2b4e21736c | |||
b5888ec441 | |||
f1b0e1e363 | |||
cd879e8af9 | |||
72a1fe8af3 | |||
2f89b9dbb9 | |||
a72f9753a2 | |||
59bf59295e | |||
eecb61b9f5 | |||
d471bfcb06 | |||
f3fec5292a | |||
05fae26549 | |||
ed89386ed9 | |||
1879b4ee0f | |||
ba0f1fb959 | |||
1dcd0ab66b | |||
29392dc72d | |||
fc32017a10 | |||
e79b4142de | |||
c6af238f06 | |||
9c1f6bee6d | |||
e1cf394362 | |||
ef06cbc90b | |||
b493209a36 | |||
a06299fb6f | |||
a16d8a6b60 | |||
edc22a91f2 | |||
b2ba381e44 | |||
d72577c245 | |||
af50d54729 | |||
cfd051be3f | |||
4d6e665a3b | |||
57e9bca8f0 | |||
2c82dbb019 | |||
50202dfdf5 | |||
a1a78d6598 | |||
1fd1bc893c | |||
ba42e009f4 |
23 changed files with 1607 additions and 523 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/data
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,3 +1,10 @@
|
||||||
/target
|
/target
|
||||||
/data
|
/data
|
||||||
config.json
|
config.json
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.zed/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"rust-analyzer.linkedProjects": [
|
|
||||||
".\\Cargo.toml"
|
|
||||||
],
|
|
||||||
"rust-analyzer.showUnlinkedFileNotification": false
|
|
||||||
}
|
|
1121
Cargo.lock
generated
1121
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "rustendo"
|
name = "rustendo"
|
||||||
version = "0.1.0"
|
version = "7.0.4"
|
||||||
authors = ["moonleay <contact@moonleay.net>"]
|
authors = ["moonleay <contact@moonleay.net>", "migueldamota <miguel@damota.de>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -9,15 +9,16 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serenity = "0.12"
|
serenity = "0.12"
|
||||||
tokio = { version = "1.36", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.39", features = ["macros", "rt-multi-thread"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
confy = "0.6.0"
|
confy = "0.6.0"
|
||||||
songbird = "0.4"
|
songbird = "0.4"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
reqwest = "0.11"
|
reqwest = "0.11" # 0.12 creates issues; don't update for now, will fix later
|
||||||
symphonia = "0.5"
|
symphonia = "0.5"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
tracing-futures = "0.2.5"
|
tracing-futures = "0.2.5"
|
||||||
futures = "0.3.1"
|
futures = "0.3.1"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
FROM rust:1.81
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y cmake yt-dlp/bookworm-backports
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
CMD ["/usr/src/app/target/release/rustendo"]
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
botendo:
|
||||||
|
container_name: botendo
|
||||||
|
image: limiteddev/rustendo:0.1.0
|
||||||
|
environment:
|
||||||
|
TZ: "Europe/Berlin"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data:/usr/src/app/data
|
|
@ -1,20 +1,17 @@
|
||||||
use chrono::Local;
|
|
||||||
use serenity::all::{CommandInteraction, Context};
|
use serenity::all::{CommandInteraction, Context};
|
||||||
use serenity::builder::{CreateCommand, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter};
|
use serenity::builder::{CreateCommand, CreateEmbed};
|
||||||
|
|
||||||
|
use crate::util::embed::Embed;
|
||||||
|
|
||||||
pub async fn run(_ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
pub async fn run(_ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
||||||
let username = command.user.name.as_str();
|
let username = command.user.name.as_str();
|
||||||
let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S");
|
|
||||||
CreateEmbed::new()
|
|
||||||
.author(CreateEmbedAuthor::new("Rustendo"))
|
|
||||||
.description("Botendo v7\ndeveloped by [moonleay](https://moonleay.net)\n\nCheck out the repository: https://git.moonleay.net/DiscordBots/Rustendo")
|
|
||||||
.footer(CreateEmbedFooter::new(format!(">{} | {}", current_time, username)))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Embed::create_success_response(username, "Botendo v7", "developed by [moonleay](https://moonleay.net)\n\nCheck out the repository: https://git.moonleay.net/DiscordBots/Rustendo")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register() -> CreateCommand {
|
pub fn register() -> CreateCommand {
|
||||||
CreateCommand::new("info").description("Infos about the bot")
|
CreateCommand::new("info").description("Infos about the bot")
|
||||||
}
|
}
|
||||||
|
|
||||||
// >18/02/2024 @ 19:01:59 - bartlo
|
// >18/02/2024 @ 19:01:59 - bartlo
|
||||||
// >2024-02-19 17:58:39 | moonleay
|
// >2024-02-19 17:58:39 | moonleay
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
pub mod info;
|
pub mod info;
|
||||||
|
pub mod now_playing;
|
||||||
pub mod play;
|
pub mod play;
|
||||||
pub mod stop;
|
pub mod skip;
|
||||||
|
pub mod stop;
|
||||||
|
|
75
src/commands/now_playing.rs
Normal file
75
src/commands/now_playing.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use crate::music::music_queue;
|
||||||
|
use crate::util::embed::Embed;
|
||||||
|
use serenity::all::{CommandInteraction, Context};
|
||||||
|
use serenity::builder::{CreateCommand, CreateEmbed};
|
||||||
|
|
||||||
|
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
||||||
|
let username = command.user.name.as_str();
|
||||||
|
|
||||||
|
let guild_id = match &command.guild_id {
|
||||||
|
Some(guild_id) => guild_id,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"guild_id not found",
|
||||||
|
"Could not find guild id.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let now_plaing = match music_queue::get_now_playing(&guild_id).await {
|
||||||
|
Some(ytdl) => ytdl,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"Not playing",
|
||||||
|
"I'm not playing anything!",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let now_handle = match music_queue::get_now_playing_track_handle(&guild_id).await {
|
||||||
|
Some(handle) => handle,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"Cannot get TrackHandle",
|
||||||
|
"The TrackHandle is empty.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Cannot get Songbird")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let _ = match manager.get(*guild_id) {
|
||||||
|
Some(handler) => handler,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"Error",
|
||||||
|
"Error while getting the audio handler.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = now_handle.get_info().await.unwrap().position;
|
||||||
|
|
||||||
|
Embed::create_yt_playing(now_plaing, username, "Currently playing")
|
||||||
|
.await
|
||||||
|
.field(
|
||||||
|
"Position",
|
||||||
|
format!(
|
||||||
|
"{}min {}sec",
|
||||||
|
position.as_secs() / 60,
|
||||||
|
position.as_secs() % 60
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register() -> CreateCommand {
|
||||||
|
CreateCommand::new("nowplaying").description("Show what is currently playing.")
|
||||||
|
}
|
|
@ -1,77 +1,57 @@
|
||||||
use chrono::Local;
|
use crate::music::music_manager;
|
||||||
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, Context, ResolvedOption, ResolvedValue};
|
|
||||||
use serenity::builder::{CreateCommand, CreateCommandOption, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter};
|
use serenity::all::{CommandDataOptionValue, CommandInteraction, Context};
|
||||||
|
use serenity::builder::{CreateCommand, CreateCommandOption, CreateEmbed};
|
||||||
use serenity::model::application::CommandOptionType;
|
use serenity::model::application::CommandOptionType;
|
||||||
|
|
||||||
|
use crate::util::embed::Embed;
|
||||||
|
|
||||||
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
||||||
let username = command.user.name.as_str();
|
let username = command.user.name.as_str();
|
||||||
let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S");
|
|
||||||
let options = &command.data.options;
|
let options = &command.data.options;
|
||||||
|
|
||||||
let query = if let Some(CommandDataOption {
|
let query = options.first().and_then(|option| {
|
||||||
value: CommandDataOptionValue::String(query), ..
|
if let CommandDataOptionValue::String(query) = &option.value {
|
||||||
}) = &options.first()
|
Some(query)
|
||||||
{
|
} else {
|
||||||
query
|
None
|
||||||
} else {
|
}
|
||||||
return CreateEmbed::new()
|
});
|
||||||
.author(CreateEmbedAuthor::new("Rustendo"))
|
|
||||||
.title("Error 400")
|
if query.is_none() {
|
||||||
.description("There is no query provied.")
|
return Embed::create_error_respose(
|
||||||
.footer(CreateEmbedFooter::new(format!(">{} | {}", current_time, username)))
|
username,
|
||||||
};
|
"400: Bad request",
|
||||||
|
"There is no query provided",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let guild_id = match &command.guild_id {
|
let guild_id = match &command.guild_id {
|
||||||
Some(guild_id) => guild_id,
|
Some(guild_id) => guild_id,
|
||||||
None => {
|
None => {
|
||||||
return CreateEmbed::new()
|
return Embed::create_error_respose(
|
||||||
.author(CreateEmbedAuthor::new("Rustendo"))
|
username,
|
||||||
.title("guildid not found")
|
"guild_id not found",
|
||||||
.description("Could not find guild id.")
|
"Could not find guild id.",
|
||||||
.footer(CreateEmbedFooter::new(format!("> {} | {}", current_time, username)));
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (guild_id, channel_id) = {
|
|
||||||
let guild = &ctx.cache.guild(guild_id).unwrap();
|
|
||||||
// This may be unsafe, idk not sure yet
|
|
||||||
let channel_id = guild
|
|
||||||
.voice_states
|
|
||||||
.get(&command.user.id)
|
|
||||||
.and_then(|voice_state| voice_state.channel_id);
|
|
||||||
(guild.id, channel_id)
|
|
||||||
};
|
|
||||||
|
|
||||||
let connect_to = match channel_id {
|
music_manager::attempt_to_queue_song(
|
||||||
Some(channel) => channel,
|
ctx,
|
||||||
None => {
|
guild_id,
|
||||||
return CreateEmbed::new()
|
&command.user.id,
|
||||||
.author(CreateEmbedAuthor::new("Rustendo"))
|
&command.user.name,
|
||||||
.title("You are not in a VC.")
|
query.unwrap(),
|
||||||
.description("Join one to start playing music.")
|
)
|
||||||
.footer(CreateEmbedFooter::new(format!("> {} | {}", current_time, username)));
|
.await
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let manager = &songbird::get(ctx)
|
|
||||||
.await
|
|
||||||
.expect("")
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
manager.join(guild_id, connect_to).await.expect("Cannot connect>...");
|
|
||||||
|
|
||||||
CreateEmbed::new()
|
|
||||||
.author(CreateEmbedAuthor::new("Rustendo"))
|
|
||||||
.title(format!("Searching for {}", query))
|
|
||||||
.footer(CreateEmbedFooter::new(format!(">{} | {}", current_time, username)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn register() -> CreateCommand {
|
pub fn register() -> CreateCommand {
|
||||||
CreateCommand::new("play")
|
CreateCommand::new("play")
|
||||||
.description("Play music")
|
.description("Play music")
|
||||||
.add_option(
|
.add_option(
|
||||||
CreateCommandOption::new(CommandOptionType::String, "query", "Link or search term")
|
CreateCommandOption::new(CommandOptionType::String, "query", "Link or search term")
|
||||||
.required(true)
|
.required(true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
26
src/commands/skip.rs
Normal file
26
src/commands/skip.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use crate::music::music_manager;
|
||||||
|
use crate::util::embed::Embed;
|
||||||
|
use serenity::all::{CommandInteraction, Context};
|
||||||
|
use serenity::builder::{CreateCommand, CreateEmbed};
|
||||||
|
|
||||||
|
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
||||||
|
let username = command.user.name.as_str();
|
||||||
|
|
||||||
|
let guild_id = match &command.guild_id {
|
||||||
|
Some(guild_id) => guild_id,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"guild_id not found",
|
||||||
|
"Could not find guild id.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
music_manager::attempt_to_skip_current_song(ctx, guild_id, &command.user.id, &command.user.name)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register() -> CreateCommand {
|
||||||
|
CreateCommand::new("skip").description("Skip to the next song in queue")
|
||||||
|
}
|
|
@ -1,20 +1,25 @@
|
||||||
use chrono::Local;
|
use crate::music::music_manager;
|
||||||
|
use crate::util::embed::Embed;
|
||||||
use serenity::all::{CommandInteraction, Context};
|
use serenity::all::{CommandInteraction, Context};
|
||||||
use serenity::builder::{CreateCommand, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter};
|
use serenity::builder::{CreateCommand, CreateEmbed};
|
||||||
|
|
||||||
pub async fn run(_ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
pub async fn run(ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
||||||
let username = command.user.name.as_str();
|
let username = command.user.name.as_str();
|
||||||
let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S");
|
|
||||||
CreateEmbed::new()
|
|
||||||
.author(CreateEmbedAuthor::new("Rustendo"))
|
|
||||||
.title("I stopped and left\nJust like your girlfriend.")
|
|
||||||
.footer(CreateEmbedFooter::new(format!(">{} | {}", current_time, username)))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let guild_id = match &command.guild_id {
|
||||||
|
Some(guild_id) => guild_id,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"guild_id not found",
|
||||||
|
"Could not find guild id.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
music_manager::attempt_to_stop(ctx, guild_id, &command.user.id, &command.user.name).await
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register() -> CreateCommand {
|
pub fn register() -> CreateCommand {
|
||||||
CreateCommand::new("stop").description("Stop playing and start leavin'")
|
CreateCommand::new("stop").description("Stop playing and start leavin'")
|
||||||
}
|
}
|
||||||
|
|
||||||
// >18/02/2024 @ 19:01:59 - bartlo
|
|
||||||
// >2024-02-19 17:58:39 | moonleay
|
|
110
src/main.rs
110
src/main.rs
|
@ -1,58 +1,56 @@
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod music;
|
||||||
mod util;
|
mod util;
|
||||||
mod handler;
|
|
||||||
|
|
||||||
use std::thread::current;
|
use serenity::all::{
|
||||||
use chrono::Local;
|
CommandInteraction, CreateInteractionResponseFollowup, OnlineStatus, VoiceState,
|
||||||
use serenity::all::{CommandInteraction, OnlineStatus};
|
};
|
||||||
use serenity::async_trait;
|
use serenity::async_trait;
|
||||||
use serenity::builder::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage};
|
use serenity::builder::CreateEmbed;
|
||||||
use serenity::gateway::ActivityData;
|
use serenity::gateway::ActivityData;
|
||||||
use serenity::model::application::{Command, Interaction};
|
use serenity::model::application::{Command, Interaction};
|
||||||
use serenity::model::gateway::Ready;
|
use serenity::model::gateway::Ready;
|
||||||
use serenity::prelude::*;
|
use serenity::prelude::*;
|
||||||
use util::config;
|
use util::{config, embed::Embed, user_util};
|
||||||
|
|
||||||
// This trait adds the `register_songbird` and `register_songbird_with` methods
|
// This trait adds the `register_songbird` and `register_songbird_with` methods
|
||||||
// to the client builder below, making it easy to install this voice client.
|
// to the client builder below, making it easy to install this voice client.
|
||||||
// The voice client can be retrieved in any command using `songbird::get(ctx).await`.
|
// The voice client can be retrieved in any command using `songbird::get(ctx).await`.
|
||||||
use songbird::SerenityInit;
|
use songbird::SerenityInit;
|
||||||
|
|
||||||
// Event related imports to detect track creation failures.
|
|
||||||
use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent};
|
|
||||||
|
|
||||||
// To turn user URLs into playable audio, we'll use yt-dlp.
|
|
||||||
use songbird::input::YoutubeDl;
|
|
||||||
|
|
||||||
// YtDl requests need an HTTP client to operate -- we'll create and store our own.
|
// YtDl requests need an HTTP client to operate -- we'll create and store our own.
|
||||||
use reqwest::Client as HttpClient;
|
use reqwest::Client as HttpClient;
|
||||||
|
|
||||||
// Import the `Context` to handle commands.
|
|
||||||
use serenity::client::Context;
|
|
||||||
|
|
||||||
struct HttpKey;
|
struct HttpKey;
|
||||||
impl TypeMapKey for HttpKey {
|
impl TypeMapKey for HttpKey {
|
||||||
type Value = HttpClient;
|
type Value = HttpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lazy static stuff. I don't like it, but it has to be here, bc it has to be @ root
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
struct Handler;
|
struct Handler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl EventHandler for Handler {
|
impl EventHandler for Handler {
|
||||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||||
if let Interaction::Command(command) = interaction {
|
if let Interaction::Command(command) = interaction {
|
||||||
let content = match command.data.name.as_str() {
|
let _ = &command.defer(&ctx.http()).await.expect("Cannot defer");
|
||||||
"info" => Some(commands::info::run(&ctx, &command)),
|
|
||||||
"play" => Some(commands::play::run(&ctx, &command)),
|
let content = Some(match command.data.name.as_str() {
|
||||||
"stop" => Some(commands::stop::run(&ctx, &command)),
|
"info" => commands::info::run(&ctx, &command).await,
|
||||||
_ => Some(respond_with_error(&ctx, &command)),
|
"play" => commands::play::run(&ctx, &command).await,
|
||||||
};
|
"stop" => commands::stop::run(&ctx, &command).await,
|
||||||
|
"skip" => commands::skip::run(&ctx, &command).await,
|
||||||
|
"nowplaying" => commands::now_playing::run(&ctx, &command).await,
|
||||||
|
_ => respond_with_error(&ctx, &command).await,
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(embed) = content {
|
if let Some(embed) = content {
|
||||||
let data = CreateInteractionResponseMessage::new().embed(embed);
|
let followup = CreateInteractionResponseFollowup::new().embed(embed);
|
||||||
let builder = CreateInteractionResponse::Message(data);
|
if let Err(why) = command.create_followup(&ctx.http, followup).await {
|
||||||
if let Err(why) = command.create_response(&ctx.http, builder).await {
|
println!("Cannot followup to slash command: {why}")
|
||||||
println!("Cannot respond to slash command: {why}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,32 +62,57 @@ impl EventHandler for Handler {
|
||||||
let _command = Command::create_global_command(&ctx.http, commands::info::register()).await;
|
let _command = Command::create_global_command(&ctx.http, commands::info::register()).await;
|
||||||
let _command = Command::create_global_command(&ctx.http, commands::stop::register()).await;
|
let _command = Command::create_global_command(&ctx.http, commands::stop::register()).await;
|
||||||
let _command = Command::create_global_command(&ctx.http, commands::play::register()).await;
|
let _command = Command::create_global_command(&ctx.http, commands::play::register()).await;
|
||||||
|
let _command = Command::create_global_command(&ctx.http, commands::skip::register()).await;
|
||||||
|
let _command =
|
||||||
|
Command::create_global_command(&ctx.http, commands::now_playing::register()).await;
|
||||||
|
println!("Commands are registered and Rustendo is ready for Freddy.");
|
||||||
|
}
|
||||||
|
|
||||||
println!("Created all public / commands");
|
async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState) {
|
||||||
|
// TODO This does not work, when switching channels
|
||||||
|
if new.channel_id.is_some() {
|
||||||
|
return; // User did not leave, ignore
|
||||||
|
}
|
||||||
|
if let Some(old) = old {
|
||||||
|
if !user_util::is_self_connected_to_vc(&ctx, &old.guild_id.unwrap()).await {
|
||||||
|
return; // Bot is not connected to a VC, ignore
|
||||||
|
}
|
||||||
|
if user_util::get_amount_of_members_in_vc(
|
||||||
|
&ctx,
|
||||||
|
&old.guild_id.unwrap(),
|
||||||
|
&old.channel_id.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
< 2
|
||||||
|
{
|
||||||
|
let manager = songbird::get(&ctx).await.expect("Cannot get Songbird");
|
||||||
|
if let Err(e) = manager.remove(old.guild_id.unwrap()).await {
|
||||||
|
println!("Failed to remove handler: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // else: new user joined, ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn respond_with_error(_ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
pub async fn respond_with_error(_ctx: &Context, command: &CommandInteraction) -> CreateEmbed {
|
||||||
let username = &command.user.name.as_str();
|
Embed::create_error_respose(
|
||||||
let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S");
|
command.user.name.as_str(),
|
||||||
|
"Command not found",
|
||||||
CreateEmbed::new()
|
"Cannot find the executed command",
|
||||||
.author(CreateEmbedAuthor::new("Rustendo"))
|
)
|
||||||
.title("Command not found")
|
|
||||||
.description("Cannot find the executed command")
|
|
||||||
.footer(CreateEmbedFooter::new(format!("> {} | {}", current_time, username)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
println!(r"__________ __ .___
|
println!(
|
||||||
\______ \__ __ _______/ |_ ____ ____ __| _/____
|
r"__________ __ .___
|
||||||
| _/ | | ___/\ __\_/ __ \ / \ / __ |/ _ \
|
\______ \__ __ _______/ |_ ____ ____ __| _/____
|
||||||
|
| _/ | | ___/\ __\_/ __ \ / \ / __ |/ _ \
|
||||||
| | \ | |___ \ | | \ ___/ | | | /_/ ( <_> )
|
| | \ | |___ \ | | \ ___/ | | | /_/ ( <_> )
|
||||||
|____|_ /____/____ > |__| \___ >|___| |____ |\____/
|
|____|_ /____/____ > |__| \___ >|___| |____ |\____/
|
||||||
\/ \/ \/ \/ \/
|
\/ \/ \/ \/ \/
|
||||||
");
|
"
|
||||||
|
);
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
let config = config::load().unwrap();
|
let config = config::load().unwrap();
|
||||||
|
@ -104,6 +127,7 @@ async fn main() {
|
||||||
.activity(activity)
|
.activity(activity)
|
||||||
.register_songbird()
|
.register_songbird()
|
||||||
.type_map_insert::<HttpKey>(HttpClient::new())
|
.type_map_insert::<HttpKey>(HttpClient::new())
|
||||||
|
.intents(GatewayIntents::all())
|
||||||
.await
|
.await
|
||||||
.expect("Error creating client");
|
.expect("Error creating client");
|
||||||
|
|
||||||
|
@ -111,9 +135,7 @@ async fn main() {
|
||||||
//
|
//
|
||||||
// Shards will automatically attempt to reconnect, and will perform exponential backoff until
|
// Shards will automatically attempt to reconnect, and will perform exponential backoff until
|
||||||
// it reconnects.
|
// it reconnects.
|
||||||
if let Err(why) = client
|
if let Err(why) = client.start().await {
|
||||||
.start()
|
|
||||||
.await {
|
|
||||||
println!("Client error: {why:?}");
|
println!("Client error: {why:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
src/music/mod.rs
Normal file
3
src/music/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod music_events;
|
||||||
|
pub mod music_manager;
|
||||||
|
pub mod music_queue;
|
56
src/music/music_events.rs
Normal file
56
src/music/music_events.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use crate::music::{music_manager, music_queue};
|
||||||
|
|
||||||
|
use serenity::all::{ChannelId, GuildId, Http};
|
||||||
|
use serenity::async_trait;
|
||||||
|
use songbird::input::Compose;
|
||||||
|
use songbird::{Event, EventContext, EventHandler};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct TrackEndNotifier {
|
||||||
|
pub guild_id: GuildId,
|
||||||
|
pub channel_id: ChannelId,
|
||||||
|
pub http: Arc<Http>,
|
||||||
|
pub cmdctx: Arc<serenity::client::Context>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for TrackEndNotifier {
|
||||||
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
||||||
|
// TODO: Does this need to be unsafe?
|
||||||
|
if let EventContext::Track(..) = ctx {
|
||||||
|
println!("The track ended!");
|
||||||
|
|
||||||
|
if music_queue::is_empty(&self.guild_id).await {
|
||||||
|
// No more songs in queue, exit the vc
|
||||||
|
let stopped = match music_manager::leave(&self.cmdctx, &self.guild_id).await {
|
||||||
|
Ok(stopped) => stopped,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Cannot stop: {:?}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if stopped {
|
||||||
|
music_queue::delete_queue(&self.guild_id).await;
|
||||||
|
println!("Stopped playing successfully.");
|
||||||
|
} else {
|
||||||
|
println!("Failed to stop playing.");
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut head = match music_queue::next(&self.guild_id).await {
|
||||||
|
Some(head) => head,
|
||||||
|
None => {
|
||||||
|
println!("Cannot get head of queue");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"Now playing: {}",
|
||||||
|
head.aux_metadata().await.unwrap().title.unwrap()
|
||||||
|
);
|
||||||
|
music_manager::play_song(&self.cmdctx, &self.guild_id, &head).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
289
src/music/music_manager.rs
Normal file
289
src/music/music_manager.rs
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
use crate::music::{music_events, music_queue};
|
||||||
|
use crate::util::embed::Embed;
|
||||||
|
use crate::util::user_util;
|
||||||
|
use crate::util::user_util::get_vc_id;
|
||||||
|
use crate::HttpKey;
|
||||||
|
use serenity::all::{Context, CreateEmbed, GuildId, UserId};
|
||||||
|
use songbird::error::JoinError;
|
||||||
|
use songbird::input::YoutubeDl;
|
||||||
|
use songbird::{Event, TrackEvent};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Either queues the song, or start playing it instantly, depending on if there is already a song playing
|
||||||
|
pub async fn attempt_to_queue_song(
|
||||||
|
ctx: &Context,
|
||||||
|
guild_id: &GuildId,
|
||||||
|
user_id: &UserId,
|
||||||
|
username: &str,
|
||||||
|
query: &str,
|
||||||
|
) -> CreateEmbed {
|
||||||
|
if !user_util::is_user_connected_to_vc(ctx, guild_id, user_id).await {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"You are not connected to a VC",
|
||||||
|
"Connect to my vc to start controlling the music.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let connect_to = match get_vc_id(ctx, guild_id, user_id).await {
|
||||||
|
Some(channel_id) => channel_id,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(username, "Error", "Cannot find channel_id.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let manager: &Arc<songbird::Songbird> = &songbird::get(ctx) // TODO match
|
||||||
|
.await
|
||||||
|
.expect("Cannot get Songbird.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let self_channel = user_util::get_self_vc_id(ctx, guild_id).await;
|
||||||
|
if !user_util::is_self_connected_to_vc(ctx, guild_id).await {
|
||||||
|
// self is connected to vc, check if user is in same vc
|
||||||
|
|
||||||
|
if self_channel.is_none() {
|
||||||
|
// Connect to VC
|
||||||
|
manager // TODO match
|
||||||
|
.join(*guild_id, connect_to)
|
||||||
|
.await
|
||||||
|
.expect("Cannot connect>...");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let self_channel = self_channel.expect("Cannot get self channel"); // TODO: match
|
||||||
|
|
||||||
|
// Check if user is in the same VC as the bot
|
||||||
|
if self_channel != connect_to {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"You are not in my VC",
|
||||||
|
"You have to be in my VC in order to control the music.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get query
|
||||||
|
let do_search = !query.starts_with("http");
|
||||||
|
let http_client = {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
data.get::<HttpKey>()
|
||||||
|
.cloned()
|
||||||
|
.expect("Guaranteed to exist in the typemap.")
|
||||||
|
};
|
||||||
|
|
||||||
|
if query.contains("youtu") && query.contains("&list=") {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"Playlists are not supported",
|
||||||
|
"I do not support playlists of any kind, please only provide links to videos"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create source
|
||||||
|
let src = if do_search {
|
||||||
|
YoutubeDl::new_search(http_client, query.to_string())
|
||||||
|
} else {
|
||||||
|
YoutubeDl::new(http_client, query.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
let currently_playing = music_queue::get_now_playing(guild_id).await;
|
||||||
|
music_queue::add_to_queue(guild_id, src.clone()).await;
|
||||||
|
if currently_playing.is_some() {
|
||||||
|
// Add to queue
|
||||||
|
return Embed::create_yt_playing(src, username, "Added to queue").await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _query = music_queue::next(guild_id)
|
||||||
|
.await
|
||||||
|
.expect("Cannot get head of queue");
|
||||||
|
music_queue::set_now_playing(guild_id, Some(src.clone())).await;
|
||||||
|
|
||||||
|
let handler_lock = match manager.get(*guild_id) {
|
||||||
|
Some(handler) => handler,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"Error",
|
||||||
|
"Cannot get handler of this guild.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start playing
|
||||||
|
let mut handler = handler_lock.lock().await;
|
||||||
|
let track_handle = handler.play_input(src.clone().into()); // TODO: Add event handlers
|
||||||
|
music_queue::set_now_playing_track_handle(guild_id, Some(track_handle)).await;
|
||||||
|
handler.add_global_event(
|
||||||
|
Event::Track(TrackEvent::End),
|
||||||
|
music_events::TrackEndNotifier {
|
||||||
|
guild_id: *guild_id,
|
||||||
|
channel_id: connect_to,
|
||||||
|
http: Arc::clone(&ctx.http),
|
||||||
|
cmdctx: Arc::new(ctx.clone()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Embed::create_yt_playing(src, username, "Now playing").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Play the provided song
|
||||||
|
pub async fn play_song(ctx: &Context, guild_id: &GuildId, target: &YoutubeDl) {
|
||||||
|
let manager = &songbird::get(ctx) // TODO match
|
||||||
|
.await
|
||||||
|
.expect("Cannot get Songbird.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
if !user_util::is_self_connected_to_vc(ctx, guild_id).await {
|
||||||
|
println!("Bot is not connected to a VC, cannot play.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
music_queue::set_now_playing(guild_id, Some(target.clone())).await;
|
||||||
|
let handler_lock = match manager.get(*guild_id) {
|
||||||
|
Some(handler) => handler,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut handler = handler_lock.lock().await;
|
||||||
|
handler.stop(); // Stop playing the current song
|
||||||
|
let track_handle = handler.play_input(target.clone().into()); // TODO: Add event handlers
|
||||||
|
music_queue::set_now_playing_track_handle(guild_id, Some(track_handle)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to skip the song, which is currently playing. Do nothing if there is no next song
|
||||||
|
pub async fn attempt_to_skip_current_song(
|
||||||
|
ctx: &Context,
|
||||||
|
guild_id: &GuildId,
|
||||||
|
user_id: &UserId,
|
||||||
|
username: &str,
|
||||||
|
) -> CreateEmbed {
|
||||||
|
if !user_util::is_user_connected_to_vc(ctx, guild_id, user_id).await {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"You are not connected to a VC",
|
||||||
|
"Connect to my vc to start controlling the music.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let connect_to = match get_vc_id(ctx, guild_id, user_id).await {
|
||||||
|
Some(channel_id) => channel_id,
|
||||||
|
None => {
|
||||||
|
return Embed::create_error_respose(username, "Error", "Cannot find channel_id.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let manager: &Arc<songbird::Songbird> = &songbird::get(ctx) // TODO match
|
||||||
|
.await
|
||||||
|
.expect("Cannot get Songbird.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let self_channel = user_util::get_self_vc_id(ctx, guild_id).await;
|
||||||
|
if !user_util::is_self_connected_to_vc(ctx, guild_id).await {
|
||||||
|
// self is connected to vc, check if user is in same vc
|
||||||
|
|
||||||
|
if self_channel.is_none() {
|
||||||
|
// Connect to VC
|
||||||
|
manager // TODO match
|
||||||
|
.join(*guild_id, connect_to)
|
||||||
|
.await
|
||||||
|
.expect("Cannot connect>...");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let self_channel = self_channel.expect("Cannot get self channel"); // TODO: match
|
||||||
|
|
||||||
|
// Check if user is in the same VC as the bot
|
||||||
|
if self_channel != connect_to {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"You are not in my VC",
|
||||||
|
"You have to be in my VC in order to controll the music.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let head = music_queue::next(guild_id).await; // TODO match
|
||||||
|
if head.is_none() {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"Cannot find a song to play",
|
||||||
|
"The queue is empty.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let head = head.unwrap();
|
||||||
|
play_song(ctx, guild_id, &head).await;
|
||||||
|
|
||||||
|
Embed::create_yt_playing(head, username, "Song skipped; Now playing").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to clear the queue and stop playing. Also leave the vc
|
||||||
|
pub async fn attempt_to_stop(
|
||||||
|
ctx: &Context,
|
||||||
|
guild_id: &GuildId,
|
||||||
|
user_id: &UserId,
|
||||||
|
username: &str,
|
||||||
|
) -> CreateEmbed {
|
||||||
|
if !user_util::is_self_connected_to_vc(ctx, guild_id).await {
|
||||||
|
// Bot is not connectd to vc; no need to dc
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"Bot is not connected",
|
||||||
|
"And therefore there is no need to do anything.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let self_channel = user_util::get_self_vc_id(ctx, guild_id)
|
||||||
|
.await
|
||||||
|
.expect("Cannot get self channel");
|
||||||
|
let connect_to = get_vc_id(ctx, guild_id, user_id)
|
||||||
|
.await
|
||||||
|
.expect("Cannot get channel id");
|
||||||
|
|
||||||
|
// Check if user is in the same VC as the bot
|
||||||
|
if self_channel != connect_to {
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"You are not in my VC.",
|
||||||
|
"Connect to my VC to controll the music.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stopped = match leave(ctx, guild_id).await {
|
||||||
|
Ok(stopped) => stopped,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error while stopping: {:?}", e);
|
||||||
|
return Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"There was an error",
|
||||||
|
"Tell moonleay to check the logs.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !stopped {
|
||||||
|
Embed::create_error_respose(
|
||||||
|
username,
|
||||||
|
"Can't stop, what ain't running",
|
||||||
|
"I am not connected.\nI cant stop doing something, when I'm not doing it.",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
music_queue::delete_queue(guild_id).await; // Clear queue
|
||||||
|
|
||||||
|
Embed::create_success_response(username, "I stopped and left", "Just like you girlfriend.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make the bot leave the voice channel. Returns Ok(true) if bot was connected, returns Ok(false) if bot was not connected. Returns Err if something went wrong.
|
||||||
|
pub async fn leave(ctx: &Context, guild_id: &GuildId) -> Result<bool, JoinError> {
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Cannot get Songbird")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let handler = manager.get(*guild_id);
|
||||||
|
let has_handler = handler.is_some();
|
||||||
|
|
||||||
|
if has_handler {
|
||||||
|
handler.unwrap().lock().await.stop();
|
||||||
|
manager.remove(*guild_id).await?;
|
||||||
|
return Ok(true); // Handler removed
|
||||||
|
}
|
||||||
|
Ok(false) // No handler, so it's already stopped
|
||||||
|
}
|
84
src/music/music_queue.rs
Normal file
84
src/music/music_queue.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use serenity::all::GuildId;
|
||||||
|
use songbird::input::YoutubeDl;
|
||||||
|
use songbird::tracks::TrackHandle;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
type MusicQueueItem = Arc<Mutex<MusicQueue>>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MusicQueue {
|
||||||
|
pub queue: VecDeque<YoutubeDl>,
|
||||||
|
pub now_playing: Option<YoutubeDl>,
|
||||||
|
pub now_playing_track_handle: Option<TrackHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref HASHMAP: Mutex<HashMap<GuildId, MusicQueueItem>> = Mutex::new(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_music_queue(guild_id: &GuildId) -> MusicQueueItem {
|
||||||
|
let mut queues = HASHMAP.lock().await;
|
||||||
|
|
||||||
|
queues
|
||||||
|
.entry(*guild_id)
|
||||||
|
.or_insert(Arc::new(Mutex::new(MusicQueue {
|
||||||
|
queue: VecDeque::new(),
|
||||||
|
now_playing: None,
|
||||||
|
now_playing_track_handle: None,
|
||||||
|
})))
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn with_music_queue<F, T>(guild_id: &GuildId, f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut MusicQueue) -> T,
|
||||||
|
T: Send,
|
||||||
|
{
|
||||||
|
let queue = get_music_queue(guild_id).await;
|
||||||
|
let mut queue = queue.lock().await;
|
||||||
|
|
||||||
|
f(&mut queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_queue(guild_id: &GuildId) {
|
||||||
|
with_music_queue(guild_id, |queue| {
|
||||||
|
queue.now_playing = None;
|
||||||
|
queue.queue.clear();
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_to_queue(guild_id: &GuildId, input: YoutubeDl) {
|
||||||
|
with_music_queue(guild_id, |queue| queue.queue.push_back(input)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get next track in queue
|
||||||
|
pub async fn next(guild_id: &GuildId) -> Option<YoutubeDl> {
|
||||||
|
with_music_queue(guild_id, |queue| queue.queue.pop_front()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_now_playing(guild_id: &GuildId, now_playing: Option<YoutubeDl>) {
|
||||||
|
with_music_queue(guild_id, |queue| queue.now_playing = now_playing).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_now_playing(guild_id: &GuildId) -> Option<YoutubeDl> {
|
||||||
|
with_music_queue(guild_id, |queue| queue.now_playing.to_owned()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_now_playing_track_handle(guild_id: &GuildId, track_handle: Option<TrackHandle>) {
|
||||||
|
with_music_queue(guild_id, |queue| {
|
||||||
|
queue.now_playing_track_handle = track_handle
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_now_playing_track_handle(guild_id: &GuildId) -> Option<TrackHandle> {
|
||||||
|
with_music_queue(guild_id, |queue| queue.now_playing_track_handle.to_owned()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_empty(guild_id: &GuildId) -> bool {
|
||||||
|
with_music_queue(guild_id, |queue| queue.queue.is_empty()).await
|
||||||
|
}
|
|
@ -6,9 +6,6 @@ use std::error::Error;
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub discord_token: String,
|
pub discord_token: String,
|
||||||
pub lavalink_address: String,
|
|
||||||
pub lavalink_password: String,
|
|
||||||
pub user_id: u64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_FILE: &str = "./data/config.json";
|
const CONFIG_FILE: &str = "./data/config.json";
|
||||||
|
@ -17,23 +14,20 @@ pub fn load() -> Result<Config, Box<dyn Error>> {
|
||||||
// TODO: load config, create empty config if there is no config, stop if there is no complete config
|
// TODO: load config, create empty config if there is no config, stop if there is no complete config
|
||||||
let config_file = match fs::File::open(CONFIG_FILE) {
|
let config_file = match fs::File::open(CONFIG_FILE) {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(_) => create_empty()
|
Err(_) => create_empty(),
|
||||||
};
|
};
|
||||||
let config_file = serde_json::from_reader(config_file).unwrap();
|
let config_file = serde_json::from_reader(config_file).unwrap();
|
||||||
Ok(config_file)
|
Ok(config_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_empty() -> fs::File{
|
fn create_empty() -> fs::File {
|
||||||
let example_config = Config {
|
let example_config = Config {
|
||||||
discord_token: "paste_your_token".to_string(),
|
discord_token: "paste_your_token".to_string(),
|
||||||
lavalink_address: "paste_your_lavalink_address".to_string(),
|
|
||||||
lavalink_password: "paste_your_lavalink_password".to_string(),
|
|
||||||
user_id: 1
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config_file = fs::File::create(CONFIG_FILE).unwrap();
|
let mut config_file = fs::File::create(CONFIG_FILE).unwrap();
|
||||||
let file_content = serde_json::to_string(&example_config).unwrap();
|
let file_content = serde_json::to_string(&example_config).unwrap();
|
||||||
config_file.write_all(&file_content.as_bytes()).unwrap();
|
config_file.write_all(file_content.as_bytes()).unwrap();
|
||||||
|
|
||||||
panic!("There is no config. But now there is a template.")
|
panic!("There is no config. But now there is a template.")
|
||||||
}
|
}
|
||||||
|
|
81
src/util/embed.rs
Normal file
81
src/util/embed.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
|
use serenity::all::{Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter};
|
||||||
|
use songbird::input::{Compose, YoutubeDl};
|
||||||
|
|
||||||
|
pub struct Embed;
|
||||||
|
|
||||||
|
impl Embed {
|
||||||
|
pub fn create_success_response(username: &str, title: &str, desc: &str) -> CreateEmbed {
|
||||||
|
let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S");
|
||||||
|
|
||||||
|
CreateEmbed::new()
|
||||||
|
.title(title)
|
||||||
|
.description(desc)
|
||||||
|
.color(Color::from_rgb(224, 49, 26))
|
||||||
|
.footer(CreateEmbedFooter::new(format!(
|
||||||
|
"> {} - {}",
|
||||||
|
current_time, username
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_error_respose(
|
||||||
|
username: &str,
|
||||||
|
error_title: &str,
|
||||||
|
error_desc: &str,
|
||||||
|
) -> CreateEmbed {
|
||||||
|
let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S");
|
||||||
|
|
||||||
|
CreateEmbed::new()
|
||||||
|
.author(CreateEmbedAuthor::new("Oops, something went wrong."))
|
||||||
|
.title(error_title)
|
||||||
|
.description(error_desc)
|
||||||
|
.color(Color::from_rgb(224, 49, 26))
|
||||||
|
.footer(CreateEmbedFooter::new(format!(
|
||||||
|
"> {} - {}",
|
||||||
|
current_time, username
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_yt_playing(
|
||||||
|
mut src: YoutubeDl,
|
||||||
|
username: &str,
|
||||||
|
show_as_author: &str,
|
||||||
|
) -> CreateEmbed {
|
||||||
|
let current_time = Local::now().format("%Y-%m-%d @ %H:%M:%S");
|
||||||
|
|
||||||
|
// Get metadata
|
||||||
|
let metadata = src.aux_metadata().await.expect("Cannot get metadata");
|
||||||
|
let title = metadata.title.unwrap_or("Unknown title".to_string());
|
||||||
|
let artist = metadata.artist.unwrap_or("Unknown artist".to_string());
|
||||||
|
let duration = metadata.duration.unwrap_or(Duration::from_millis(0));
|
||||||
|
let thumbnail = metadata
|
||||||
|
.thumbnail
|
||||||
|
.unwrap_or("https://http.cat/images/403.jpg".to_string());
|
||||||
|
let link = metadata
|
||||||
|
.source_url
|
||||||
|
.unwrap_or("https://piped.moonleay.net/403".to_string());
|
||||||
|
|
||||||
|
CreateEmbed::new()
|
||||||
|
.author(CreateEmbedAuthor::new(show_as_author))
|
||||||
|
.title(title)
|
||||||
|
.url(link)
|
||||||
|
.thumbnail(thumbnail)
|
||||||
|
.field("Artist", artist, true)
|
||||||
|
.field(
|
||||||
|
"Duration",
|
||||||
|
format!(
|
||||||
|
"{}min {}sec",
|
||||||
|
duration.as_secs() / 60,
|
||||||
|
duration.as_secs() % 60
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.color(Color::from_rgb(81, 224, 26))
|
||||||
|
.footer(CreateEmbedFooter::new(format!(
|
||||||
|
"> {} - {}",
|
||||||
|
current_time, username
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,3 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod embed;
|
||||||
|
pub mod user_util;
|
||||||
|
|
69
src/util/user_util.rs
Normal file
69
src/util/user_util.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use serenity::all::{ChannelId, Context, Guild, GuildId, PartialGuild, UserId, VoiceState};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Request a guild by id, get it from cache
|
||||||
|
pub fn request_guild(ctx: &Context, guild_id: &GuildId) -> Guild {
|
||||||
|
match guild_id.to_guild_cached(&ctx.cache) {
|
||||||
|
Some(guild) => guild.clone(),
|
||||||
|
None => {
|
||||||
|
panic!("Cannot get guild with id {:?}!", guild_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request a guild by id, get it from Discord, not from cache, this is a partial guild
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn request_partial_guild(ctx: &Context, guild_id: &GuildId) -> PartialGuild {
|
||||||
|
match ctx.http.get_guild(*guild_id).await {
|
||||||
|
Ok(guild) => guild,
|
||||||
|
Err(error) => {
|
||||||
|
panic!("error whilest getting guild from Discord {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the voice channel id of a user
|
||||||
|
pub async fn get_vc_id(ctx: &Context, guild_id: &GuildId, user_id: &UserId) -> Option<ChannelId> {
|
||||||
|
let guild = request_guild(ctx, guild_id);
|
||||||
|
guild
|
||||||
|
.voice_states
|
||||||
|
.get(user_id)
|
||||||
|
.and_then(|voice_state| voice_state.channel_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the bot is connected to a voice channel
|
||||||
|
pub async fn is_self_connected_to_vc(ctx: &Context, guild_id: &GuildId) -> bool {
|
||||||
|
get_self_vc_id(ctx, guild_id).await.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a user is connected to a voice channel
|
||||||
|
pub async fn is_user_connected_to_vc(ctx: &Context, guild_id: &GuildId, user_id: &UserId) -> bool {
|
||||||
|
get_vc_id(ctx, guild_id, user_id).await.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the voice channel id of the bot
|
||||||
|
pub async fn get_self_vc_id(ctx: &Context, guild_id: &GuildId) -> Option<ChannelId> {
|
||||||
|
let user_id = ctx.cache.current_user().id;
|
||||||
|
|
||||||
|
get_vc_id(ctx, guild_id, &user_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all voice states of a guild
|
||||||
|
pub async fn get_voice_states(ctx: &Context, guild_id: &GuildId) -> HashMap<UserId, VoiceState> {
|
||||||
|
let guild = request_guild(ctx, guild_id);
|
||||||
|
guild.voice_states.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the amount of members in a voice channel
|
||||||
|
pub async fn get_amount_of_members_in_vc(
|
||||||
|
ctx: &Context,
|
||||||
|
guild_id: &GuildId,
|
||||||
|
channel_id: &ChannelId,
|
||||||
|
) -> usize {
|
||||||
|
let voice_states = get_voice_states(ctx, guild_id).await;
|
||||||
|
let amount = voice_states
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, voice_state)| voice_state.channel_id == Some(*channel_id))
|
||||||
|
.count();
|
||||||
|
amount
|
||||||
|
}
|
Loading…
Reference in a new issue