Breakin' out modules
This commit is contained in:
parent
851c68267e
commit
7cd74f95a2
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -464,6 +464,16 @@ dependencies = [
|
||||||
"websocket",
|
"websocket",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "discord_bot"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"discord",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlv-list"
|
name = "dlv-list"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -830,7 +840,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client"
|
name = "http_client"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"reqwest-middleware 0.2.5",
|
"reqwest-middleware 0.2.5",
|
||||||
|
@ -3228,7 +3238,7 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"config",
|
"config",
|
||||||
"discord",
|
"discord_bot",
|
||||||
"http_client",
|
"http_client",
|
||||||
"redact",
|
"redact",
|
||||||
"reqwest 0.12.5",
|
"reqwest 0.12.5",
|
||||||
|
|
|
@ -3,7 +3,11 @@ resolver = "2"
|
||||||
members = ["apps/yourcloud"]
|
members = ["apps/yourcloud"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
|
||||||
http_client = { path = "libs/http_client" }
|
http_client = { path = "libs/http_client" }
|
||||||
|
discord_bot = { path = "libs/discord_bot" }
|
||||||
|
thiserror = "1.0.63"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
|
||||||
[profile.dev.package.backtrace]
|
[profile.dev.package.backtrace]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
@ -7,7 +7,6 @@ edition = "2021"
|
||||||
axum = "0.7.5"
|
axum = "0.7.5"
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
config = "0.14.0"
|
config = "0.14.0"
|
||||||
discord = { git = "https://github.com/SpaceManiac/discord-rs" }
|
|
||||||
redact = { version = "0.1.9", features = ["serde"] }
|
redact = { version = "0.1.9", features = ["serde"] }
|
||||||
reqwest = { version = "0.12.3", features = ["json", "socks"] }
|
reqwest = { version = "0.12.3", features = ["json", "socks"] }
|
||||||
reqwest-middleware = "0.3.0"
|
reqwest-middleware = "0.3.0"
|
||||||
|
@ -16,11 +15,12 @@ reqwest-tracing = "0.5.0"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
serde_json = "1.0.115"
|
serde_json = "1.0.115"
|
||||||
serde_with = "3.7.0"
|
serde_with = "3.7.0"
|
||||||
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { workspace = true }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
http_client = { workspace = true }
|
http_client = { workspace = true }
|
||||||
|
discord_bot = { workspace = true }
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// TODO: family reminders?
|
// TODO: family reminders?
|
||||||
// TODO: connect to Discord
|
// TODO: connect to Discord
|
||||||
// TODO: handle messages
|
// TODO: data persistence? (sqlite? sled? postgres?)
|
||||||
// TODO: data persistence? (sqlite? sled?)
|
|
||||||
|
|
||||||
use std::{any::Any, future};
|
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
use discord::model::{ChannelId, Event, Message, ReadyEvent};
|
use discord::model::{ChannelId, Event, Message, ReadyEvent};
|
||||||
|
use std::{any::Any, future};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
const COMMAND_PREFIX: &str = "!";
|
const COMMAND_PREFIX: &str = "!";
|
||||||
|
@ -249,8 +248,6 @@ impl Discord {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
pub async fn start(conf: Arc<Config>) -> Result<()> {
|
pub async fn start(conf: Arc<Config>) -> Result<()> {
|
||||||
if conf.discord.is_none() {
|
if conf.discord.is_none() {
|
||||||
warn!("Chatbot starting without Discord token. Nothing will happen.");
|
warn!("Chatbot starting without Discord token. Nothing will happen.");
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
mod chatbot;
|
|
||||||
mod config;
|
mod config;
|
||||||
mod minecraft_server_status;
|
mod minecraft_server_status;
|
||||||
mod observe;
|
mod observe;
|
||||||
|
@ -24,7 +23,15 @@ async fn main() -> Result<()> {
|
||||||
let mut set = tokio::task::JoinSet::new();
|
let mut set = tokio::task::JoinSet::new();
|
||||||
|
|
||||||
set.spawn(webserver::start(conf.clone()));
|
set.spawn(webserver::start(conf.clone()));
|
||||||
set.spawn(chatbot::start(conf.clone()));
|
|
||||||
|
if let Some(bot_token) = conf
|
||||||
|
.clone()
|
||||||
|
.discord
|
||||||
|
.clone()
|
||||||
|
.map(|d| d.bot_token.expose_secret().clone())
|
||||||
|
{
|
||||||
|
set.spawn(async move { discord_bot::start(&bot_token).await.map_err(|e| e.into()) });
|
||||||
|
}
|
||||||
|
|
||||||
let result = set.join_next().await;
|
let result = set.join_next().await;
|
||||||
match result {
|
match result {
|
||||||
|
|
10
libs/discord_bot/Cargo.toml
Normal file
10
libs/discord_bot/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "discord_bot"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
discord = { git = "https://github.com/SpaceManiac/discord-rs" }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
268
libs/discord_bot/src/lib.rs
Normal file
268
libs/discord_bot/src/lib.rs
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
// TODO: family reminders?
|
||||||
|
// TODO: connect to Discord
|
||||||
|
// TODO: data persistence? (sqlite? sled? postgres?)
|
||||||
|
|
||||||
|
pub use discord;
|
||||||
|
use discord::model::{ChannelId, Event, Message, ReadyEvent};
|
||||||
|
use std::{any::Any, sync::Arc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::{error, info, instrument, trace, warn};
|
||||||
|
|
||||||
|
const COMMAND_PREFIX: &str = "!";
|
||||||
|
|
||||||
|
struct Discord {
|
||||||
|
discord: discord::Discord,
|
||||||
|
connection: Option<Arc<Mutex<discord::Connection>>>,
|
||||||
|
current_user: Option<discord::model::CurrentUser>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("discord error: {0}")]
|
||||||
|
Discord(#[from] discord::Error),
|
||||||
|
#[error("already connected")]
|
||||||
|
AlreadyConnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
impl Discord {
|
||||||
|
pub fn try_new(bot_token: &str) -> Result<Self> {
|
||||||
|
let discord = discord::Discord::from_bot_token(bot_token)?;
|
||||||
|
Ok(Self {
|
||||||
|
discord,
|
||||||
|
connection: None,
|
||||||
|
current_user: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect(&mut self) -> Result<ReadyEvent> {
|
||||||
|
if self.connection.is_some() {
|
||||||
|
return Err(Error::AlreadyConnected);
|
||||||
|
}
|
||||||
|
let (connection, ready_ev) = self.discord.connect()?;
|
||||||
|
self.connection = Some(Arc::new(Mutex::new(connection)));
|
||||||
|
self.current_user = self.discord.get_current_user().ok();
|
||||||
|
info!("Discord current user: {:?}", self.current_user);
|
||||||
|
Ok(ready_ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_commands(self: Arc<Self>) -> Result<()> {
|
||||||
|
if let Some(conn) = self.connection.clone() {
|
||||||
|
loop {
|
||||||
|
// TODO: I'm suspicious this will just always be locked here and
|
||||||
|
// I'm doing something stupid.
|
||||||
|
let mut conn = conn.lock().await;
|
||||||
|
while let Ok(event) = conn.recv_event() {
|
||||||
|
let rself = self.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match rself.handle_event(event).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to handle Discord event: {err}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn handle_event(&self, event: Event) -> Result<()> {
|
||||||
|
match event {
|
||||||
|
Event::Ready(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::Resumed { trace: _ } => return Ok(self.ignore_event(event)),
|
||||||
|
Event::UserUpdate(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::UserNoteUpdate(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::UserSettingsUpdate {
|
||||||
|
detect_platform_accounts: _,
|
||||||
|
developer_mode: _,
|
||||||
|
enable_tts_command: _,
|
||||||
|
inline_attachment_media: _,
|
||||||
|
inline_embed_media: _,
|
||||||
|
locale: _,
|
||||||
|
message_display_compact: _,
|
||||||
|
render_embeds: _,
|
||||||
|
server_positions: _,
|
||||||
|
show_current_game: _,
|
||||||
|
status: _,
|
||||||
|
theme: _,
|
||||||
|
convert_emoticons: _,
|
||||||
|
friend_source_flags: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::UserServerSettingsUpdate(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::VoiceStateUpdate(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::VoiceServerUpdate {
|
||||||
|
server_id: _,
|
||||||
|
channel_id: _,
|
||||||
|
endpoint: _,
|
||||||
|
token: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::CallCreate(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::CallUpdate {
|
||||||
|
channel_id: _,
|
||||||
|
message_id: _,
|
||||||
|
region: _,
|
||||||
|
ringing: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::CallDelete(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ChannelRecipientAdd(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ChannelRecipientRemove(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::TypingStart {
|
||||||
|
channel_id: _,
|
||||||
|
user_id: _,
|
||||||
|
timestamp: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::PresenceUpdate {
|
||||||
|
presence: _,
|
||||||
|
server_id: _,
|
||||||
|
roles: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::PresencesReplace(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::RelationshipAdd(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::RelationshipRemove(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::MessageCreate(msg) => return Ok(self.handle_message(msg)?),
|
||||||
|
Event::MessageUpdate {
|
||||||
|
id: _,
|
||||||
|
channel_id: _,
|
||||||
|
kind: _,
|
||||||
|
content: _,
|
||||||
|
nonce: _,
|
||||||
|
tts: _,
|
||||||
|
pinned: _,
|
||||||
|
timestamp: _,
|
||||||
|
edited_timestamp: _,
|
||||||
|
author: _,
|
||||||
|
mention_everyone: _,
|
||||||
|
mentions: _,
|
||||||
|
mention_roles: _,
|
||||||
|
attachments: _,
|
||||||
|
embeds: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::MessageAck {
|
||||||
|
channel_id: _,
|
||||||
|
message_id: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::MessageDelete {
|
||||||
|
channel_id: _,
|
||||||
|
message_id: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::MessageDeleteBulk {
|
||||||
|
channel_id: _,
|
||||||
|
ids: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerCreate(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerUpdate(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerDelete(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerMemberAdd(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerMemberUpdate {
|
||||||
|
server_id: _,
|
||||||
|
roles: _,
|
||||||
|
user: _,
|
||||||
|
nick: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerMemberRemove(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerMembersChunk(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerSync {
|
||||||
|
server_id: _,
|
||||||
|
large: _,
|
||||||
|
members: _,
|
||||||
|
presences: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerRoleCreate(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerRoleUpdate(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerRoleDelete(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerBanAdd(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerBanRemove(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerIntegrationsUpdate(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ServerEmojisUpdate(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ChannelCreate(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ChannelUpdate(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ChannelDelete(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ChannelPinsAck {
|
||||||
|
channel_id: _,
|
||||||
|
timestamp: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ChannelPinsUpdate {
|
||||||
|
channel_id: _,
|
||||||
|
last_pin_timestamp: _,
|
||||||
|
} => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ReactionAdd(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::ReactionRemove(_) => return Ok(self.ignore_event(event)),
|
||||||
|
Event::Unknown(_, _) => return Ok(self.ignore_event(event)),
|
||||||
|
_ => return Ok(self.ignore_event(event)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ignore_event(&self, event: Event) -> () {
|
||||||
|
let event_type_id = event.type_id();
|
||||||
|
info!("Ignoring Discord event of type: {event_type_id:?}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn handle_message(&self, msg: Message) -> Result<()> {
|
||||||
|
if let Some(current_user) = &self.current_user {
|
||||||
|
if msg.author.id == current_user.id {
|
||||||
|
trace!("Ignoring message from self");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if msg.author.bot {
|
||||||
|
info!(
|
||||||
|
"Ignoring bot message in channel {} with content: {}",
|
||||||
|
msg.channel_id, msg.content
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Recieved Discord message in channel {} with content: {}",
|
||||||
|
msg.channel_id, msg.content
|
||||||
|
);
|
||||||
|
|
||||||
|
let text = msg.content.clone();
|
||||||
|
let trimmed_text = text.trim();
|
||||||
|
if trimmed_text.starts_with(COMMAND_PREFIX) {
|
||||||
|
self.handle_text_command(trimmed_text, msg)?
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
fn send_message(&self, channel: ChannelId, text: &str) -> Result<Message> {
|
||||||
|
self.discord
|
||||||
|
.send_message(channel, text, "", false)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
fn handle_text_command(&self, trimmed_text: &str, msg: Message) -> Result<()> {
|
||||||
|
let trimmed_text = trimmed_text.trim_start_matches(COMMAND_PREFIX);
|
||||||
|
match trimmed_text {
|
||||||
|
s if s.starts_with("ping") => {
|
||||||
|
let _ = self.send_message(msg.channel_id, "`pong`")?;
|
||||||
|
}
|
||||||
|
s if s.starts_with("dm-me") => {
|
||||||
|
let dm = self.discord.create_dm(msg.author.id)?;
|
||||||
|
self.send_message(
|
||||||
|
dm.id,
|
||||||
|
format!("DM'ing you as requested via `{}dm-me", COMMAND_PREFIX).as_str(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(bot_token: &str) -> Result<()> {
|
||||||
|
let mut discord = Discord::try_new(bot_token)?;
|
||||||
|
let ready_ev = discord.connect().await;
|
||||||
|
info!("Discord connection ready: {ready_ev:?}");
|
||||||
|
let adiscord = Arc::new(discord);
|
||||||
|
adiscord.handle_commands().await
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "http_client"
|
name = "http_client"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -10,6 +10,6 @@ reqwest-retry = "0.4.0"
|
||||||
reqwest-tracing = "0.4.8"
|
reqwest-tracing = "0.4.8"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
serde_json = "1.0.114"
|
serde_json = "1.0.114"
|
||||||
thiserror = "1.0.63"
|
thiserror = { workspace = true }
|
||||||
tracing = "0.1.40"
|
tracing = { workspace = true }
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
|
|
Loading…
Reference in a new issue