From 540db21c45c71a443124794914be81608eae86f6 Mon Sep 17 00:00:00 2001 From: Anish Nayak Date: Tue, 12 Aug 2025 19:59:53 +0100 Subject: [PATCH] moved message structs and ran cargo fmt --- src/cli.rs | 8 +- src/database/models.rs | 156 ++++++++++-------- src/framework/bot.rs | 33 +--- src/framework/interactions/message_command.rs | 12 +- .../interactions/message_command_registry.rs | 21 ++- src/framework/interactions/mod.rs | 4 +- src/framework/mod.rs | 74 ++++----- src/groups/calls/commands.rs | 38 +++-- src/groups/calls/events.rs | 8 +- src/logging.rs | 20 +-- src/state.rs | 8 +- 11 files changed, 211 insertions(+), 171 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index e5813c6..9766593 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -45,16 +45,16 @@ //! ```shell //! $ cargo run -- start [TOKEN] //! ``` -//! +//! //! # Running the Documentation Server -//! +//! //! Payphone has a built-in documentation server that serves the project's documentation and //! refreshes it on changes. To start the documentation server, run the following command: -//! +//! //! ```shell //! $ cargo run -- docs --open //! ``` -//! +//! //! The `--open` flag is optional and will open the documentation in the default browser after //! starting the server. You can also use the `--bind` argument to specify the address to bind the //! server to, which defaults to `0.0.0.0:3000`. diff --git a/src/database/models.rs b/src/database/models.rs index 5d07eac..5e4e721 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,66 +1,4 @@ -use scylla::{SerializeValue, DeserializeValue}; -#[derive(Debug, Clone, SerializeValue, DeserializeValue)] -pub struct MessageMetadata { - pub call_id: i64, - pub author_id: i64, - pub author_name: Option, - pub author_avatar_url: Option, - pub author_mask: Option, - pub content: Option, - pub attachments: Option>, - pub sticker: Option, - pub propagation_message_id: Option, - pub propagation_channel_id: Option, - pub replying_to_id: Option, - pub message_replied_to_metadata: Option>, - pub is_deleted: Option, - pub is_edited: Option, - pub is_censored: Option, -} - -impl<'a> Object<'a> for MessageMetadata { - type Row = ( - i64, i64, Option, Option, Option, Option, Option>, Option, Option, Option, Option, Option>, Option, Option, Option - ); - fn from_row(row: Self::Row) -> Self { - Self { - call_id: row.0, - author_id: row.1, - author_name: row.2, - author_avatar_url: row.3, - author_mask: row.4, - content: row.5, - attachments: row.6, - sticker: row.7, - propagation_message_id: row.8, - propagation_channel_id: row.9, - replying_to_id: row.10, - message_replied_to_metadata: row.11, - is_deleted: row.12, - is_edited: row.13, - is_censored: row.14, - } - } - fn to_row(value: Self) -> Self::Row { - ( - value.call_id, - value.author_id, - value.author_name, - value.author_avatar_url, - value.author_mask, - value.content, - value.attachments, - value.sticker, - value.propagation_message_id, - value.propagation_channel_id, - value.replying_to_id, - value.message_replied_to_metadata, - value.is_deleted, - value.is_edited, - value.is_censored, - ) - } -} +use scylla::{DeserializeValue, SerializeValue}; use scylla::{ client::session::Session, deserialize::row::DeserializeRow, @@ -253,6 +191,83 @@ impl From for User { } } +#[derive(Debug, Clone, SerializeValue, DeserializeValue)] +pub struct MessageMetadata { + pub call_id: i64, + pub author_id: i64, + pub author_name: Option, + pub author_avatar_url: Option, + pub author_mask: Option, + pub content: Option, + pub attachments: Option>, + pub sticker: Option, + pub propagation_message_id: Option, + pub propagation_channel_id: Option, + pub replying_to_id: Option, + pub message_replied_to_metadata: Option>, + pub is_deleted: Option, + pub is_edited: Option, + pub is_censored: Option, +} + +impl<'a> Object<'a> for MessageMetadata { + type Row = ( + i64, + i64, + Option, + Option, + Option, + Option, + Option>, + Option, + Option, + Option, + Option, + Option>, + Option, + Option, + Option, + ); + fn from_row(row: Self::Row) -> Self { + Self { + call_id: row.0, + author_id: row.1, + author_name: row.2, + author_avatar_url: row.3, + author_mask: row.4, + content: row.5, + attachments: row.6, + sticker: row.7, + propagation_message_id: row.8, + propagation_channel_id: row.9, + replying_to_id: row.10, + message_replied_to_metadata: row.11, + is_deleted: row.12, + is_edited: row.13, + is_censored: row.14, + } + } + fn to_row(value: Self) -> Self::Row { + ( + value.call_id, + value.author_id, + value.author_name, + value.author_avatar_url, + value.author_mask, + value.content, + value.attachments, + value.sticker, + value.propagation_message_id, + value.propagation_channel_id, + value.replying_to_id, + value.message_replied_to_metadata, + value.is_deleted, + value.is_edited, + value.is_censored, + ) + } +} + // --- BEGIN: Payphone Message and Report Models --- #[derive(Debug, Clone)] @@ -263,7 +278,11 @@ pub struct Message { } impl Message { - pub async fn get_by_id(call_id: u64, message_id: u64, session: &Session) -> Result, DatabaseError> { + pub async fn get_by_id( + call_id: u64, + message_id: u64, + session: &Session, + ) -> Result, DatabaseError> { let call_id = u64_to_i64(call_id); let message_id = u64_to_i64(message_id); let result = session @@ -287,7 +306,9 @@ impl Message { message_metadata: row.2, })), Err(error) => match error { - SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => Err(DatabaseError::DeserializationFailed), + SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => { + Err(DatabaseError::DeserializationFailed) + } SingleRowError::UnexpectedRowCount(_) => Ok(None), }, } @@ -325,7 +346,10 @@ impl Report { &self.message, self.created_at, self.reviewed_at, - self.created_by_ids.iter().map(|&id| u64_to_i64(id)).collect::>(), + self.created_by_ids + .iter() + .map(|&id| u64_to_i64(id)) + .collect::>(), self.reviewed_by_id.map(u64_to_i64), self.resolution.as_ref(), ), diff --git a/src/framework/bot.rs b/src/framework/bot.rs index 99a24c9..6655de6 100644 --- a/src/framework/bot.rs +++ b/src/framework/bot.rs @@ -37,24 +37,14 @@ pub trait IntoPrefixes { #[async_trait] impl IntoPrefixes for String { - async fn get( - &self, - _: &Runtime, - _: Option, - _: Option, - ) -> Vec { + async fn get(&self, _: &Runtime, _: Option, _: Option) -> Vec { vec![self.to_string()] } } #[async_trait] impl IntoPrefixes for &str { - async fn get( - &self, - _: &Runtime, - _: Option, - _: Option, - ) -> Vec { + async fn get(&self, _: &Runtime, _: Option, _: Option) -> Vec { vec![self.to_string()] } } @@ -64,12 +54,7 @@ impl IntoPre where T: ToString, { - async fn get( - &self, - _: &Runtime, - _: Option, - _: Option, - ) -> Vec { + async fn get(&self, _: &Runtime, _: Option, _: Option) -> Vec { self.iter().map(|s| s.to_string()).collect() } } @@ -80,12 +65,7 @@ where F: Fn() -> Fut + Send + Sync + 'static, Fut: Future> + Send, { - async fn get( - &self, - _: &Runtime, - _: Option, - _: Option, - ) -> Vec { + async fn get(&self, _: &Runtime, _: Option, _: Option) -> Vec { (self)().await } } @@ -267,7 +247,10 @@ impl Bot { } Event::InteractionCreate(interaction) => { // First, try message command registry - if let Err(e) = message_command_registry.execute(&interaction, task_rt.clone()).await { + if let Err(e) = message_command_registry + .execute(&interaction, task_rt.clone()) + .await + { // If not a message command, fall back to interaction handlers for handler in &task_bot.on_interaction_handlers { handler diff --git a/src/framework/interactions/message_command.rs b/src/framework/interactions/message_command.rs index 6a7eb5f..d50a4c5 100644 --- a/src/framework/interactions/message_command.rs +++ b/src/framework/interactions/message_command.rs @@ -1,7 +1,7 @@ -use std::fmt::Debug; -use async_trait::async_trait; -use twilight_model::gateway::payload::incoming::InteractionCreate; use crate::framework::interactions::{InteractionError, context::InteractionContext}; +use async_trait::async_trait; +use std::fmt::Debug; +use twilight_model::gateway::payload::incoming::InteractionCreate; #[async_trait] pub trait MessageCommand: Send + Sync + Debug { @@ -9,5 +9,9 @@ pub trait MessageCommand: Send + Sync + Debug { fn description(&self) -> &'static str { "A message context menu command duh." } - async fn execute(&self, ctx: InteractionContext, target_message_id: u64) -> Result<(), InteractionError>; + async fn execute( + &self, + ctx: InteractionContext, + target_message_id: u64, + ) -> Result<(), InteractionError>; } diff --git a/src/framework/interactions/message_command_registry.rs b/src/framework/interactions/message_command_registry.rs index e6d85cc..19b6bb6 100644 --- a/src/framework/interactions/message_command_registry.rs +++ b/src/framework/interactions/message_command_registry.rs @@ -1,12 +1,15 @@ -use std::sync::Arc; -use scc::HashMap; -use twilight_model::{ - application::interaction::InteractionData, application::command::CommandType, gateway::payload::incoming::InteractionCreate, -}; use crate::framework::{ - interactions::{InteractionError, context::InteractionContext, message_command::MessageCommand}, + interactions::{ + InteractionError, context::InteractionContext, message_command::MessageCommand, + }, runtime::Runtime, }; +use scc::HashMap; +use std::sync::Arc; +use twilight_model::{ + application::command::CommandType, application::interaction::InteractionData, + gateway::payload::incoming::InteractionCreate, +}; #[derive(Default)] pub struct MessageCommandRegistry { @@ -53,7 +56,11 @@ where if let Some(target_id) = data.target_id { command .execute( - InteractionContext::new(event.clone(), rt.clone(), prefix.to_string()), + InteractionContext::new( + event.clone(), + rt.clone(), + prefix.to_string(), + ), target_id.get(), ) .await diff --git a/src/framework/interactions/mod.rs b/src/framework/interactions/mod.rs index 28f51f4..4d8103e 100644 --- a/src/framework/interactions/mod.rs +++ b/src/framework/interactions/mod.rs @@ -2,10 +2,10 @@ use thiserror::Error; pub mod components; pub mod context; -pub mod registry; -pub mod runner; pub mod message_command; pub mod message_command_registry; +pub mod registry; +pub mod runner; #[derive(Debug, Error)] pub enum InteractionError { diff --git a/src/framework/mod.rs b/src/framework/mod.rs index a48d515..3823c8e 100644 --- a/src/framework/mod.rs +++ b/src/framework/mod.rs @@ -1,35 +1,35 @@ //! A framework for building Discord bots, built with Twilight. -//! +//! //! Since Twilight is a low-level library, it can be hard to build a bot with it. Payphone's //! framework provides a high-level interface for building Discord bots, with a focus on modularity //! and ease of use. The framework is designed to be extensible, allowing you to add new features //! and commands without modifying the core codebase. It also provides a set of utilities for //! common tasks, such as sending messages, handling events, and managing state. -//! +//! //! # Creating a Bot -//! +//! //! Creating a bot with Payphone is as simple as creating a new instance of the `Bot` struct and //! running it. The `Bot` struct is the main entry point for the framework and provides methods for //! running the bot, handling events, and managing state. -//! +//! //! ``` //! use crate::framework::bot::Bot; //! use crate::state::State; -//! +//! //! // Your app's state. //! let state = State::new(); -//! +//! //! // Create a new bot instance with the state and a list of prefixes. //! let bot: Bot = Bot::new(state, ["p."]); -//! +//! //! bot.run(&token).await.unwrap(); //! ``` -//! +//! //! ### Prefixes -//! +//! //! The framework supports multiple prefixes per command and allows you to set them dynamically //! based on server and user IDs. -//! +//! //! ``` //! async fn get_prefixes( //! rt: &Runtime, @@ -37,15 +37,15 @@ //! user_id: Option //! ) -> Vec { //! // The runtime contains your state at [`Runtime::state`]. -//! +//! //! vec!["p.".to_string()] //! } -//! +//! //! let bot: Bot = Bot::new(state, get_prefixes); //! ``` -//! +//! //! Otherwise, you can just pass a static list of prefixes as shown in the previous example. -//! +//! //! ``` //! // All of these are valid ways to create a bot with a prefix. //! let bot: Bot = Bot::new(state, ["p."]); @@ -53,77 +53,77 @@ //! let bot: Bot = Bot::new(state, "p.".to_string()); //! let bot: Bot = Bot::new(state, ["p.".to_string()]); //! ``` -//! +//! //! Setting no prefix will make the bot raise errors when receiving messages. -//! +//! //! ## Adding Commands -//! +//! //! Creating a command is quite simple. You just need to implement the `Command` trait and register //! it with the bot. -//! +//! //! ``` //! use crate::framework::commands::{Command, CommandError, context::CommandContext}; -//! +//! //! pub struct MyCommand; -//! +//! //! #[async_trait::async_trait] //! impl Command for MyCommand { //! fn name(&self) -> &'static str { //! "my_command" //! } -//! +//! //! async fn execute(&self, ctx: CommandContext<'_, T>, _args: &str) -> Result<(), CommandError> { //! // Your command logic here. //! ctx.send() //! .content("Hello, world!") //! .await //! .map_err(|_| CommandError::MessageSendingFailed)?; -//! +//! //! Ok(()) //! } //! } //! ``` -//! +//! //! Then you just need to register the command with the bot: -//! +//! //! ``` //! let bot = Bot::new(state, ["p."]).command(MyCommand); -//! +//! //! bot.run(&token).await.unwrap(); //! ``` -//! +//! //! Log into your Discord account and invite the bot to your server. Then you can test the command //! by sending `p.my_command` in a channel the bot has access to. -//! +//! //! ### Configuring Commands Further -//! +//! //! Commands can be configured with additional metadata, such as aliases, descriptions, and //! arguments. The following is a full example of a command with all these features: -//! +//! //! ``` //! use crate::framework::commands::{ //! Command, CommandError, context::CommandContext, arguments::NeedyGreedyString //! }; //! use crate::state::State; -//! +//! //! pub struct HelloCommand; -//! +//! //! #[async_trait::async_trait] //! impl Command for HelloCommand { //! fn name(&self) -> &'static str { //! "hello" //! } -//! +//! //! /// A list of alternative names for the command. //! fn aliases(&self) -> &'static [&'static str] { //! &["hola", "hey", "hi", "konnichiwa"] //! } -//! +//! //! /// A description of the command and its functionality. //! fn description(&self) -> &'static str { //! "This command responds with a greeting." //! } -//! +//! //! /// A list of arguments the command accepts. These are solely for documentation purposes, //! /// they won't affect the parsing you do in the command's `execute` method. //! fn arguments(&self) -> &'static [Argument] { @@ -135,7 +135,7 @@ //! }, //! ] //! } -//! +//! //! async fn execute( //! &self, //! ctx: CommandContext<'_, T>, @@ -146,12 +146,12 @@ //! args //! ) //! .unwrap_or(NeedyGreedyString("???".to_string())); -//! +//! //! ctx.send() //! .content(format!("Hello, {}!", name.0)) //! .await //! .map_err(|_| CommandError::MessageSendingFailed)?; -//! +//! //! Ok(()) //! } //! } diff --git a/src/groups/calls/commands.rs b/src/groups/calls/commands.rs index 5997a40..b1ff478 100644 --- a/src/groups/calls/commands.rs +++ b/src/groups/calls/commands.rs @@ -5,7 +5,9 @@ use twilight_model::id::Id; use crate::{ database::models::{Call, Channel, Message, Report}, framework::commands::{Command, CommandError, context::CommandContext}, - framework::interactions::{message_command::MessageCommand, InteractionError, context::InteractionContext}, + framework::interactions::{ + InteractionError, context::InteractionContext, message_command::MessageCommand, + }, state::State, utils::{Bye, get_random_u64}, }; @@ -22,15 +24,26 @@ impl MessageCommand for ReportMessageCommand { "Report a message to the moderators." } - async fn execute(&self, ctx: InteractionContext, target_message_id: u64) -> Result<(), InteractionError> { - + async fn execute( + &self, + ctx: InteractionContext, + target_message_id: u64, + ) -> Result<(), InteractionError> { // Extract channel_id from resolved data or fallback to event.channel (Discord API v10+) - let channel_id = ctx.event.channel.as_ref().map(|c| c.id.get()) + let channel_id = ctx + .event + .channel + .as_ref() + .map(|c| c.id.get()) .or_else(|| ctx.event.channel.as_ref().map(|c| c.id.get())) .ok_or(InteractionError::InteractionNotFound)?; let db = &ctx.rt.state.db; - let channel = Channel::get_by_id(channel_id, db).await.map_err(|_| InteractionError::InteractionNotFound)?; - let call_id = channel.and_then(|c| c.call_id).ok_or(InteractionError::InteractionNotFound)?; + let channel = Channel::get_by_id(channel_id, db) + .await + .map_err(|_| InteractionError::InteractionNotFound)?; + let call_id = channel + .and_then(|c| c.call_id) + .ok_or(InteractionError::InteractionNotFound)?; // Fetch the message from the database let message = Message::get_by_id(call_id, target_message_id, db) @@ -41,8 +54,11 @@ impl MessageCommand for ReportMessageCommand { // If the message is a reply, fetch the replied-to message and denormalize its metadata let mut message_metadata = message.message_metadata.clone(); if let Some(replying_to_id) = message_metadata.replying_to_id { - if let Ok(Some(reply_msg)) = Message::get_by_id(call_id, replying_to_id as u64, db).await { - message_metadata.message_replied_to_metadata = Some(Box::new(reply_msg.message_metadata)); + if let Ok(Some(reply_msg)) = + Message::get_by_id(call_id, replying_to_id as u64, db).await + { + message_metadata.message_replied_to_metadata = + Some(Box::new(reply_msg.message_metadata)); } } @@ -56,10 +72,12 @@ impl MessageCommand for ReportMessageCommand { reviewed_by_id: None, resolution: None, }; - // Insert the report - report.insert(db).await.map_err(|_| InteractionError::DeferringFailed)?; + report + .insert(db) + .await + .map_err(|_| InteractionError::DeferringFailed)?; // Respond to the user ctx.rt.http diff --git a/src/groups/calls/events.rs b/src/groups/calls/events.rs index f357cec..b284c8d 100644 --- a/src/groups/calls/events.rs +++ b/src/groups/calls/events.rs @@ -6,10 +6,14 @@ use twilight_model::{gateway::payload::incoming::MessageCreate, id::Id}; use twilight_validate::request::webhook_username; use crate::{ - database::models::{Call, Channel}, discord::{images, sanitizing::SanitizeDiscordMarkdown}, framework::{ + database::models::{Call, Channel}, + discord::{images, sanitizing::SanitizeDiscordMarkdown}, + framework::{ events::traits::{EventResult, OnMessageHandler}, runtime::Runtime, - }, groups::calls::moderation::escape_message, state::State + }, + groups::calls::moderation::escape_message, + state::State, }; #[derive(Debug)] diff --git a/src/logging.rs b/src/logging.rs index 7badf89..84863e4 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,16 +1,16 @@ //! Log handling and formatting of the console's output. -//! +//! //! # Logging -//! +//! //! Payphone uses the [`log`] crate for logging, and the [`colored`] crate for formatting the //! output. -//! +//! //! Any logs that are not coming from the `payphone` crate are ignored. -//! +//! //! ## Logging Levels -//! +//! //! There are 5 different logging levels: -//! +//! //! - `trace`: The most verbose level, used for debugging purposes. //! - `debug`: Events that are useful to be displayed during development, but not in production. //! - `info`: Used for general information about the bot's state and milestones. @@ -19,9 +19,9 @@ //! - `error`: Used for errors, when something goes wrong and the bot cannot continue running. //! //! ## Usage -//! +//! //! Logging is quite simple as it works like any [`println!`] calls. -//! +//! //! ``` //! log::trace!("This is a trace message."); //! log::debug!("This is a debug message."); @@ -29,9 +29,9 @@ //! log::warn!("This is a warning message."); //! log::error!("This is an error message."); //! ``` -//! +//! //! Logging supports formatting, so you can use it like any other [`println!`] call: -//! +//! //! ``` //! let value = 42; //! log::info!("The value is: {value}"); diff --git a/src/state.rs b/src/state.rs index fd3f923..c7f44d1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,18 +1,18 @@ //! Application state at runtime including shared resources. -//! +//! //! Currently, it only contains the database connection, but it can be extended in the future to //! include other resources that need to be shared across the application, such as caches or //! configuration settings. -//! +//! //! It is shared around the application using an `Arc`, allowing it to be cloned and passed around //! without needing to worry about ownership or lifetimes. This is particularly useful in an //! asynchronous context. -//! +//! //! ``` //! let state = State { //! db: database::init("127.0.0.1:9042").await?, //! }; -//! +//! //! fn handler_which_needs_state(state: Arc) { //! // Use the state here //! }