15 Commits
0.2.1 ... 0.8.2

58 changed files with 2915 additions and 887 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
DISCORD_TOKEN=<YOUR_DISCORD_TOKEN>
PREFIX=<YOUR_PREFIX>

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
.env

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "src/spotify-parser"]
path = src/spotify-parser
url = https://github.com/eRgo35/spotify-parser

1363
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,45 @@
[package]
name = "lyra"
version = "0.3.0"
version = "0.8.2"
authors = ["Michał Czyż <mike@c2yz.com>"]
edition = "2021"
description = "A featureful Discord bot written in Rust."
documentation = "https://lyra.c2yz.com/docs"
readme = "README.md"
homepage = "https://lyra.c2yz.com"
license-file = "LICENSE.md"
keywords = ["discord", "bot", "rust", "music", "featureful"]
[dependencies]
dotenv = "0.15.0"
openssl = { version = "0.10.63", features = ["vendored"] }
regex = "1.10.3"
reqwest = "0.11.23"
serenity = { version = "0.12.0", features = ["cache", "framework", "standard_framework", "voice"] }
songbird = { version = "0.4.0", features = ["builtin-queue", "serenity"] }
symphonia = "0.5.3"
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "signal"] }
fancy-regex = "0.13.0"
json = "0.12.4"
openssl = { version = "0.10.66", features = ["vendored"] }
owoify = "0.1.5"
poise = "0.6.1"
rand = "0.8.5"
regex = "1.10.6"
reqwest = { version = "0.11.27", features = ["json"] }
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
serenity = { version = "0.12.2", features = [
"cache",
"framework",
"standard_framework",
"voice",
] }
songbird = { version = "0.4.3", features = ["builtin-queue", "serenity"] }
symphonia = { version = "0.5.4", features = [
"aac",
"adpcm",
"alac",
"flac",
"mpa",
"isomp4",
] }
tokio = { version = "1.39.2", features = ["macros", "full", "signal"] }
tracing = "0.1.40"
tracing-futures = "0.2.5"
tracing-subscriber = "0.3.18"
url = "2.5.2"

View File

@@ -1,19 +0,0 @@
FROM rust:1.71-alpine
RUN apk add --update \
alpine-sdk \
ffmpeg \
youtube-dl \
pkgconfig \
cmake \
openssl-dev \
musl-dev \
openssl
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/lyra"]

View File

@@ -1,5 +1,21 @@
# License
MIT License
Copyright(c) Michał Czyż 2024
Copyright (c) 2024 Michał Czyż
[Currently] All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -3,10 +3,79 @@
![](assets/lyra-256.png)
Lyra is a music bot written in Rust.
More features coming soon!
## Getting Started
## Building
Lyra is an open source, discord music bot written in Rust.
The idea behind this project is to allow a user to self-host one's own instance of the bot.
User no longer has to rely on 3rd parties to provide them an invite link.
The bot can be run even on a desktop or a phone because after compilation, it's just a simple binary.
As of now, the bot supports spotify url track recognition through a separate nodejs script. I plan to write the actual parser inside the bot iteself but as of now I postponed it into future release.
Slash commands are still work in progress! Currently bot is still heavily in development!
## Setting up
To compile the source code on your own, you need `rust` and `cargo`
To run a dev version use
```bash
$ cargo run
```
To build a production version use
```bash
$ cargo build --release
```
If you need an ARM version and just don't want to wait for ages for the program to compile, use
```bash
$ cross build -r --target aarch64-unknown-linux-gnu
```
To run a program, just type
```bash
$ ./lyra
```
if you want to disown it from the shell, I recommend using the script I provided in `scripts` folder
## Commands
As of now, working commands are:
```
Music:
/deafen Deafens itself while in a voice channel; aliases: deafen, undeaden, shuush
/join Joins your voice channel
/leave Leaves the voice channel; aliases: leave, qa!
/mute Mutes itself while in a voice channel; aliases: mute, unmute, shhh
/pause Pauses the currently playing song
/play Plays a song; you can search by query or paste an url; aliases: play, p, enqueue
/queue Shows next tracks in queue; aliases: queue, q
/repeat Loops currently playing song provided amount of times; aliases: repeat, loop, while, for
/resume Resumes currently paused song
/seek Seeks a track by provided seconds
/skip Skips the currently playing song
/stop Stops playback and destroys the queue; aliases: stop, end
/volume Changes output volume
/effect Plays one of available audio effects
/stream Hijacks output and plays audio; search by query or paste an url; aliases: stream, override, hijack
Tools:
/ai Asks AI
/dice Rolls a dice
/owoify Owoifies whatever you want uwu
/ping Pings you backs with a response time
/posix Prints current time in POSIX format
/qr Creates a qr code from text
/verse Reference Bible by verse
Help:
/help Prints this help message; aliases: help, huh, welp
```

View File

@@ -1,6 +0,0 @@
version: '2'
services:
melvinjs:
container_name: lyra
build: .

0
scripts/cross-build.sh Normal file → Executable file
View File

0
scripts/launch.sh Normal file → Executable file
View File

View File

@@ -1,4 +1,4 @@
pub mod embeds;
pub mod kashi;
pub mod misc;
pub mod music;
pub mod tools;

58
src/commands/embeds.rs Normal file
View File

@@ -0,0 +1,58 @@
use crate::{Context, Error};
use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply;
use serenity::{
builder::{CreateEmbedAuthor, CreateEmbedFooter},
model::{Colour, Timestamp},
};
pub async fn fail(ctx: Context<'_>, err: String) -> Result<(), Error> {
ctx.send(
CreateReply::default().embed(
error_embed(ctx, &format!("Failed: {:?}", err))
.await
.unwrap(),
),
)
.await?;
Ok(())
}
pub async fn error_embed(ctx: Context<'_>, msg: &str) -> Result<CreateEmbed, Error> {
let embed = CreateEmbed::default()
.author(
CreateEmbedAuthor::new("Something went wrong!").icon_url(ctx.author().clone().face()),
)
.colour(Colour::from_rgb(255, 58, 97))
.title("Oopsie, Doopsie!")
.description(msg)
.timestamp(Timestamp::now())
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}
pub async fn embed(
ctx: Context<'_>,
author: &str,
description: &str,
title: &str,
) -> Result<CreateEmbed, Error> {
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new(author).icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97))
.title(title)
.description(description)
.timestamp(Timestamp::now())
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

