feat: early steps of storage and configuration
This commit is contained in:
178
src/command/debug.rs
Normal file
178
src/command/debug.rs
Normal 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");
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
24
src/command/opt_in.rs
Normal 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!();
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
49
src/main.rs
49
src/main.rs
@@ -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
39
src/storage.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user