diff --git a/src/database/migrations/0001_init.cql b/src/database/migrations/0001_init.cql index 7227970..79c64eb 100644 --- a/src/database/migrations/0001_init.cql +++ b/src/database/migrations/0001_init.cql @@ -5,25 +5,29 @@ CREATE KEYSPACE IF NOT EXISTS payphone } AND TABLETS = { 'enabled': false }; +CREATE TYPE IF NOT EXISTS payphone.message ( + id BIGINT, -CREATE TYPE IF NOT EXISTS payphone.message_metadata ( - call_id BIGINT, author_id BIGINT, author_name TEXT, author_avatar_url TEXT, author_mask ASCII, + content TEXT, attachments LIST, sticker BIGINT, + propagation_message_id BIGINT, propagation_channel_id BIGINT, - replying_to_id BIGINT, - message_replied_to_metadata FROZEN, + + replying_to_id, + is_deleted BOOLEAN, is_edited BOOLEAN, - is_censored BOOLEAN + is_censored BOOLEAN, ); + CREATE TABLE IF NOT EXISTS payphone.users ( user_id BIGINT PRIMARY KEY, @@ -82,7 +86,9 @@ CREATE TABLE IF NOT EXISTS payphone.participants ( CREATE TABLE IF NOT EXISTS payphone.messages ( call_id BIGINT, message_id BIGINT, - message_metadata FROZEN, + + data FROZEN, + PRIMARY KEY (call_id, message_id) ); @@ -92,31 +98,44 @@ CREATE TABLE IF NOT EXISTS payphone.servers ( name TEXT, prefix TEXT, ); - CREATE TABLE IF NOT EXISTS payphone.reports ( report_id BIGINT, - message FROZEN, + + message FROZEN, + replying_to FROZEN, + created_at TIMESTAMP, reviewed_at TIMESTAMP, + created_by_ids SET, + reviewed_by_id BIGINT, resolution ASCII, - PRIMARY KEY (report_id) + + PRIMARY KEY (message_author_id, report_id) ); CREATE TABLE IF NOT EXISTS payphone.user_incidents ( user_id BIGINT, incident_id BIGINT, + -- ban, warning type ASCII, + call_id BIGINT, report_id BIGINT, - message FROZEN, + + message FROZEN, + replying_to FROZEN, + reason TEXT, + created_by_id BIGINT, + created_at TIMESTAMP, expires_at TIMESTAMP, duration ASCII, + PRIMARY KEY (user_id) ); diff --git a/src/database/models.rs b/src/database/models.rs index 5e4e721..6d5e725 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,5 +1,5 @@ -use scylla::{DeserializeValue, SerializeValue}; use scylla::{ + DeserializeValue, SerializeValue, client::session::Session, deserialize::row::DeserializeRow, errors::{ExecutionError, SingleRowError}, @@ -192,8 +192,8 @@ impl From for User { } #[derive(Debug, Clone, SerializeValue, DeserializeValue)] -pub struct MessageMetadata { - pub call_id: i64, +pub struct Message { + pub id: i64, pub author_id: i64, pub author_name: Option, pub author_avatar_url: Option, @@ -204,13 +204,12 @@ pub struct MessageMetadata { 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 { +impl<'a> Object<'a> for Message { type Row = ( i64, i64, @@ -223,14 +222,13 @@ impl<'a> Object<'a> for MessageMetadata { Option, Option, Option, - Option>, Option, Option, Option, ); fn from_row(row: Self::Row) -> Self { Self { - call_id: row.0, + id: row.0, author_id: row.1, author_name: row.2, author_avatar_url: row.3, @@ -241,15 +239,14 @@ impl<'a> Object<'a> for MessageMetadata { 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, + is_deleted: row.11, + is_edited: row.12, + is_censored: row.13, } } fn to_row(value: Self) -> Self::Row { ( - value.call_id, + value.id, value.author_id, value.author_name, value.author_avatar_url, @@ -260,7 +257,6 @@ impl<'a> Object<'a> for MessageMetadata { 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, @@ -268,17 +264,14 @@ impl<'a> Object<'a> for MessageMetadata { } } -// --- BEGIN: Payphone Message and Report Models --- - -#[derive(Debug, Clone)] -pub struct Message { +pub struct MessageRow { pub call_id: u64, pub message_id: u64, - pub message_metadata: MessageMetadata, + pub data: Message, } -impl Message { - pub async fn get_by_id( +impl MessageRow { + pub async fn get_by_call_and_message_id( call_id: u64, message_id: u64, session: &Session, @@ -288,7 +281,7 @@ impl Message { let result = session .query_unpaged( r#" - SELECT call_id, message_id, message_metadata + SELECT call_id, message_id, data FROM payphone.messages WHERE call_id = ? AND message_id = ? LIMIT 1; @@ -299,12 +292,8 @@ impl Message { .map_err(DatabaseError::QueryError)? .into_rows_result() .map_err(|_| DatabaseError::ResultNotRows)?; - match result.single_row::<(i64, i64, MessageMetadata)>() { - Ok(row) => Ok(Some(Self { - call_id: i64_to_u64(row.0), - message_id: i64_to_u64(row.1), - message_metadata: row.2, - })), + match result.single_row::<::Row>() { + Ok(row) => Ok(Some(::from_row(row))), Err(error) => match error { SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => { Err(DatabaseError::DeserializationFailed) @@ -315,10 +304,54 @@ impl Message { } } +impl<'a> Object<'a> for Report { + type Row = ( + i64, // report_id + Message, + Option, + OffsetDateTime, + Option, + Vec, + Option, + Option, + ); + fn from_row(row: Self::Row) -> Self { + Self { + report_id: i64_to_u64(row.0), + message: row.1, + replying_to: row.2, + created_at: row.3, + reviewed_at: row.4, + created_by_ids: row.5.iter().map(|&id| i64_to_u64(id)).collect(), + reviewed_by_id: row.6.map(i64_to_u64), + resolution: row.7, + } + } + fn to_row(value: Self) -> Self::Row { + ( + u64_to_i64(value.report_id), + value.message, + value.replying_to, + value.created_at, + value.reviewed_at, + value + .created_by_ids + .iter() + .map(|&id| u64_to_i64(id)) + .collect(), + value.reviewed_by_id.map(u64_to_i64), + value.resolution, + ) + } +} + +// --- BEGIN: Payphone Message and Report Models --- + #[derive(Debug, Clone)] pub struct Report { pub report_id: u64, - pub message: MessageMetadata, + pub message: Message, + pub replying_to: Option, pub created_at: OffsetDateTime, pub reviewed_at: Option, pub created_by_ids: Vec, @@ -326,33 +359,42 @@ pub struct Report { pub resolution: Option, } +impl<'a> Object<'a> for MessageRow { + type Row = (i64, i64, >::Row); + fn from_row(row: Self::Row) -> Self { + Self { + call_id: i64_to_u64(row.0), + message_id: i64_to_u64(row.1), + data: ::from_row(row.2), + } + } + fn to_row(value: Self) -> Self::Row { + ( + u64_to_i64(value.call_id), + u64_to_i64(value.message_id), + ::to_row(value.data), + ) + } +} + impl Report { pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> { + let row = Self::to_row(self.clone()); session .query_unpaged( r#" INSERT INTO payphone.reports ( report_id, message, + replying_to, created_at, reviewed_at, created_by_ids, reviewed_by_id, resolution - ) VALUES (?, ?, ?, ?, ?, ?, ?); + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?); "#, - ( - u64_to_i64(self.report_id), - &self.message, - self.created_at, - self.reviewed_at, - self.created_by_ids - .iter() - .map(|&id| u64_to_i64(id)) - .collect::>(), - self.reviewed_by_id.map(u64_to_i64), - self.resolution.as_ref(), - ), + row, ) .await .map_err(DatabaseError::QueryError)?; diff --git a/src/framework/bot.rs b/src/framework/bot.rs index 6655de6..461722e 100644 --- a/src/framework/bot.rs +++ b/src/framework/bot.rs @@ -1,24 +1,24 @@ -use std::{fmt::Debug, sync::Arc}; - +use crate::framework::{ + commands::{Command, runner::CommandRunner}, + events::traits::{ + OnInteractionHandler, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler, + OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler, + OnServerUpdateHandler, + }, + interactions::{ + components::Component, message_command::MessageCommand, + message_command_registry::MessageCommandRegistry, + message_command_runner::MessageCommandRunner, registry::ComponentRegistry, + }, + runtime::Runtime, +}; +use crate::utils::Bye; use async_trait::async_trait; +use std::{fmt::Debug, sync::Arc}; use thiserror::Error; use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt}; use twilight_http::Client; -use crate::{ - framework::{ - commands::{Command, runner::CommandRunner}, - events::traits::{ - OnInteractionHandler, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler, - OnReadyHandler, OnReconnectHandler, OnServerCreateHandler, OnServerDeleteHandler, - OnServerUpdateHandler, - }, - interactions::{components::Component, registry::ComponentRegistry}, - runtime::Runtime, - }, - utils::Bye, -}; - #[derive(Debug, Error)] pub enum RuntimeError { #[error("The application's details could not be fetched")] @@ -70,10 +70,7 @@ where } } -use crate::framework::interactions::message_command::MessageCommand; -use crate::framework::interactions::message_command_registry::MessageCommandRegistry; - -pub struct Bot { +pub struct Bot { state: Arc, prefixes: Arc + Send + Sync>, intents: Intents, @@ -91,7 +88,7 @@ pub struct Bot { on_server_delete_handlers: Vec>>, } -impl Bot { +impl Bot { pub fn new(state: T, prefixes: impl IntoPrefixes + Send + Sync + 'static) -> Self { Self { state: Arc::new(state), @@ -102,7 +99,7 @@ impl Bot { message_commands: Vec::new(), on_ready_handlers: Vec::new(), on_reconnect_handlers: Vec::new(), - on_interaction_handlers: Vec::new(), + on_interaction_handlers: Vec::new(), // Will push MessageCommandRunner in run() on_message_handlers: vec![Arc::new(CommandRunner)], on_message_edit_handlers: Vec::new(), on_message_delete_handlers: Vec::new(), @@ -202,7 +199,14 @@ impl Bot { message_command_registry.register(command.name(), command.clone()); } - let bot = Arc::new(self); + let mut bot = self; + bot.on_interaction_handlers.insert( + 0, + Arc::new(MessageCommandRunner { + registry: message_command_registry.clone(), + }), + ); + let bot = Arc::new(bot); let runtime = Arc::new(Runtime { http, @@ -219,8 +223,6 @@ impl Bot { if let Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()).await { let task_rt = runtime.clone(); let task_bot = bot.clone(); - - let message_command_registry = message_command_registry.clone(); tokio::spawn(async move { match event { Event::Ready(event) => { @@ -246,24 +248,14 @@ impl Bot { } } Event::InteractionCreate(interaction) => { - // First, try message command registry - 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 - .on_interaction(&interaction, task_rt.clone()) - .await - .inspect_err( - |e| log::error!("An error occurred while handling on interaction for {handler:?}! {e:?}") - ) - .bye(); - } - if !matches!(e, crate::framework::interactions::InteractionError::InteractionTypeUnsent) { - log::error!("Message command dispatch error: {e:?}"); - } + for handler in &task_bot.on_interaction_handlers { + handler + .on_interaction(&interaction, task_rt.clone()) + .await + .inspect_err( + |e| log::error!("An error occurred while handling on interaction for {handler:?}! {e:?}") + ) + .bye(); } } Event::MessageCreate(message) => { diff --git a/src/framework/interactions/context.rs b/src/framework/interactions/context.rs index fa83e8d..14cf949 100644 --- a/src/framework/interactions/context.rs +++ b/src/framework/interactions/context.rs @@ -1,5 +1,4 @@ use std::sync::Arc; - use twilight_model::{ gateway::payload::incoming::InteractionCreate, http::interaction::{InteractionResponse, InteractionResponseType}, @@ -17,6 +16,11 @@ pub struct InteractionContext { } impl InteractionContext { + pub fn send( + self: &Arc, + ) -> crate::framework::interactions::messages::InteractionResponder { + crate::framework::interactions::messages::InteractionResponder::new(self.clone()) + } pub fn new(event: InteractionCreate, rt: Arc>, prefix: String) -> Self { Self { event, rt, prefix } } diff --git a/src/framework/interactions/message_command.rs b/src/framework/interactions/message_command.rs index d50a4c5..e7cbfbd 100644 --- a/src/framework/interactions/message_command.rs +++ b/src/framework/interactions/message_command.rs @@ -7,7 +7,7 @@ use twilight_model::gateway::payload::incoming::InteractionCreate; pub trait MessageCommand: Send + Sync + Debug { fn name(&self) -> &'static str; fn description(&self) -> &'static str { - "A message context menu command duh." + "A Payphone context menu command." } async fn execute( &self, diff --git a/src/framework/interactions/message_command_registry.rs b/src/framework/interactions/message_command_registry.rs index 19b6bb6..c183059 100644 --- a/src/framework/interactions/message_command_registry.rs +++ b/src/framework/interactions/message_command_registry.rs @@ -1,3 +1,4 @@ +// to be later integrated into interaction registry...currently component registry use crate::framework::{ interactions::{ InteractionError, context::InteractionContext, message_command::MessageCommand, @@ -11,7 +12,7 @@ use twilight_model::{ gateway::payload::incoming::InteractionCreate, }; -#[derive(Default)] +#[derive(Default, Debug)] pub struct MessageCommandRegistry { inner: HashMap>>, } diff --git a/src/framework/interactions/message_command_runner.rs b/src/framework/interactions/message_command_runner.rs new file mode 100644 index 0000000..b6b1c85 --- /dev/null +++ b/src/framework/interactions/message_command_runner.rs @@ -0,0 +1,32 @@ +use async_trait::async_trait; +use std::sync::Arc; +use twilight_model::gateway::payload::incoming::InteractionCreate; + +use crate::framework::{ + events::traits::{EventResult, OnInteractionHandler}, + interactions::message_command_registry::MessageCommandRegistry, + runtime::Runtime, +}; + +#[derive(Debug)] +pub struct MessageCommandRunner { + pub registry: Arc>, +} + +#[async_trait] +impl OnInteractionHandler + for MessageCommandRunner +{ + async fn on_interaction(&self, event: &InteractionCreate, rt: Arc>) -> EventResult { + if let Err(e) = self.registry.execute(event, rt).await { + // Only log errors that aren't "not a message command" + if !matches!( + e, + crate::framework::interactions::InteractionError::InteractionTypeUnsent + ) { + log::error!("Message command dispatch error: {e:?}"); + } + } + Ok(()) + } +} diff --git a/src/framework/interactions/messages.rs b/src/framework/interactions/messages.rs new file mode 100644 index 0000000..573ed8a --- /dev/null +++ b/src/framework/interactions/messages.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; +use twilight_model::http::interaction::{ + InteractionResponse, InteractionResponseData, InteractionResponseType, +}; + +use crate::framework::{ + interactions::{InteractionError, context::InteractionContext}, + runtime::Runtime, +}; + +pub struct InteractionResponder { + ctx: Arc>, +} + +impl InteractionResponder { + pub fn new(ctx: Arc>) -> Self { + Self { ctx } + } + + pub async fn content(&self, content: impl Into) -> Result<(), InteractionError> { + self.ctx + .rt + .http + .interaction(self.ctx.rt.app.id) + .create_response( + self.ctx.event.id, + &self.ctx.event.token, + &InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(InteractionResponseData { + content: Some(content.into()), + ..Default::default() + }), + }, + ) + .await + .map_err(|_| InteractionError::DeferringFailed)?; + Ok(()) + } +} diff --git a/src/framework/interactions/mod.rs b/src/framework/interactions/mod.rs index 4d8103e..4cd313d 100644 --- a/src/framework/interactions/mod.rs +++ b/src/framework/interactions/mod.rs @@ -1,5 +1,6 @@ +pub mod message_command_runner; +pub mod messages; use thiserror::Error; - pub mod components; pub mod context; pub mod message_command; @@ -25,4 +26,7 @@ pub enum InteractionError { #[error("No prefixes were returned for the event")] PrefixesVectorEmpty, + + #[error(transparent)] + DatabaseError(#[from] crate::database::models::DatabaseError), } diff --git a/src/groups/calls/commands.rs b/src/groups/calls/commands.rs index b1ff478..47db542 100644 --- a/src/groups/calls/commands.rs +++ b/src/groups/calls/commands.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; use scc::HashSet; +use std::sync::Arc; use twilight_model::id::Id; use crate::{ - database::models::{Call, Channel, Message, Report}, + database::models::{Call, Channel, Message, MessageRow, Report}, framework::commands::{Command, CommandError, context::CommandContext}, framework::interactions::{ InteractionError, context::InteractionContext, message_command::MessageCommand, @@ -46,26 +47,29 @@ impl MessageCommand for ReportMessageCommand { .ok_or(InteractionError::InteractionNotFound)?; // Fetch the message from the database - let message = Message::get_by_id(call_id, target_message_id, db) + let message_row = MessageRow::get_by_call_and_message_id(call_id, target_message_id, db) .await .map_err(|_| InteractionError::InteractionNotFound)? .ok_or(InteractionError::InteractionNotFound)?; - // 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 + // If the message is a reply, fetch the replied-to message and store in replying_to + let replying_to = if let Some(replying_to_id) = message_row.data.replying_to_id { + if let Ok(Some(reply_msg_row)) = + MessageRow::get_by_call_and_message_id(call_id, replying_to_id as u64, db).await { - message_metadata.message_replied_to_metadata = - Some(Box::new(reply_msg.message_metadata)); + Some(reply_msg_row.data) + } else { + None } - } + } else { + None + }; - // Build the report using the new UDT-based message metadata + // Build the report using the new flat message and replying_to let report = Report { report_id: get_random_u64(), - message: message_metadata, + message: message_row.data, + replying_to, created_at: time::OffsetDateTime::now_utc(), reviewed_at: None, created_by_ids: vec![ctx.event.author_id().map(|id| id.get()).unwrap_or(0)], @@ -77,24 +81,13 @@ impl MessageCommand for ReportMessageCommand { report .insert(db) .await - .map_err(|_| InteractionError::DeferringFailed)?; + .map_err(InteractionError::DatabaseError)?; // Respond to the user - ctx.rt.http - .interaction(ctx.rt.app.id) - .create_response( - ctx.event.id, - &ctx.event.token, - &twilight_model::http::interaction::InteractionResponse { - kind: twilight_model::http::interaction::InteractionResponseType::ChannelMessageWithSource, - data: Some(twilight_model::http::interaction::InteractionResponseData { - content: Some("Message has been reported to the moderators.".to_string()), - ..Default::default() - }), - }, - ) - .await - .map_err(|_| InteractionError::DeferringFailed)?; + Arc::new(ctx) + .send() + .content("Message has been reported to the moderators.") + .await?; Ok(()) } } diff --git a/src/main.rs b/src/main.rs index af8f3b2..b4b09c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,13 +107,7 @@ use std::{ sync::mpsc, }; -use axum::{Router, response::Redirect, routing}; -use clap::Parser; -use notify::{Event, EventKind, Watcher}; -use tokio::net::TcpListener; -use tower_http::services::ServeDir; -use twilight_gateway::Intents; - +use crate::groups::calls::commands::ReportMessageCommand; use crate::{ cli::{Args, Commands}, framework::bot::Bot, @@ -128,6 +122,12 @@ use crate::{ }, state::State, }; +use axum::{Router, response::Redirect, routing}; +use clap::Parser; +use notify::{Event, EventKind, Watcher}; +use tokio::net::TcpListener; +use tower_http::services::ServeDir; +use twilight_gateway::Intents; mod cli; mod constants; @@ -185,7 +185,7 @@ async fn start(token: String, database_addr: String) { .command(CallCommand::new()) .command(HangupCommand) .component(HelpGoBackButton) - .message_command(crate::groups::calls::commands::ReportMessageCommand) + .message_command(ReportMessageCommand) .on_ready(ReadyHandler) .on_reconnect(ReconnectHandler) .on_message(MessageRedirector) diff --git a/src/state.rs b/src/state.rs index c7f44d1..7969184 100644 --- a/src/state.rs +++ b/src/state.rs @@ -19,6 +19,7 @@ //! ``` use scylla::client::session::Session; +#[derive(Debug)] pub struct State { pub db: Session,