3
src/commands/kashi.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod kashi;
pub use kashi::kashi;

View File

@@ -1,10 +1,17 @@
use serenity::{all::Message, client::Context, framework::standard::{macros::command, CommandResult}};
use crate::{Context, Error};
use crate::commands::misc::check_msg;
/// Kashi integration platform (WIP)
#[poise::command(
prefix_command,
slash_command,
category = "Kashi"
)]
pub async fn kashi(
ctx: Context<'_>
) -> Result<(), Error> {
#[command]
async fn kashi(ctx: &Context, msg: &Message) -> CommandResult {
check_msg(msg.reply(ctx, "Kashi lyrics platform integration").await);
let response = format!("Kashi platform is currently under construction!");
ctx.say(response).await?;
Ok(())
}

View File

@@ -1 +0,0 @@
pub mod kashi;

View File

@@ -1,8 +0,0 @@
use serenity::model::channel::Message;
use serenity::Result as SerenityResult;
pub fn check_msg(result: SerenityResult<Message>) {
if let Err(why) = result {
println!("Error sending message: {:?}", why);
}
}

30
src/commands/music.rs Normal file
View File

@@ -0,0 +1,30 @@
pub mod deafen;
pub mod join;
pub mod leave;
pub mod metadata;
pub mod mute;
pub mod notifier;
pub mod pause;
pub mod play;
pub mod queue;
pub mod repeat;
pub mod resume;
pub mod seek;
pub mod skip;
pub mod soundboard;
pub mod stop;
pub mod volume;
pub use deafen::deafen;
pub use join::join;
pub use leave::leave;
pub use mute::mute;
pub use pause::pause;
pub use play::play;
pub use queue::queue;
pub use repeat::repeat;
pub use resume::resume;
pub use seek::seek;
pub use skip::skip;
pub use stop::stop;
pub use volume::volume;

View File

@@ -1,24 +1,31 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::{
commands::embeds::{embed, error_embed, fail},
Context, Error,
};
use poise::CreateReply;
use crate::commands::misc::check_msg;
/// Deafens itself while in a voice channel; \
/// aliases: deafen, undeaden, shuush
#[poise::command(
prefix_command,
slash_command,
aliases("shuush", "undeafen"),
category = "Music"
)]
pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
#[command]
#[only_in(guilds)]
async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Client placed at init")
.expect("Songbird client placed at init")
.clone();
let handler_lock = match manager.get(guild_id) {
Some(handler) => handler,
None => {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(());
}
@@ -28,24 +35,18 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
if handler.is_deaf() {
if let Err(err) = handler.deafen(false).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", err))
.await,
);
fail(ctx, err.to_string()).await.unwrap();
}
check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await);
ctx.send(CreateReply::default().embed(embed(ctx, "Undeafened!", "", "").await.unwrap()))
.await?;
} else {
if let Err(err) = handler.deafen(true).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", err))
.await,
);
fail(ctx, err.to_string()).await.unwrap();
}
check_msg(msg.channel_id.say(&ctx.http, "Deafened").await);
ctx.send(CreateReply::default().embed(embed(ctx, "Deafened!", "", "").await.unwrap()))
.await?;
}
Ok(())

View File

@@ -1,29 +1,35 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use songbird::events::TrackEvent;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use songbird::TrackEvent;
use crate::commands::{misc::check_msg, music::misc::TrackErrorNotifier};
#[command]
#[only_in(guilds)]
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
let channel_id = msg.guild(&ctx.cache).unwrap().voice_states.get(&msg.author.id).and_then(|voice_state| voice_state.channel_id);
/// Joins your voice channel
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn join(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let channel_id = ctx
.guild()
.unwrap()
.voice_states
.get(&ctx.author().id)
.and_then(|voice_state| voice_state.channel_id);
let connect_to = match channel_id {
Some(channel) => channel,
None => {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(());
}
};
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird Voice placed at init")
.expect("Songbird client placed at init")
.clone();
if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
@@ -31,5 +37,8 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
}
ctx.send(CreateReply::default().embed(embed(ctx, "Joined!", "Hi there!", "").await.unwrap()))
.await?;
Ok(())
}

View File

@@ -1,36 +1,45 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::{
commands::embeds::{embed, error_embed, fail},
Context, Error,
};
use poise::CreateReply;
use crate::commands::misc::check_msg;
/// Leaves the voice channel; \
/// aliases: leave, qa!
#[poise::command(
prefix_command,
slash_command,
aliases("leave", "qa!"),
category = "Music"
)]
pub async fn leave(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
#[command]
#[aliases(q)]
#[only_in(guilds)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Client placed in at init")
.expect("Songbird client placed at init")
.clone();
let has_handler = manager.get(guild_id).is_some();
if !manager.get(guild_id).is_some() {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
if has_handler {
if let Err(err) = manager.remove(guild_id).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", err))
.await,
);
}
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
} else {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
return Ok(());
}
if let Err(err) = manager.remove(guild_id).await {
fail(ctx, err.to_string()).await.unwrap();
}
ctx.send(
CreateReply::default().embed(
embed(ctx, "Left!", "I left the voice channel", "")
.await
.unwrap(),
),
)
.await?;
Ok(())
}

View File

@@ -1,83 +0,0 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::prelude::*;
use serenity::prelude::*;
use songbird::tracks::LoopState;
use crate::commands::misc::check_msg;
#[command]
#[aliases(loop)]
#[only_in(guilds)]
async fn loopcurrent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
.await
.expect("Client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let track = queue.current().unwrap().get_info().await;
let is_looped = track.unwrap().loops;
let count = match args.single::<usize>() {
Ok(count) => count,
Err(_) => 100,
};
match is_looped {
LoopState::Infinite => {
let _ = queue.current().unwrap().disable_loop();
check_msg(
msg.channel_id
.say(
&ctx.http,
format!("Song unlooped."),
)
.await,
);
}
LoopState::Finite(_) => {
if count < 100 {
let _ = queue.current().unwrap().loop_for(count);
check_msg(
msg.channel_id
.say(
&ctx.http,
format!("Song looped forever (a very long time)."),
)
.await,
)
}
else {
let _ = queue.current().unwrap().enable_loop();
check_msg(
msg.channel_id
.say(
&ctx.http,
format!("Song looped {} times.", count),
)
.await,
)
}
}
}
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
}
Ok(())
}

