feat: early steps of storage and configuration

This commit is contained in:
2026-04-09 22:39:02 -04:00
parent 7d3a309d2b
commit 7885526944
14 changed files with 393 additions and 54 deletions

178
src/command/debug.rs Normal file
View File

@@ -0,0 +1,178 @@
use std::sync::LazyLock;
use async_compression::futures::bufread::BrotliDecoder;
use capnp::message::ReaderOptions;
use futures::AsyncReadExt;
use opendal::ErrorKind;
use snafu::{OptionExt, Snafu};
use twilight_model::{
application::{
command::{Command, CommandType},
interaction::Interaction,
},
channel::message::{Embed, MessageFlags},
http::interaction::{InteractionResponse, InteractionResponseType},
id::{Id, marker::UserMarker},
};
use twilight_util::builder::{
InteractionResponseDataBuilder,
command::CommandBuilder,
embed::{EmbedBuilder, EmbedFieldBuilder},
};
use crate::{bot_capnp, command::State};
const NAME: &str = "debug";
const DESCRIPTION: &str =
"(Only the bot owner can use this) Show various information for debugging purposes";
pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput)
.validate()
.expect("command wasn't correct")
.build()
});
#[derive(Debug, Snafu)]
enum NoPermission {
/// there isn't a user who invoked this command
NoUser,
/// the user isn't allowed to use this command because they're not the bot owner
NotInvokedByBotOwner,
}
fn no_permission_to_embed(error: NoPermission) -> Embed {
match error {
NoPermission::NoUser => {
EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build()
},
NoPermission::NotInvokedByBotOwner => {
EmbedBuilder::new().title("No permission to see debug info").description("Only the owner of this bot is allowed to see its information for debugging purposes.").validate().unwrap().build()
},
}
}
fn check_permission(
interaction: &Interaction,
bot_owner_user_id: Id<UserMarker>,
) -> Result<(), NoPermission> {
let user_id = interaction
.member
.as_ref()
.and_then(|member| member.user.as_ref().map(|user| user.id))
.context(NoUserSnafu)?;
if user_id != bot_owner_user_id {
return Err(NoPermission::NotInvokedByBotOwner);
}
Ok(())
}
#[tracing::instrument]
pub async fn handle(state: State, interaction: Interaction) {
if let Err(no_permission) = check_permission(&interaction, state.discord_bot_owner_user_id) {
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.embeds([no_permission_to_embed(no_permission)])
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},
)
.await
.expect("TODO");
return;
}
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.flags(MessageFlags::EPHEMERAL)
.content("some debug info is coming your way!")
.build(),
),
},
)
.await
.expect("TODO");
let heat_script_description = {
let compressed_result = state
.bot_data
.reader("data.bin.brotli")
.await
.expect("TODO")
.into_futures_async_read(..)
.await;
let mut buf = Vec::default();
let mut message = capnp::message::TypedBuilder::<bot_capnp::bot::Owned>::new_default();
let fallback = message.init_root();
let message_reader;
let mut bot_data = fallback.into_reader();
match compressed_result {
Ok(compressed) => {
let mut decompressed = BrotliDecoder::new(compressed);
decompressed.read_to_end(&mut buf).await.expect("TODO");
message_reader =
capnp::serialize_packed::read_message(&*buf, ReaderOptions::default())
.expect("TODO");
bot_data = message_reader
.get_root::<bot_capnp::bot::Reader>()
.expect("TODO");
}
Err(error) if error.kind() == ErrorKind::NotFound => {
tracing::error!("TODO: proceeding with fallback");
}
Err(error) => {
tracing::error!(?error, "TODO");
return;
}
}
let heat_script_option = bot_data
.has_heat_script()
.then(|| bot_data.get_heat_script().expect("TODO"));
let heat_script_option =
heat_script_option.map(|heat_script| heat_script.to_str().expect("TODO"));
heat_script_option.map_or("none set yet".into(), |heat_script| {
format!("```\n{heat_script}\n```")
})
};
state
.discord_client
.interaction(state.discord_application_id)
.create_followup(&interaction.token)
.embeds(&[EmbedBuilder::new()
.field(EmbedFieldBuilder::new("Heat Script", heat_script_description).build())
.validate()
.unwrap()
.build()])
.flags(MessageFlags::EPHEMERAL)
.await
.expect("TODO");
}

View File

