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",
|
||||
]
|
||||
|
||||
[[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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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 {
|
||||
|
|
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]
|
||||
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"
|
||||
|
|
Loading…
Reference in a new issue