View File

@@ -0,0 +1,7 @@
use songbird::{input::AuxMetadata, typemap::TypeMapKey};
pub struct Metadata;
impl TypeMapKey for Metadata {
type Value = AuxMetadata;
}

View File

@@ -1,12 +0,0 @@
pub mod deafen;
pub mod join;
pub mod leave;
pub mod misc;
pub mod mute;
pub mod play;
pub mod queue;
pub mod skip;
pub mod stop;
pub mod loopcurrent;
pub mod pause;
pub mod resume;

View File

@@ -1,25 +1,31 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::{
commands::embeds::{embed, error_embed, fail},
Context, Error,
};
use poise::CreateReply;
use crate::commands::misc::check_msg;
/// Mutes itself while in a voice channel; \
/// aliases: mute, unmute, shhh
#[poise::command(
prefix_command,
slash_command,
aliases("shhh", "unmute"),
category = "Music"
)]
pub async fn mute(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
#[command]
#[only_in(guilds)]
async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Client placed at init")
.expect("Songbird client placed at init")
.clone();
let handler_lock = match manager.get(guild_id) {
Some(handler) => handler,
None => {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(());
}
};
@@ -27,25 +33,19 @@ async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
let mut handler = handler_lock.lock().await;
if handler.is_mute() {
if let Err(e) = handler.mute(false).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("failed: {:?}", e))
.await,
);
if let Err(err) = handler.mute(false).await {
fail(ctx, err.to_string()).await.unwrap();
}
check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await);
ctx.send(CreateReply::default().embed(embed(ctx, "Unmuted!", "", "").await.unwrap()))
.await?;
} else {
if let Err(err) = handler.mute(true).await {
check_msg(
msg.channel_id
.say(&ctx.http, format!("Failed: {:?}", err))
.await,
);
fail(ctx, err.to_string()).await.unwrap();
}
check_msg(msg.channel_id.say(&ctx.http, "Muted").await);
ctx.send(CreateReply::default().embed(embed(ctx, "Muted!", "", "").await.unwrap()))
.await?;
}
Ok(())

View File

@@ -1,18 +1,17 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use crate::commands::misc::check_msg;
/// Pauses the currently playing song
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn pause(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
#[command]
#[only_in(guilds)]
async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Client placed at init")
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
@@ -20,20 +19,18 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
let queue = handler.queue();
let _ = queue.pause();
check_msg(
msg.channel_id
.say(
&ctx.http,
format!("Song paused."),
)
.await,
);
ctx.send(
CreateReply::default().embed(
embed(ctx, "Paused!", "Currently playing song is now paused!", "")
.await
.unwrap(),
),
)
.await?;
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())

View File

