push 1 checkpoiint *holy hell i sure hope it works*
This commit is contained in:
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user