Breakin' out modules

This commit is contained in:
Daniel Flanagan 2024-08-05 19:52:50 -05:00
parent 851c68267e
commit 7cd74f95a2
8 changed files with 311 additions and 15 deletions

14
Cargo.lock generated
View file

@ -464,6 +464,16 @@ dependencies = [
"websocket",
]
[[package]]
name = "discord_bot"
version = "1.0.0"
dependencies = [
"discord",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
@ -830,7 +840,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"reqwest 0.11.27",
"reqwest-middleware 0.2.5",
@ -3228,7 +3238,7 @@ dependencies = [
"axum",
"color-eyre",
"config",
"discord",
"discord_bot",
"http_client",
"redact",
"reqwest 0.12.5",

View file

@ -3,7 +3,11 @@ resolver = "2"
members = ["apps/yourcloud"]
[workspace.dependencies]
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
http_client = { path = "libs/http_client" }
discord_bot = { path = "libs/discord_bot" }
thiserror = "1.0.63"
tracing = "0.1.40"
[profile.dev.package.backtrace]
opt-level = 3

View file

@ -7,7 +7,6 @@ edition = "2021"
axum = "0.7.5"
color-eyre = "0.6.3"
config = "0.14.0"
discord = { git = "https://github.com/SpaceManiac/discord-rs" }
redact = { version = "0.1.9", features = ["serde"] }
reqwest = { version = "0.12.3", features = ["json", "socks"] }
reqwest-middleware = "0.3.0"
@ -16,11 +15,12 @@ reqwest-tracing = "0.5.0"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
serde_with = "3.7.0"
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
tokio = { workspace = true }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
urlencoding = "2.1.3"
http_client = { workspace = true }
discord_bot = { workspace = true }
[profile.dev]
opt-level = 1

View file

@ -1,11 +1,10 @@
// TODO: family reminders?
// TODO: connect to Discord
// TODO: handle messages
// TODO: data persistence? (sqlite? sled?)
use std::{any::Any, future};
// TODO: data persistence? (sqlite? sled? postgres?)
use crate::prelude::*;
use discord::model::{ChannelId, Event, Message, ReadyEvent};
use std::{any::Any, future};
use tokio::sync::Mutex;
const COMMAND_PREFIX: &str = "!";
@ -249,8 +248,6 @@ impl Discord {
}
}
use crate::prelude::*;
pub async fn start(conf: Arc<Config>) -> Result<()> {
if conf.discord.is_none() {
warn!("Chatbot starting without Discord token. Nothing will happen.");

View file

@ -1,6 +1,5 @@
#![forbid(unsafe_code)]
mod chatbot;
mod config;
mod minecraft_server_status;
mod observe;
@ -24,7 +23,15 @@ async fn main() -> Result<()> {
let mut set = tokio::task::JoinSet::new();
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;
match result {

View 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
View 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
}

View file

@ -1,6 +1,6 @@
[package]
name = "http_client"
version = "0.1.0"
version = "1.0.0"
edition = "2021"
[dependencies]
@ -10,6 +10,6 @@ reqwest-retry = "0.4.0"
reqwest-tracing = "0.4.8"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
thiserror = "1.0.63"
tracing = "0.1.40"
thiserror = { workspace = true }
tracing = { workspace = true }
url = "2.5.2"