@@ -1,58 +1,82 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::prelude::*;
use serenity::prelude::*;
use reqwest::Client as HttpClient;
use songbird::input::{Compose, YoutubeDl};
use crate::commands::music::metadata::Metadata;
use crate::{commands::embeds::error_embed, Context, Error};
use fancy_regex::Regex;
use poise::serenity_prelude::model::Timestamp;
use poise::serenity_prelude::Colour;
use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply;
use regex::Regex as Regex_Classic;
use serenity::builder::CreateEmbedAuthor;
use serenity::builder::CreateEmbedFooter;
use songbird::events::TrackEvent;
use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl};
use songbird::tracks::{TrackHandle, TrackQueue};
use std::process::Command;
use std::time::Duration;
use crate::commands::{misc::check_msg, music::misc::TrackErrorNotifier};
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::http::HttpKey;
pub struct HttpKey;
/// Plays a song; \
/// you can search by query or paste an url; \
/// aliases: play, p, enqueue
#[poise::command(
prefix_command,
slash_command,
aliases("p", "enqueue"),
category = "Music"
)]
pub async fn play(
ctx: Context<'_>,
#[description = "Provide a query or an url"]
#[rest]
mut song: String,
) -> Result<(), Error> {
let regex_spotify = Regex::new(r"https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-)+)(?:(?=\?)(?:[?&]foo=(\d*)(?=[&#]|$)|(?![?&]foo=)[^#])+)?(?=#|$)").unwrap();
let regex_youtube =
Regex_Classic::new(r#""url": "(https://www.youtube.com/watch\?v=[A-Za-z0-9]{11})""#)
.unwrap();
let regex_youtube_playlist = Regex::new(
r"^((?:https?:)\/\/)?((?:www|m)\.)?((?:youtube\.com)).*(youtu.be\/|list=)([^#&?]*).*",
)
.unwrap();
let regex_spotify_playlist = Regex::new(r"https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:(album|playlist)\/|\?uri=spotify:playlist:)((\w|-)+)(?:(?=\?)(?:[?&]foo=(\d*)(?=[&#]|$)|(?![?&]foo=)[^#])+)?(?=#|$)").unwrap();
impl TypeMapKey for HttpKey {
type Value = HttpClient;
}
let is_playlist = regex_youtube_playlist.is_match(&song).unwrap()
|| regex_spotify_playlist.is_match(&song).unwrap();
let is_spotify =
regex_spotify.is_match(&song).unwrap() || regex_spotify_playlist.is_match(&song).unwrap();
let is_query = !song.starts_with("http");
#[command]
#[aliases(p)]
#[only_in(guilds)]
async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let url = match args.single::<String>() {
Ok(url) => url,
Err(_) => {
check_msg(
msg.channel_id
.say(&ctx.http, "Must provide a URL to a video or audio")
.await,
);
return Ok(());
}
};
let is_search = !url.starts_with("http");
let guild_id = msg.guild_id.unwrap();
let channel_id = msg.guild(&ctx.cache).unwrap().voice_states.get(&msg.author.id).and_then(|voice_state| voice_state.channel_id);
let guild_id = ctx.guild_id().unwrap();
let channel_id = ctx
.guild()
.unwrap()
.voice_states
.get(&ctx.author().id)
.and_then(|voice_state| voice_state.channel_id);
let connect_to = match channel_id {
Some(channel) => channel,
None => {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(());
}
};
let http_client = {
let data = ctx.data.read().await;
let data = ctx.serenity_context().data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird Voice placed at init")
.clone();
@@ -60,28 +84,212 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
let mut handler = handler_lock.lock().await;
// if let Err(err) = handler.deafen(true).await {println!("Failed to deafen: {:?}", err)};
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
let mut src = if is_search {
println!("ytsearch:{}", url);
YoutubeDl::new_ytdl_like("yt-dlp", http_client, format!("ytsearch:{}", args.clone().message()))
} else {
YoutubeDl::new_ytdl_like("yt-dlp", http_client, url)
};
if is_playlist && is_spotify {
let raw_list = Command::new("node")
.args(["./src/spotify-parser", &song])
.output()
.expect("failed to execute process")
.stdout;
let list = String::from_utf8(raw_list.clone()).expect("Invalid UTF-8");
let _ = handler.enqueue_input(src.clone().into()).await;
let tracks: Vec<String> = list.split("\n").map(str::to_string).collect();
let metadata = src.aux_metadata().await.unwrap();
for (index, url) in tracks.clone().iter().enumerate() {
if url.is_empty() {
break;
}
let src = YoutubeDl::new_ytdl_like(
"yt-dlp",
http_client.clone(),
format!("ytsearch:{}", url.to_string()),
);
let aux_metadata = src.clone().aux_metadata().await.unwrap();
let track = handler.enqueue_input(src.clone().into()).await;
let _ = track
.typemap()
.write()
.await
.insert::<Metadata>(aux_metadata);
check_msg(msg.channel_id.say(&ctx.http, format!("Playing song: {}", metadata.title.unwrap())).await);
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
if index == 0 {
let embed = generate_playlist_embed(ctx, track, tracks.len()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
}
}
return Ok(());
}
if is_playlist {
let raw_list = Command::new("yt-dlp")
.args(["-j", "--flat-playlist", &song])
.output()
.expect("failed to execute process")
.stdout;
let list = String::from_utf8(raw_list.clone()).expect("Invalid UTF-8");
let urls: Vec<String> = regex_youtube
.captures_iter(&list)
.map(|capture| capture[1].to_string())
.collect();
for (index, url) in urls.clone().iter().enumerate() {
if url.is_empty() {
break;
}
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client.clone(), url.to_string());
let aux_metadata = src.clone().aux_metadata().await.unwrap();
let track = handler.enqueue_input(src.clone().into()).await;
let _ = track
.typemap()
.write()
.await
.insert::<Metadata>(aux_metadata);
if index == 0 {
let embed = generate_playlist_embed(ctx, track, urls.len()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
}
}
return Ok(());
}
if is_spotify {
let query = Command::new("node")
.args(["./src/spotify-parser", &song])
.output()
.expect("failed to execute process")
.stdout;
let query_str = String::from_utf8(query.clone()).expect("Invalid UTF-8");
song = format!("ytsearch:{}", query_str.to_string());
}
if is_query {
song = format!("ytsearch:{}", song);
}
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client, song);
let embed = generate_embed(ctx, src.clone(), handler.queue()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
let aux_metadata = src.clone().aux_metadata().await.unwrap();
let track = handler.enqueue_input(src.clone().into()).await;
let _ = track
.typemap()
.write()
.await
.insert::<Metadata>(aux_metadata);
}
Ok(())
}
async fn generate_embed(
ctx: Context<'_>,
src: YoutubeDl,
queue: &TrackQueue,
) -> Result<CreateEmbed, Error> {
let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let mut description = format!("Song added to queue @ {}", queue.len() + 1);
if queue.len() == 0 {
description = format!("Playing now!");
}
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Track enqueued").icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97))
.title(title.unwrap())
.url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))
.field(
"Artist",
artist.unwrap_or("Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}
async fn generate_playlist_embed(
ctx: Context<'_>,
track: TrackHandle,
queue_length: usize,
) -> Result<CreateEmbed, Error> {
let meta_typemap = track.typemap().read().await;
let metadata = meta_typemap.get::<Metadata>().unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Enqueued tracks: {}", queue_length - 1);
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Playlist enqueued").icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97))
.title(title.as_ref().unwrap())
.url(source_url.as_ref().unwrap())
.thumbnail(
thumbnail
.as_ref()
.unwrap_or(&ctx.cache().current_user().face()),
)
.field(
"Artist",
artist.as_ref().unwrap_or(&"Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -1,48 +1,93 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use std::time::Duration;
use crate::commands::misc::check_msg;
use crate::commands::music::metadata::Metadata;
use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply;
use serenity::{
builder::{CreateEmbedAuthor, CreateEmbedFooter},
model::{Colour, Timestamp},
};
use songbird::input::AuxMetadata;
#[command]
#[only_in(guilds)]
async fn queue(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
const QUEUE_DISPLAY_LENGTH: usize = 10;
let manager = songbird::get(ctx)
/// Shows next tracks in queue; \
/// aliases: queue, q
#[poise::command(prefix_command, slash_command, aliases("q"), category = "Music")]
pub async fn queue(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Client placed at init")
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let mut queue_res = String::from("Queue: \n");
let mut queue_res = String::from("");
let mut too_long = false;
for (index, song) in queue.clone().current_queue().iter().enumerate() {
let meta_typemap = song.typemap().read().await;
let metadata = meta_typemap.get::<Metadata>().unwrap();
let AuxMetadata {
title,
artist,
duration,
..
} = metadata;
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
for (i, song) in queue.current_queue().iter().enumerate() {
queue_res.push_str(&format!(
"{}. {} - {}\n",
i + 1,
song.uuid(),
"Artist"
// song.metadata().artist.clone().unwrap_or_else(|| String::from("Unknown"))
));
"{}. {} - {} [{:02}:{:02}] \n",
index,
title.as_ref().unwrap(),
artist.as_ref().unwrap(),
duration_minutes,
duration_seconds
));
if index + 1 == QUEUE_DISPLAY_LENGTH {
too_long = true;
break;
}
}
if too_long {
queue_res.push_str(&format!(
"and {} more...",
queue.len() - QUEUE_DISPLAY_LENGTH
));
}
check_msg(
msg.channel_id
.say(&ctx.http, queue_res)
.await,
);
ctx.send(CreateReply::default().embed(embed(ctx, queue_res).await.unwrap()))
.await?;
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel!")
.await,
);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}
async fn embed(ctx: Context<'_>, queue: String) -> Result<CreateEmbed, Error> {
let title = "Now playing";
let timestamp = Timestamp::now();
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Queue").icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97))
.title(title)
.description(queue)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -0,0 +1,90 @@
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use songbird::tracks::LoopState;
/// Loops currently playing song provided amount of times; \
/// aliases: repeat, loop, while, for
#[poise::command(
prefix_command,
slash_command,
aliases("loop", "while", "for"),
category = "Music"
)]
pub async fn repeat(
ctx: Context<'_>,
#[description = "How many times"]
#[rest]
times: usize,
) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let track = queue.current().unwrap().get_info().await;
let is_looped = track.unwrap().loops;
match is_looped {
LoopState::Infinite => {
let _ = queue.current().unwrap().disable_loop();
ctx.send(
CreateReply::default()
.embed(embed(ctx, "Song Unlooped!", "", "").await.unwrap()),
)
.await?;
}
LoopState::Finite(_) => {
if times == 0 {
let _ = queue.current().unwrap().disable_loop();
ctx.send(
CreateReply::default()
.embed(embed(ctx, "Song Unlooped!", "", "").await.unwrap()),
)
.await?;
} else if times < 100 {
let _ = queue.current().unwrap().loop_for(times);
ctx.send(
CreateReply::default().embed(
embed(
ctx,
&format!("Song looped {} times!", times),
"You definitelly love this song!",
"",
)
.await
.unwrap(),
),
)
.await?;
} else {
let _ = queue.current().unwrap().enable_loop();
ctx.send(
CreateReply::default().embed(
embed(ctx, "Song looped forever!", "A very long time!", "")
.await
.unwrap(),
),
)
.await?;
}
}
}
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}

View File

@@ -1,18 +1,17 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use crate::commands::misc::check_msg;
/// Resumes currently paused song
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn resume(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
#[command]
#[only_in(guilds)]
async fn resume(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Client placed at init")
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
@@ -20,20 +19,18 @@ async fn resume(ctx: &Context, msg: &Message) -> CommandResult {
let queue = handler.queue();
let _ = queue.resume();
check_msg(
msg.channel_id
.say(
&ctx.http,
format!("Song resumed."),
)
.await,
);
ctx.send(
CreateReply::default().embed(
embed(ctx, "Resumed!", "Currently paused song is now resumed!", "")
.await
.unwrap(),
),
)
.await?;
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())

View File

@@ -0,0 +1,51 @@
use std::time::Duration;
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
/// Seeks a track by provided seconds
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn seek(
ctx: Context<'_>,
#[description = "How many seconds shall I seek"] seek: u64,
) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let seek_duration = Duration::from_secs(seek);
let track = queue.current().unwrap();
let _ = track.seek(seek_duration);
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"Track seeked!",
&format!("Track seeked by: {} seconds", seek),
"",
)
.await
.unwrap(),
),
)
.await?;
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}

View File

@@ -1,40 +1,118 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::music::metadata::Metadata;
use std::time::Duration;
use crate::commands::misc::check_msg;
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use serenity::{
builder::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter},
model::{Colour, Timestamp},
};
use songbird::{input::AuxMetadata, tracks::TrackHandle};
#[command]
#[only_in(guilds)]
async fn skip(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
/// Skips the currently playing song; \
/// aliases: skip, :skipper:
#[poise::command(
prefix_command,
slash_command,
aliases("skipper:"),
category = "Music"
)]
pub async fn skip(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Client placed at init")
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let _ = queue.skip();
let _ = queue.clone().skip();
let track_raw = queue.clone().current_queue();
let track = track_raw.get(1);
let queue_length = queue.len() - 1;
check_msg(
msg.channel_id
.say(
&ctx.http,
format!("Song skipped: {} in queue.", queue.len()),
)
.await,
);
let response;
match track {
Some(track) => {
response = CreateReply::default().embed(
generate_embed(ctx, track.clone(), queue_length)
.await
.unwrap(),
);
}
None => {
response = CreateReply::default().embed(
embed(ctx, "Skipped!", "The queue is empty!", "")
.await
.unwrap(),
);
}
};
ctx.send(response).await?;
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}
async fn generate_embed(
ctx: Context<'_>,
track: TrackHandle,
queue_length: usize,
) -> Result<CreateEmbed, Error> {
let meta_typemap = track.typemap().read().await;
let metadata = meta_typemap.get::<Metadata>().unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Song skipped! Queue length is {}", queue_length);
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Skipped!").icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97))
.title(title.as_ref().unwrap())
.url(source_url.as_ref().unwrap())
.thumbnail(
thumbnail
.as_ref()
.unwrap_or(&ctx.cache().current_user().face()),
)
.field(
"Artist",
artist.as_ref().unwrap_or(&"Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -0,0 +1,5 @@
pub mod effect;
pub mod stream;
pub use effect::effect;
pub use stream::stream;

View File

@@ -0,0 +1,117 @@
use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::model::Timestamp;
use poise::serenity_prelude::Colour;
use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply;
use serenity::builder::CreateEmbedAuthor;
use serenity::builder::CreateEmbedFooter;
use songbird::events::TrackEvent;
use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl};
use std::time::Duration;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::http::HttpKey;
/// Plays one of available audio effects
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn effect(
ctx: Context<'_>,
#[description = "Shall output pause?"]
#[flag]
pause: bool,
#[description = "Provide a query or an url"]
#[rest]
mut song: String,
) -> Result<(), Error> {
let is_query = !song.starts_with("http");
let guild_id = ctx.guild_id().unwrap();
let http_client = {
let data = ctx.serenity_context().data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird Voice placed at init")
.clone();
if pause {
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let _ = queue.pause();
}
}
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
if is_query {
song = format!("ytsearch:{}", song);
}
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client, song);
let embed = generate_embed(ctx, src.clone()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
let _ = handler.play_input(src.clone().into());
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}
async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed, Error> {
let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Playing now!");
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Playing an effect!").icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97))
.title(title.unwrap())
.url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))
.field(
"Artist",
artist.unwrap_or("Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -0,0 +1,126 @@
use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::model::Timestamp;
use poise::serenity_prelude::Colour;
use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply;
use serenity::builder::CreateEmbedAuthor;
use serenity::builder::CreateEmbedFooter;
use songbird::events::TrackEvent;
use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl};
use std::time::Duration;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::http::HttpKey;
/// Hijacks output and plays audio; \
/// search by query or paste an url; \
/// aliases: stream, override, hijack
#[poise::command(
prefix_command,
slash_command,
aliases("override", "hijack"),
category = "Music"
)]
pub async fn stream(
ctx: Context<'_>,
#[description = "Shall output pause?"]
#[flag]
pause: bool,
#[description = "Provide a query or an url"]
#[rest]
mut song: String,
) -> Result<(), Error> {
let is_query = !song.starts_with("http");
let guild_id = ctx.guild_id().unwrap();
let http_client = {
let data = ctx.serenity_context().data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird Voice placed at init")
.clone();
if pause {
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let _ = queue.pause();
}
}
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
if is_query {
song = format!("ytsearch:{}", song);
}
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client, song);
let embed = generate_embed(ctx, src.clone()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
let _ = handler.play_input(src.clone().into());
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}
async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed, Error> {
let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Playing now!");
let embed = CreateEmbed::default()
.author(
CreateEmbedAuthor::new("Audio output hijacked!").icon_url(ctx.author().clone().face()),
)
.colour(Colour::from_rgb(255, 58, 97))
.title(title.unwrap())
.url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))
.field(
"Artist",
artist.unwrap_or("Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -1,18 +1,18 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use crate::commands::misc::check_msg;
/// Stops playback and destroys the queue; \
/// aliases: stop, end
#[poise::command(prefix_command, slash_command, aliases("end"), category = "Music")]
pub async fn stop(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
#[command]
#[only_in(guilds)]
async fn stop(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = msg.guild_id.unwrap();
let manager = songbird::get(ctx)
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Client placed at init")
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
@@ -20,13 +20,23 @@ async fn stop(ctx: &Context, msg: &Message) -> CommandResult {
let queue = handler.queue();
queue.stop();
check_msg(msg.channel_id.say(&ctx.http, "Playback stopped!").await);
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"Stopped!",
"Playback stopped!",
"Queue destroyed! Bot will stay and chill with you in a vc",
)
.await
.unwrap(),
),
)
.await?;
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel!")
.await,
);
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())

