Compare commits

...

2 Commits

Author SHA1 Message Date
dfda319ab4 fix: listen for voice events 2026-04-10 00:53:23 -04:00
7885526944 feat: early steps of storage and configuration 2026-04-09 22:39:02 -04:00
14 changed files with 410 additions and 58 deletions

60
Cargo.lock generated
View File

@@ -199,6 +199,18 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "async-compression"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
dependencies = [
"compression-codecs",
"compression-core",
"futures-io",
"pin-project-lite",
]
[[package]] [[package]]
name = "async-executor" name = "async-executor"
version = "1.14.0" version = "1.14.0"
@@ -479,6 +491,17 @@ dependencies = [
"piper", "piper",
] ]
[[package]]
name = "brotli"
version = "8.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]] [[package]]
name = "brotli-decompressor" name = "brotli-decompressor"
version = "5.0.0" version = "5.0.0"
@@ -768,6 +791,22 @@ dependencies = [
"tokio-util", "tokio-util",
] ]
[[package]]
name = "compression-codecs"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
dependencies = [
"brotli",
"compression-core",
]
[[package]]
name = "compression-core"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -1652,6 +1691,8 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
name = "fomo-reducer" name = "fomo-reducer"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-compression",
"async-trait",
"capnp", "capnp",
"capnpc", "capnpc",
"clap", "clap",
@@ -1664,6 +1705,7 @@ dependencies = [
"secrecy 0.10.3", "secrecy 0.10.3",
"snafu", "snafu",
"songbird", "songbird",
"time",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
@@ -3487,9 +3529,9 @@ dependencies = [
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]] [[package]]
name = "num-derive" name = "num-derive"
@@ -6563,30 +6605,30 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.44" version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
"num-conv", "num-conv",
"powerfmt", "powerfmt",
"serde", "serde_core",
"time-core", "time-core",
"time-macros", "time-macros",
] ]
[[package]] [[package]]
name = "time-core" name = "time-core"
version = "0.1.6" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.24" version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",

View File

@@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
async-compression = { version = "0.4.41", features = ["brotli", "futures-io"] }
async-trait = "0.1.89"
capnp = "0.25.3" capnp = "0.25.3"
clap = { version = "4.5.40", features = ["derive", "env"] } clap = { version = "4.5.40", features = ["derive", "env"] }
dashmap = "6.1.0" dashmap = "6.1.0"
@@ -58,6 +60,7 @@ songbird = { version = "0.6.0", default-features = false, features = [
"twilight", "twilight",
"tws", "tws",
] } ] }
time = "0.3.47"
tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal"] } tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal"] }
tokio-util = "0.7.18" tokio-util = "0.7.18"
tracing = "0.1.41" tracing = "0.1.41"

5
bot.capnp Normal file
View File

@@ -0,0 +1,5 @@
@0x993a671d7aa374f3;
struct Bot {
heatScript @0 :Text;
}

7
build.rs Normal file
View File

