diff --git a/src/commands/fun/mod.rs b/src/commands/fun/mod.rs new file mode 100644 index 0000000..6f1d871 --- /dev/null +++ b/src/commands/fun/mod.rs @@ -0,0 +1 @@ +pub mod xkcd; diff --git a/src/commands/fun/xkcd.rs b/src/commands/fun/xkcd.rs new file mode 100644 index 0000000..52b63d5 --- /dev/null +++ b/src/commands/fun/xkcd.rs @@ -0,0 +1,47 @@ +use poise::command; +use reqwest::StatusCode; +use serde::Deserialize; +use serenity::all::{CreateActionRow, CreateButton, CreateEmbed, CreateEmbedFooter}; + +use crate::{data::ReqwestContainer, Context, Error}; + +#[derive(Debug, Deserialize)] +struct XkcdComic { + num: u16, // the numeric ID of the xkcd comic. + alt: String, // the caption of the xkcd comic. + img: String, // the image URL of the xkcd comic. + title: String, // the title of the xkcd comic. +} + +/// Retrieves the latest or a specific comic from xkcd. +#[command(slash_command)] +pub async fn xkcd(ctx: Context<'_>, #[description = "The specific comic no. to retrieve."] number: Option) -> Result<(), Error> { + let comic = match number { + None => "https://xkcd.com/info.0.json".to_string(), + Some(number) => format!("https://xkcd.com/{number}/info.0.json").to_string(), + }; + + let client = ctx.serenity_context().data.read().await.get::().cloned().unwrap(); + let request = client.get(comic).send().await?; + if request.status() == StatusCode::NOT_FOUND { + ctx.reply("You did not provide a valid comic id.").await?; + return Ok(()); + } + + let response: XkcdComic = request.json().await?; + let num = response.num; + let page = format!("https://xkcd.com/{num}/"); + let wiki = format!("https://explainxkcd.com/wiki/index.php/{num}"); + + let embed = CreateEmbed::new() + .title(&response.title) + .color(0xfafafa) + .description(&response.alt) + .image(&response.img) + .footer(CreateEmbedFooter::new(format!("xkcd comic no. {num}"))); + + let links = CreateActionRow::Buttons(vec![CreateButton::new_link(page).label("View on xkcd"), CreateButton::new_link(wiki).label("View wiki")]); + ctx.send(poise::CreateReply::default().embed(embed).components(vec![links])).await?; + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 89db166..a55e032 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1 +1,2 @@ +pub mod fun; pub mod utilities; diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..6a3ca8a --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,2 @@ +/// The user agent used for the reqwest client. +pub const REQWEST_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); diff --git a/src/data.rs b/src/data.rs index b4aae8d..4a4e1b4 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,28 +1,8 @@ -use aspotify::Client as SpotifyClient; use reqwest::Client as ReqwestClient; -use serenity::{client::bridge::gateway::ShardManager, prelude::TypeMapKey}; -use std::sync::Arc; -use tokio::sync::Mutex; +use serenity::prelude::TypeMapKey; -use crate::config::ConfigurationData; - -pub struct ShardManagerContainer; -pub struct ConfigContainer; pub struct ReqwestContainer; -pub struct SpotifyContainer; - -impl TypeMapKey for ShardManagerContainer { - type Value = Arc>; -} - -impl TypeMapKey for ConfigContainer { - type Value = ConfigurationData; -} impl TypeMapKey for ReqwestContainer { type Value = ReqwestClient; } - -impl TypeMapKey for SpotifyContainer { - type Value = SpotifyClient; -} diff --git a/src/main.rs b/src/main.rs index 2f05446..800ebe6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ use crate::serenity::GatewayIntents; +use constants::REQWEST_USER_AGENT; +use data::ReqwestContainer; use listeners::handler::Handler; use poise::serenity_prelude as serenity; use poise::{Framework, FrameworkOptions}; +use reqwest::redirect::Policy; +use reqwest::Client; use tracing::{info, Level}; use tracing_log::LogTracer; use tracing_subscriber::{EnvFilter, FmtSubscriber}; @@ -9,6 +13,8 @@ use utils::read_config; mod commands; mod config; +mod constants; +mod data; mod listeners; mod utils; @@ -45,7 +51,12 @@ async fn main() -> Result<(), Error> { let token = configuration.bot.discord.token; let framework = Framework::builder() .options(FrameworkOptions { - commands: vec![commands::utilities::hello(), commands::utilities::help(), commands::utilities::source()], + commands: vec![ + commands::fun::xkcd::xkcd(), + commands::utilities::hello(), + commands::utilities::help(), + commands::utilities::source() + ], ..Default::default() }) .setup(move |ctx, _ready, framework| { @@ -62,8 +73,17 @@ async fn main() -> Result<(), Error> { let commands_str: String = framework.options().commands.iter().map(|c| &c.name).cloned().collect::>().join(", "); info!("Initialized {} commands: {}", command_count, commands_str); - let client = serenity::Client::builder(token, GatewayIntents::all()).event_handler(Handler).framework(framework).await; - client.unwrap().start().await.unwrap(); + let mut client = serenity::Client::builder(token, GatewayIntents::all()).event_handler(Handler).framework(framework).await?; + + { + let mut data = client.data.write().await; + let http = Client::builder().user_agent(REQWEST_USER_AGENT).redirect(Policy::none()).build()?; + data.insert::(http); + } + + if let Err(why) = client.start_autosharded().await { + eprintln!("An error occurred while running the client: {why:?}"); + } Ok(()) }