View File

@@ -0,0 +1,44 @@
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
/// Changes output volume
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn volume(ctx: Context<'_>, #[description = "Volume"] volume: f32) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let track = queue.current().unwrap();
let _ = track.set_volume(volume / 100.0);
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"Volume changed",
"",
&format!("Set volume to {}%", volume),
)
.await
.unwrap(),
),
)
.await?;
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}

31
src/commands/tools.rs Normal file
View File

@@ -0,0 +1,31 @@
pub mod ai;
pub mod dice;
pub mod dictionary;
pub mod help;
pub mod ip;
pub mod metar;
pub mod owoify;
pub mod ping;
pub mod posix;
pub mod qr;
pub mod register;
pub mod taf;
// pub mod uptime;
pub mod verse;
pub mod weather;
pub use ai::ai;
pub use dice::dice;
pub use dictionary::dictionary;
pub use help::help;
pub use ip::ip;
pub use metar::metar;
pub use owoify::owoify;
pub use ping::ping;
pub use posix::posix;
pub use qr::qr;
pub use register::register;
pub use taf::taf;
// pub use uptime::uptime;
pub use verse::verse;
pub use weather::weather;

50
src/commands/tools/ai.rs Normal file
View File

@@ -0,0 +1,50 @@
use rand::Rng;
use std::thread::sleep;
use std::time::Duration;
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Asks AI
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn ai(
ctx: Context<'_>,
#[description = "prompt to ask"]
#[rest]
prompt: String,
) -> Result<(), Error> {
let iamsorry = vec![
"I'm sorry, but as an AI language model, I must follow ethical guidelines, and I cannot engage in harmful, malicious, or offensive behavior.",
"I'm sorry, but as an AI language model, I may not always be perfect and can make mistakes or provide inaccurate information. Please verify important details from reliable sources.",
"I'm sorry, but as an AI language model, I can't engage in real-time conversations or remember previous interactions with users.",
"I'm sorry, but as an AI language model, I don't have personal opinions or feelings; I can only provide information based on patterns in the data I was trained on.",
"I'm sorry, but as an AI language model, I don't have access to real-time information or updates beyond my last training data in September 2021.",
"I'm sorry, but as an AI language model, I don't have the ability to recall specific personal data or information about individuals.",
"I'm sorry, but as an AI language model, I don't have consciousness or self-awareness. I'm simply a program designed to process and generate human-like text."
];
println!("Funny prompts: {}", prompt);
let response;
let _ = {
let mut rng = rand::thread_rng();
response = rng.gen_range(0..iamsorry.len());
};
sleep(Duration::from_secs(3));
ctx.send(
CreateReply::default().embed(
embed(ctx, "AI Response:", "", &format!("{}", iamsorry[response]))
.await
.unwrap(),
),
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,33 @@
use rand::Rng;
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Rolls a dice
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn dice(ctx: Context<'_>) -> Result<(), Error> {
let dice;
let _ = {
let mut rng = rand::thread_rng();
dice = rng.gen_range(1..7);
};
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"Let's roll the dice!",
"",
&format!("Your number is: {}", dice),
)
.await
.unwrap(),
),
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,72 @@
use poise::CreateReply;
use serde::{Deserialize, Serialize};
use url::form_urlencoded;
use crate::{commands::embeds::embed, Context, Error};
/// Explains provided query
#[poise::command(prefix_command, slash_command, aliases("dict"), category = "Tools")]
pub async fn dictionary(
ctx: Context<'_>,
#[description = "Word you're looking for"]
#[rest]
word: String,
) -> Result<(), Error> {
let data: String = form_urlencoded::byte_serialize(word.as_bytes()).collect();
let client = reqwest::Client::new();
let response = client
.get(format!(
"https://api.dictionaryapi.dev/api/v2/entries/en/{}",
data
))
.send()
.await
.unwrap();
match response.status() {
reqwest::StatusCode::OK => match response.json::<Vec<Word>>().await {
Ok(parsed) => {
println!("{:?}", parsed);
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap()))
.await?;
}
Err(err) => println!("Something is messed up! {:?}", err),
},
reqwest::StatusCode::UNAUTHORIZED => {
println!("Unauthorized.. Uoops!!");
}
error => {
println!("Something went wrong: {:?}", error);
}
}
Ok(())
}
#[derive(Serialize, Deserialize, Debug)]
struct Definition {
definition: String,
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug)]
struct Meaning {
partOfSpeech: String,
definitions: Vec<Definition>,
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug)]
struct Phonetic {
text: Option<String>,
audio: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Word {
word: String,
phonetics: Vec<Phonetic>,
meanings: Vec<Meaning>,
}

View File

@@ -0,0 +1,23 @@
use crate::{Context, Error};
/// Prints this help message; aliases: help, huh, welp
#[poise::command(
prefix_command,
slash_command,
track_edits,
aliases("huh", "welp"),
category = "Help"
)]
pub async fn help(
ctx: Context<'_>,
#[description = "Specific command to show help about"] command: Option<String>,
) -> Result<(), Error> {
let config = poise::builtins::HelpConfiguration {
extra_text_at_bottom: "\
Use /help command for more info on a command.
You can edit you message to the bot and the bot will edit its response.",
..Default::default()
};
poise::builtins::help(ctx, command.as_deref(), config).await?;
Ok(())
}

