Initialize Twilight project, set up basic framework, initialize DB module

This commit is contained in:
2025-08-07 04:32:34 -03:00
parent 00d61545eb
commit 24d2344f37
29 changed files with 4095 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
use std::sync::Arc;
use async_trait::async_trait;
use twilight_model::channel::message::{
Component as TwilightComponent,
component::{ActionRow as TwilightActionRow, Button as TwilightButton, ButtonStyle},
};
use crate::framework::interactions::{InteractionError, context::InteractionContext};
pub struct ActionRow {
components: Vec<(Option<String>, Arc<dyn Component>)>,
}
impl ActionRow {
pub fn new(components: &[(Option<String>, Arc<dyn Component>)]) -> Self {
Self {
components: components.to_vec(),
}
}
pub fn add(&mut self, id: Option<String>, component: Arc<dyn Component>) {
self.components.push((id, component));
}
pub fn len(&self) -> usize {
self.components.len()
}
pub fn build(self) -> TwilightComponent {
TwilightComponent::ActionRow(TwilightActionRow {
components: self
.components
.into_iter()
.map(|(id, c)| c.build(id))
.collect(),
})
}
}
#[async_trait]
pub trait Component: Send + Sync {
fn custom_id(&self) -> Option<String> {
Some(String::from("component"))
}
fn build(&self, id: Option<String>) -> TwilightComponent;
async fn execute(&self, ctx: InteractionContext) -> Result<(), InteractionError>;
}
#[async_trait]
impl<T> Component for T
where
T: Button,
{
fn custom_id(&self) -> Option<String> {
Button::id(self)
}
fn build(&self, id: Option<String>) -> TwilightComponent {
TwilightComponent::Button(TwilightButton {
custom_id: id,
disabled: false,
emoji: None,
label: self.label().map(|i| i.to_string()),
style: if self.url().is_none() {
self.style()
} else {
ButtonStyle::Link
},
url: self.url().map(|i| i.to_string()),
sku_id: None,
})
}
async fn execute(&self, ctx: InteractionContext) -> Result<(), InteractionError> {
self.on_press(ctx).await
}
}
#[async_trait]
pub trait Button: Send + Sync {
fn id(&self) -> Option<String> {
if self.url().is_none() {
Some(String::from("Button"))
} else {
None
}
}
fn style(&self) -> ButtonStyle {
ButtonStyle::Secondary
}
fn label(&self) -> Option<&'static str> {
None
}
fn url(&self) -> Option<&'static str> {
None
}
async fn on_press(&self, ctx: InteractionContext) -> Result<(), InteractionError> {
ctx.defer().await
}
}
+88
View File
@@ -0,0 +1,88 @@
use std::sync::Arc;
use twilight_http::Client;
use twilight_model::{
gateway::payload::incoming::InteractionCreate,
http::interaction::{InteractionResponse, InteractionResponseType},
id::{Id, marker::InteractionMarker},
oauth::Application,
};
use crate::framework::{
commands::messages::OriginalMessageUpdater, handlers::Handlers, interactions::{registry::Registry, InteractionError}
};
pub struct InteractionContext {
pub event: InteractionCreate,
pub handlers: Arc<Handlers>,
}
impl InteractionContext {
pub fn new(
event: InteractionCreate,
handlers: Arc<Handlers>,
) -> Self {
Self {
event,
handlers
}
}
pub async fn defer(&self) -> Result<(), InteractionError> {
self.handlers.http
.interaction(self.handlers.app.id)
.create_response(
self.event.id,
&self.event.token,
&InteractionResponse {
data: None,
kind: InteractionResponseType::DeferredUpdateMessage,
},
)
.await
.map_err(|_| InteractionError::DeferringFailed)?;
Ok(())
}
pub async fn think(&self) -> Result<(), InteractionError> {
self.handlers.http
.interaction(self.handlers.app.id)
.create_response(
self.event.id,
&self.event.token,
&InteractionResponse {
data: None,
kind: InteractionResponseType::DeferredChannelMessageWithSource,
},
)
.await
.map_err(|_| InteractionError::DeferringFailed)?;
Ok(())
}
pub fn original_message(&self) -> Option<OriginalMessage> {
self.event.message.as_ref().map(|_| OriginalMessage {
interaction_id: self.event.id,
interaction_token: self.event.token.clone(),
handlers: self.handlers.clone()
})
}
}
pub struct OriginalMessage {
interaction_id: Id<InteractionMarker>,
interaction_token: String,
handlers: Arc<Handlers>
}
impl OriginalMessage {
pub fn edit(&self) -> OriginalMessageUpdater {
OriginalMessageUpdater::new(
self.interaction_id,
self.interaction_token.clone(),
self.handlers.clone(),
)
}
}
+22
View File
@@ -0,0 +1,22 @@
use thiserror::Error;
pub mod components;
pub mod context;
pub mod registry;
#[derive(Debug, Error)]
pub enum InteractionError {
#[error(
"The ID for this interaction did not point to any existing interaction in the registry"
)]
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,
#[error("The original message was attempted to be edited, but it failed")]
OriginalMessageEditingFailed,
}
+55
View File
@@ -0,0 +1,55 @@
use std::sync::Arc;
use scc::HashMap;
use twilight_http::Client;
use twilight_model::{
application::interaction::InteractionData, gateway::payload::incoming::InteractionCreate,
oauth::Application,
};
use crate::framework::{handlers::Handlers, interactions::{context::InteractionContext, InteractionError}};
use super::components::Component;
pub struct Registry {
inner: HashMap<String, Arc<dyn Component>>,
}
impl Registry {
pub fn new() -> Self {
Self {
inner: Default::default(),
}
}
pub fn register(&self, id: impl ToString, component: Arc<dyn Component>) {
self.inner.upsert(id.to_string(), component);
}
pub fn unregister(&self, id: impl ToString) {
self.inner.remove(&id.to_string());
}
pub async fn execute(
&self,
event: InteractionCreate,
handlers: Arc<Handlers>,
) -> Result<(), InteractionError> {
if let Some(data) = &event.data {
match data {
InteractionData::MessageComponent(data) => {
if let Some(component) = self.inner.get_async(&data.custom_id).await {
component
.execute(InteractionContext::new(event, handlers))
.await
} else {
Err(InteractionError::InteractionNotFound)
}
}
_ => Err(InteractionError::InteractionTypeUnsent),
}
} else {
Err(InteractionError::InteractionTypeUnsent)
}
}
}