24 Commits

Author SHA1 Message Date
aeb10d9baf ops: default branch yeet 2024-08-12 20:58:01 +02:00
af247aa6d5 ci: release please setup 2024-08-12 20:01:06 +02:00
eae957c6d7 Create ci.yml 2024-08-12 19:33:07 +02:00
5399f3ba6d oop 2024-08-12 19:31:51 +02:00
076dfd0a93 fixed release to build arm64 2024-08-12 19:08:59 +02:00
212abd9bdd chore: release 0.10.2 2024-08-12 18:46:53 +02:00
8d787f2a11 fix: crates bumped 2024-08-12 18:46:21 +02:00
486f403d10 chore: release 0.10.1 2024-08-12 18:20:24 +02:00
bfd301c9c2 fix: deps broken 2024-08-12 18:14:09 +02:00
670b8dd30e gh: actions 2024-08-12 18:13:59 +02:00
c607aaf403 updated image in README.md 2024-08-12 13:37:10 +02:00
f41fa7e09b updated README.md 2024-08-12 13:36:36 +02:00
10b7d4d7e3 queue system improved 2024-08-08 20:46:22 +02:00
772d5d2a97 spotify-parser is now a submodule 2024-08-05 16:03:52 +02:00
f1b52d2be2 unchecked submodules for cross compile 2024-08-05 15:12:16 +02:00
e77818e09b 0.9.1 heartbeat no longer dropping 2024-08-05 15:01:45 +02:00
5a026afc52 songbird and serenity submodule 2024-08-05 14:39:13 +02:00
8a0d08de4e added songbird as a submodule 2024-08-05 11:32:23 +02:00
f224ccfbdf reduced ai timeout, impl uptime 2024-08-04 21:26:45 +02:00
d0af34833b lyra nightly avatar 2024-08-04 20:11:31 +02:00
c2e653b3ea [0.9.0] new parser 2024-08-04 18:03:21 +02:00
78a1937210 [0.8.2] lyra package update 2024-08-03 00:12:44 +02:00
fc42427437 updated packages and closing #2
Signed-off-by: Michael Czyż <mike@c2yz.com>
2024-05-18 23:46:25 +02:00
9474577233 Lyra is now open source! 2024-02-22 13:55:11 +01:00
39 changed files with 1932 additions and 844 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: eRgo35