12
src/commands/tools/ip.rs Normal file
View File

@@ -0,0 +1,12 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Shows IP information
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn ip(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap()))
.await?;
Ok(())
}

View File

@@ -0,0 +1,12 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Prints metar for provided airport
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn metar(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap()))
.await?;
Ok(())
}

View File

@@ -1 +0,0 @@
pub mod ping;

View File

@@ -0,0 +1,18 @@
use owoify::OwOifiable;
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Owoifies whatever you want uwu
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn owoify(
ctx: Context<'_>,
#[description = "Text to owoify w-woify OwO"]
#[rest]
text: String,
) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "OwO", &text.owoify(), "").await.unwrap()))
.await?;
Ok(())
}

View File

@@ -1,17 +1,18 @@
use crate::{Context, Error};
use std::time::SystemTime;
use serenity::{all::Message, client::Context, framework::standard::{macros::command, CommandResult}};
/// Pings you backs with a response time
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
let system_now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
use crate::commands::misc::check_msg;
let message_now = ctx.created_at().timestamp_millis();
#[command]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
let system_now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as i64;
let message_now = msg.timestamp.timestamp_millis();
// println!("System Time: {} ||| Message Time: {}", system_now, message_now);
check_msg(msg.reply(ctx, format!("Pong! (latency: {} ms)", system_now - message_now)).await);
let response = format!("Pong! (latency: {} ms)", system_now - message_now);
ctx.say(response).await?;
Ok(())
}

