Skip to content

Commit 32cbb4f

Browse files
committed
Refactor chat bridge to allow bridges between servers, allow multiple tech servers
1 parent 7c01dc7 commit 32cbb4f

File tree

8 files changed

+388
-97
lines changed

8 files changed

+388
-97
lines changed

src/application.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ struct ApplicationEmbeds<'a> {
9393
embeds: Vec<ApplicationEmbed<'a>>,
9494
}
9595

96-
impl<'a> ApplicationEmbeds<'a> {
96+
impl ApplicationEmbeds<'_> {
9797
fn create(app: Application) -> Self {
9898
let (actual_questions, meta_questions): (Vec<_>, Vec<_>) = app
9999
.items
@@ -268,7 +268,7 @@ struct ApplicationEmbed<'a> {
268268
fields: Vec<ApplicationField<'a>>,
269269
}
270270

271-
impl<'a> ApplicationEmbed<'a> {
271+
impl ApplicationEmbed<'_> {
272272
fn char_count(&self) -> usize {
273273
self.title.len()
274274
+ self.author.len()
@@ -286,7 +286,7 @@ struct ApplicationField<'a> {
286286
value: Cow<'a, str>,
287287
}
288288

289-
impl<'a> ApplicationField<'a> {
289+
impl ApplicationField<'_> {
290290
fn char_count(&self) -> usize {
291291
self.title.len() + self.value.len()
292292
}

src/config.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use crate::pterodactyl::{
2-
PterodactylAllPerms, PterodactylEmails, PterodactylServer, PterodactylServerCategoryFilter,
2+
PterodactylAllPerms, PterodactylChatBridge, PterodactylEmails, PterodactylServer,
3+
PterodactylServerCategoryFilter,
34
};
5+
use log::warn;
46
use serde::Deserialize;
57
use serenity::model::id::{ChannelId, GuildId, RoleId};
8+
use std::collections::HashSet;
69
use std::fs::File;
710
use std::sync::{Arc, OnceLock, RwLock};
811

@@ -32,14 +35,41 @@ pub struct Config {
3235
pub pterodactyl_servers: Vec<PterodactylServer>,
3336
pub pterodactyl_emails: PterodactylEmails,
3437
pub pterodactyl_perms: PterodactylAllPerms,
38+
pub pterodactyl_chat_bridges: Vec<PterodactylChatBridge>,
3539
pub special_channels: SpecialChannels,
3640
pub special_roles: SpecialRoles,
3741
}
3842

3943
impl Config {
4044
fn load() -> Result<Config, crate::Error> {
4145
let file = File::open("config.json")?;
42-
Ok(serde_json::from_reader(file)?)
46+
let config: Config = serde_json::from_reader(file)?;
47+
config.lint();
48+
Ok(config)
49+
}
50+
51+
fn lint(&self) {
52+
let mut seen_bridge_servers = HashSet::new();
53+
let mut seen_bridge_channels = HashSet::new();
54+
for chat_bridge in &self.pterodactyl_chat_bridges {
55+
for server_name in &chat_bridge.ptero_servers {
56+
if !self
57+
.pterodactyl_servers
58+
.iter()
59+
.any(|server| &server.name == server_name)
60+
{
61+
warn!("Unknown server: {}", server_name);
62+
}
63+
if !seen_bridge_servers.insert(server_name) {
64+
warn!("Duplicate server: {}", server_name);
65+
}
66+
}
67+
for channel in &chat_bridge.discord_channels {
68+
if !seen_bridge_channels.insert(channel.id) {
69+
warn!("Duplicate channel: {}", channel.id);
70+
}
71+
}
72+
}
4373
}
4474

4575
pub fn pterodactyl_servers(
@@ -50,6 +80,27 @@ impl Config {
5080
.iter()
5181
.filter(move |server| filter.test(server.category))
5282
}
83+
84+
pub fn chat_bridge_by_ptero_server_name(
85+
&self,
86+
server_name: &str,
87+
) -> Option<&PterodactylChatBridge> {
88+
self.pterodactyl_chat_bridges
89+
.iter()
90+
.find(|bridge| bridge.ptero_servers.iter().any(|name| name == server_name))
91+
}
92+
93+
pub fn chat_bridge_by_discord_channel(
94+
&self,
95+
discord_channel: ChannelId,
96+
) -> Option<&PterodactylChatBridge> {
97+
self.pterodactyl_chat_bridges.iter().find(|bridge| {
98+
bridge
99+
.discord_channels
100+
.iter()
101+
.any(|channel| channel.id == discord_channel)
102+
})
103+
}
53104
}
54105

55106
#[derive(Deserialize)]

src/discord_bot/mod.rs

Lines changed: 115 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ mod update_copy;
1515

1616
use crate::config;
1717
use crate::discord_bot::guild_storage::GuildStorage;
18-
use crate::pterodactyl::{send_command_safe, PterodactylServer};
18+
use crate::pterodactyl::{tellraw, PterodactylChatBridge, PterodactylServer};
1919
use async_trait::async_trait;
20+
use dashmap::{DashMap, Entry};
21+
use futures::future::try_join_all;
2022
use log::{error, info, warn};
21-
use serde::Serialize;
23+
use serenity::all::Webhook;
2224
use serenity::builder::{
23-
CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage,
24-
EditInteractionResponse,
25+
CreateAttachment, CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage,
26+
CreateMessage, EditInteractionResponse, ExecuteWebhook,
2527
};
2628
use serenity::client::{Context, EventHandler};
2729
use serenity::http::Http;
@@ -39,6 +41,7 @@ use std::sync::Arc;
3941
pub(crate) type Handle = Arc<Http>;
4042

4143
struct Handler {
44+
webhook_cache: Arc<DashMap<String, Webhook>>,
4245
pterodactyl: Arc<pterodactyl_api::client::Client>,
4346
}
4447

@@ -119,45 +122,116 @@ async fn process_command(
119122

120123
async fn process_chatbridge(
121124
ctx: Context,
125+
webhook_cache: &DashMap<String, Webhook>,
122126
pterodactyl: &pterodactyl_api::client::Client,
123-
chatbridge_server: &PterodactylServer,
127+
chat_bridge: &PterodactylChatBridge,
124128
new_message: &Message,
125129
) -> Result<(), crate::Error> {
126-
let ptero_server = pterodactyl.get_server(&chatbridge_server.id);
130+
let config = config::get();
131+
try_join_all(
132+
chat_bridge
133+
.ptero_servers
134+
.iter()
135+
.filter_map(|server_name| {
136+
config
137+
.pterodactyl_servers
138+
.iter()
139+
.find(|server| &server.name == server_name)
140+
})
141+
.map(|server| send_chatbridge_message(&ctx, pterodactyl, server, new_message)),
142+
)
143+
.await?;
144+
try_join_all(
145+
chat_bridge
146+
.discord_channels
147+
.iter()
148+
.filter(|channel| channel.id != new_message.channel_id)
149+
.map(|channel| {
150+
send_chatbridge_message_to_discord(
151+
&ctx,
152+
webhook_cache,
153+
&channel.webhook,
154+
new_message,
155+
)
156+
}),
157+
)
158+
.await?;
159+
Ok(())
160+
}
127161

128-
#[derive(Serialize)]
129-
struct TextComponent {
130-
text: String,
131-
}
162+
async fn send_chatbridge_message(
163+
ctx: &Context,
164+
pterodactyl: &pterodactyl_api::client::Client,
165+
server: &PterodactylServer,
166+
message: &Message,
167+
) -> Result<(), crate::Error> {
168+
let ptero_server = pterodactyl.get_server(&server.id);
132169

133-
let sanitized_message = new_message.content_safe(&ctx);
170+
let sanitized_message = message.content_safe(ctx);
134171
if !sanitized_message.is_empty() {
135-
let text_component = TextComponent {
136-
text: format!("[{}] {}", new_message.author.name, sanitized_message),
137-
};
138-
let text_component = serde_json::to_string(&text_component)?;
139-
send_command_safe(&ptero_server, format!("tellraw @a {}", text_component)).await?;
172+
tellraw(
173+
&ptero_server,
174+
format!("[Discord] [{}] {}", message.author.name, sanitized_message),
175+
)
176+
.await?;
140177
}
141178

142-
if !new_message.attachments.is_empty() {
143-
let attachment_message = if new_message.attachments.len() == 1 {
179+
if !message.attachments.is_empty() {
180+
let attachment_message = if message.attachments.len() == 1 {
144181
"an attachment"
145182
} else {
146183
"multiple attachments"
147184
};
148-
let text_component = TextComponent {
149-
text: format!(
185+
tellraw(
186+
&ptero_server,
187+
format!(
150188
"{} posted {} in Discord.",
151-
new_message.author.name, attachment_message
189+
message.author.name, attachment_message
152190
),
153-
};
154-
let text_component = serde_json::to_string(&text_component)?;
155-
send_command_safe(&ptero_server, format!("tellraw @a {}", text_component)).await?;
191+
)
192+
.await?;
156193
}
157194

158195
Ok(())
159196
}
160197

198+
async fn send_chatbridge_message_to_discord(
199+
ctx: &Context,
200+
webhook_cache: &DashMap<String, Webhook>,
201+
webhook: &str,
202+
message: &Message,
203+
) -> Result<(), crate::Error> {
204+
let webhook = match webhook_cache.entry(webhook.to_owned()) {
205+
Entry::Occupied(entry) => entry.get().clone(),
206+
Entry::Vacant(entry) => entry.insert(Webhook::from_url(ctx, webhook).await?).clone(),
207+
};
208+
webhook
209+
.execute(
210+
ctx,
211+
false,
212+
ExecuteWebhook::new()
213+
.content(&message.content)
214+
.files(
215+
try_join_all(
216+
message
217+
.attachments
218+
.iter()
219+
.map(|attachment| CreateAttachment::url(ctx, &attachment.url)),
220+
)
221+
.await?,
222+
)
223+
.username(&message.author.name)
224+
.avatar_url(
225+
message
226+
.author
227+
.avatar_url()
228+
.unwrap_or_else(|| message.author.default_avatar_url()),
229+
),
230+
)
231+
.await?;
232+
Ok(())
233+
}
234+
161235
#[async_trait]
162236
impl EventHandler for Handler {
163237
async fn guild_member_addition(&self, ctx: Context, new_member: Member) {
@@ -211,10 +285,11 @@ impl EventHandler for Handler {
211285
None => return,
212286
};
213287
let pterodactyl = self.pterodactyl.clone();
288+
let webhook_cache = self.webhook_cache.clone();
214289

215290
tokio::runtime::Handle::current().spawn(async move {
216291
enum MessageHandling<'a> {
217-
ChatBridge(&'a PterodactylServer),
292+
ChatBridge(&'a PterodactylChatBridge),
218293
Command(&'a str),
219294
IncCounter(&'a str),
220295
PermanentLatest,
@@ -228,15 +303,10 @@ impl EventHandler for Handler {
228303
MessageHandling::SimpleWords
229304
} else if new_message.author.bot {
230305
return;
231-
} else if let Some(chatbridge_server) =
232-
config.pterodactyl_servers.iter().find(|server| {
233-
server
234-
.bridge
235-
.as_ref()
236-
.is_some_and(|bridge| bridge.discord_channel == new_message.channel_id)
237-
})
306+
} else if let Some(chat_bridge) =
307+
config.chat_bridge_by_discord_channel(new_message.channel_id)
238308
{
239-
MessageHandling::ChatBridge(chatbridge_server)
309+
MessageHandling::ChatBridge(chat_bridge)
240310
} else {
241311
let storage = GuildStorage::get(guild_id).await;
242312
if storage
@@ -264,7 +334,14 @@ impl EventHandler for Handler {
264334

265335
if let Err(err) = match message_handling {
266336
MessageHandling::ChatBridge(chatbridge_server) => {
267-
process_chatbridge(ctx, &pterodactyl, chatbridge_server, &new_message).await
337+
process_chatbridge(
338+
ctx,
339+
&webhook_cache,
340+
&pterodactyl,
341+
chatbridge_server,
342+
&new_message,
343+
)
344+
.await
268345
}
269346
MessageHandling::Command(command) => {
270347
commands::run(command, guild_id, ctx, &new_message).await
@@ -380,7 +457,10 @@ pub(crate) async fn create_client(
380457
| GatewayIntents::MESSAGE_CONTENT
381458
| GatewayIntents::GUILD_MESSAGE_REACTIONS;
382459
Ok(Client::builder(&config::get().discord_token, intents)
383-
.event_handler(Handler { pterodactyl })
460+
.event_handler(Handler {
461+
webhook_cache: Arc::new(DashMap::new()),
462+
pterodactyl,
463+
})
384464
.await?)
385465
}
386466

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ fn main() {
147147
!= Some(true)
148148
{
149149
for server in config::get().pterodactyl_servers.iter().cloned() {
150-
if server.category.is_minecraft() {
150+
if server.category.is_proto_minecraft() {
151151
let protobot_data = protobot_data.clone();
152152
runtime.spawn(async move {
153153
let server_name = server.name.clone();

0 commit comments

Comments
 (0)