@@ -1,6 +1,9 @@
use crate::{VCs, command::State};
use async_trait::async_trait;
use snafu::{OptionExt, Snafu};
use std::sync::LazyLock;
use songbird::{CoreEvent, Event, EventContext, EventHandler};
use time::UtcDateTime;
use std::{sync::LazyLock, time::Instant};
use twilight_model::{
application::{
command::{Command, CommandType},
@@ -79,6 +82,45 @@ fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Emb
}
}
#[derive(Debug, Clone)]
struct Handler {
start_instant: Instant,
start_utc: UtcDateTime,
}
#[async_trait]
impl EventHandler for Handler {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
tracing::error!(?ctx, "TODO");
let Some(core_event) = ctx.to_core_event() else {
return None;
};
tracing::error!(?core_event, "TODO");
let elapsed = self.start_instant.elapsed();
let elapsed = elapsed.try_into().expect("TODO");
let now_utc = self.start_utc.checked_add(elapsed).expect("TODO");
tracing::error!(?now_utc, "TODO");
match core_event {
CoreEvent::SpeakingStateUpdate => todo!(),
CoreEvent::VoiceTick => todo!(),
CoreEvent::RtpPacket => todo!(),
CoreEvent::RtcpPacket => todo!(),
CoreEvent::ClientDisconnect => todo!(),
CoreEvent::DriverConnect => todo!(),
CoreEvent::DriverReconnect => todo!(),
CoreEvent::DriverDisconnect => todo!(),
_ => todo!(),
}
None
}
}
#[tracing::instrument(skip(state))]
pub async fn handle(state: State, interaction: Interaction) {
let vcs = state.vcs;
@@ -131,6 +173,15 @@ pub async fn handle(state: State, interaction: Interaction) {
tracing::error!(?call, "successfully joined");
let start_instant = Instant::now();
let start_utc = UtcDateTime::now();
let handler = Handler { start_instant, start_utc };
call.lock().await.add_global_event(
CoreEvent::RtpPacket.into(),
handler,
);
let channel_mention = format!("<#{voice_channel_id}>");
state

View File

@@ -128,7 +128,7 @@ pub async fn handle(state: State, interaction: Interaction) {
data: Some(
InteractionResponseDataBuilder::new()
.embeds([
EmbedBuilder::new().title("No permission to make this bot leave").description("Only the owner of the bot is allowed to make the bot leave RIGHT NOW. You might be looking for the opt out command.").validate().unwrap().build()
EmbedBuilder::new().title("No permission to make this bot leave").description("Only the owner of this bot is allowed to make the bot leave RIGHT NOW. You might be looking for the opt out command.").validate().unwrap().build()
])
.flags(MessageFlags::EPHEMERAL)
.build(),

View File

@@ -1,6 +1,7 @@
use std::{fmt::Debug, sync::Arc};
use futures::future::BoxFuture;
use opendal::Operator;
use patricia_tree::StringPatriciaMap;
use songbird::Songbird;
use tokio_util::sync::CancellationToken;
@@ -14,18 +15,23 @@ use twilight_model::{
use crate::VCs;
mod debug;
mod join;
mod leave;
mod opt_in;
mod opt_out;
#[derive(Debug, Clone)]
pub struct State {
pub bot_data: Operator,
pub cancellation_token: CancellationToken,
pub discord_application_id: Id<ApplicationMarker>,
pub discord_bot_owner_user_id: Id<UserMarker>,
pub discord_client: Arc<twilight_http::Client>,
pub discord_user_id: Id<UserMarker>,
pub recording_data: Operator,
pub songbird: Arc<Songbird>,
pub user_data: Operator,
pub vcs: Arc<VCs>,
}
@@ -42,8 +48,10 @@ where
pub fn all() -> Vec<(&'static Command, BoxedHandler)> {
vec![
(&debug::COMMAND, box_handler(debug::handle)),
(&join::COMMAND, box_handler(join::handle)),
(&leave::COMMAND, box_handler(leave::handle)),
(&opt_in::COMMAND, box_handler(opt_in::handle)),
(&opt_out::COMMAND, box_handler(opt_out::handle)),
]
}
@@ -54,7 +62,7 @@ pub struct Router {
}
impl Router {
fn add_route<Fut, Handler>(&mut self, name: &str, handler: Handler)
pub fn add_route<Fut, Handler>(&mut self, name: &str, handler: Handler)
where
Fut: Future<Output = Return> + Send + 'static,
Handler: Send + Sync + Fn(State, Interaction) -> Fut + 'static,

24
src/command/opt_in.rs Normal file
View File

@@ -0,0 +1,24 @@
use std::sync::LazyLock;
use twilight_model::application::{
command::{Command, CommandType},
interaction::Interaction,
};
use twilight_util::builder::command::CommandBuilder;
use crate::command::State;
const NAME: &str = "opt-in";
const DESCRIPTION: &str = "Opt in to being recorded";
pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput)
.validate()
.expect("command wasn't correct")
.build()
});
#[tracing::instrument]
pub async fn handle(state: State, interaction: Interaction) {
todo!();
}

View File

@@ -9,7 +9,7 @@ use twilight_util::builder::command::CommandBuilder;
use crate::command::State;
const NAME: &str = "opt-out";
const DESCRIPTION: &str = "Opt out of being recorded (duration option TODO)";
const DESCRIPTION: &str = "Opt out of being recorded";
pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput)

View File

@@ -2,13 +2,17 @@ mod command;
mod one_to_many;
mod one_to_many_with_data;
mod one_to_one;
mod storage;
mod track_vcs;
mod vc_user;
pub use command::{Router as CommandRouter, State, all as all_commands};
pub use one_to_many::OneToManyUniqueBTreeMap;
pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData;
pub use one_to_one::OneToOneBTreeMap;
pub use command::{Router as CommandRouter, State, all as all_commands};
pub use storage::Storage;
pub use track_vcs::{VCs, initialize_vcs, update_vcs};
pub use vc_user::{UserInVCData, VoiceStatus};
capnp::generated_code!(pub mod user_capnp);
capnp::generated_code!(pub mod bot_capnp);

View File

@@ -1,55 +1,19 @@
use clap::Parser;
use fomo_reducer::{CommandRouter, State, all_commands, initialize_vcs, update_vcs};
use opendal::{IntoOperatorUri, Operator, OperatorUri};
use fomo_reducer::{CommandRouter, State, Storage, all_commands, initialize_vcs, update_vcs};
use secrecy::{ExposeSecret, SecretString};
use snafu::Snafu;
use songbird::{Songbird, shards::TwilightMap};
use std::{fmt::Debug, str::FromStr, sync::Arc};
use std::{fmt::Debug, sync::Arc};
use tokio::{select, signal::ctrl_c, task::JoinSet};
use tokio_util::{sync::CancellationToken, time::FutureExt as _};
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, StreamExt};
use twilight_model::{
application::interaction::InteractionData,
gateway::payload::incoming::InteractionCreate,
id::{Id, marker::UserMarker},
};
#[derive(Clone)]
struct Storage {
uri: OperatorUri,
operator: Operator,
}
impl FromStr for Storage {
type Err = opendal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let uri = s.into_operator_uri()?;
let operator = Operator::from_uri(&uri)?;
Ok(Self { uri, operator })
}
}
impl Storage {
fn into_inner(self) -> Operator {
self.operator
}
}
impl From<Storage> for Operator {
fn from(wrapper: Storage) -> Self {
wrapper.into_inner()
}
}
impl Debug for Storage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.uri, f)
}
}
#[derive(Debug, Parser)]
struct AppArgs {
#[arg(long, env)]
@@ -198,13 +162,20 @@ async fn main() -> Result<(), MainError> {
let songbird = Arc::new(songbird);
let vcs = Arc::new(vcs);
let bot_data = bot_data.into_inner();
let recording_data = recording_data.into_inner();
let user_data = user_data.into_inner();
let state = State {
bot_data,
cancellation_token: cancellation_token.clone(),
discord_application_id,
discord_bot_owner_user_id: bot_owner,
discord_client,
discord_user_id,
recording_data,
songbird,
user_data,
vcs,
};

39
src/storage.rs Normal file
View File

@@ -0,0 +1,39 @@
use std::{fmt::Debug, str::FromStr};
use opendal::{IntoOperatorUri, Operator, OperatorUri};
#[derive(Clone)]
pub struct Storage {
uri: OperatorUri,
operator: Operator,
}
impl FromStr for Storage {
type Err = opendal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let uri = s.into_operator_uri()?;
let operator = Operator::from_uri(&uri)?;
Ok(Self { uri, operator })
}
}
impl Storage {
pub fn into_inner(self) -> Operator {
self.operator
}
}
impl From<Storage> for Operator {
fn from(wrapper: Storage) -> Self {
wrapper.into_inner()
}
}
impl Debug for Storage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.uri, f)
}
}