feat: improve transparency of the join command
This commit is contained in:
@@ -1,12 +1,23 @@
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use twilight_model::application::{
|
use snafu::{OptionExt, Snafu};
|
||||||
command::{Command, CommandType},
|
use twilight_model::{
|
||||||
interaction::{Interaction, application_command::CommandData},
|
application::{
|
||||||
|
command::{Command, CommandType},
|
||||||
|
interaction::Interaction,
|
||||||
|
},
|
||||||
|
channel::message::{Embed, MessageFlags},
|
||||||
|
http::interaction::{InteractionResponse, InteractionResponseType},
|
||||||
|
id::{
|
||||||
|
Id,
|
||||||
|
marker::{ChannelMarker, GuildMarker},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use twilight_util::builder::{
|
||||||
|
InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder,
|
||||||
};
|
};
|
||||||
use twilight_util::builder::command::CommandBuilder;
|
|
||||||
|
|
||||||
use crate::command::State;
|
use crate::{VCs, command::State};
|
||||||
|
|
||||||
const NAME: &str = "join";
|
const NAME: &str = "join";
|
||||||
const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)";
|
const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)";
|
||||||
@@ -18,24 +29,103 @@ pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
|
|||||||
.build()
|
.build()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
enum GetGuildAndChannelIdError {
|
||||||
|
/// this command was not used inside a guild (Discord server)
|
||||||
|
NotInGuild,
|
||||||
|
|
||||||
|
/// there is no user who invoked this command
|
||||||
|
NoUser,
|
||||||
|
|
||||||
|
/// there are no voice chats in this guild
|
||||||
|
NoVCsInGuild,
|
||||||
|
|
||||||
|
/// the user is not in a voice chat in this guild
|
||||||
|
UserNotInVC,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetGuildAndChannelIdError> for Embed {
|
||||||
|
fn from(error: GetGuildAndChannelIdError) -> Embed {
|
||||||
|
match error {
|
||||||
|
GetGuildAndChannelIdError::NotInGuild => {
|
||||||
|
EmbedBuilder::new().title("Use this in a server").description("This bot can't find a VC to join if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build()
|
||||||
|
}
|
||||||
|
GetGuildAndChannelIdError::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(),
|
||||||
|
GetGuildAndChannelIdError::NoVCsInGuild => {
|
||||||
|
EmbedBuilder::new().title("No VCs in this server").description("This bot can't find a VC to join because there aren't any in this server right now.").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
GetGuildAndChannelIdError::UserNotInVC => {
|
||||||
|
|
||||||
|
EmbedBuilder::new().title("You're not in a VC").description("This bot can't follow you into VC if you aren't in one in this server.").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
fn get_guild_and_channel_id(
|
||||||
|
interaction: &Interaction,
|
||||||
|
vcs: &VCs,
|
||||||
|
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndChannelIdError> {
|
||||||
|
let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
|
||||||
|
|
||||||
|
let user_id = interaction
|
||||||
|
.member
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|member| member.user.as_ref().map(|user| user.id))
|
||||||
|
.context(NoUserSnafu)?;
|
||||||
|
|
||||||
|
let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?;
|
||||||
|
|
||||||
|
let &channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?;
|
||||||
|
|
||||||
|
Ok((guild_id, channel_id))
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn handle(state: State, interaction: Interaction) {
|
pub async fn handle(state: State, interaction: Interaction) {
|
||||||
let vcs = state.vcs;
|
let vcs = state.vcs;
|
||||||
|
|
||||||
let guild_id = interaction.guild_id.expect("TODO");
|
let (guild_id, channel_id) = match get_guild_and_channel_id(&interaction, &vcs) {
|
||||||
// let user_id = data.user.map(|user| user.id).expect("TODO");
|
Ok((guild_id, channel_id)) => (guild_id, channel_id),
|
||||||
|
Err(error) => {
|
||||||
|
state
|
||||||
|
.discord_client
|
||||||
|
.interaction(state.discord_application_id)
|
||||||
|
.create_response(
|
||||||
|
interaction.id,
|
||||||
|
&interaction.token,
|
||||||
|
&InteractionResponse {
|
||||||
|
kind: InteractionResponseType::ChannelMessageWithSource,
|
||||||
|
data: Some(
|
||||||
|
InteractionResponseDataBuilder::new()
|
||||||
|
.embeds([error.into()])
|
||||||
|
.flags(MessageFlags::EPHEMERAL)
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("TODO");
|
||||||
|
|
||||||
let user_id = interaction
|
return;
|
||||||
.member
|
}
|
||||||
.and_then(|member| member.user.map(|user| user.id))
|
};
|
||||||
|
|
||||||
|
state
|
||||||
|
.discord_client
|
||||||
|
.interaction(state.discord_application_id)
|
||||||
|
.create_response(
|
||||||
|
interaction.id,
|
||||||
|
&interaction.token,
|
||||||
|
&InteractionResponse {
|
||||||
|
kind: InteractionResponseType::DeferredChannelMessageWithSource,
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
.expect("TODO");
|
.expect("TODO");
|
||||||
|
|
||||||
let guild_vcs = vcs.get(&guild_id).expect("TODO");
|
|
||||||
tracing::error!(?guild_vcs, "TODO");
|
|
||||||
|
|
||||||
let (&channel_id, user_in_vc_data) = guild_vcs.get_left_and_data_for(&user_id).expect("TODO");
|
|
||||||
tracing::error!(?user_in_vc_data, "TODO");
|
|
||||||
|
|
||||||
let call = tokio::spawn({
|
let call = tokio::spawn({
|
||||||
let songbird = state.songbird.clone();
|
let songbird = state.songbird.clone();
|
||||||
async move { songbird.join(guild_id, channel_id).await }
|
async move { songbird.join(guild_id, channel_id).await }
|
||||||
@@ -44,5 +134,20 @@ pub async fn handle(state: State, interaction: Interaction) {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.expect("TODO");
|
.expect("TODO");
|
||||||
|
|
||||||
|
let channel_mention = format!("<#{channel_id}>");
|
||||||
|
|
||||||
|
state
|
||||||
|
.discord_client
|
||||||
|
.interaction(state.discord_application_id)
|
||||||
|
.update_response(
|
||||||
|
&interaction.token,
|
||||||
|
).embeds(Some(&[
|
||||||
|
EmbedBuilder::new().title("Joined VC with intent to record").description(&format!("This bot joined {channel_mention} and intends to start recording after a responsible disclosure and consent.")).validate().unwrap().build()
|
||||||
|
]))
|
||||||
|
.await
|
||||||
|
.expect("TODO");
|
||||||
|
|
||||||
tracing::error!(?call, "TODO");
|
tracing::error!(?call, "TODO");
|
||||||
|
|
||||||
|
let call_guard = call.lock().await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +1,93 @@
|
|||||||
use std::{fmt::Debug, sync::Arc};
|
use std::{fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use patricia_tree::StringPatriciaMap;
|
use patricia_tree::StringPatriciaMap;
|
||||||
use songbird::Songbird;
|
use songbird::Songbird;
|
||||||
use twilight_model::application::{command::Command, interaction::Interaction};
|
use twilight_model::{
|
||||||
|
application::{command::Command, interaction::Interaction},
|
||||||
use crate::VCs;
|
id::{Id, marker::ApplicationMarker},
|
||||||
|
};
|
||||||
mod join;
|
|
||||||
mod leave;
|
use crate::VCs;
|
||||||
mod opt_out;
|
|
||||||
|
mod join;
|
||||||
#[derive(Debug, Clone)]
|
mod leave;
|
||||||
pub struct State {
|
mod opt_out;
|
||||||
pub vcs: Arc<VCs>,
|
|
||||||
pub songbird: Arc<Songbird>,
|
#[derive(Debug, Clone)]
|
||||||
}
|
pub struct State {
|
||||||
|
pub discord_client: Arc<twilight_http::Client>,
|
||||||
type Return = ();
|
pub discord_application_id: Id<ApplicationMarker>,
|
||||||
type BoxedHandler = Box<dyn Fn(State, Interaction) -> BoxFuture<'static, Return>>;
|
pub songbird: Arc<Songbird>,
|
||||||
|
pub vcs: Arc<VCs>,
|
||||||
fn box_handler<Handler, Fut>(handler: Handler) -> BoxedHandler
|
}
|
||||||
where
|
|
||||||
Fut: Future<Output = Return> + Send + 'static,
|
type Return = ();
|
||||||
Handler: Fn(State, Interaction) -> Fut + 'static,
|
type BoxedHandler = Box<dyn Fn(State, Interaction) -> BoxFuture<'static, Return>>;
|
||||||
{
|
|
||||||
Box::new(move |state, interaction| Box::pin(handler(state, interaction)))
|
fn box_handler<Handler, Fut>(handler: Handler) -> BoxedHandler
|
||||||
}
|
where
|
||||||
|
Fut: Future<Output = Return> + Send + 'static,
|
||||||
pub fn all() -> Vec<(&'static Command, BoxedHandler)> {
|
Handler: Fn(State, Interaction) -> Fut + 'static,
|
||||||
vec![
|
{
|
||||||
(&join::COMMAND, box_handler(join::handle)),
|
Box::new(move |state, interaction| Box::pin(handler(state, interaction)))
|
||||||
(&leave::COMMAND, box_handler(leave::handle)),
|
}
|
||||||
(&opt_out::COMMAND, box_handler(opt_out::handle)),
|
|
||||||
]
|
pub fn all() -> Vec<(&'static Command, BoxedHandler)> {
|
||||||
}
|
vec![
|
||||||
|
(&join::COMMAND, box_handler(join::handle)),
|
||||||
#[derive(Default)]
|
(&leave::COMMAND, box_handler(leave::handle)),
|
||||||
pub struct Router {
|
(&opt_out::COMMAND, box_handler(opt_out::handle)),
|
||||||
map: StringPatriciaMap<BoxedHandler>,
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Router {
|
#[derive(Default)]
|
||||||
fn add_route<'s, 'a, Fut, Handler>(&'s mut self, name: &'a str, handler: Handler)
|
pub struct Router {
|
||||||
where
|
map: StringPatriciaMap<BoxedHandler>,
|
||||||
Fut: Future<Output = Return> + Send + 'static,
|
}
|
||||||
Handler: Fn(State, Interaction) -> Fut + 'static,
|
|
||||||
{
|
impl Router {
|
||||||
self.add_route_already_boxed(name, box_handler(handler));
|
fn add_route<'s, 'a, Fut, Handler>(&'s mut self, name: &'a str, handler: Handler)
|
||||||
}
|
where
|
||||||
|
Fut: Future<Output = Return> + Send + 'static,
|
||||||
fn add_route_already_boxed<'s, 'a>(&'s mut self, name: &'a str, boxed_handler: BoxedHandler) {
|
Handler: Fn(State, Interaction) -> Fut + 'static,
|
||||||
self.map.insert(name, boxed_handler);
|
{
|
||||||
}
|
self.add_route_already_boxed(name, box_handler(handler));
|
||||||
|
}
|
||||||
pub async fn handle(
|
|
||||||
&self,
|
fn add_route_already_boxed<'s, 'a>(&'s mut self, name: &'a str, boxed_handler: BoxedHandler) {
|
||||||
state: State,
|
self.map.insert(name, boxed_handler);
|
||||||
command_name: &str,
|
}
|
||||||
interaction: Interaction,
|
|
||||||
) -> Option<Return> {
|
pub async fn handle(
|
||||||
let handler = self.map.get(command_name)?;
|
&self,
|
||||||
|
state: State,
|
||||||
Some(handler(state, interaction).await)
|
command_name: &str,
|
||||||
}
|
interaction: Interaction,
|
||||||
}
|
) -> Option<Return> {
|
||||||
|
let handler = self.map.get(command_name)?;
|
||||||
impl<'a> FromIterator<(&'a Command, BoxedHandler)> for Router {
|
|
||||||
#[inline]
|
Some(handler(state, interaction).await)
|
||||||
fn from_iter<T: IntoIterator<Item = (&'a Command, BoxedHandler)>>(iter: T) -> Self {
|
}
|
||||||
let mut this = Self::default();
|
}
|
||||||
|
|
||||||
this.extend(iter);
|
impl<'a> FromIterator<(&'a Command, BoxedHandler)> for Router {
|
||||||
|
#[inline]
|
||||||
this
|
fn from_iter<T: IntoIterator<Item = (&'a Command, BoxedHandler)>>(iter: T) -> Self {
|
||||||
}
|
let mut this = Self::default();
|
||||||
}
|
|
||||||
|
this.extend(iter);
|
||||||
impl<'a> Extend<(&'a Command, BoxedHandler)> for Router {
|
|
||||||
#[inline]
|
this
|
||||||
fn extend<T: IntoIterator<Item = (&'a Command, BoxedHandler)>>(&mut self, iter: T) {
|
}
|
||||||
for (command, boxed_handler) in iter {
|
}
|
||||||
let name = &command.name;
|
|
||||||
self.add_route_already_boxed(name, boxed_handler);
|
impl<'a> Extend<(&'a Command, BoxedHandler)> for Router {
|
||||||
}
|
#[inline]
|
||||||
}
|
fn extend<T: IntoIterator<Item = (&'a Command, BoxedHandler)>>(&mut self, iter: T) {
|
||||||
}
|
for (command, boxed_handler) in iter {
|
||||||
|
let name = &command.name;
|
||||||
|
self.add_route_already_boxed(name, boxed_handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
16
src/main.rs
16
src/main.rs
@@ -132,7 +132,7 @@ async fn main() -> Result<(), MainError> {
|
|||||||
.await
|
.await
|
||||||
.expect("couldn't get current Discord application"); // TODO
|
.expect("couldn't get current Discord application"); // TODO
|
||||||
|
|
||||||
let application_id = current_application.id;
|
let discord_application_id = current_application.id;
|
||||||
|
|
||||||
let shard_id = ShardId::new(0, 1);
|
let shard_id = ShardId::new(0, 1);
|
||||||
let intents = Intents::GUILD_VOICE_STATES;
|
let intents = Intents::GUILD_VOICE_STATES;
|
||||||
@@ -150,7 +150,7 @@ async fn main() -> Result<(), MainError> {
|
|||||||
let event_types = EventTypeFlags::GUILD_VOICE_STATES | EventTypeFlags::INTERACTION_CREATE;
|
let event_types = EventTypeFlags::GUILD_VOICE_STATES | EventTypeFlags::INTERACTION_CREATE;
|
||||||
let mut next_event = shard.next_event(event_types);
|
let mut next_event = shard.next_event(event_types);
|
||||||
|
|
||||||
let interaction_client = discord_client.interaction(application_id);
|
let interaction_client = discord_client.interaction(discord_application_id);
|
||||||
|
|
||||||
let commands = all_commands();
|
let commands = all_commands();
|
||||||
|
|
||||||
@@ -172,11 +172,17 @@ async fn main() -> Result<(), MainError> {
|
|||||||
let command_router = CommandRouter::from_iter(commands);
|
let command_router = CommandRouter::from_iter(commands);
|
||||||
|
|
||||||
let vcs = initialize_vcs(&discord_client).await;
|
let vcs = initialize_vcs(&discord_client).await;
|
||||||
|
|
||||||
|
let discord_client = Arc::new(discord_client);
|
||||||
|
let songbird = Arc::new(songbird);
|
||||||
let vcs = Arc::new(vcs);
|
let vcs = Arc::new(vcs);
|
||||||
|
|
||||||
let songbird = Arc::new(songbird);
|
let state = State {
|
||||||
|
discord_application_id,
|
||||||
let state = State { vcs, songbird };
|
discord_client,
|
||||||
|
songbird,
|
||||||
|
vcs,
|
||||||
|
};
|
||||||
|
|
||||||
while let Some(event_res) = next_event.await {
|
while let Some(event_res) = next_event.await {
|
||||||
match event_res {
|
match event_res {
|
||||||
|
|||||||
Reference in New Issue
Block a user