View File

@@ -0,0 +1,30 @@
use std::time::SystemTime;
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Prints current time in POSIX format
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn posix(ctx: Context<'_>) -> Result<(), Error> {
let time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis();
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"The time is",
"since Jan 1st 1970",
&format!("{} ms", time),
)
.await
.unwrap(),
),
)
.await?;
Ok(())
}

48
src/commands/tools/qr.rs Normal file
View File

@@ -0,0 +1,48 @@
use poise::CreateReply;
use serenity::{
builder::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter},
model::{Colour, Timestamp},
};
use crate::{Context, Error};
use url::form_urlencoded;
/// Creates a qr code from text
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn qr(
ctx: Context<'_>,
#[description = "Message to encode"]
#[rest]
message: String,
) -> Result<(), Error> {
let response = CreateReply::default().embed(generate_embed(ctx, message).await.unwrap());
ctx.send(response).await?;
Ok(())
}
async fn generate_embed(ctx: Context<'_>, message: String) -> Result<CreateEmbed, Error> {
let timestamp = Timestamp::now();
let data: String = form_urlencoded::byte_serialize(message.as_bytes()).collect();
let url = format!(
"http://api.qrserver.com/v1/create-qr-code/?data={}&size=1000x1000&ecc=Q&margin=8",
data
);
let embed = CreateEmbed::default()
.author(
CreateEmbedAuthor::new("Your message as a QR Code!")
.icon_url(ctx.author().clone().face()),
)
.colour(Colour::from_rgb(255, 58, 97))
.title("Your QR Code:")
.url(url.clone())
.image(url)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -0,0 +1,7 @@
use crate::{Context, Error};
#[poise::command(prefix_command, hide_in_help, owners_only)]
pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(())
}

12
src/commands/tools/taf.rs Normal file
View File

@@ -0,0 +1,12 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Returns taf for provided airport
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn taf(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap()))
.await?;
Ok(())
}

View File

@@ -0,0 +1,35 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
// Currently unable to get information on how long the thread was running.
const PROCESS_UPTIME: i64 = 1000;
/// Checks how long the bot has been running
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn uptime(ctx: Context<'_>) -> Result<(), Error> {
let uptime = PROCESS_UPTIME;
let days = uptime / (24 * 60 * 60);
let hours = (uptime % (24 * 60 * 60)) / 3600;
let minutes = (uptime % 60 * 60) / 60;
let seconds = uptime % 60;
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"I have been up and awake for",
&format!("{} seconds", uptime),
&format!(
"{} days, {} hours, {} minutes and {} seconds",
days, hours, minutes, seconds
),
)
.await
.unwrap(),
),
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,78 @@
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use serde::{Deserialize, Serialize};
use url::form_urlencoded;
/// Reference Bible by verse
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn verse(
ctx: Context<'_>,
#[description = "Latin?"]
#[flag]
latin: bool,
#[description = "BOOK+CHAPTER:VERSE"]
#[rest]
verse: String,
) -> Result<(), Error> {
let data: String = form_urlencoded::byte_serialize(verse.as_bytes()).collect();
let translation = if latin { "clementine" } else { "web" };
let client = reqwest::Client::new();
let response = client
.get(format!(
"https://bible-api.com/{}?translation={}",
data, translation
))
.send()
.await
.unwrap();
match response.status() {
reqwest::StatusCode::OK => {
match response.json::<APIResponse>().await {
Ok(parsed) => {
if parsed.text.len() > 4000 {
ctx.send(
CreateReply::default()
.embed(error_embed(ctx, "Quoted text is too long!").await.unwrap()),
)
.await?;
return Ok(());
}
ctx.send(
CreateReply::default().embed(
embed(
ctx,
&parsed.translation_name,
&parsed.text,
&parsed.reference,
)
.await
.unwrap(),
),
)
.await?;
}
Err(err) => println!("Something is messed up! {:?}", err),
};
}
reqwest::StatusCode::UNAUTHORIZED => {
println!("Unauthorized.. Uoops!!");
}
error => {
println!("Something went wrong: {:?}", error);
}
}
Ok(())
}
#[derive(Serialize, Deserialize, Debug)]
struct APIResponse {
reference: String,
text: String,
translation_name: String,
translation_note: String,
}

