Compare commits

...

3 Commits

Author SHA1 Message Date
158633d077 Added spotify link functionality 2025-07-30 22:31:19 -05:00
045d19c4c8 Increment version number 2025-07-30 22:22:15 -05:00
4841ab9f06 Added more functionality 2025-07-30 17:56:01 -05:00
4 changed files with 229 additions and 21 deletions

120
Cargo.lock generated
View File

@ -713,6 +713,18 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "enum_dispatch"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@ -979,8 +991,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@ -1766,6 +1780,17 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "maybe-async"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "maybe-rayon"
version = "0.1.1"
@ -2574,6 +2599,7 @@ dependencies = [
"system-configuration 0.6.1",
"tokio",
"tokio-native-tls",
"tokio-socks",
"tower",
"tower-service",
"url",
@ -2638,6 +2664,64 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rspotify"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77beedc33ecff4c39e8ef0e6f7ebc8d849f3ffebbeb786f9997d96f0d9cf4017"
dependencies = [
"async-stream",
"async-trait",
"base64 0.22.1",
"chrono",
"futures",
"getrandom 0.2.16",
"log",
"maybe-async",
"rspotify-http",
"rspotify-macros",
"rspotify-model",
"serde",
"serde_json",
"sha2",
"thiserror 2.0.12",
"url",
]
[[package]]
name = "rspotify-http"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde1ea9e2a49698cffbc994a83f5f909b37736c31cccb202f9577e8a32df3a63"
dependencies = [
"async-trait",
"log",
"maybe-async",
"reqwest 0.12.15",
"serde_json",
"thiserror 2.0.12",
]
[[package]]
name = "rspotify-macros"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee3dfb51ee54bd754ad76e96ad60a3b64bc70ae33a89261d9dbabc4c148a496f"
[[package]]
name = "rspotify-model"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "018f29a6a8c47cfe7923c48140ed546a395f660c7af05b73e6001d4505f89c8d"
dependencies = [
"chrono",
"enum_dispatch",
"serde",
"serde_json",
"strum",
"thiserror 2.0.12",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -3347,6 +3431,27 @@ dependencies = [
"unicode-properties",
]
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -3684,6 +3789,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-socks"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
dependencies = [
"either",
"futures-util",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
@ -4153,7 +4270,7 @@ dependencies = [
[[package]]
name = "watcat"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"arabic_reshaper",
"dotenvy",
@ -4165,6 +4282,7 @@ dependencies = [
"palette",
"reqwest 0.12.15",
"reqwest-middleware",
"rspotify",
"serenity",
"sqlx",
"tokio",

View File

@ -1,6 +1,6 @@
[package]
name = "watcat"
version = "0.2.0"
version = "0.4.0"
edition = "2024"
[dependencies]
@ -11,6 +11,7 @@ reqwest = "0.12"
reqwest-middleware = "0.4.2"
http-cache-reqwest = "0.15.1"
palette = "0.7.6"
rspotify = "0.15.0"
# text rendering dependencies
arabic_reshaper = "0.4.2"
unicode-bidi = "0.3.18"

View File

@ -28,7 +28,11 @@ init the database ``sqlx migrate run``
TODO
----
+ ~~literally just add the text~~
+ add easter egg
+ ~~add art and lastfm url fetching~~
+ add help command
+ ~~refactor to combine repeated parts of art, url, and fmi.~~
+ use macro for commands
+ use Option instead of empty string sentinel value
+ ~~add spotify link fetching~~
+ make various parameters user-configurable
+ use tiny-skia instead of magick-rust?

View File

@ -14,7 +14,8 @@ extern crate magick_rust;
use magick_rust::{ColorspaceType, CompositeOperator, MagickWand, PixelWand, magick_wand_genesis, FilterType};
use std::env;
use std::sync::Once;
use std::sync::{Once, Arc};
use tokio::sync::Mutex;
use palette::color_difference::ImprovedCiede2000;
use palette::lab::Lab;
@ -23,6 +24,9 @@ use palette::white_point::D65;
extern crate sqlx;
use sqlx::sqlite::SqlitePool;
use rspotify::{ClientCredsSpotify, Credentials, clients::BaseClient,};
use rspotify::model::{enums::types::SearchType, search::SearchResult};
use imagetext::fontdb::FontDB;
use imagetext::superfont::SuperFont;
@ -114,23 +118,77 @@ fn validate_color(col: &PixelWand) -> Option<Lab> {
Some(Lab::<D65>::from_components(color_raw))
}
async fn fmi(ctx: &Context, arg: &str, id: UserId, avatar: Option<String>) -> Reply {
async fn get_track(ctx: &Context, arg: &str, id: UserId) -> Result<lastfm::track::NowPlayingTrack, Reply> {
let lastfm_user = match arg {
"" => get_lastfm_username(ctx, id).await,
_ => Some(arg.to_string()),
};
let lastfm_client = match lastfm_user {
Some(s) => lastfm::Client::<String, String>::from_env(s),
None => return Reply::Text("No last.fm username set.".to_string()),
None => return Err(Reply::Text("No last.fm username set.".to_string())),
};
let now_playing = match lastfm_client.now_playing().await {
Ok(np) => np,
Err(e) => return Reply::Text(format!("Error: grabbing last.fm user data failed {e}")),
Err(e) => return Err(Reply::Text(format!("Error: grabbing last.fm user data failed {e}"))),
};
let track = match now_playing {
let track = match now_playing {
Some(track) => track,
None => return Reply::Text("Nothing playing.".to_string()),
None => return Err(Reply::Text("Nothing playing.".to_string())),
};
Ok(track)
}
async fn art(ctx: &Context, arg: &str, id: UserId) -> Reply {
let track = match get_track(ctx, arg, id).await {
Ok(track) => track,
Err(e) => return e,
};
let track_art = match track.image.extralarge {
Some(track_art) => track_art,
None => return Reply::Text("Error: getting image uri failed".to_string()),
};
Reply::Text(track_art.to_string())
}
async fn url(ctx: &Context, arg: &str, id: UserId) -> Reply {
let track = match get_track(ctx, arg, id).await {
Ok(track) => track,
Err(e) => return e,
};
Reply::Text(track.url)
}
async fn spot(ctx: &Context, arg: &str, id: UserId) -> Reply {
let track = match get_track(ctx, arg, id).await {
Ok(track) => track,
Err(e) => return e,
};
let mut data = ctx.data.write().await;
let spotify_option = data.get_mut::<SpotifyHaver>().expect("Failed to have spotify option");
if spotify_option.is_none() {
return Reply::Text("Unable to use Spotify command: Contact bot Administrator.".to_owned())
}
let spotify_arc_mutex = spotify_option.as_mut().unwrap();
//let spotify_arc_mutex_ref = Arc::clone(&spotify_arc_mutex);
let spotify_client = spotify_arc_mutex.lock().await;
spotify_client.request_token().await.unwrap();
let search = spotify_client.search(format!("{} {}", track.name, track.artist.name).as_str(), SearchType::Track, None, None, None, None).await;
let tracks = match search {
Ok(SearchResult::Tracks(track_page)) => track_page,
Err(e) => return Reply::Text(format!("Failed to get track {e}")),
_ => return Reply::Text("Spotify search failed".to_owned()),
};
match &tracks.items[0].external_urls.get("spotify") {
Some(url) => Reply::Text(url.to_owned().to_owned()),
None => Reply::Text("Unable to get spotify url".to_owned()),
}
}
async fn fmi(ctx: &Context, arg: &str, id: UserId, avatar: Option<String>) -> Reply {
let track = match get_track(ctx, arg, id).await {
Ok(track) => track,
Err(e) => return e,
};
let track_info = text::TrackInfo {
title: track.name,
artist: track.artist.name,
@ -143,7 +201,7 @@ async fn fmi(ctx: &Context, arg: &str, id: UserId, avatar: Option<String>) -> Re
let image = match get_image(ctx, image_uri.as_str()).await {
Ok(i) => i,
Err(e) => return Reply::Text(format!("{}", e)),
Err(e) => return Reply::Text(format!("{e}")),
};
let mut base_color = PixelWand::new();
let mut white = PixelWand::new();
@ -212,7 +270,6 @@ async fn fmi(ctx: &Context, arg: &str, id: UserId, avatar: Option<String>) -> Re
"Failed to set accent color"
);
}
let use_dark_color = match validate_color(&colors[0]) {
Some(color) => {
@ -335,24 +392,43 @@ async fn fmi(ctx: &Context, arg: &str, id: UserId, avatar: Option<String>) -> Re
async fn set(ctx: &Context, arg: &str, id: UserId) -> Reply {
set_lastfm_username(ctx, id, arg.to_string()).await;
Reply::Text(format!("set user {}", arg))
Reply::Text(format!("set user {arg}"))
}
//macro_rules! cmd {
// (c:lit, abbr:lit, a:ident) => {
// (".k", $c, $a) | (".k", $a, $c) | (concat!(".k", $abbr), $ident, "")
// }
//}
#[async_trait]
impl EventHandler for Handler {
async fn message(&self, ctx: Context, msg: Message) {
let mut cmd_iter = msg.content.split(" ");
let cmd = cmd_iter.next().unwrap_or("");
let arg = cmd_iter.next().unwrap_or("");
let resp = match cmd {
".fmi" => {
log!("{} received", msg.content);
Some(fmi(&ctx, arg, msg.author.id, msg.author.avatar_url()).await)
}
".set" => {
let arg1 = cmd_iter.next().unwrap_or("");
let arg2 = cmd_iter.next().unwrap_or("");
let resp = match (cmd, arg1, arg2) {
(".set", arg, "") | (".k", "set", arg) | (".kset", arg, "") => {
log!("{} received", msg.content);
Some(set(&ctx, arg, msg.author.id).await)
}
},
(".k", "art", arg) | (".k", arg, "art") | (".ka" | ".kart", arg, "") => {
log!("{} received", msg.content);
Some(art(&ctx, arg, msg.author.id).await)
},
(".k", "url" | "uri" | "link", arg) | (".k", arg, "url" | "uri" | "link") | (".ku" | ".kl" | ".kurl", arg, "") => {
log!("{} received", msg.content);
Some(url(&ctx, arg, msg.author.id).await)
},
(".k", "spot" | "spotify", arg) | (".k", arg, "spot" | "spotify") | (".ks" | ".kspot", arg, "") => {
log!("{} received", msg.content);
Some(spot(&ctx, arg, msg.author.id).await)
},
(".fmi" | ".k" | ".kf" | ".ki" | ".kfmi", arg, "") | (".k", "fm" | "fmi" | "get" | "img", arg) => {
log!("{} received", msg.content);
Some(fmi(&ctx, arg, msg.author.id, msg.author.avatar_url()).await)
},
_ => None,
};
if let Some(reply) = resp {
@ -387,6 +463,11 @@ impl TypeMapKey for FontsHaver {
type Value = (SuperFont<'static>, SuperFont<'static>);
}
struct SpotifyHaver;
impl TypeMapKey for SpotifyHaver {
type Value = Option<Arc<Mutex<ClientCredsSpotify>>>;
}
struct DBResponse {
lastfm_username: String,
}
@ -510,6 +591,9 @@ async fn main() {
Unifont
").expect("Failed to load bold fonts");
let spotify_creds = Credentials::from_env();
let spotify = spotify_creds.map(ClientCredsSpotify::new).map(Mutex::new).map(Arc::new);
let mut discord_client = Client::builder(&token, intents)
.event_handler(Handler)
.await
@ -520,6 +604,7 @@ async fn main() {
data.insert::<PoolHaver>(pool);
data.insert::<HttpHaver>(http);
data.insert::<FontsHaver>((regular_fonts, bold_fonts));
data.insert::<SpotifyHaver>(spotify);
}
if let Err(why) = discord_client.start().await {