22
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ "senpai" ]
pull_request:
branches: [ "senpai" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

47
.github/workflows/release-phase.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Release Please
on:
push:
branches:
- senpai
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
# this assumes that you have created a personal access token
# (PAT) and configured it as a GitHub action secret named
# `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
token: ${{ secrets.GITHUB_TOKEN }}
# this is a built-in strategy in release-please, see "Action Inputs"
# for more options
release-type: rust
release-rust:
if: ${{needs.release-please.outputs.rust--release_created}}
runs-on: ubuntu-latest
needs: release-please
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Test
uses: clechasseur/rs-cargo@v2
with:
command: test
args: --verbose --manifest-path hello-rust/Cargo.toml
- name: Build
uses: clechasseur/rs-cargo@v2
with:
command: build
args: --verbose --release --manifest-path hello-rust/Cargo.toml --target x86_64-unknown-linux-gnu
- name: Upload Release Artifact
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${{ needs.release-please.outputs.hello-rust--tag_name }} hello-rust/target/x86_64-unknown-linux-gnu/release/hello_rust

2
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

1
CHANGELOG.md Normal file
View File

@@ -0,0 +1 @@
# Changelog

1853
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,53 @@
[package] [package]
name = "lyra" name = "lyra"
version = "0.8.0" version = "0.10.2"
authors = ["Michał Czyż <mike@c2yz.com>"] authors = ["Michał Czyż <mike@c2yz.com>"]
edition = "2021" edition = "2021"
description = "A featureful Discord bot written in Rust." description = "A featureful Discord bot written in Rust."
documentation = "https://lyra.c2yz.com/docs" documentation = "https://github.com/eRgo35/lyra"
readme = "README.md" readme = "README.md"
homepage = "https://lyra.c2yz.com" homepage = "https://lyra.c2yz.com"
license-file = "LICENSE.md" license-file = "LICENSE.md"
keywords = ["discord", "bot", "rust", "music", "featureful"] keywords = ["discord", "bot", "rust", "music", "featureful"]
[dependencies] [dependencies]
spotify-parser = "1.0.1"
dotenv = "0.15.0" dotenv = "0.15.0"
fancy-regex = "0.13.0"
json = "0.12.4" json = "0.12.4"
openssl = { version = "0.10.63", features = ["vendored"] } openssl = { version = "0.10.66", features = ["vendored"] }
owoify = "0.1.5" owoify = "0.1.5"
poise = "0.6.1" poise = { default-features = true, version = "0.6.1" }
rand = "0.8.5" rand = "0.8.5"
regex = "1.10.3" regex = "1.10.6"
reqwest = { version = "0.11.23", features = ["json"]} reqwest = { version = "0.11.27", features = ["json"] }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.122"
serenity = { version = "0.12.0", features = ["cache", "framework", "standard_framework", "voice"] } songbird = { version = "0.4.3", default-features = true, features = [
songbird = { version = "0.4.0", features = ["builtin-queue", "serenity"] } "builtin-queue",
symphonia = { version = "0.5.3", features = ["aac", "adpcm", "alac", "flac", "mpa", "isomp4"] } ] }
tokio = { version = "1.35.1", features = ["macros", "full", "signal"] } serenity = { default-features = true, features = [
"cache",
"framework",
"standard_framework",
"voice",
"http",
"rustls_backend",
], version = "0.12" }
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 = "0.1.40"
tracing-futures = "0.2.5" tracing-futures = "0.2.5"
tracing-subscriber = "0.3.18" tracing-subscriber = "0.3.18"
url = "2.5.0" url = "2.5.2"
once_cell = "1.19.0"
[patch.crates-io.serenity-voice-model]
git = "https://github.com/serenity-rs/serenity"
branch = "current"

View File

@@ -1,19 +0,0 @@
FROM rust:1.75.0-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,7 +1,21 @@
# License MIT License
Copyright (C) 2024 Michał Czyż Copyright (c) 2024 Michał Czyż
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:
This build is private and shall not be distributed nor modified without permission. 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.

120
README.md
View File

@@ -1,12 +1,120 @@
# Lyra <h2 align="center">
<a href="https://lyra.c2yz.com" target="blank_">
<img height="256" alt="Lyra" src="assets/lyra-256.png" />
</a>
<br />
Lyra: a discord music bot written in rust :crab:
</h2>
![](assets/lyra-256.png) 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 a service.
The bot can be run on a desktop, a vps or a phone as it's just a simple binary.
Lyra is a music bot written in Rust. Currently bot is still heavily in development!
More features coming soon!
## Getting Started ## Getting started
## Building Before you start, you need to create a discord bot and get a token.
You can do it [here](https://discord.com/developers/applications).
After you create a bot, you need to invite it to your server.
Then, head to download section and download the latest release (or compile it yourself).
After you download the binary, you need to create a `.env` file in the same directory as the binary.
Example can be found in `.env.example` file.
```
DISCORD_TOKEN=<YOUR_DISCORD_TOKEN>
PREFIX=<YOUR_PREFIX>
```
DISCORD_TOKEN is the token you got from discord developers page and PREFIX is the prefix you want to use for your bot.
Bot by default reacts only to the prefix. To enable slash commands, while the bot is running type `:register` in the chat (where `:` is your bot prefix).
## Features
- Music playback
- Audio effects (soon)
- Some multipurpose commands
- Slash commands
- Self-hosted
## Compilation
To compile the source code on your own, you need `rust` and `cargo`
To run a dev version, `cd` into the project directory and type
```bash
$ cargo run
```
To build a production version use
```bash
$ cargo build --release
```
If you need a version for a different system or architecture, you can use `cross` crate
```bash
$ cross build -r --target aarch64-unknown-linux-gnu
```
To run a program, just type
```bash
$ ./lyra
```
Remember to provide a `.env` file in the same directory as the binary.
If you want to disown the bot from the shell, I recommend using the script I provided in `scripts` folder
## Commands ## Commands
As of now, the 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; aliases: skip, :skipper:
/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
/dictionary Explains provided query
/ip Shows IP information
/metar Prints metar for provided airport
/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
/taf Returns taf for provided airport
/uptime Checks how long the bot has been running
/verse Reference Bible by verse
/weather Shows weather for provided location
Help:
/help Prints this help message; aliases: help, huh, welp
Use /help command for more info on a command.
You can edit you message to the bot and the bot will edit its response.
```

BIN
assets/lyra-nightly.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

View File

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

View File

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

View File

@@ -1,11 +1,9 @@
use crate::{Context, Error}; use crate::{Context, Error};
use poise::serenity_prelude::CreateEmbed; use poise::serenity_prelude::{
use poise::CreateReply; Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
use serenity::{
builder::{CreateEmbedAuthor, CreateEmbedFooter},
model::{Colour, Timestamp},
}; };
use poise::CreateReply;
pub async fn fail(ctx: Context<'_>, err: String) -> Result<(), Error> { pub async fn fail(ctx: Context<'_>, err: String) -> Result<(), Error> {
ctx.send( ctx.send(
@@ -25,7 +23,7 @@ pub async fn error_embed(ctx: Context<'_>, msg: &str) -> Result<CreateEmbed, Err
.author( .author(
CreateEmbedAuthor::new("Something went wrong!").icon_url(ctx.author().clone().face()), CreateEmbedAuthor::new("Something went wrong!").icon_url(ctx.author().clone().face()),
) )
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title("Oopsie, Doopsie!") .title("Oopsie, Doopsie!")
.description(msg) .description(msg)
.timestamp(Timestamp::now()) .timestamp(Timestamp::now())
@@ -45,7 +43,7 @@ pub async fn embed(
) -> Result<CreateEmbed, Error> { ) -> Result<CreateEmbed, Error> {
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new(author).icon_url(ctx.author().clone().face())) .author(CreateEmbedAuthor::new(author).icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title(title) .title(title)
.description(description) .description(description)
.timestamp(Timestamp::now()) .timestamp(Timestamp::now())

View File

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

View File

@@ -1,17 +0,0 @@
use crate::{Context, Error};
/// Kashi integration platform (WIP)
#[poise::command(
prefix_command,
slash_command,
category = "Kashi"
)]
pub async fn kashi(
ctx: Context<'_>
) -> Result<(), Error> {
let response = format!("Kashi platform is currently under construction!");
ctx.say(response).await?;
Ok(())
}

View File

@@ -15,7 +15,7 @@ use poise::CreateReply;
pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> { pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -27,7 +27,7 @@ pub async fn join(ctx: Context<'_>) -> Result<(), Error> {
} }
}; };
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -15,12 +15,12 @@ use poise::CreateReply;
pub async fn leave(ctx: Context<'_>) -> Result<(), Error> { pub async fn leave(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
if !manager.get(guild_id).is_some() { if manager.get(guild_id).is_none() {
let msg = "I am not in a voice channel!"; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap())) ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?; .await?;

View File

@@ -15,7 +15,7 @@ use poise::CreateReply;
pub async fn mute(ctx: Context<'_>) -> Result<(), Error> { pub async fn mute(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -9,7 +9,7 @@ use poise::CreateReply;
pub async fn pause(ctx: Context<'_>) -> Result<(), Error> { pub async fn pause(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -1,24 +1,24 @@
use crate::commands::music::metadata::Metadata; use crate::commands::music::metadata::Metadata;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::{commands::embeds::error_embed, Context, Error}; use crate::{commands::embeds::error_embed, Context, Error};
use fancy_regex::Regex; use poise::serenity_prelude::{
use poise::serenity_prelude::model::Timestamp; Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
use poise::serenity_prelude::Colour; };
use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply; use poise::CreateReply;
use regex::Regex as Regex_Classic; use regex::Regex as Regex_Classic;
use serenity::builder::CreateEmbedAuthor; use reqwest::Client;
use serenity::builder::CreateEmbedFooter; use serenity::all::GuildId;
use songbird::events::TrackEvent; use songbird::events::TrackEvent;
use songbird::input::AuxMetadata; use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl}; use songbird::input::{Compose, YoutubeDl};
use songbird::tracks::{TrackHandle, TrackQueue}; use songbird::tracks::TrackQueue;
use songbird::Call;
use spotify_parser;
use std::collections::VecDeque;
use std::process::Command; use std::process::Command;
use std::time::Duration; use std::time::Duration;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::http::HttpKey;
/// Plays a song; \ /// Plays a song; \
/// you can search by query or paste an url; \ /// you can search by query or paste an url; \
/// aliases: play, p, enqueue /// aliases: play, p, enqueue
@@ -32,161 +32,151 @@ pub async fn play(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Provide a query or an url"] #[description = "Provide a query or an url"]
#[rest] #[rest]
mut song: String, song: String,
) -> Result<(), Error> { ) -> 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();
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");
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let channel_id = ctx let http_client = ctx.data().http_client.clone();
.guild() let manager = songbird::get(ctx.serenity_context()).await.unwrap().clone();
.unwrap()
.voice_states
.get(&ctx.author().id)
.and_then(|voice_state| voice_state.channel_id);
let connect_to = match channel_id { let mut rest_playlist: VecDeque<String> = VecDeque::new();
Some(channel) => channel,
None => { if manager.get(guild_id).is_none() {
let msg = "I am not in a voice channel!"; 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 => {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(());
}
};
if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
rest_playlist = handle_play(ctx, song, handler, http_client.clone())
.await
.unwrap();
} else {
let msg = "Failed to join the voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap())) ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?; .await?;
return Ok(());
} }
}; } else {
let handler = manager.get(guild_id).unwrap();
let http_client = { let mut handler = handler.lock().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.serenity_context())
.await
.expect("Songbird Voice placed at init")
.clone();
if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
rest_playlist = handle_play(ctx, song, handler, http_client.clone())
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 tracks: Vec<String> = list.split("\n").map(str::to_string).collect();
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);
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 .await
.insert::<Metadata>(aux_metadata); .unwrap();
}
handle_playlist(rest_playlist, manager, guild_id, http_client)
.await
.unwrap();
Ok(())
}
async fn handle_play<'a>(
ctx: Context<'a>,
song: String,
mut handler: tokio::sync::MutexGuard<'a, Call>,
http_client: Client,
) -> Result<VecDeque<String>, Error> {
let mut results = parse_data(song).await;
let src: YoutubeDl =
YoutubeDl::new_ytdl_like("yt-dlp", http_client, results.pop_front().unwrap());
ctx.send(
CreateReply::default().embed(
generate_embed(ctx, src.clone(), handler.queue(), results.clone())
.await
.unwrap(),
),
)
.await?;
let aux_metadata = src.clone().aux_metadata().await.unwrap();
handler
.enqueue_input(src.clone().into())
.await
.typemap()
.write()
.await
.insert::<Metadata>(aux_metadata);
Ok(results)
}
async fn parse_data(data: String) -> VecDeque<String> {
let tracks = spotify_parser::retrieve_async_url(&data)
.await
.unwrap_or(vec![data])
.iter()
.flat_map(|track| {
if track.contains("?list=") {
let regex_youtube = Regex_Classic::new(
r#""url": "(https://www.youtube.com/watch\?v=[A-Za-z0-9]{11})""#,
)
.unwrap();
let list = Command::new("yt-dlp")
.args(["-j", "--flat-playlist", track])
.output()
.expect("Failed to execute process")
.stdout;
let list = String::from_utf8(list).unwrap();
regex_youtube
.captures_iter(&list)
.map(|capture| capture.get(1).unwrap().as_str().to_string())
.collect::<Vec<String>>()
} else if track.starts_with("http") {
vec![track.clone()]
} else {
vec![format!("ytsearch:{}", track)]
}
})
.collect();
tracks
}
async fn handle_playlist(
playlist: VecDeque<String>,
manager: std::sync::Arc<songbird::Songbird>,
guild_id: GuildId,
http_client: Client,
) -> Result<(), Error> {
for song in playlist {
if manager.get(guild_id).is_some() {
let handler = manager.get(guild_id).unwrap();
let mut handler = handler.lock().await;
let src: YoutubeDl =
YoutubeDl::new_ytdl_like("yt-dlp", http_client.clone(), song.clone());
let aux_metadata = src.clone().aux_metadata().await.unwrap();
handler
.enqueue_input(src.clone().into())
.await
.typemap()
.write()
.await
.insert::<Metadata>(aux_metadata);
}
} }
Ok(()) Ok(())
@@ -196,6 +186,7 @@ async fn generate_embed(
ctx: Context<'_>, ctx: Context<'_>,
src: YoutubeDl, src: YoutubeDl,
queue: &TrackQueue, queue: &TrackQueue,
results: VecDeque<String>,
) -> Result<CreateEmbed, Error> { ) -> Result<CreateEmbed, Error> {
let metadata = src.clone().aux_metadata().await.unwrap(); let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata { let AuxMetadata {
@@ -206,18 +197,22 @@ async fn generate_embed(
duration, duration,
.. ..
} = metadata; } = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; 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 duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let mut description = format!("Song added to queue @ {}", queue.len() + 1); let mut description = format!("Enqueued @ {}", queue.len() + 1);
let mut tracks = "Tracks enqueued";
if queue.len() == 0 { if results.len() == 1 {
description = format!("Playing now!"); tracks = "Track enqueued";
}
if queue.is_empty() {
description = "Playing now!".to_string();
} }
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Track enqueued").icon_url(ctx.author().clone().face())) .author(CreateEmbedAuthor::new(tracks).icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title(title.unwrap()) .title(title.unwrap())
.url(source_url.unwrap()) .url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face())) .thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))
@@ -233,59 +228,7 @@ async fn generate_embed(
) )
.field("DJ", ctx.author().name.clone(), true) .field("DJ", ctx.author().name.clone(), true)
.description(description) .description(description)
.timestamp(timestamp) .timestamp(Timestamp::now())
.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( .footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string()) CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()), .icon_url(ctx.cache().current_user().face()),

View File

@@ -2,12 +2,10 @@ use std::time::Duration;
use crate::commands::music::metadata::Metadata; use crate::commands::music::metadata::Metadata;
use crate::{commands::embeds::error_embed, Context, Error}; use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::CreateEmbed; use poise::serenity_prelude::{
use poise::CreateReply; Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
use serenity::{
builder::{CreateEmbedAuthor, CreateEmbedFooter},
model::{Colour, Timestamp},
}; };
use poise::CreateReply;
use songbird::input::AuxMetadata; use songbird::input::AuxMetadata;
const QUEUE_DISPLAY_LENGTH: usize = 10; const QUEUE_DISPLAY_LENGTH: usize = 10;
@@ -18,7 +16,7 @@ const QUEUE_DISPLAY_LENGTH: usize = 10;
pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { pub async fn queue(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -80,7 +78,7 @@ async fn embed(ctx: Context<'_>, queue: String) -> Result<CreateEmbed, Error> {
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Queue").icon_url(ctx.author().clone().face())) .author(CreateEmbedAuthor::new("Queue").icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title(title) .title(title)
.description(queue) .description(queue)
.timestamp(timestamp) .timestamp(timestamp)

View File

@@ -21,7 +21,7 @@ pub async fn repeat(
) -> Result<(), Error> { ) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -9,7 +9,7 @@ use poise::CreateReply;
pub async fn resume(ctx: Context<'_>) -> Result<(), Error> { pub async fn resume(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -14,7 +14,7 @@ pub async fn seek(
) -> Result<(), Error> { ) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -5,19 +5,19 @@ use crate::{
commands::embeds::{embed, error_embed}, commands::embeds::{embed, error_embed},
Context, Error, Context, Error,
}; };
use poise::CreateReply; use poise::serenity_prelude::{
use serenity::{ Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
builder::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter},
model::{Colour, Timestamp},
}; };
use poise::CreateReply;
use songbird::{input::AuxMetadata, tracks::TrackHandle}; use songbird::{input::AuxMetadata, tracks::TrackHandle};
/// Skips the currently playing song /// Skips the currently playing song; \
#[poise::command(prefix_command, slash_command, category = "Music")] /// aliases: skip, :skipper:
#[poise::command(prefix_command, slash_command, aliases("skipper:"), category = "Music")]
pub async fn skip(ctx: Context<'_>) -> Result<(), Error> { pub async fn skip(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -30,23 +30,18 @@ pub async fn skip(ctx: Context<'_>) -> Result<(), Error> {
let track = track_raw.get(1); let track = track_raw.get(1);
let queue_length = queue.len() - 1; let queue_length = queue.len() - 1;
let response; let mut response = CreateReply::default().embed(
embed(ctx, "Skipped!", "The queue is empty!", "")
.await
.unwrap(),
);
match track { if let Some(track) = track {
Some(track) => { response = CreateReply::default().embed(
response = CreateReply::default().embed( generate_embed(ctx, track.clone(), queue_length)
generate_embed(ctx, track.clone(), queue_length) .await
.await .unwrap(),
.unwrap(), );
);
}
None => {
response = CreateReply::default().embed(
embed(ctx, "Skipped!", "The queue is empty!", "")
.await
.unwrap(),
);
}
}; };
ctx.send(response).await?; ctx.send(response).await?;
@@ -82,7 +77,7 @@ async fn generate_embed(
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Skipped!").icon_url(ctx.author().clone().face())) .author(CreateEmbedAuthor::new("Skipped!").icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title(title.as_ref().unwrap()) .title(title.as_ref().unwrap())
.url(source_url.as_ref().unwrap()) .url(source_url.as_ref().unwrap())
.thumbnail( .thumbnail(

View File

@@ -1,11 +1,9 @@
use crate::{commands::embeds::error_embed, Context, Error}; use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::model::Timestamp; use poise::serenity_prelude::{
use poise::serenity_prelude::Colour; Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
use poise::serenity_prelude::CreateEmbed; };
use poise::CreateReply; use poise::CreateReply;
use serenity::builder::CreateEmbedAuthor;
use serenity::builder::CreateEmbedFooter;
use songbird::events::TrackEvent; use songbird::events::TrackEvent;
use songbird::input::AuxMetadata; use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl}; use songbird::input::{Compose, YoutubeDl};
@@ -36,7 +34,7 @@ pub async fn effect(
.expect("Guaranteed to exist in the typemap.") .expect("Guaranteed to exist in the typemap.")
}; };
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird Voice placed at init") .expect("Songbird Voice placed at init")
.clone(); .clone();
@@ -87,11 +85,11 @@ async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed,
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; 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 duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Playing now!"); let description = "Playing now!";
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Playing an effect!").icon_url(ctx.author().clone().face())) .author(CreateEmbedAuthor::new("Playing an effect!").icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title(title.unwrap()) .title(title.unwrap())
.url(source_url.unwrap()) .url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face())) .thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))

View File

@@ -1,11 +1,9 @@
use crate::{commands::embeds::error_embed, Context, Error}; use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::model::Timestamp; use poise::serenity_prelude::{
use poise::serenity_prelude::Colour; Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
use poise::serenity_prelude::CreateEmbed; };
use poise::CreateReply; use poise::CreateReply;
use serenity::builder::CreateEmbedAuthor;
use serenity::builder::CreateEmbedFooter;
use songbird::events::TrackEvent; use songbird::events::TrackEvent;
use songbird::input::AuxMetadata; use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl}; use songbird::input::{Compose, YoutubeDl};
@@ -43,7 +41,7 @@ pub async fn stream(
.expect("Guaranteed to exist in the typemap.") .expect("Guaranteed to exist in the typemap.")
}; };
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird Voice placed at init") .expect("Songbird Voice placed at init")
.clone(); .clone();
@@ -94,13 +92,13 @@ async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed,
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; 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 duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Playing now!"); let description = "Playing now!";
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author( .author(
CreateEmbedAuthor::new("Audio output hijacked!").icon_url(ctx.author().clone().face()), CreateEmbedAuthor::new("Audio output hijacked!").icon_url(ctx.author().clone().face()),
) )
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title(title.unwrap()) .title(title.unwrap())
.url(source_url.unwrap()) .url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face())) .thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))

View File

@@ -10,7 +10,7 @@ use poise::CreateReply;
pub async fn stop(ctx: Context<'_>) -> Result<(), Error> { pub async fn stop(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -9,7 +9,7 @@ use poise::CreateReply;
pub async fn volume(ctx: Context<'_>, #[description = "Volume"] volume: f32) -> Result<(), Error> { pub async fn volume(ctx: Context<'_>, #[description = "Volume"] volume: f32) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -10,7 +10,7 @@ pub mod posix;
pub mod qr; pub mod qr;
pub mod register; pub mod register;
pub mod taf; pub mod taf;
// pub mod uptime; pub mod uptime;
pub mod verse; pub mod verse;
pub mod weather; pub mod weather;
@@ -26,6 +26,6 @@ pub use posix::posix;
pub use qr::qr; pub use qr::qr;
pub use register::register; pub use register::register;
pub use taf::taf; pub use taf::taf;
// pub use uptime::uptime; pub use uptime::uptime;
pub use verse::verse; pub use verse::verse;
pub use weather::weather; pub use weather::weather;

View File

@@ -15,7 +15,7 @@ pub async fn ai(
#[rest] #[rest]
prompt: String, prompt: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let iamsorry = vec![ let iamsorry = &[
"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 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 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 can't engage in real-time conversations or remember previous interactions with users.",
@@ -27,19 +27,17 @@ pub async fn ai(
println!("Funny prompts: {}", prompt); println!("Funny prompts: {}", prompt);
let response; let response = {
let _ = {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
response = rng.gen_range(0..iamsorry.len()); rng.gen_range(0..iamsorry.len())
}; };
sleep(Duration::from_secs(3)); sleep(Duration::from_secs(1));
ctx.send( ctx.send(
CreateReply::default().embed( CreateReply::default().embed(
embed(ctx, "AI Response:", "", &format!("{}", iamsorry[response])) embed(ctx, "AI Response:", "", iamsorry[response])
.await .await
.unwrap(), .unwrap(),
), ),

View File

@@ -7,12 +7,9 @@ use crate::{commands::embeds::embed, Context, Error};
/// Rolls a dice /// Rolls a dice
#[poise::command(prefix_command, slash_command, category = "Tools")] #[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn dice(ctx: Context<'_>) -> Result<(), Error> { pub async fn dice(ctx: Context<'_>) -> Result<(), Error> {
let dice; let dice = {
let _ = {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
rng.gen_range(1..=6)
dice = rng.gen_range(1..7);
}; };
ctx.send( ctx.send(

View File

@@ -1,8 +1,7 @@
use poise::CreateReply; use poise::serenity_prelude::{
use serenity::{ Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
builder::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter},
model::{Colour, Timestamp},
}; };
use poise::CreateReply;
use crate::{Context, Error}; use crate::{Context, Error};
use url::form_urlencoded; use url::form_urlencoded;
@@ -34,7 +33,7 @@ async fn generate_embed(ctx: Context<'_>, message: String) -> Result<CreateEmbed
CreateEmbedAuthor::new("Your message as a QR Code!") CreateEmbedAuthor::new("Your message as a QR Code!")
.icon_url(ctx.author().clone().face()), .icon_url(ctx.author().clone().face()),
) )
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title("Your QR Code:") .title("Your QR Code:")
.url(url.clone()) .url(url.clone())
.image(url) .image(url)

View File

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

View File

@@ -1,3 +1,4 @@
use commands::tools::uptime::PROCESS_UPTIME;
use poise::serenity_prelude::{self as serenity, ActivityData}; use poise::serenity_prelude::{self as serenity, ActivityData};
use reqwest::Client as HttpClient; use reqwest::Client as HttpClient;
use songbird::SerenityInit; use songbird::SerenityInit;
@@ -8,7 +9,6 @@ use tracing::{error, info, warn};
mod commands; mod commands;
mod http; mod http;
use crate::commands::kashi;
use crate::commands::music; use crate::commands::music;
use crate::commands::tools; use crate::commands::tools;
use crate::http::HttpKey; use crate::http::HttpKey;
@@ -16,7 +16,9 @@ use crate::http::HttpKey;
type Error = Box<dyn std::error::Error + Send + Sync>; type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>; type Context<'a> = poise::Context<'a, Data, Error>;
pub struct Data; pub struct Data {
pub http_client: HttpClient,
}
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
match error { match error {
@@ -37,12 +39,13 @@ async fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
dotenv::dotenv().expect("Failed to load .env file."); dotenv::dotenv().expect("Failed to load .env file.");
let _ = *PROCESS_UPTIME.lock().unwrap();
let token = let token =
std::env::var("DISCORD_TOKEN").expect("Environment variable `DISCORD_TOKEN` not found!"); 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 prefix = std::env::var("PREFIX").expect("Environment variable `PREFIX` not found!");
let commands = vec![ let commands = vec![
kashi::kashi(),
music::deafen(), music::deafen(),
music::join(), music::join(),
music::leave(), music::leave(),
@@ -70,7 +73,7 @@ async fn main() {
tools::qr(), tools::qr(),
tools::register(), tools::register(),
tools::taf(), tools::taf(),
// tools::uptime(), tools::uptime(),
tools::verse(), tools::verse(),
tools::weather(), tools::weather(),
]; ];
@@ -78,7 +81,7 @@ async fn main() {
let options = poise::FrameworkOptions { let options = poise::FrameworkOptions {
commands, commands,
prefix_options: poise::PrefixFrameworkOptions { prefix_options: poise::PrefixFrameworkOptions {
prefix: Some(prefix.to_string().into()), prefix: Some(prefix.to_string()),
edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan(
Duration::from_secs(3600), Duration::from_secs(3600),
))), ))),
@@ -123,7 +126,7 @@ async fn main() {
}; };
let framework = poise::Framework::builder() let framework = poise::Framework::builder()
.setup(move |ctx, ready, _framework| { .setup(move |ctx, ready, framework| {
Box::pin(async move { Box::pin(async move {
info!( info!(
"{} [{}] connected successfully!", "{} [{}] connected successfully!",
@@ -131,8 +134,16 @@ async fn main() {
); );
ctx.set_activity(Some(ActivityData::listening(prefix + "help"))); ctx.set_activity(Some(ActivityData::listening(prefix + "help")));
// poise::builtins::register_globally(ctx, &framework.options().commands).await?; // poise::builtins::register_globally(ctx, &framework.options().commands).await?;
poise::builtins::register_in_guild(
ctx,
&framework.options().commands,
512680330495524873.into(),
)
.await?;
Ok(Data {}) Ok(Data {
http_client: HttpClient::new(),
})
}) })
}) })
.options(options) .options(options)