Add basic moderator commands

This commit is contained in:
2025-08-23 00:02:10 -03:00
parent 8868b18be3
commit eaa38f3474
27 changed files with 752 additions and 249 deletions
+18 -6
View File
@@ -5,7 +5,7 @@ use std::{
use thiserror::Error;
use crate::framework::commands::context::CommandContext;
use crate::{database::models::DatabaseError, framework::commands::context::CommandContext};
#[derive(Debug, Error)]
pub enum ArgumentParsingError {
@@ -16,7 +16,7 @@ pub enum ArgumentParsingError {
InvalidArgument,
#[error("A database operation failed")]
DatabaseFailed,
DatabaseFailed(#[from] DatabaseError),
}
pub trait FromArguments<S, E>: Sized {
@@ -218,7 +218,10 @@ impl<T, S, E> ParseArguments<S, E> for Arg<T>
where
T: FromArguments<S, E>,
{
async fn parse(args: &str, ctx: &CommandContext<'_, S, E>) -> Result<Self, ArgumentParsingError> {
async fn parse(
args: &str,
ctx: &CommandContext<'_, S, E>,
) -> Result<Self, ArgumentParsingError> {
let (value, _) = T::from_args(args, ctx).await?;
Ok(Arg(value))
@@ -229,7 +232,10 @@ impl<A, S, E> ParseArguments<S, E> for (A,)
where
A: FromArguments<S, E>,
{
async fn parse(args: &str, ctx: &CommandContext<'_, S, E>) -> Result<Self, ArgumentParsingError> {
async fn parse(
args: &str,
ctx: &CommandContext<'_, S, E>,
) -> Result<Self, ArgumentParsingError> {
let (a, _) = A::from_args(args, ctx).await?;
Ok((a,))
@@ -241,7 +247,10 @@ where
A: FromArguments<S, E>,
B: FromArguments<S, E>,
{
async fn parse(args: &str, ctx: &CommandContext<'_, S, E>) -> Result<Self, ArgumentParsingError> {
async fn parse(
args: &str,
ctx: &CommandContext<'_, S, E>,
) -> Result<Self, ArgumentParsingError> {
let (a, args) = A::from_args(args, ctx).await?;
let (b, _) = B::from_args(args, ctx).await?;
@@ -255,7 +264,10 @@ where
B: FromArguments<S, E>,
C: FromArguments<S, E>,
{
async fn parse(args: &str, ctx: &CommandContext<'_, S, E>) -> Result<Self, ArgumentParsingError> {
async fn parse(
args: &str,
ctx: &CommandContext<'_, S, E>,
) -> Result<Self, ArgumentParsingError> {
let (a, args) = A::from_args(args, ctx).await?;
let (b, args) = B::from_args(args, ctx).await?;
let (c, _) = C::from_args(args, ctx).await?;
+64 -2
View File
@@ -1,8 +1,15 @@
use std::sync::Arc;
use twilight_model::gateway::payload::incoming::MessageCreate;
use twilight_http::{Error, Response};
use twilight_model::{
channel::{Message, message::Component},
gateway::payload::incoming::MessageCreate,
};
use crate::framework::{commands::messages::MessageSender, runtime::Runtime};
use crate::framework::{
commands::messages::MessageSender, interactions::components::ActionRow,
messages::MessageBuilder, runtime::Runtime,
};
pub struct CommandContext<'a, T, E> {
pub event: &'a MessageCreate,
@@ -33,4 +40,59 @@ where
pub fn send(&self) -> MessageSender<T, E> {
MessageSender::new(self.event.channel_id, self.rt.clone())
}
pub async fn send_from_builder(
&self,
builder: MessageBuilder<T, E>,
) -> Result<Response<Message>, Error> {
let mut sender = self.rt.http.create_message(self.event.channel_id);
if let Some(content) = &builder.content {
sender = sender.content(content);
}
let mut current_row = ActionRow::new(&[]);
let mut current_index = 0;
let mut rows: Vec<Component> = vec![];
let mut custom_ids: Vec<String> = vec![];
for (component, index) in builder.components {
if index != current_index {
if !current_row.is_empty() {
rows.push(current_row.build());
current_row = ActionRow::new(&[]);
}
current_index += 1;
}
let custom_id = component.custom_id();
if let Some(custom_id) = &custom_id {
custom_ids.push(custom_id.clone());
self.rt.registry.register(custom_id, component.clone());
}
current_row.add(custom_id.clone(), component.clone());
}
if !current_row.is_empty() {
rows.push(current_row.build());
}
sender = sender.components(&rows);
sender = sender.embeds(&builder.embeds);
let result = sender.await;
if result.is_err() {
for custom_id in custom_ids {
self.rt.registry.unregister(custom_id);
}
}
result
}
}
+6 -6
View File
@@ -5,9 +5,9 @@ use deadpool_redis::{cluster::PoolError, redis::RedisError};
use thiserror::Error;
use twilight_http::response::DeserializeBodyError;
use crate::framework::{commands::{
use crate::{database::models::{DatabaseError, DatabaseOrDiscordError}, framework::{commands::{
arguments::Argument, context::CommandContext, permissions::Permission,
}, runtime::Runtime};
}, runtime::Runtime}};
pub mod arguments;
pub mod context;
@@ -18,11 +18,8 @@ pub mod runner;
#[derive(Debug, Error)]
pub enum CommandError {
#[error("A message was attempted to be sent but it failed")]
MessageSendingFailed,
#[error("A database query failed")]
DatabaseFailed,
DatabaseFailed(#[from] DatabaseError),
#[error("A database operation failed due to an inconsistent state")]
DatabaseInconsistentState,
@@ -41,6 +38,9 @@ pub enum CommandError {
#[error(transparent)]
RedisFailed(#[from] RedisError),
#[error(transparent)]
DatabaseOrDiscordError(#[from] DatabaseOrDiscordError)
}
#[async_trait]
+10 -3
View File
@@ -64,7 +64,7 @@ where
fn build(&self, id: Option<String>) -> TwilightComponent {
TwilightComponent::Button(TwilightButton {
custom_id: id,
disabled: false,
disabled: self.disabled(),
emoji: None,
label: self.label().map(|i| i.to_string()),
style: if self.url().is_none() {
@@ -95,16 +95,23 @@ where
None
}
}
fn style(&self) -> ButtonStyle {
ButtonStyle::Secondary
}
fn label(&self) -> Option<&'static str> {
fn label(&self) -> Option<String> {
None
}
fn url(&self) -> Option<&'static str> {
fn url(&self) -> Option<String> {
None
}
fn disabled(&self) -> bool {
false
}
async fn on_press(&self, ctx: InteractionContext<S, E>) -> Result<(), InteractionError> {
ctx.defer().await
}
+86 -31
View File
@@ -1,13 +1,16 @@
use std::sync::Arc;
use twilight_http::{Error, Response, response::marker::EmptyBody};
use twilight_model::{
gateway::payload::incoming::InteractionCreate,
http::interaction::{InteractionResponse, InteractionResponseType},
id::{Id, marker::InteractionMarker},
};
use twilight_util::builder::InteractionResponseDataBuilder;
use crate::framework::{
commands::messages::OriginalMessageUpdater, interactions::InteractionError, runtime::Runtime,
interactions::{InteractionError, components::ActionRow},
messages::MessageEditBuilder,
runtime::Runtime,
};
pub struct InteractionContext<T, E> {
@@ -17,7 +20,11 @@ pub struct InteractionContext<T, E> {
pub extras: E,
}
impl<T, E> InteractionContext<T, E> {
impl<T, E> InteractionContext<T, E>
where
T: Send + Sync + 'static,
E: Send + Sync + 'static,
{
pub fn new(
event: InteractionCreate,
rt: Arc<Runtime<T, E>>,
@@ -44,37 +51,85 @@ impl<T, E> InteractionContext<T, E> {
kind: InteractionResponseType::DeferredUpdateMessage,
},
)
.await
.map_err(|_| InteractionError::DeferringFailed)?;
.await?;
Ok(())
}
pub fn original_message(&self) -> Option<OriginalMessage<T, E>> {
self.event.message.as_ref().map(|_| OriginalMessage {
interaction_id: self.event.id,
interaction_token: self.event.token.clone(),
rt: self.rt.clone(),
})
}
}
pub struct OriginalMessage<T, E> {
interaction_id: Id<InteractionMarker>,
interaction_token: String,
rt: Arc<Runtime<T, E>>,
}
impl<T, E> OriginalMessage<T, E>
where
T: Send + Sync + 'static,
E: Send + Sync + 'static,
{
pub fn edit(&self) -> OriginalMessageUpdater<T, E> {
OriginalMessageUpdater::new(
self.interaction_id,
self.interaction_token.clone(),
self.rt.clone(),
)
pub async fn edit_original_message(
&self,
builder: MessageEditBuilder<T, E>,
) -> Result<Response<EmptyBody>, Error> {
let mut sender = InteractionResponseDataBuilder::new();
if let Some(content) = &builder.content {
if let Some(content) = content {
sender = sender.content(content);
} else {
sender = sender.content("");
}
}
let mut current_row = ActionRow::new(&[]);
let mut current_index = 0;
let mut custom_ids: Vec<String> = vec![];
if let Some(components) = builder.components {
let mut rows = vec![];
for (component, index) in components {
if index != current_index {
if !current_row.is_empty() {
rows.push(current_row.build());
current_row = ActionRow::new(&[]);
}
current_index += 1;
}
let custom_id = component.custom_id();
if let Some(custom_id) = &custom_id {
custom_ids.push(custom_id.clone());
self.rt.registry.register(custom_id, component.clone());
}
current_row.add(custom_id.clone(), component.clone());
}
if !current_row.is_empty() {
rows.push(current_row.build());
}
sender = sender.components(rows);
}
if let Some(embeds) = builder.embeds {
sender = sender.embeds(embeds);
}
let result = self
.rt
.http
.interaction(self.rt.app.id)
.create_response(
self.event.id,
&self.event.token,
&InteractionResponse {
data: Some(sender.build()),
kind: InteractionResponseType::UpdateMessage,
},
)
.await;
if result.is_err() {
for custom_id in custom_ids {
self.rt.registry.unregister(custom_id);
}
}
result
}
}
+8 -3
View File
@@ -1,5 +1,7 @@
use thiserror::Error;
use crate::database::models::DatabaseError;
pub mod components;
pub mod context;
pub mod registry;
@@ -12,9 +14,6 @@ pub enum InteractionError {
)]
InteractionNotFound,
#[error("The interaction was attempted to be deferred, but it failed to be done")]
DeferringFailed,
#[error("Discord responded with an interaction type Payphone does not use")]
InteractionTypeUnsent,
@@ -26,4 +25,10 @@ pub enum InteractionError {
#[error("The extras getter failed to retrieve extras")]
ExtrasGetterFailed,
#[error(transparent)]
DatabaseError(#[from] DatabaseError),
#[error(transparent)]
DiscordAPIError(#[from] twilight_http::Error),
}
+9 -2
View File
@@ -47,7 +47,10 @@ where
if let Some(data) = &event.data {
match data {
InteractionData::MessageComponent(data) => {
if let Some(component) = self.inner.get_async(&data.custom_id).await {
if let Some(guarded_component) = self.inner.get(&data.custom_id) {
let component = guarded_component.clone();
drop(guarded_component);
let prefixes = rt
.get_prefixes(
event.guild_id.map(|id| id.get()),
@@ -60,7 +63,7 @@ where
let extras = extras
.get_extra(
&event.author().expect("Component interaction had no author"),
event.author().expect("Component interaction had no author"),
&rt,
)
.await
@@ -77,6 +80,10 @@ where
extras,
))
.await
.inspect(|_| log::debug!("Executed component interaction"))
.inspect_err(|e| {
log::error!("Interaction execution failed: {e:?}")
})
} else {
Err(InteractionError::InteractionNotFound)
}
+96
View File
@@ -0,0 +1,96 @@
use std::sync::Arc;
use twilight_model::channel::message::Embed;
use crate::framework::interactions::components::Component;
type ComponentWithRowIndex<S, E> = (Arc<dyn Component<S, E>>, usize);
pub struct MessageBuilder<S, E> {
pub content: Option<String>,
pub embeds: Vec<Embed>,
pub components: Vec<ComponentWithRowIndex<S, E>>,
}
impl<S, E> MessageBuilder<S, E> {
pub fn new() -> Self {
Self {
content: None,
embeds: Vec::new(),
components: Vec::new(),
}
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
pub fn embed(mut self, embed: Embed) -> Self {
self.embeds.push(embed);
self
}
pub fn component(mut self, component: impl Component<S, E> + 'static, row: usize) -> Self {
self.components.push((Arc::new(component), row));
self
}
}
pub struct MessageEditBuilder<S, E> {
pub content: Option<Option<String>>,
pub embeds: Option<Vec<Embed>>,
pub components: Option<Vec<ComponentWithRowIndex<S, E>>>,
}
impl<S, E> MessageEditBuilder<S, E> {
pub fn new() -> Self {
Self {
content: None,
embeds: None,
components: None,
}
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(Some(content.into()));
self
}
pub fn embed(mut self, embed: Embed) -> Self {
if self.embeds.is_none() {
self.embeds = Some(Vec::new());
}
self.embeds.as_mut().unwrap().push(embed);
self
}
pub fn component(mut self, component: impl Component<S, E> + 'static, row: usize) -> Self {
if self.components.is_none() {
self.components = Some(Vec::new());
}
self.components
.as_mut()
.unwrap()
.push((Arc::new(component), row));
self
}
}
impl<S, E> From<MessageBuilder<S, E>> for MessageEditBuilder<S, E> {
fn from(builder: MessageBuilder<S, E>) -> Self {
Self {
content: builder.content.map(Some),
embeds: Some(builder.embeds),
components: Some(builder.components),
}
}
}
+3 -4
View File
@@ -76,8 +76,7 @@
//! // Your command logic here.
//! ctx.send()
//! .content("Hello, world!")
//! .await
//! .map_err(|_| CommandError::MessageSendingFailed)?;
//! .await?;
//!
//! Ok(())
//! }
@@ -149,8 +148,7 @@
//!
//! ctx.send()
//! .content(format!("Hello, {}!", name.0))
//! .await
//! .map_err(|_| CommandError::MessageSendingFailed)?;
//! .await?;
//!
//! Ok(())
//! }
@@ -161,4 +159,5 @@ pub mod bot;
pub mod commands;
pub mod events;
pub mod interactions;
pub mod messages;
pub mod runtime;