push 1 checkpoiint *holy hell i sure hope it works*

This commit is contained in:
Anish Nayak
2025-08-12 18:01:01 +01:00
parent 79f0ca08b2
commit 9549e70593
8 changed files with 310 additions and 67 deletions
+21 -55
View File
@@ -6,6 +6,23 @@ CREATE KEYSPACE IF NOT EXISTS payphone
'enabled': false
};
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<TEXT>,
sticker BIGINT,
propagation_message_id BIGINT,
propagation_channel_id BIGINT,
replying_to_id BIGINT,
is_deleted BOOLEAN,
is_edited BOOLEAN,
is_censored BOOLEAN
);
CREATE TABLE IF NOT EXISTS payphone.users (
user_id BIGINT PRIMARY KEY,
@@ -64,25 +81,7 @@ CREATE TABLE IF NOT EXISTS payphone.participants (
CREATE TABLE IF NOT EXISTS payphone.messages (
call_id BIGINT,
message_id BIGINT,
author_id BIGINT,
author_name TEXT,
author_avatar_url TEXT,
author_mask ASCII,
content TEXT,
attachments LIST<TEXT>,
sticker BIGINT,
propagation_message_id BIGINT,
propagation_channel_id BIGINT,
replying_to_id BIGINT,
is_deleted BOOLEAN,
is_edited BOOLEAN,
is_censored BOOLEAN,
message_metadata FROZEN<payphone.message_metadata>,
PRIMARY KEY (call_id, message_id)
);
@@ -95,61 +94,28 @@ CREATE TABLE IF NOT EXISTS payphone.servers (
CREATE TABLE IF NOT EXISTS payphone.reports (
report_id BIGINT,
message_author_id BIGINT,
message_author_name TEXT,
message_author_avatar_url TEXT,
message_author_mask ASCII,
message_id BIGINT,
message_content TEXT,
message_attachments LIST<TEXT>,
message_sticker BIGINT,
message_replying_to_id BIGINT,
message_replying_to_content TEXT,
message_replying_to_attachments LIST<TEXT>,
message_replying_to_sticker BIGINT,
message FROZEN<payphone.message_metadata>,
created_at TIMESTAMP,
reviewed_at TIMESTAMP,
created_by_ids SET<BIGINT>,
reviewed_by_id BIGINT,
resolution ASCII,
PRIMARY KEY (message_author_id, report_id)
PRIMARY KEY (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_id BIGINT,
message_content TEXT,
message_attachments LIST<TEXT>,
message_sticker BIGINT,
message_replying_to_id BIGINT,
message_replying_to_content TEXT,
message_replying_to_attachments LIST<TEXT>,
message_replying_to_sticker BIGINT,
message FROZEN<payphone.message_metadata>,
reason TEXT,
created_by_id BIGINT,
created_at TIMESTAMP,
expires_at TIMESTAMP,
duration ASCII,
PRIMARY KEY (user_id)
);
+101
View File
@@ -1,3 +1,21 @@
use scylla::{SerializeValue, DeserializeValue};
#[derive(Debug, Clone, SerializeValue, DeserializeValue)]
pub struct MessageMetadata {
pub call_id: i64,
pub author_id: i64,
pub author_name: Option<String>,
pub author_avatar_url: Option<String>,
pub author_mask: Option<String>,
pub content: Option<String>,
pub attachments: Option<Vec<String>>,
pub sticker: Option<i64>,
pub propagation_message_id: Option<i64>,
pub propagation_channel_id: Option<i64>,
pub replying_to_id: Option<i64>,
pub is_deleted: Option<bool>,
pub is_edited: Option<bool>,
pub is_censored: Option<bool>,
}
use scylla::{
client::session::Session,
deserialize::row::DeserializeRow,
@@ -190,6 +208,89 @@ impl From<TwilightUser> for User {
}
}
// --- BEGIN: Payphone Message and Report Models ---
#[derive(Debug, Clone)]
pub struct Message {
pub call_id: u64,
pub message_id: u64,
pub message_metadata: MessageMetadata,
}
impl Message {
pub async fn get_by_id(call_id: u64, message_id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
let call_id = u64_to_i64(call_id);
let message_id = u64_to_i64(message_id);
let result = session
.query_unpaged(
r#"
SELECT call_id, message_id, message_metadata
FROM payphone.messages
WHERE call_id = ? AND message_id = ?
LIMIT 1;
"#,
(call_id, message_id),
)
.await
.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,
})),
Err(error) => match error {
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => Err(DatabaseError::DeserializationFailed),
SingleRowError::UnexpectedRowCount(_) => Ok(None),
},
}
}
}
#[derive(Debug, Clone)]
pub struct Report {
pub report_id: u64,
pub message: MessageMetadata,
pub created_at: OffsetDateTime,
pub reviewed_at: Option<OffsetDateTime>,
pub created_by_ids: Vec<u64>,
pub reviewed_by_id: Option<u64>,
pub resolution: Option<String>,
}
impl Report {
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.reports (
report_id,
message,
created_at,
reviewed_at,
created_by_ids,
reviewed_by_id,
resolution
) 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::<Vec<i64>>(),
self.reviewed_by_id.map(u64_to_i64),
self.resolution.as_ref(),
),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Server {
pub id: u64,
+31 -10
View File
@@ -90,12 +90,16 @@ where
}
}
use crate::framework::interactions::message_command::MessageCommand;
use crate::framework::interactions::message_command_registry::MessageCommandRegistry;
pub struct Bot<T: Send + Sync = ()> {
state: Arc<T>,
prefixes: Arc<dyn IntoPrefixes<T> + Send + Sync>,
intents: Intents,
commands: Vec<Arc<dyn Command<T>>>,
components: Vec<Arc<dyn Component<T>>>,
message_commands: Vec<Arc<dyn MessageCommand<T>>>,
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<T>>>,
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<T>>>,
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<T>>>,
@@ -115,6 +119,7 @@ impl<T: 'static + Send + Sync> Bot<T> {
intents: Intents::empty(),
commands: Vec::new(),
components: Vec::new(),
message_commands: Vec::new(),
on_ready_handlers: Vec::new(),
on_reconnect_handlers: Vec::new(),
on_interaction_handlers: Vec::new(),
@@ -126,6 +131,10 @@ impl<T: 'static + Send + Sync> Bot<T> {
on_server_delete_handlers: Vec::new(),
}
}
pub fn message_command(mut self, command: impl MessageCommand<T> + 'static) -> Self {
self.message_commands.push(Arc::new(command));
self
}
pub fn intent(mut self, intent: Intents) -> Self {
self.intents.insert(intent);
@@ -198,17 +207,21 @@ impl<T: 'static + Send + Sync> Bot<T> {
.await
.inspect_err(|e| log::error!("Could not parse application data! {e:?}"))
.map_err(|_| RuntimeError::ApplicationFetchingFailed)?;
let registry = ComponentRegistry::new();
let registry = ComponentRegistry::new();
for component in &self.components {
let component_id = component.custom_id();
registry.register(
component_id.expect("The component registered by default must have a custom ID"),
component.clone(),
);
}
let message_command_registry = std::sync::Arc::new(MessageCommandRegistry::new());
for command in &self.message_commands {
message_command_registry.register(command.name(), command.clone());
}
let bot = Arc::new(self);
let runtime = Arc::new(Runtime {
@@ -227,6 +240,7 @@ impl<T: 'static + Send + Sync> Bot<T> {
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) => {
@@ -252,14 +266,21 @@ impl<T: 'static + Send + Sync> Bot<T> {
}
}
Event::InteractionCreate(interaction) => {
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();
// 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:?}");
}
}
}
Event::MessageCreate(message) => {
@@ -0,0 +1,13 @@
use std::fmt::Debug;
use async_trait::async_trait;
use twilight_model::gateway::payload::incoming::InteractionCreate;
use crate::framework::interactions::{InteractionError, context::InteractionContext};
#[async_trait]
pub trait MessageCommand<T>: Send + Sync + Debug {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str {
"A message context menu command duh."
}
async fn execute(&self, ctx: InteractionContext<T>, target_message_id: u64) -> Result<(), InteractionError>;
}
@@ -0,0 +1,76 @@
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},
runtime::Runtime,
};
#[derive(Default)]
pub struct MessageCommandRegistry<T> {
inner: HashMap<String, Arc<dyn MessageCommand<T>>>,
}
impl<T> MessageCommandRegistry<T>
where
T: Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
inner: Default::default(),
}
}
pub fn register(&self, name: impl ToString, command: Arc<dyn MessageCommand<T>>) {
self.inner.upsert(name.to_string(), command);
}
pub fn unregister(&self, name: impl ToString) {
self.inner.remove(&name.to_string());
}
pub async fn execute(
&self,
event: &InteractionCreate,
rt: Arc<Runtime<T>>,
) -> Result<(), InteractionError> {
if let Some(data) = &event.data {
if let InteractionData::ApplicationCommand(data) = data {
if data.kind == CommandType::Message {
if let Some(command) = self.inner.get_async(&data.name).await {
let prefixes = rt
.get_prefixes(
event.guild_id.map(|id| id.get()),
event.author_id().map(|id| id.get()),
)
.await;
let prefix = prefixes
.first()
.ok_or(InteractionError::PrefixesVectorEmpty)?;
// Message commands have a resolved target_id
if let Some(target_id) = data.target_id {
command
.execute(
InteractionContext::new(event.clone(), rt.clone(), prefix.to_string()),
target_id.get(),
)
.await
} else {
Err(InteractionError::InteractionNotFound)
}
} else {
Err(InteractionError::InteractionNotFound)
}
} else {
Err(InteractionError::InteractionTypeUnsent)
}
} else {
Err(InteractionError::InteractionTypeUnsent)
}
} else {
Err(InteractionError::InteractionTypeUnsent)
}
}
}
+2
View File
@@ -4,6 +4,8 @@ pub mod components;
pub mod context;
pub mod registry;
pub mod runner;
pub mod message_command;
pub mod message_command_registry;
#[derive(Debug, Error)]
pub enum InteractionError {
+65 -2
View File
@@ -3,11 +3,74 @@ use scc::HashSet;
use twilight_model::id::Id;
use crate::{
database::models::{Call, Channel},
database::models::{Call, Channel, Message, Report},
framework::commands::{Command, CommandError, context::CommandContext},
framework::interactions::{message_command::MessageCommand, InteractionError, context::InteractionContext},
state::State,
utils::Bye,
utils::{Bye, get_random_u64},
};
#[derive(Debug, Default)]
pub struct ReportMessageCommand;
#[async_trait]
impl MessageCommand<State> for ReportMessageCommand {
fn name(&self) -> &'static str {
"Report Message"
}
fn description(&self) -> &'static str {
"Report a message to the moderators."
}
async fn execute(&self, ctx: InteractionContext<State>, 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())
.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)?;
// Fetch the message from the database
let message = Message::get_by_id(call_id, target_message_id, db)
.await
.map_err(|_| InteractionError::InteractionNotFound)?
.ok_or(InteractionError::InteractionNotFound)?;
// Build the report using the new UDT-based message metadata
let report = Report {
report_id: get_random_u64(),
message: message.message_metadata.clone(),
created_at: time::OffsetDateTime::now_utc(),
reviewed_at: None,
created_by_ids: vec![ctx.event.author_id().map(|id| id.get()).unwrap_or(0)],
reviewed_by_id: None,
resolution: None,
};
// Insert the report
report.insert(db).await.map_err(|_| InteractionError::DeferringFailed)?;
// 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)?;
Ok(())
}
}
#[derive(Default, Debug)]
pub struct CallCommand {
+1
View File
@@ -185,6 +185,7 @@ async fn start(token: String, database_addr: String) {
.command(CallCommand::new())
.command(HangupCommand)
.component(HelpGoBackButton)
.message_command(crate::groups::calls::commands::ReportMessageCommand)
.on_ready(ReadyHandler)
.on_reconnect(ReconnectHandler)
.on_message(MessageRedirector)