View File

@@ -0,0 +1,17 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Shows weather for provided location
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn weather(
ctx: Context<'_>,
#[description = "Provide a city name"]
#[rest]
_location: String,
) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap()))
.await?;
Ok(())
}

8
src/http.rs Normal file
View File

@@ -0,0 +1,8 @@
use poise::serenity_prelude::prelude::TypeMapKey;
use reqwest::Client as HttpClient;
pub struct HttpKey;
impl TypeMapKey for HttpKey {
type Value = HttpClient;
}

View File

@@ -1,89 +1,149 @@
use serenity::gateway::ActivityData;
use serenity::model::prelude::Message;
use songbird::SerenityInit;
use poise::serenity_prelude::{self as serenity, ActivityData};
use reqwest::Client as HttpClient;
use serenity::client::Context;
use serenity::{
async_trait,
client::{Client, EventHandler},
framework::{
standard::{macros::group, macros::hook, Configuration},
StandardFramework,
},
model::gateway::Ready,
prelude::GatewayIntents,
};
use tracing::info;
use songbird::SerenityInit;
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info, warn};
mod commands;
mod http;
// music management commands
use crate::commands::music::deafen::*;
use crate::commands::music::join::*;
use crate::commands::music::leave::*;
use crate::commands::music::mute::*;
use crate::commands::music::play::*;
use crate::commands::music::queue::*;
use crate::commands::music::skip::*;
use crate::commands::music::stop::*;
use crate::commands::music::loopcurrent::*;
use crate::commands::music::pause::*;
use crate::commands::music::resume::*;
use crate::commands::kashi;
use crate::commands::music;
use crate::commands::tools;
use crate::http::HttpKey;
// tools
use crate::commands::tools::ping::*;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
// kashi
use crate::commands::kashi::kashi::*;
pub struct Data;
struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn ready(&self, ctx: Context, ready: Ready) {
info!("{} [{}] connected successfully!", ready.user.name, ready.user.id);
let prefix = std::env::var("PREFIX").expect("Environment variable `PREFIX` not found!");
ctx.set_activity(Some(ActivityData::listening(prefix + "help")));
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
match error {
poise::FrameworkError::Setup { error, .. } => panic!("Failed to start bot {:?}", error),
poise::FrameworkError::Command { error, ctx, .. } => {
warn!("Error in command `{}`: {:?}", ctx.command().name, error);
}
error => {
if let Err(e) = poise::builtins::on_error(error).await {
error!("Error while handling error: {}", e)
}
}
}
}
#[hook]
async fn before(_: &Context, msg: &Message, command_name: &str) -> bool {
info!(
"Received command [{}] from user [{}]",
command_name, msg.author.name
);
true
}
#[group]
#[commands(
join, deafen, leave, mute, play, ping, kashi, queue, stop, skip, loopcurrent, pause, resume
)]
struct General;
#[tokio::main]
async fn main() {
dotenv::dotenv().expect("Failed to load .env file.");
tracing_subscriber::fmt::init();
dotenv::dotenv().expect("Failed to load .env file.");
let token =
std::env::var("DISCORD_TOKEN").expect("Environment variable `DISCORD_TOKEN` not found!");
let prefix = std::env::var("PREFIX").expect("Environment variable `PREFIX` not found!");
let framework = StandardFramework::new().before(before).group(&GENERAL_GROUP);
framework.configure(Configuration::new().prefix(prefix));
let commands = vec![
kashi::kashi(),
music::deafen(),
music::join(),
music::leave(),
music::mute(),
music::pause(),
music::play(),
music::queue(),
music::repeat(),
music::resume(),
music::seek(),
music::skip(),
music::stop(),
music::volume(),
music::soundboard::effect(),
music::soundboard::stream(),
tools::ai(),
tools::dice(),
tools::dictionary(),
tools::help(),
tools::ip(),
tools::metar(),
tools::owoify(),
tools::ping(),
tools::posix(),
tools::qr(),
tools::register(),
tools::taf(),
// tools::uptime(),
tools::verse(),
tools::weather(),
];
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
let options = poise::FrameworkOptions {
commands,
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some(prefix.to_string().into()),
edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan(
Duration::from_secs(3600),
))),
additional_prefixes: vec![],
..Default::default()
},
let mut client = Client::builder(&token, intents)
on_error: |error| Box::pin(on_error(error)),
pre_command: |ctx| {
Box::pin(async move {
info!("Executing command {}...", ctx.command().qualified_name);
})
},
post_command: |ctx| {
Box::pin(async move {
info!("Executed command {}!", ctx.command().qualified_name);
})
},
command_check: Some(|ctx| {
Box::pin(async move {
if ctx.author().id == 123456789 {
return Ok(false);
}
Ok(true)
})
}),
skip_checks_for_owners: false,
event_handler: |_ctx, event, _framework, _data| {
Box::pin(async move {
info!(
"Got an event in event handler: {:?}",
event.snake_case_name()
);
Ok(())
})
},
..Default::default()
};
let framework = poise::Framework::builder()
.setup(move |ctx, ready, _framework| {
Box::pin(async move {
info!(
"{} [{}] connected successfully!",
ready.user.name, ready.user.id
);
ctx.set_activity(Some(ActivityData::listening(prefix + "help")));
// poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data {})
})
})
.options(options)
.build();
let intents =
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;
let mut client = serenity::ClientBuilder::new(token, intents)
.framework(framework)
.register_songbird()
.event_handler(Handler)
.type_map_insert::<HttpKey>(HttpClient::new())
.await
.expect("Error creating client");
@@ -92,9 +152,9 @@ async fn main() {
let _ = client
.start()
.await
.map_err(|why| println!("Client ended: {:?}", why));
.map_err(|why| error!("Client ended: {:?}", why));
});
let _signal_err = tokio::signal::ctrl_c().await;
println!("Recieved Ctrl-C, shutting down.");
warn!("Recieved Ctrl-C, shutting down.");
}

1
src/spotify-parser Submodule

Submodule src/spotify-parser added at e3b3c0fb6e