Skip to content

Commit 2e24384

Browse files
committed
Implement chat bridge
1 parent 66b6256 commit 2e24384

File tree

4 files changed

+135
-20
lines changed

4 files changed

+135
-20
lines changed

src/discord_bot/mod.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ mod update_copy;
1515

1616
use crate::config;
1717
use crate::discord_bot::guild_storage::GuildStorage;
18+
use crate::pterodactyl::{send_command_safe, PterodactylServer};
1819
use async_trait::async_trait;
1920
use log::{error, info, warn};
21+
use serde::Serialize;
2022
use serenity::builder::{
2123
CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage,
2224
EditInteractionResponse,
@@ -115,6 +117,47 @@ async fn process_command(
115117
Ok(())
116118
}
117119

120+
async fn process_chatbridge(
121+
ctx: Context,
122+
pterodactyl: &pterodactyl_api::client::Client,
123+
chatbridge_server: &PterodactylServer,
124+
new_message: &Message,
125+
) -> Result<(), crate::Error> {
126+
let ptero_server = pterodactyl.get_server(&chatbridge_server.id);
127+
128+
#[derive(Serialize)]
129+
struct TextComponent {
130+
text: String,
131+
}
132+
133+
let sanitized_message = new_message.content_safe(&ctx);
134+
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?;
140+
}
141+
142+
if !new_message.attachments.is_empty() {
143+
let attachment_message = if new_message.attachments.len() == 1 {
144+
"an attachment"
145+
} else {
146+
"multiple attachments"
147+
};
148+
let text_component = TextComponent {
149+
text: format!(
150+
"{} posted {} in Discord.",
151+
new_message.author.name, attachment_message
152+
),
153+
};
154+
let text_component = serde_json::to_string(&text_component)?;
155+
send_command_safe(&ptero_server, format!("tellraw @a {}", text_component)).await?;
156+
}
157+
158+
Ok(())
159+
}
160+
118161
#[async_trait]
119162
impl EventHandler for Handler {
120163
async fn guild_member_addition(&self, ctx: Context, new_member: Member) {
@@ -167,20 +210,33 @@ impl EventHandler for Handler {
167210
Some(guild_id) => guild_id,
168211
None => return,
169212
};
213+
let pterodactyl = self.pterodactyl.clone();
170214

171215
tokio::runtime::Handle::current().spawn(async move {
172216
enum MessageHandling<'a> {
217+
ChatBridge(&'a PterodactylServer),
173218
Command(&'a str),
174219
IncCounter(&'a str),
175220
PermanentLatest,
176221
SimpleWords,
177222
}
178223

224+
let config = config::get();
225+
179226
let message_handling = {
180-
if config::get().simple_words_channel == Some(new_message.channel_id) {
227+
if config.simple_words_channel == Some(new_message.channel_id) {
181228
MessageHandling::SimpleWords
182229
} else if new_message.author.bot {
183230
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+
})
238+
{
239+
MessageHandling::ChatBridge(chatbridge_server)
184240
} else {
185241
let storage = GuildStorage::get(guild_id).await;
186242
if storage
@@ -207,6 +263,9 @@ impl EventHandler for Handler {
207263
};
208264

209265
if let Err(err) = match message_handling {
266+
MessageHandling::ChatBridge(chatbridge_server) => {
267+
process_chatbridge(ctx, &pterodactyl, chatbridge_server, &new_message).await
268+
}
210269
MessageHandling::Command(command) => {
211270
commands::run(command, guild_id, ctx, &new_message).await
212271
}

src/pterodactyl/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use pterodactyl_api::client::ServerState;
12
use serde::Deserialize;
3+
use serenity::model::id::ChannelId;
24
use std::collections::BTreeMap;
35

46
pub mod perms_sync;
@@ -10,6 +12,8 @@ pub struct PterodactylServer {
1012
pub id: String,
1113
pub name: String,
1214
pub category: PterodactylServerCategory,
15+
#[serde(default)]
16+
pub bridge: Option<PterodactylChatBridge>,
1317
}
1418

1519
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
@@ -90,3 +94,21 @@ impl PterodactylPerms {
9094
}
9195
}
9296
}
97+
98+
#[derive(Debug, Clone, Deserialize)]
99+
pub struct PterodactylChatBridge {
100+
pub discord_channel: ChannelId,
101+
pub webhook: String,
102+
}
103+
104+
pub async fn send_command_safe(
105+
server: &pterodactyl_api::client::Server<'_>,
106+
command: impl Into<String>,
107+
) -> Result<(), crate::Error> {
108+
if let Err(err) = server.send_command(command).await {
109+
if server.get_resources().await?.current_state == ServerState::Running {
110+
return Err(err.into());
111+
}
112+
}
113+
Ok(())
114+
}

src/pterodactyl/smp_commands.rs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use nom::sequence::tuple;
99
use nom::Finish;
1010
use pterodactyl_api::client::backups::{Backup, BackupParams};
1111
use pterodactyl_api::client::websocket::{PteroWebSocketHandle, PteroWebSocketListener};
12+
use serenity::builder::ExecuteWebhook;
13+
use serenity::model::webhook::Webhook;
1214

1315
pub(crate) async fn create_backup(
1416
server: &pterodactyl_api::client::Server<'_>,
@@ -50,7 +52,9 @@ pub(crate) async fn create_backup(
5052

5153
async fn handle_chat_message<H: PteroWebSocketHandle>(
5254
handle: &mut H,
53-
server: &pterodactyl_api::client::Server<'_>,
55+
data: &ProtobotData,
56+
chat_bridge_webhook: Option<&Webhook>,
57+
ptero_server: &pterodactyl_api::client::Server<'_>,
5458
sender: &str,
5559
message: &str,
5660
) -> Result<(), crate::Error> {
@@ -88,7 +92,7 @@ async fn handle_chat_message<H: PteroWebSocketHandle>(
8892
}
8993
"backup" => {
9094
create_backup(
91-
server,
95+
ptero_server,
9296
if args.len() > 1 {
9397
Some(args[1..].join(" "))
9498
} else {
@@ -104,14 +108,27 @@ async fn handle_chat_message<H: PteroWebSocketHandle>(
104108
}
105109
_ => {}
106110
}
111+
} else if let Some(chat_bridge_webhook) = chat_bridge_webhook {
112+
chat_bridge_webhook
113+
.execute(
114+
&data.discord_handle,
115+
false,
116+
ExecuteWebhook::new()
117+
.content(message)
118+
.username(sender)
119+
.avatar_url(format!("https://visage.surgeplay.com/face/256/{sender}")),
120+
)
121+
.await?;
107122
}
108123

109124
Ok(())
110125
}
111126

112127
async fn handle_server_log<H: PteroWebSocketHandle>(
113128
handle: &mut H,
114-
server: &pterodactyl_api::client::Server<'_>,
129+
data: &ProtobotData,
130+
chat_bridge_webhook: Option<&Webhook>,
131+
ptero_server: &pterodactyl_api::client::Server<'_>,
115132
message: &str,
116133
) -> Result<(), crate::Error> {
117134
let parse_result: Result<(&str, (&str, &str)), nom::error::Error<&str>> = map(
@@ -133,15 +150,24 @@ async fn handle_server_log<H: PteroWebSocketHandle>(
133150
)(message)
134151
.finish();
135152
if let Ok((_, (sender, message))) = parse_result {
136-
handle_chat_message(handle, server, sender, message).await?;
153+
handle_chat_message(
154+
handle,
155+
data,
156+
chat_bridge_webhook,
157+
ptero_server,
158+
sender,
159+
message,
160+
)
161+
.await?;
137162
}
138163

139164
Ok(())
140165
}
141166

142167
struct WebsocketListener<'a> {
168+
data: ProtobotData,
143169
ptero_server: pterodactyl_api::client::Server<'a>,
144-
server: PterodactylServer,
170+
chat_bridge_webhook: Option<Webhook>,
145171
}
146172

147173
impl<H: PteroWebSocketHandle> PteroWebSocketListener<H> for WebsocketListener<'_> {
@@ -150,7 +176,15 @@ impl<H: PteroWebSocketHandle> PteroWebSocketListener<H> for WebsocketListener<'_
150176
handle: &mut H,
151177
output: &str,
152178
) -> pterodactyl_api::Result<()> {
153-
if let Err(err) = handle_server_log(handle, &self.ptero_server, output).await {
179+
if let Err(err) = handle_server_log(
180+
handle,
181+
&self.data,
182+
self.chat_bridge_webhook.as_ref(),
183+
&self.ptero_server,
184+
output,
185+
)
186+
.await
187+
{
154188
error!("Error handling console output: {}", err);
155189
}
156190
Ok(())
@@ -159,12 +193,21 @@ impl<H: PteroWebSocketHandle> PteroWebSocketListener<H> for WebsocketListener<'_
159193

160194
pub(crate) async fn run(server: PterodactylServer, data: ProtobotData) -> Result<(), crate::Error> {
161195
info!("Starting websocket for server {}", server.name);
196+
let chat_bridge_webhook = match &server.bridge {
197+
Some(bridge) => Some(Webhook::from_url(&data.discord_handle, &bridge.webhook).await?),
198+
None => None,
199+
};
200+
let listener = WebsocketListener {
201+
data: data.clone(),
202+
ptero_server: data.pterodactyl.get_server(&server.id),
203+
chat_bridge_webhook,
204+
};
162205
let ptero_server = data.pterodactyl.get_server(&server.id);
163206
tokio::select! {
164207
_ = crate::wait_shutdown() => {}
165208
result = ptero_server.run_websocket_loop(|url| async {
166209
Ok(async_tungstenite::tokio::connect_async(url).await?.0)
167-
}, WebsocketListener { ptero_server: data.pterodactyl.get_server(&server.id), server }) => {
210+
}, listener) => {
168211
result?;
169212
}
170213
}

src/pterodactyl/whitelist.rs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
use crate::pterodactyl::PterodactylServerCategory;
1+
use crate::pterodactyl::{send_command_safe, PterodactylServerCategory};
22
use crate::{config, ProtobotData};
33
use futures::future::try_join_all;
44
use git_version::git_version;
55
use log::{error, info};
6-
use pterodactyl_api::client::ServerState;
76
use serde::de::value::StrDeserializer;
87
use serde::{Deserialize, Serialize};
98
use std::collections::BTreeSet;
@@ -257,11 +256,7 @@ async fn set_whitelist(
257256
ptero_server
258257
.write_file("whitelist.json", whitelist_json)
259258
.await?;
260-
if let Err(err) = ptero_server.send_command("whitelist reload").await {
261-
if ptero_server.get_resources().await?.current_state == ServerState::Running {
262-
return Err(err.into());
263-
}
264-
}
259+
send_command_safe(&ptero_server, "whitelist reload").await?;
265260
info!("{}", message);
266261
Ok::<(), crate::Error>(())
267262
}
@@ -282,11 +277,7 @@ async fn run_command(
282277
let message = message(&server.name);
283278
async move {
284279
let ptero_server = data.pterodactyl.get_server(&server.id);
285-
if let Err(err) = ptero_server.send_command(command).await {
286-
if ptero_server.get_resources().await?.current_state == ServerState::Running {
287-
return Err(err.into());
288-
}
289-
}
280+
send_command_safe(&ptero_server, command).await?;
290281
info!("{}", message);
291282
Ok::<(), crate::Error>(())
292283
}

0 commit comments

Comments
 (0)