diff --git a/aether-core/Cargo.toml b/aether-core/Cargo.toml index f5124eb..99d8d61 100644 --- a/aether-core/Cargo.toml +++ b/aether-core/Cargo.toml @@ -2,6 +2,7 @@ name = "aether-core" version = "0.3.0-alpha.7" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.dev] @@ -21,7 +22,7 @@ anyhow = "1.0.83" azalea = { git = "https://github.com/as1100k-forks/azalea.git", branch = "better-1.20.6" } azalea-task-manager = { path = "../plugins/task-manager", features = ["anti-afk"] } azalea-anti-afk = { path = "../plugins/anti-afk" } -azalea-discord = { path = "../plugins/discord", features = ["chat-bridge", "log-bridge"] } +azalea-discord = { git = "https://github.com/as1100k/aether", tag = "azalea-discord@v0.1.0", features = ["chat-bridge", "log-bridge"] } serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.117" tokio = { version = "1.37.0", features = ["macros"] } diff --git a/examples/anti-afk/Cargo.toml b/examples/anti-afk/Cargo.toml index 51dc38d..c7f8a0f 100644 --- a/examples/anti-afk/Cargo.toml +++ b/examples/anti-afk/Cargo.toml @@ -2,6 +2,7 @@ name = "anti-afk" version = "0.1.0" edition = "2021" +publish = false [dependencies] azalea = { git = "https://github.com/as1100k-forks/azalea.git", branch = "better-1.20.6" } diff --git a/examples/stone-miner/Cargo.toml b/examples/stone-miner/Cargo.toml index 0f59384..253923b 100644 --- a/examples/stone-miner/Cargo.toml +++ b/examples/stone-miner/Cargo.toml @@ -2,6 +2,7 @@ name = "stone-miner" version = "0.2.0" edition = "2021" +publish = false [profile.dev] opt-level = 1 diff --git a/plugins/README.md b/plugins/README.md index e181c88..4c5e5ba 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -7,6 +7,5 @@ This directory contains small plugins that can be added to any of our bots. * [Anti-AFK](./anti-afk/README.md) This Plugin Attempts to make the bot not get AFK Kicked. * [Auto-Mine](./auto-mine/README.md) This plugins is the implementation of holding left-click in minecraft. * [Task-Manager](./task-manager/README.md) This plugin acts as the task manger and executes tasks one by one. -* [Discord](./discord/README.md) This plugin is the bridge between minecraft and discord. _(Currently it can only send -messages and can't receive them)_ -* [Utility](./utility/README.md) A small collection of necessary plugins for a bot like auto-eat, kill aura (WIP). \ No newline at end of file +* [Bevy Discord](./discord/README.md) ![Crates.io Version](https://img.shields.io/crates/v/bevy-discord) A bevy plugin that can send messages to discord _(currently, only webhooks are supported)_. +* [Utility](./utility/README.md) A small collection of necessary plugins for a bot like auto-eat, kill aura (WIP). diff --git a/plugins/anti-afk/Cargo.toml b/plugins/anti-afk/Cargo.toml index f42edf0..c682dbd 100644 --- a/plugins/anti-afk/Cargo.toml +++ b/plugins/anti-afk/Cargo.toml @@ -3,6 +3,7 @@ name = "azalea-anti-afk" version = "0.1.0" edition = "2021" authors = ["Aditya Kumar <117935160+AS1100K@users.noreply.github.com>"] +publish = false [dependencies] azalea = { git = "https://github.com/as1100k-forks/azalea.git", branch = "better-1.20.6" } diff --git a/plugins/auto-mine/Cargo.toml b/plugins/auto-mine/Cargo.toml index be22065..c5853ab 100644 --- a/plugins/auto-mine/Cargo.toml +++ b/plugins/auto-mine/Cargo.toml @@ -3,6 +3,7 @@ name = "azalea-auto-mine" version = "0.1.0" edition = "2021" authors = ["Aditya Kumar <117935160+AS1100K@users.noreply.github.com>"] +publish = false [dependencies] azalea = { git = "https://github.com/as1100k-forks/azalea.git", branch = "better-1.20.6" } diff --git a/plugins/discord/Cargo.toml b/plugins/discord/Cargo.toml index 85c2802..b2115bf 100644 --- a/plugins/discord/Cargo.toml +++ b/plugins/discord/Cargo.toml @@ -1,23 +1,24 @@ [package] -name = "azalea-discord" -version = "0.1.0" +name = "bevy-discord" +description = "A bevy plugin that can send messages to discord." +version = "0.2.0-alpha.1" edition = "2021" authors = ["Aditya Kumar <117935160+AS1100K@users.noreply.github.com>"] - -[features] -chat-bridge = ["dep:uuid", "dep:tokio"] -log-bridge = ["dep:tracing-subscriber", "dep:serde_json"] +readme = "README.md" +repository = "https://github.com/AS1100K/aether/tree/main/plugins/discord" +publish = true +license = "GPL-3.0-only" +keywords = ["bevy", "plugin", "discord"] [dependencies] -azalea = { git = "https://github.com/as1100k-forks/azalea.git", branch = "better-1.20.6"} -reqwest = { version = "0.12.5", features = ["json"]} +bevy_app = "0.13.2" +bevy_ecs = "0.13.2" +reqwest = { version = "0.12.5", features = ["json", "rustls-tls"]} serde = { version = "1.0.203", features = ["derive"] } tracing = "0.1.40" -uuid = { version = "1.8.0", features = ["v4"], optional = true } -tokio = { version = "1.38.0", optional = true } -tracing-subscriber = { version = "0.3.18", optional = true } -serde_json = { version = "1.0.117", optional = true } +tokio = { version = "1.38.0", features = ["rt-multi-thread", "rt"] } +serde_json = { version = "1.0.117" } [dev-dependencies] anyhow = "1.0.86" -tokio = "1.38.0" \ No newline at end of file +azalea = "0.9.1" \ No newline at end of file diff --git a/plugins/discord/README.md b/plugins/discord/README.md index 231c8be..5904c9e 100644 --- a/plugins/discord/README.md +++ b/plugins/discord/README.md @@ -1,24 +1,36 @@ -# Azalea Discord Plugin +# Bevy Discord Plugin -A very simple, discord plugin that let you send messages through discord webhooks. _In Future releases, this plugin will support +![GitHub License](https://img.shields.io/github/license/AS1100K/aether) +![Crates.io Version](https://img.shields.io/crates/v/bevy-discord) + + +A very simple, bevy plugin that let you send messages through discord webhooks. _In Future releases, this plugin will support discord applications & bots and can send & receive messages by them._ ## Example +This example is shown inside azalea, but this plugin can be used with any bevy app. ```rust,no_run -use azalea_discord::DiscordPlugin; -use azalea_discord::DiscordExt; -use azalea_discord::SendDiscordMessage; use azalea::prelude::*; +use azalea::Vec3; +use bevy_discord::common::DiscordMessage; +use bevy_discord::webhook::{DiscordWebhookPlugin, DiscordWebhookRes, SendMessageEvent}; #[tokio::main] async fn main() { - let account = azalea::Account::offline("_aether"); - + let account = Account::offline("_aether"); + + let discord_webhook = DiscordWebhookRes::new() + .add_channel( + "channel_name", + "webhook_url", + "", + "" + ); ClientBuilder::new() .set_handler(handle) - .add_plugins(DiscordPlugin) - .start(account, "10.9.12.3") + .add_plugins(DiscordWebhookPlugin::new(discord_webhook)) + .start(account, "localhost") .await .unwrap(); } @@ -28,13 +40,13 @@ pub struct State {} async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> { match event { - Event::Login => { - bot.send_discord_message(SendDiscordMessage { - webhook: "https://discord.com".to_string(), - contents: "Logged into the server".to_string(), - username: None, - avatar_url: None - }); + Event::Chat(m) => { + let content = m.message(); + println!("{}", &content.to_ansi()); + let message = DiscordMessage::new() + .content(content.to_string()); + + bot.ecs.lock().send_event(SendMessageEvent::new("channel_name", message)); } _ => {} } @@ -42,10 +54,3 @@ async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> Ok(()) } ``` - -## Modules Available - -1. Chat Bridge -> _only on feature `chat-bridge`_ - Stream all the chats in minecraft to discord. Check this [example](./src/chat_bridge.rs) to learn how to use it. -2. Logs Bridge -> _only on feature `log-bridge`_ - Stream all the logs _only supports `tracing`_ to discord. Check this [example](./src/log_bridge.rs) to learn how to use it. \ No newline at end of file diff --git a/plugins/discord/src/chat_bridge.rs b/plugins/discord/src/chat_bridge.rs deleted file mode 100644 index edee001..0000000 --- a/plugins/discord/src/chat_bridge.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::SendDiscordMessage; -use azalea::app::{App, Plugin, Update}; -use azalea::chat::ChatReceivedEvent; -use azalea::ecs::prelude::*; -use azalea::entity::metadata::Player; -use azalea::entity::LocalEntity; -use azalea::prelude::*; -use azalea::TabList; -use uuid::Uuid; - -/// This plugin will send all the chat messages on the server to discord via webhook. -/// -/// # Examples -/// ```rust,no_run -/// # use azalea::prelude::*; -/// use azalea_discord::chat_bridge::DiscordChatBridgePlugin; -/// use azalea_discord::chat_bridge::DiscordChatBridgeExt; -/// -/// #[tokio::main] -/// async fn main() { -/// # let account = Account::offline("_aether"); -/// ClientBuilder::new() -/// .set_handler(handle) -/// .add_plugins(DiscordChatBridgePlugin) -/// .start(account, "localhost") -/// .await -/// .unwrap(); -/// } -/// # -/// # #[derive(Default, Component, Copy, Clone)] -/// # struct State; -/// -/// async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> { -/// match event { -/// Event::Login => { -/// // Start bridging mc chat on discord -/// bot.set_discord_chat_bridge(true, "Aether Bot", Some("https://url-of-discord-webhook.com".to_string())); -/// } -/// _ => {} -/// } -/// -/// Ok(()) -/// } -/// ``` -pub struct DiscordChatBridgePlugin; - -impl Plugin for DiscordChatBridgePlugin { - fn build(&self, app: &mut App) { - app.add_systems(Update, handle_chat_event); - } -} - -#[derive(Component, Clone)] -pub struct DiscordChatBridge { - webhook: String, - default_username: &'static str, -} - -#[allow(clippy::complexity)] -fn handle_chat_event( - mut events: EventReader, - query: Query< - (&TabList, &DiscordChatBridge), - (With, With, With), - >, - mut send_discord_message: EventWriter, -) { - for event in events.read() { - if let Ok((tab_list, discord_chat_bridge)) = query.get(event.entity) { - let (username, content) = event.packet.split_sender_and_content(); - - let new_username = if let Some(uname) = &username { - uname.to_owned() - } else { - discord_chat_bridge.default_username.to_string() - }; - - let mut avatar = "https://avatars.akamai.steamstatic.com/8d9a6a75e45129943fadcc869bfae2ee3bb2a535_full.jpg".to_string(); - if let Some(uname) = username { - let uuid = extract_uuid_from_tab_list(tab_list, uname); - if let Some(x) = uuid { - avatar = format!("https://minotar.net/avatar/{}", x); - } - } - - let send_discord_message_content = SendDiscordMessage { - webhook: discord_chat_bridge.webhook.to_owned(), - contents: content.to_owned(), - username: Some(new_username), - avatar_url: Some(avatar), - }; - - send_discord_message.send(send_discord_message_content); - } - } -} - -fn extract_uuid_from_tab_list(tab_list: &TabList, username: String) -> Option { - for (uuid, player_info) in tab_list.iter() { - if player_info.profile.name == username { - return Some(*uuid); - } - } - - None -} - -pub trait DiscordChatBridgeExt { - fn set_discord_chat_bridge( - &self, - enabled: bool, - default_username: &'static str, - webhook: Option, - ); -} - -impl DiscordChatBridgeExt for Client { - fn set_discord_chat_bridge( - &self, - enabled: bool, - default_username: &'static str, - webhook: Option, - ) { - let mut ecs = self.ecs.lock(); - let mut world = ecs.entity_mut(self.entity); - - if enabled { - world.insert(DiscordChatBridge { - webhook: webhook.expect( - "If you want to enable discord chat bridge, you need to provide webhook", - ), - default_username, - }); - } else { - world.remove::(); - } - } -} diff --git a/plugins/discord/src/common.rs b/plugins/discord/src/common.rs new file mode 100644 index 0000000..60e134f --- /dev/null +++ b/plugins/discord/src/common.rs @@ -0,0 +1,298 @@ +use serde::Serialize; +#[macro_export] +macro_rules! new { + () => { + #[must_use] + #[doc = "Creates a new empty struct."] + pub fn new() -> Self { + Self::default() + } + }; +} + +#[macro_export] +macro_rules! override_field { + ($name:ident, $type:ty) => { + #[doc = concat!("Adds `", stringify!($name), "` field.")] + pub fn $name(mut self, $name: $type) -> Self { + self.$name = Some($name); + self + } + }; +} + +#[macro_export] +macro_rules! initialize_field { + ($name:ident, $type:ty) => { + #[doc = concat!("Adds `", stringify!($name), "` field.")] + pub fn $name(mut self, $name: $type) -> Self { + self.$name = $name; + self + } + }; +} + +/// Representation of discord message +#[derive(Default, Serialize, Clone)] +pub struct DiscordMessage { + /// the message contents (up to 2000 characters) + pub content: String, + /// override the default username of the webhook + pub username: Option, + /// override the default avatar of the webhook + pub avatar_url: Option, + /// true if this is a TTS message + pub tts: Option, + /// embedded [rich](DiscordEmbedType::Rich) content (upto 10 embeds) + pub embeds: Option>, + /// allowed mentions for the message + pub allowed_mentions: Option, + /// the components to include with the message + pub components: Option>, + // attachment,files[n], payload_json, flags, thread_name, applied_tags, poll isn't supported yet +} + +impl DiscordMessage { + new!(); + initialize_field!(content, String); + override_field!(username, String); + override_field!(avatar_url, String); + override_field!(tts, bool); + override_field!(embeds, Vec); + override_field!(allowed_mentions, DiscordAllowedMentions); + override_field!(components, Vec); +} + +#[derive(Default, Serialize, Clone)] +pub struct DiscordEmbed { + /// title of embed + pub title: Option, + // field "type" isn't allowed + /// [type of embed](DiscordEmbedType) (always ["rich"](DiscordEmbedType::Rich) for webhook embeds) + pub r#type: DiscordEmbedType, + /// description of embed + pub description: Option, + /// url of embed + pub url: Option, + // timestamp is optional, so ignoring it + // pub timestamp: Option<> + /// color code of the embed, use [DiscordEmbed::color] to use hex code + pub color: Option, + /// footer information + pub footer: Option, + /// image information + pub image: Option, + /// thumbnail information [DiscordImageEmbed] is also used for it + pub thumbnail: Option, + /// video information [DiscordImageEmbed] is also used for it + pub video: Option, + /// provider information + pub provider: Option, + /// fields information, max of 25 + pub fields: Option>, + /// author information + pub author: Option +} + +impl DiscordEmbed { + new!(); + override_field!(title, String); + override_field!(description, String); + override_field!(url, String); + override_field!(footer, DiscordFooterEmbed); + override_field!(image, DiscordImageEmbed); + override_field!(thumbnail, DiscordImageEmbed); + override_field!(video, DiscordImageEmbed); + override_field!(provider, DiscordProviderEmbed); + override_field!(fields, Vec); + override_field!(author, DiscordAuthorEmbed); + initialize_field!(r#type, DiscordEmbedType); + + /// color should be hex value without `#` + pub fn color(mut self, color: &str) -> Self { + let parsed_color: i64 = i64::from_str_radix(color, 16).expect("Unable to parse color code"); + + self.color = Some(parsed_color); + self + } +} + +#[derive(Default, Serialize, Clone)] +pub enum DiscordEmbedType { + /// generic embed rendered from embed attributes + #[default] + Rich, + /// image embed + Image, + /// video embed + Video, + /// animated gif image embed rendered as a video embed + GIFV, + /// article embed + Article, + /// link embed + Link +} + +#[derive(Default, Serialize, Clone)] +pub struct DiscordFooterEmbed { + /// footer text + pub text: String, + /// url of footer icon (only supports http(s) and attachments) + pub icon_url: Option, + /// a proxied url of footer icon + pub proxy_icon_url: Option +} + +impl DiscordFooterEmbed { + new!(); + initialize_field!(text, String); + override_field!(icon_url, String); + override_field!(proxy_icon_url, String); +} + +#[derive(Default, Serialize, Clone)] +pub struct DiscordFieldEmbed { + /// name of the field + pub name: String, + /// value of the field + pub value: String, + /// whether or not this field should display inline + pub inline: Option +} + +impl DiscordFieldEmbed { + new!(); + initialize_field!(name, String); + initialize_field!(value, String); + override_field!(inline, bool); +} + +#[derive(Default, Serialize, Clone)] +pub struct DiscordImageEmbed { + /// source url of image (only supports http(s) and attachments) + pub url: String, + /// a proxied url of the image + pub proxy_url: Option, + /// height of image + pub height: Option, + /// width of image + pub width: Option +} + +impl DiscordImageEmbed { + new!(); + initialize_field!(url, String); + override_field!(proxy_url, String); + override_field!(height, i64); + override_field!(width, i64); +} + +#[derive(Default, Serialize, Clone)] +pub struct DiscordProviderEmbed { + /// name of provider + pub name: Option, + /// url of provider + pub url: Option +} + +impl DiscordProviderEmbed { + new!(); + override_field!(name, String); + override_field!(url, String); +} + +#[derive(Default, Serialize, Clone)] +pub struct DiscordAuthorEmbed { + /// name of author + pub name: String, + /// url of author (only supports http(s)) + pub url: Option, + /// url of author icon (only supports http(s) and attachments) + pub icon_url: Option, + /// a proxied url of author icon + pub proxy_icon_url: Option +} + +impl DiscordAuthorEmbed { + new!(); + initialize_field!(name, String); + override_field!(url, String); + override_field!(icon_url, String); + override_field!(proxy_icon_url, String); +} + +#[derive(Default, Serialize, Clone)] +pub struct DiscordAllowedMentions { + /// An array of [allowed mention types](DiscordAllowedMentionTypes) to parse from the content. + pub parse: Option>, + /// Array of role_ids to mention (Max size of 100) + pub roles: Option>, + /// Array of role_ids to mention (Max size of 100) + pub users: Option>, + /// For replies, whether to mention the author of the message being replied to (default false) + pub replied_user: bool +} + +impl DiscordAllowedMentions { + new!(); + override_field!(parse, Vec); + override_field!(roles, Vec); + override_field!(users, Vec); + initialize_field!(replied_user, bool); +} + +#[derive(Serialize, Clone)] +pub enum DiscordAllowedMentionTypes { + /// Controls role mentions + Roles, + /// Controls user mentions + Users, + /// Controls `@everyone` and `@here` mentions + Everyone +} + +#[derive(Default, Serialize, Clone)] +/// See to +/// learn more +pub struct DiscordMessageComponent { + pub r#type: u8, + pub label: Option, + pub style: Option, + pub custom_id: Option, + pub components: Option> +} + +impl DiscordMessageComponent { + new!(); + override_field!(label, String); + override_field!(style, String); + override_field!(custom_id, String); + override_field!(components, Vec); + + pub fn r#type(mut self, r#type: DiscordMessageComponentTypes) -> Self { + self.r#type = r#type as u8; + self + } +} + +#[derive(Default, Serialize)] +pub enum DiscordMessageComponentTypes { + /// Container for other components + ActionRow = 1, + /// Button object + #[default] + Button = 2, + /// Select menu for picking from defined text options + StringSelect = 3, + /// Text input object + TextInput = 4, + /// Select menu for users + UserSelect = 5, + /// Select menu for roles + RoleSelect = 6, + /// Select menu for mentionables (users and roles) + MentionableSelect = 7, + /// Select menu for channels + ChannelSelect = 8 +} \ No newline at end of file diff --git a/plugins/discord/src/lib.rs b/plugins/discord/src/lib.rs index 5633f81..399bed0 100644 --- a/plugins/discord/src/lib.rs +++ b/plugins/discord/src/lib.rs @@ -1,79 +1,11 @@ #![doc = include_str!("../README.md")] -#[cfg(feature = "chat-bridge")] -pub mod chat_bridge; +use bevy_ecs::schedule::SystemSet; -#[cfg(feature = "log-bridge")] -pub mod log_bridge; +pub mod webhook; +pub mod common; +mod runtime; -use azalea::app::{Plugin, Update}; -use azalea::ecs::prelude::*; -use azalea::prelude::*; -use serde::Serialize; -use tracing::warn; - -pub struct DiscordPlugin; - -impl Plugin for DiscordPlugin { - fn build(&self, app: &mut azalea::app::App) { - app.add_event::() - .add_systems(Update, handle_send_discord_message); - } -} - -#[derive(Event)] -pub struct SendDiscordMessage { - pub webhook: String, - pub contents: String, - pub username: Option, - pub avatar_url: Option, -} - -#[derive(Serialize)] -struct Context { - content: String, - username: Option, - avatar_url: Option, -} - -fn handle_send_discord_message(mut events: EventReader) { - for event in events.read() { - let webhook = event.webhook.to_owned(); - - let content = event.contents.to_owned(); - let username = event.username.to_owned(); - let avatar_url = event.avatar_url.to_owned(); - - let context = Context { - content, - username, - avatar_url, - }; - - tokio::spawn(async move { - let client = reqwest::Client::new(); - let res = client - .post(format!("{}?wait=true", webhook)) - .json(&context) - .send() - .await; - - if let Ok(response) = res { - if response.status() != 200 { - warn!("Unable to send message"); - } - } - }); - } -} - -pub trait DiscordExt { - fn send_discord_message(&self, context: SendDiscordMessage); -} - -impl DiscordExt for Client { - fn send_discord_message(&self, context: SendDiscordMessage) { - let mut ecs = self.ecs.lock(); - ecs.send_event(context); - } -} +/// Bevy [`SystemSet`] that contains all system of this plugin. +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct DiscordSet; \ No newline at end of file diff --git a/plugins/discord/src/log_bridge.rs b/plugins/discord/src/log_bridge.rs deleted file mode 100644 index e78c2e6..0000000 --- a/plugins/discord/src/log_bridge.rs +++ /dev/null @@ -1,105 +0,0 @@ -use reqwest::Client; -use std::sync::Arc; -use serde_json::json; -use tracing::{Event, Subscriber}; -use tracing::field::Field; -use tracing_subscriber::layer::{Context, Layer}; -use tracing_subscriber::prelude::*; -use tracing_subscriber::registry::LookupSpan; -use tracing_subscriber::EnvFilter; -pub use tracing::Level; -use tracing_subscriber::field::Visit; - -struct DiscordLayer { - discord_webhook_url: String, - http_client: Arc, - log_level: Level, -} - -impl Layer for DiscordLayer -where - S: Subscriber + for<'a> LookupSpan<'a>, -{ - fn on_event(&self, event: &Event, _ctx: Context) { - if event.metadata().level() <= &self.log_level { - let mut visitor = JsonVisitor::new(); - - event.record(&mut visitor); - - let log_message = format!("{}", visitor.0["message"]); - let client = self.http_client.clone(); - let webhook_url = self.discord_webhook_url.clone(); - - tokio::spawn(async move { - // Create the payload for the Discord webhook - let payload = serde_json::json!({ - "content": log_message, - }); - - // Send the payload to the Discord webhook - let _ = client.post(&webhook_url).json(&payload).send().await; - }); - } - } -} - -impl DiscordLayer { - fn new(discord_webhook_url: String, log_level: Level) -> Self { - DiscordLayer { - discord_webhook_url, - http_client: Arc::new(Client::new()), - log_level, - } - } -} - -struct JsonVisitor(serde_json::Value); - -impl JsonVisitor { - fn new() -> Self { - JsonVisitor(json!({})) - } -} - -impl Visit for JsonVisitor { - fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { - self.0[field.name()] = json!(format!("{:?}", value)); - } -} - -pub struct DiscordLogBridge; - -impl DiscordLogBridge { - /// All the logs will be bridged to discord after running this function. - /// - /// ## Examples - /// ```rust,no_run - /// use azalea_discord::log_bridge::{DiscordLogBridge, Level}; - /// use tracing::info; - /// - /// #[derive(Default)] - /// struct Config { - /// log_bridge: Option - /// } - /// - /// #[tokio::main] - /// async fn main() { - /// let config = Config::default(); - /// - /// if let Some(webhook) = config.log_bridge { - /// DiscordLogBridge::init(webhook, Level::INFO); - /// } - /// - /// info!("This message will be shown in discord."); - /// } - /// ``` - pub fn init(webhook: String, log_level: Level) { - let discord_layer = DiscordLayer::new(webhook, log_level); - - tracing_subscriber::registry() - .with(discord_layer) - .with(EnvFilter::new(format!("{}", log_level))) - .with(tracing_subscriber::fmt::layer()) - .init(); - } -} \ No newline at end of file diff --git a/plugins/discord/src/runtime.rs b/plugins/discord/src/runtime.rs new file mode 100644 index 0000000..8ae8f67 --- /dev/null +++ b/plugins/discord/src/runtime.rs @@ -0,0 +1,9 @@ +use std::sync::OnceLock; +use tokio::runtime::Runtime; + +pub(crate) fn tokio_runtime() -> &'static Runtime { + static RUNTIME: OnceLock = OnceLock::new(); + RUNTIME.get_or_init(|| { + Runtime::new().expect("Setting up tokio runtime needs to succeed.") + }) +} \ No newline at end of file diff --git a/plugins/discord/src/webhook/mod.rs b/plugins/discord/src/webhook/mod.rs new file mode 100644 index 0000000..4d7f325 --- /dev/null +++ b/plugins/discord/src/webhook/mod.rs @@ -0,0 +1,123 @@ +use crate::common::DiscordMessage; +use crate::runtime::tokio_runtime; +use crate::DiscordSet; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use reqwest::StatusCode; +use std::collections::HashMap; +use tracing::{error, trace}; + +#[derive(Clone)] +pub struct DiscordWebhookPlugin(DiscordWebhookRes); + +#[derive(Resource, Clone, Default)] +pub struct DiscordWebhookRes { + channels: HashMap<&'static str, Channel<'static>>, +} + +#[derive(Clone)] +pub struct Channel<'a> { + /// Prefix in every message of this channel. Mainly you would use mention here, like `@everyone` + /// NOTE: When text are joined with prefix, it automatically adds a space. + pub message_prefix: &'a str, + /// Similar to `Channel::message_prefix` but at the end of the message. + pub message_suffix: &'a str, + pub webhook_url: &'a str, +} + +impl DiscordWebhookPlugin { + /// Create a new discord Plugin + pub fn new(discord_webhook_res: DiscordWebhookRes) -> Self { + Self(discord_webhook_res) + } +} + +/// Discord Plugin Resource +impl DiscordWebhookRes { + pub fn new() -> Self { + Self { + channels: HashMap::new(), + } + } + + /// Adds a new channel + /// To know more about its fields see, [`Channel`] + pub fn add_channel( + mut self, + name_identifier: &'static str, + webhook_url: &'static str, + message_prefix: &'static str, + message_suffix: &'static str, + ) -> Self { + self.channels.insert( + name_identifier, + Channel { + message_prefix, + message_suffix, + webhook_url, + }, + ); + self + } +} + +impl Plugin for DiscordWebhookPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(self.0.clone()) + .add_event::() + .add_systems(Update, handle_send_message.in_set(DiscordSet)); + } +} + +/// Sending this event will send a message on the channel. +#[derive(Event, Clone)] +pub struct SendMessageEvent { + name_identifier: &'static str, + message: DiscordMessage, +} + +impl SendMessageEvent { + /// Create a new [`SendMessageEvent`] + pub fn new(name_identifier: &'static str, message: DiscordMessage) -> Self { + Self { + name_identifier, + message, + } + } +} + +fn handle_send_message( + mut events: EventReader, + discord_webhook_res: Res, +) { + for event in events.read() { + if let Some(channel) = discord_webhook_res.channels.get(event.name_identifier) { + let channel_clone = channel.clone(); + let event_clone = event.clone(); + + tokio_runtime().spawn(async move { + let client = reqwest::Client::new(); + trace!("body => {:?}", serde_json::to_string(&event_clone.message)); + + let res = client.post(channel_clone.webhook_url) + .query(&[("wait", true)]) + .json(&event_clone.message) + .send() + .await; + + match res { + Ok(response) => { + if response.status() != StatusCode::OK { + error!("Got response code {}. The message might contains problem in body. Make sure messages are compliant with discord webhook API. Learn more at https://discord.com/developers/docs/resources/webhook#execute-webhook", response.status()) + } + } + Err(err) => { + error!("Unable to send message to discord webhook, error => {:?}", err.without_url()) + } + } + }); + } else { + error!("Unable to find discord channel."); + } + } +} diff --git a/plugins/task-manager/Cargo.toml b/plugins/task-manager/Cargo.toml index ef53478..0794dc8 100644 --- a/plugins/task-manager/Cargo.toml +++ b/plugins/task-manager/Cargo.toml @@ -3,6 +3,7 @@ name = "azalea-task-manager" version = "0.1.0" edition = "2021" authors = ["Aditya Kumar <117935160+AS1100K@users.noreply.github.com>"] +publish = false [features] anti-afk = ["dep:azalea-anti-afk"]