Add basic moderator commands
This commit is contained in:
@@ -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?;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user