@@ -0,0 +1,7 @@
fn main() {
capnpc::CompilerCommand::new()
.file("bot.capnp")
.file("user.capnp")
.run()
.expect("couldn't compile capnproto schemas");
}

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 crate::{VCs, command::State};
use async_trait::async_trait;
use snafu::{OptionExt, Snafu}; 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::{ use twilight_model::{
application::{ application::{
command::{Command, CommandType}, 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))] #[tracing::instrument(skip(state))]
pub async fn handle(state: State, interaction: Interaction) { pub async fn handle(state: State, interaction: Interaction) {
let vcs = state.vcs; let vcs = state.vcs;
@@ -131,6 +173,15 @@ pub async fn handle(state: State, interaction: Interaction) {
tracing::error!(?call, "successfully joined"); 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}>"); let channel_mention = format!("<#{voice_channel_id}>");
state state

View File

@@ -128,7 +128,7 @@ pub async fn handle(state: State, interaction: Interaction) {
data: Some( data: Some(
InteractionResponseDataBuilder::new() InteractionResponseDataBuilder::new()
.embeds([ .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) .flags(MessageFlags::EPHEMERAL)
.build(), .build(),
@@ -150,15 +150,25 @@ pub async fn handle(state: State, interaction: Interaction) {
state state
.discord_client .discord_client
.interaction(state.discord_application_id) .interaction(state.discord_application_id)
.update_response(&interaction.token) .create_response(interaction.id, &interaction.token,
.embeds(Some(&[EmbedBuilder::new() &InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.embeds([
EmbedBuilder::new()
.title("Left VC") .title("Left VC")
.description(format!( .description(format!(
"This bot left {channel_mention} (and is thereby unable to record anymore)." "This bot left {channel_mention} (and is thereby unable to record anymore)."
)) ))
.validate() .validate()
.unwrap() .unwrap()
.build()])) .build()
])
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},)
.await .await
.expect("TODO"); .expect("TODO");
} }

View File

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

View File

@@ -2,13 +2,17 @@ mod command;
mod one_to_many; mod one_to_many;
mod one_to_many_with_data; mod one_to_many_with_data;
mod one_to_one; mod one_to_one;
mod storage;
mod track_vcs; mod track_vcs;
mod vc_user; 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::OneToManyUniqueBTreeMap;
pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData; pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData;
pub use one_to_one::OneToOneBTreeMap; pub use one_to_one::OneToOneBTreeMap;
pub use storage::Storage;
pub use command::{Router as CommandRouter, State, all as all_commands};
pub use track_vcs::{VCs, initialize_vcs, update_vcs}; pub use track_vcs::{VCs, initialize_vcs, update_vcs};
pub use vc_user::{UserInVCData, VoiceStatus}; 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 clap::Parser;
use fomo_reducer::{CommandRouter, State, all_commands, initialize_vcs, update_vcs}; use fomo_reducer::{CommandRouter, State, Storage, all_commands, initialize_vcs, update_vcs};
use opendal::{IntoOperatorUri, Operator, OperatorUri};
use secrecy::{ExposeSecret, SecretString}; use secrecy::{ExposeSecret, SecretString};
use snafu::Snafu; use snafu::Snafu;
use songbird::{Songbird, shards::TwilightMap}; 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::{select, signal::ctrl_c, task::JoinSet};
use tokio_util::{sync::CancellationToken, time::FutureExt as _}; use tokio_util::{sync::CancellationToken, time::FutureExt as _};
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan}; 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::{ use twilight_model::{
application::interaction::InteractionData, application::interaction::InteractionData,
gateway::payload::incoming::InteractionCreate, gateway::payload::incoming::InteractionCreate,
id::{Id, marker::UserMarker}, 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)] #[derive(Debug, Parser)]
struct AppArgs { struct AppArgs {
#[arg(long, env)] #[arg(long, env)]
@@ -198,13 +162,20 @@ async fn main() -> Result<(), MainError> {
let songbird = Arc::new(songbird); let songbird = Arc::new(songbird);
let vcs = Arc::new(vcs); 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 { let state = State {
bot_data,
cancellation_token: cancellation_token.clone(), cancellation_token: cancellation_token.clone(),
discord_application_id, discord_application_id,
discord_bot_owner_user_id: bot_owner, discord_bot_owner_user_id: bot_owner,
discord_client, discord_client,
discord_user_id, discord_user_id,
recording_data,
songbird, songbird,
user_data,
vcs, vcs,
}; };
@@ -241,7 +212,10 @@ async fn main() -> Result<(), MainError> {
#[tracing::instrument(skip(command_router, state))] #[tracing::instrument(skip(command_router, state))]
async fn handle_events(command_router: Arc<CommandRouter>, state: State, mut shard: Shard) { async fn handle_events(command_router: Arc<CommandRouter>, state: State, mut shard: Shard) {
let event_types = EventTypeFlags::GUILD_VOICE_STATES | EventTypeFlags::INTERACTION_CREATE; let event_types = EventTypeFlags::GUILD_VOICE_STATES
| EventTypeFlags::INTERACTION_CREATE
| EventTypeFlags::VOICE_SERVER_UPDATE
| EventTypeFlags::VOICE_STATE_UPDATE;
while let Some(Some(event_res)) = shard while let Some(Some(event_res)) = shard
.next_event(event_types) .next_event(event_types)

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

7
user.capnp Normal file
View File

@@ -0,0 +1,7 @@
@0xc3e8b8ea9947b0c5;
struct User {
notificationScript @0 :Text;
voiceRecordingConsent @1 :Bool;
}