Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/bin/mock_server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serenity::all::UserId;
use UnchainedPlugin::features::discord::DiscordConfig;
use UnchainedPlugin::events::models::{GameChatMessage, Join, Kill, GameEvent, ChatSource, ChatType, CommandActor, CommandSource, CommandExecuted};
use UnchainedPlugin::events::models::{GameChatMessage, Join, Kill, GameEvent, ChatSource, ChatType, CommandActor, CommandSource, CommandExecuted, Damage, DamageSource};
use UnchainedPlugin::features::events::EVENT_SYSTEM;
use UnchainedPlugin::sinfo;
use UnchainedPlugin::tools::logger::init_syslog;
Expand All @@ -13,8 +13,10 @@ fn main() {
let config = DiscordConfig {
bot_token: "YOUR_TOKEN_HERE".to_string(),
admin_role_id: None,
general_channel_id: serenity::all::ChannelId::new(1),
admin_channel_id: None,
dashboard_channel_id: Some(serenity::all::ChannelId::new(1)),
general_chat_channel_id: Some(serenity::all::ChannelId::new(1)),
admin_notification_channel_id: Some(serenity::all::ChannelId::new(1)),
event_log_channel_id: Some(serenity::all::ChannelId::new(1)),
mention_on_admin: true,
};

Expand All @@ -40,7 +42,15 @@ fn main() {
EVENT_SYSTEM.game_event_publisher.publish(GameEvent::KillEvent(Kill {
killer: k.to_string(),
victim: v.to_string(),
weapon: "MockSword".to_string()
source: Damage {
attacker: k.to_string(),
victim: v.to_string(),
damage: DamageSource {
amount: 50.0,
source: "MockSword".to_string(),
attack_type: "Slash".to_string(),
},
}
}));
}
["chat", ..] => {
Expand Down
41 changes: 12 additions & 29 deletions src/events/broadcast_message/discord.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
use std::sync::Arc;
use async_trait::async_trait;
use serenity::all::{ChannelId, Http, RoleId};
use serenity::all::{Http, RoleId};
use serenity::builder::{CreateAllowedMentions, CreateMessage};
use crate::events::broadcast::{BroadcastMessage, Notify};
use crate::events::bus::Subscriber;
use crate::features::discord::{send_to_channel, SharedDiscordConfig};

pub struct DiscordBroadcastSubscriber {
channel_id: ChannelId,
_admin_channel_id: Option<ChannelId>,
admin_role_id: Option<RoleId>,
discord_config: SharedDiscordConfig,
discord_http: Arc<Http>,
}

impl DiscordBroadcastSubscriber {
pub fn new(
channel_id: ChannelId,
admin_channel_id: Option<ChannelId>,
admin_role_id: Option<RoleId>,
discord_http: Arc<Http>,
) -> Self {
pub fn new(discord_http: Arc<Http>, discord_config: SharedDiscordConfig) -> Self {
Self {
channel_id,
_admin_channel_id: admin_channel_id,
admin_role_id,
discord_config,
discord_http,
}
}
Expand All @@ -35,17 +27,17 @@ impl Subscriber<BroadcastMessage> for DiscordBroadcastSubscriber {
}

async fn on_event(&mut self, event: &BroadcastMessage) {
let config = self.discord_config.read().await.clone();
let admin_role_id = config.admin_role_id.map(RoleId::new);

let mut discord_message: CreateMessage = event.clone().into();
let http = Arc::clone(&self.discord_http);
let channel_id = self.channel_id;

if !event.notify_roles().is_empty() {
let mut content = event.content_ref().map(str::to_owned).unwrap_or_default();

for notify_role in event.notify_roles().iter() {
let notify_role_id = match notify_role {
Notify::Admin => self.admin_role_id,
// Add others as we create them
Notify::Admin => admin_role_id,
};

if let Some(notify_role_id) = notify_role_id {
Expand All @@ -57,18 +49,9 @@ impl Subscriber<BroadcastMessage> for DiscordBroadcastSubscriber {
discord_message = discord_message.content(content);
}

let allowed_mention =
CreateAllowedMentions::new()
.roles(self.admin_role_id.into_iter());


let allowed_mention = CreateAllowedMentions::new().roles(admin_role_id.into_iter());
discord_message = discord_message.allowed_mentions(allowed_mention);

tokio::spawn(async move {
let _ = channel_id.send_message(
&http,
discord_message
.allowed_mentions(allowed_mention)
).await;
});
send_to_channel(&self.discord_http, config.general_chat_channel_id, discord_message).await;
}
}
25 changes: 22 additions & 3 deletions src/events/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ pub struct Crash {
pub struct Kill {
pub killer: String,
pub victim: String,
pub weapon: String,
pub source: Damage,
}

#[derive(Debug, Clone)]
pub struct DamageSource {
pub amount: f32,
pub source: String, // weapon or throwable name
pub attack_type: String // slash/overhead/throw/stab/etc;
}

/// Triggered when the server changes maps
#[derive(Debug, Clone)]
pub struct MapChange {
pub new_map: String
pub new_map_url: String,
pub new_map_name: String,
}

/// Triggered when a match finishes (before the map change)
Expand Down Expand Up @@ -148,6 +156,16 @@ pub struct CommandExecuted {
pub source: CommandSource,
}

#[derive(Debug, Clone)]
pub struct CommandRejected {
pub name: String,
pub args: Vec<String>,
pub raw_args: String,
pub actor: CommandActor,
pub source: CommandSource,
pub rejection_reason: String,
}

#[derive(Clone, Debug)]
pub struct ServerStatus {
pub name: String,
Expand All @@ -172,7 +190,7 @@ pub struct DuelStart { pub challenger: String, pub opponent: String }
#[derive(Debug, Clone)]
pub struct Attack { pub attacker: String, pub attack_type: String, pub was_parried: bool }
#[derive(Debug, Clone)]
pub struct Damage { pub attacker: String, pub victim: String, pub damage: f32 }
pub struct Damage { pub attacker: String, pub victim: String, pub damage: DamageSource }

// --- The Unified GameEvent Enum ---

Expand All @@ -187,6 +205,7 @@ pub enum GameEvent {
GameChatMessageEvent(GameChatMessage),
CommandRequestEvent(CommandRequest),
CommandExecutedEvent(CommandExecuted),
CommandRejectedEvent(CommandRejected),
ServerStatusEvent(ServerStatus),
AdminAlertEvent(AdminAlert),
DuelStartEvent(DuelStart), // Never dispatched
Expand Down
130 changes: 82 additions & 48 deletions src/features/discord.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use std::sync::Arc;
use serenity::all::{ChannelId, Context, GatewayIntents, Http, Message, RoleId};
use serenity::all::{ChannelId, Context, CreateMessage, GatewayIntents, Http, Message, RoleId};
use serenity::Client;
use serenity::client::EventHandler as DiscordHandler;
use tokio::sync::RwLock;
use crate::events::bus::EventPublisher;
use crate::events::models::{ActorIdentity, ActorPermissions, ChatSource, ChatType, CommandActor, CommandSource, GameChatMessage, CommandRequest, GameEvent, PermissionFlags};
use crate::events::broadcast_message::discord::DiscordBroadcastSubscriber;
use crate::modules::chat::DiscordChatSink;
use crate::modules::discord::admin_alert::AdminAlertModule;
use crate::modules::discord::dashboard::DashboardSubscriber;
use crate::modules::discord::event_log::EventLogSubscriber;
use crate::features::events::EVENT_SYSTEM;
use crate::features::tokio_runtime::TOKIO_RUNTIME;
use crate::{sinfo, swarn};
Expand All @@ -16,25 +18,53 @@ use crate::{sinfo, swarn};
pub struct DiscordConfig {
pub bot_token: String,
pub admin_role_id: Option<u64>,
pub general_channel_id: ChannelId,
pub admin_channel_id: Option<ChannelId>,
pub dashboard_channel_id: Option<ChannelId>,
pub general_chat_channel_id: Option<ChannelId>,
pub admin_notification_channel_id: Option<ChannelId>,
pub event_log_channel_id: Option<ChannelId>,
pub mention_on_admin: bool,
}

pub type SharedDiscordConfig = Arc<RwLock<DiscordConfig>>;

pub async fn send_to_channel(
http: &Http,
channel_id: Option<ChannelId>,
message: CreateMessage,
) {
match channel_id {
Some(id) => {
if let Err(e) = id.send_message(http, message).await {
swarn!(f; "Failed to send to channel {}: {}", id, e);
}
}
None => {
swarn!(f; "Attempted to send to a channel that is not configured.");
}
}
}

pub struct Discord {
config: DiscordConfig,
config: SharedDiscordConfig,
event_publisher: EventPublisher<GameEvent>,
}

pub fn initialize_discord_system(config: DiscordConfig) {
pub fn initialize_discord_system(config: DiscordConfig) -> SharedDiscordConfig {
let event_publisher = EVENT_SYSTEM.game_event_publisher.clone();
let shared_config = Arc::new(RwLock::new(config));
let task_config = Arc::clone(&shared_config);

TOKIO_RUNTIME.spawn(async move {
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;

let mut client = match Client::builder(&config.bot_token, intents)

let bot_token = {
let config = task_config.read().await;
config.bot_token.clone()
};

let mut client = match Client::builder(&bot_token, intents)
.event_handler(Discord {
config: config.clone(),
config: Arc::clone(&task_config),
event_publisher
})
.await {
Expand All @@ -45,61 +75,59 @@ pub fn initialize_discord_system(config: DiscordConfig) {
}
};

register_discord_subscribers(config.clone(), client.http.clone()).await;
add_discord_chat_sink(config.clone(), client.http.clone());
register_discord_subscribers(Arc::clone(&task_config), client.http.clone()).await;
add_discord_chat_sink(Arc::clone(&task_config), client.http.clone());

if let Err(e) = client.start().await {
swarn!(f; "Discord bot error: {}", e);
}
});

shared_config
}

async fn register_discord_subscribers(config: DiscordConfig, http: Arc<Http>) {
async fn register_discord_subscribers(config: SharedDiscordConfig, http: Arc<Http>) {
let broadcast_message_bus = &EVENT_SYSTEM.message_broadcast_event_bus;

let discord_subscriber = DiscordBroadcastSubscriber::new(
config.general_channel_id,
config.admin_channel_id,
config.admin_role_id.map(serenity::all::RoleId::new),
http.clone(),
);
let discord_subscriber = DiscordBroadcastSubscriber::new(http.clone(), Arc::clone(&config));
let _ = broadcast_message_bus.subscribe(Box::new(discord_subscriber)).await;

// Admin Alert Module
if let Some(admin_channel_id) = config.admin_channel_id {
let admin_alert_module = AdminAlertModule::new(
config.mention_on_admin,
http.clone(),
admin_channel_id,
config.admin_role_id.map(RoleId::new),
&EVENT_SYSTEM.game_event_publisher,
);
let _ = EVENT_SYSTEM.game_event_bus.subscribe(Box::new(admin_alert_module.clone())).await;

let command_subscriber = EVENT_SYSTEM.command_subscriber.clone();
TOKIO_RUNTIME.spawn(async move {
let mut command_subscriber = command_subscriber.lock().await;
command_subscriber.register(admin_alert_module);
});
}
let admin_alert_module = AdminAlertModule::new(
http.clone(),
Arc::clone(&config),
&EVENT_SYSTEM.game_event_publisher,
);
let _ = EVENT_SYSTEM.game_event_bus.subscribe(Box::new(admin_alert_module.clone())).await;

let command_subscriber = EVENT_SYSTEM.command_subscriber.clone();
TOKIO_RUNTIME.spawn(async move {
let mut command_subscriber = command_subscriber.lock().await;
command_subscriber.register(admin_alert_module);
});

// Dashboard Subscriber
// Assuming we use the general channel for the dashboard, or we could add a dedicated field to DiscordConfig
let dashboard_subscriber = DashboardSubscriber::new(http.clone(), config.general_channel_id);
let dashboard_subscriber = DashboardSubscriber::new(http.clone(), Arc::clone(&config));
let _ = EVENT_SYSTEM.game_event_bus.subscribe(Box::new(dashboard_subscriber)).await;

// Event Log Subscriber
let should_register_event_log = {
let cfg = config.read().await;
cfg.event_log_channel_id.is_some()
};
if should_register_event_log {
let event_log_subscriber = EventLogSubscriber::new(http.clone(), Arc::clone(&config));
let _ = EVENT_SYSTEM.game_event_bus.subscribe(Box::new(event_log_subscriber)).await;
} else {
swarn!(f; "Discord event log subscriber not registered because event log channel is not configured.");
}
}

fn add_discord_chat_sink(discord_config: DiscordConfig, discord_http: Arc<Http>) {
fn add_discord_chat_sink(discord_config: SharedDiscordConfig, discord_http: Arc<Http>) {
TOKIO_RUNTIME.spawn(async move {
if let Some(admin_id) = discord_config.admin_channel_id {
EVENT_SYSTEM.chat_relay_subscriber.add_sink(Box::new(DiscordChatSink::new(
discord_config.general_channel_id,
admin_id,
discord_http,
))).await;
} else {
swarn!(f; "Discord chat sink not added because admin channel id is missing.");
}
EVENT_SYSTEM.chat_relay_subscriber
.add_sink(Box::new(DiscordChatSink::new(discord_config, discord_http)))
.await;
});
}

Expand All @@ -114,12 +142,18 @@ impl DiscordHandler for Discord {
return;
}

if Some(msg.channel_id) != Some(self.config.general_channel_id) && Some(msg.channel_id) != self.config.admin_channel_id {
let config = self.config.read().await.clone();

let is_known_channel =
config.general_chat_channel_id == Some(msg.channel_id)
|| config.admin_notification_channel_id == Some(msg.channel_id);

if !is_known_channel {
return;
}

let roles = msg.member.as_ref().map(|m| m.roles.clone()).unwrap_or_default();
let is_admin = self.config.admin_role_id.map(|id| roles.contains(&RoleId::new(id))).unwrap_or(false);
let is_admin = config.admin_role_id.map(|id| roles.contains(&RoleId::new(id))).unwrap_or(false);

let actor = CommandActor {
display_name: msg.author.name.clone(),
Expand Down Expand Up @@ -154,7 +188,7 @@ impl DiscordHandler for Discord {
actor,
source: CommandSource::Discord,
}));
} else if Some(msg.channel_id) == Some(self.config.general_channel_id) {
} else if config.general_chat_channel_id == Some(msg.channel_id) {
self.event_publisher.publish(GameEvent::GameChatMessageEvent(GameChatMessage {
chat_source: ChatSource::Discord,
chat_type: ChatType::Global,
Expand Down
Loading
Loading