Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
458f89a3a3 | ||
|
|
4df7c8ce7d | ||
|
|
28f8c8e702 |
@@ -6,28 +6,6 @@ CREATE KEYSPACE IF NOT EXISTS payphone
|
||||
'enabled': false
|
||||
};
|
||||
|
||||
CREATE TYPE IF NOT EXISTS payphone.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,
|
||||
|
||||
is_deleted BOOLEAN,
|
||||
is_edited BOOLEAN,
|
||||
is_censored BOOLEAN,
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.users (
|
||||
user_id BIGINT PRIMARY KEY,
|
||||
|
||||
@@ -86,8 +64,24 @@ CREATE TABLE IF NOT EXISTS payphone.participants (
|
||||
CREATE TABLE IF NOT EXISTS payphone.messages (
|
||||
call_id BIGINT,
|
||||
message_id BIGINT,
|
||||
author_id BIGINT,
|
||||
|
||||
data FROZEN<payphone.message>,
|
||||
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,
|
||||
|
||||
PRIMARY KEY (call_id, message_id)
|
||||
);
|
||||
@@ -98,11 +92,24 @@ CREATE TABLE IF NOT EXISTS payphone.servers (
|
||||
name TEXT,
|
||||
prefix TEXT,
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payphone.reports (
|
||||
report_id BIGINT,
|
||||
|
||||
message FROZEN<payphone.message>,
|
||||
replying_to FROZEN<payphone.message>,
|
||||
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,
|
||||
|
||||
created_at TIMESTAMP,
|
||||
reviewed_at TIMESTAMP,
|
||||
@@ -125,8 +132,15 @@ CREATE TABLE IF NOT EXISTS payphone.user_incidents (
|
||||
call_id BIGINT,
|
||||
report_id BIGINT,
|
||||
|
||||
message FROZEN<payphone.message>,
|
||||
replying_to FROZEN<payphone.message>,
|
||||
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,
|
||||
|
||||
reason TEXT,
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use scylla::{
|
||||
DeserializeValue, SerializeValue,
|
||||
client::session::Session,
|
||||
deserialize::row::DeserializeRow,
|
||||
errors::{ExecutionError, SingleRowError},
|
||||
@@ -191,218 +190,6 @@ impl From<TwilightUser> for User {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, SerializeValue, DeserializeValue)]
|
||||
pub struct Message {
|
||||
pub id: i64,
|
||||
pub author_id: i64,
|
||||
pub author_name: String,
|
||||
pub author_avatar_url: String,
|
||||
pub author_mask: Option<String>,
|
||||
pub content: 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: bool,
|
||||
pub is_edited: bool,
|
||||
pub is_censored: bool,
|
||||
}
|
||||
|
||||
impl<'a> Object<'a> for Message {
|
||||
type Row = (
|
||||
i64,
|
||||
i64,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
Option<Vec<String>>,
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
bool,
|
||||
bool,
|
||||
bool,
|
||||
);
|
||||
fn from_row(row: Self::Row) -> Self {
|
||||
Self {
|
||||
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,
|
||||
is_deleted: row.11,
|
||||
is_edited: row.12,
|
||||
is_censored: row.13,
|
||||
}
|
||||
}
|
||||
fn to_row(value: Self) -> Self::Row {
|
||||
(
|
||||
value.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.is_deleted,
|
||||
value.is_edited,
|
||||
value.is_censored,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MessageRow {
|
||||
pub call_id: u64,
|
||||
pub message_id: u64,
|
||||
pub data: Message,
|
||||
}
|
||||
|
||||
impl MessageRow {
|
||||
pub async fn get_by_call_and_message_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, data
|
||||
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::<<MessageRow as Object>::Row>() {
|
||||
Ok(row) => Ok(Some(<MessageRow as Object>::from_row(row))),
|
||||
Err(error) => match error {
|
||||
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
|
||||
Err(DatabaseError::DeserializationFailed)
|
||||
}
|
||||
SingleRowError::UnexpectedRowCount(_) => Ok(None),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Object<'a> for Report {
|
||||
type Row = (
|
||||
i64, // report_id
|
||||
Message,
|
||||
Option<Message>,
|
||||
OffsetDateTime,
|
||||
Option<OffsetDateTime>,
|
||||
Vec<i64>,
|
||||
Option<i64>,
|
||||
Option<String>,
|
||||
);
|
||||
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: Message,
|
||||
pub replying_to: Option<Message>,
|
||||
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<'a> Object<'a> for MessageRow {
|
||||
type Row = (i64, i64, <Message as Object<'a>>::Row);
|
||||
fn from_row(row: Self::Row) -> Self {
|
||||
Self {
|
||||
call_id: i64_to_u64(row.0),
|
||||
message_id: i64_to_u64(row.1),
|
||||
data: <Message as Object>::from_row(row.2),
|
||||
}
|
||||
}
|
||||
fn to_row(value: Self) -> Self::Row {
|
||||
(
|
||||
u64_to_i64(value.call_id),
|
||||
u64_to_i64(value.message_id),
|
||||
<Message as Object>::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 (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
row,
|
||||
)
|
||||
.await
|
||||
.map_err(DatabaseError::QueryError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Server {
|
||||
pub id: u64,
|
||||
|
||||
+46
-38
@@ -1,24 +1,24 @@
|
||||
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 async_trait::async_trait;
|
||||
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")]
|
||||
@@ -37,14 +37,24 @@ pub trait IntoPrefixes<T> {
|
||||
|
||||
#[async_trait]
|
||||
impl<T> IntoPrefixes<T> for String {
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
async fn get(
|
||||
&self,
|
||||
_: &Runtime<T>,
|
||||
_: Option<u64>,
|
||||
_: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
vec![self.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> IntoPrefixes<T> for &str {
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
async fn get(
|
||||
&self,
|
||||
_: &Runtime<T>,
|
||||
_: Option<u64>,
|
||||
_: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
vec![self.to_string()]
|
||||
}
|
||||
}
|
||||
@@ -54,7 +64,12 @@ impl<T: Send + Sync + 'static, const N: usize, S: Send + Sync + 'static> IntoPre
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
async fn get(&self, _: &Runtime<S>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
async fn get(
|
||||
&self,
|
||||
_: &Runtime<S>,
|
||||
_: Option<u64>,
|
||||
_: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
self.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
}
|
||||
@@ -65,20 +80,23 @@ where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = Vec<String>> + Send,
|
||||
{
|
||||
async fn get(&self, _: &Runtime<T>, _: Option<u64>, _: Option<u64>) -> Vec<String> {
|
||||
async fn get(
|
||||
&self,
|
||||
_: &Runtime<T>,
|
||||
_: Option<u64>,
|
||||
_: Option<u64>,
|
||||
) -> Vec<String> {
|
||||
(self)().await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Bot<T: Send + Sync + Debug = ()> {
|
||||
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>>>,
|
||||
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<T>>>,
|
||||
message_commands: Vec<Arc<dyn MessageCommand<T>>>,
|
||||
message_command_registry: Arc<MessageCommandRegistry<T>>,
|
||||
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<T>>>,
|
||||
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<T>>>,
|
||||
on_message_handlers: Vec<Arc<dyn OnMessageHandler<T>>>,
|
||||
@@ -89,22 +107,17 @@ pub struct Bot<T: Send + Sync + Debug = ()> {
|
||||
on_server_delete_handlers: Vec<Arc<dyn OnServerDeleteHandler<T>>>,
|
||||
}
|
||||
|
||||
impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
impl<T: 'static + Send + Sync> Bot<T> {
|
||||
pub fn new(state: T, prefixes: impl IntoPrefixes<T> + Send + Sync + 'static) -> Self {
|
||||
let message_command_registry = Arc::new(MessageCommandRegistry::new());
|
||||
Self {
|
||||
state: Arc::new(state),
|
||||
prefixes: Arc::new(prefixes),
|
||||
intents: Intents::empty(),
|
||||
commands: Vec::new(),
|
||||
components: Vec::new(),
|
||||
message_commands: Vec::new(),
|
||||
message_command_registry: message_command_registry.clone(),
|
||||
on_ready_handlers: Vec::new(),
|
||||
on_reconnect_handlers: Vec::new(),
|
||||
on_interaction_handlers: vec![Arc::new(MessageCommandRunner {
|
||||
registry: message_command_registry,
|
||||
})],
|
||||
on_interaction_handlers: Vec::new(),
|
||||
on_message_handlers: vec![Arc::new(CommandRunner)],
|
||||
on_message_edit_handlers: Vec::new(),
|
||||
on_message_delete_handlers: Vec::new(),
|
||||
@@ -114,13 +127,6 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_command(mut self, command: impl MessageCommand<T> + 'static) -> Self {
|
||||
let arc_command = Arc::new(command);
|
||||
self.message_command_registry.register(arc_command.name(), arc_command.clone());
|
||||
self.message_commands.push(arc_command);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn intent(mut self, intent: Intents) -> Self {
|
||||
self.intents.insert(intent);
|
||||
self
|
||||
@@ -192,10 +198,11 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
.await
|
||||
.inspect_err(|e| log::error!("Could not parse application data! {e:?}"))
|
||||
.map_err(|_| RuntimeError::ApplicationFetchingFailed)?;
|
||||
|
||||
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(),
|
||||
@@ -219,6 +226,7 @@ impl<T: 'static + Send + Sync + Debug> Bot<T> {
|
||||
if let Some(Ok(event)) = gateway.next_event(EventTypeFlags::all()).await {
|
||||
let task_rt = runtime.clone();
|
||||
let task_bot = bot.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match event {
|
||||
Event::Ready(event) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use twilight_model::{
|
||||
gateway::payload::incoming::InteractionCreate,
|
||||
http::interaction::{InteractionResponse, InteractionResponseType},
|
||||
@@ -6,7 +7,6 @@ use twilight_model::{
|
||||
};
|
||||
|
||||
use crate::framework::{
|
||||
interactions::messages::InteractionResponder,
|
||||
commands::messages::OriginalMessageUpdater, interactions::InteractionError, runtime::Runtime,
|
||||
};
|
||||
|
||||
@@ -17,11 +17,6 @@ pub struct InteractionContext<T> {
|
||||
}
|
||||
|
||||
impl<T> InteractionContext<T> {
|
||||
pub fn send(
|
||||
self: &Arc<Self>,
|
||||
) -> InteractionResponder<T> {
|
||||
InteractionResponder::new(self.clone())
|
||||
}
|
||||
pub fn new(event: InteractionCreate, rt: Arc<Runtime<T>>, prefix: String) -> Self {
|
||||
Self { event, rt, prefix }
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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<T>: Send + Sync + Debug {
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"A Payphone context menu command."
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: InteractionContext<T>,
|
||||
target_message_id: u64,
|
||||
) -> Result<(), InteractionError>;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// to be later integrated into interaction registry...currently component registry
|
||||
use crate::framework::{
|
||||
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, Debug)]
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use crate::framework::interactions::InteractionError::InteractionTypeUnsent;
|
||||
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<T> {
|
||||
pub registry: Arc<MessageCommandRegistry<T>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Sync + std::fmt::Debug + 'static> OnInteractionHandler<T>
|
||||
for MessageCommandRunner<T>
|
||||
{
|
||||
async fn on_interaction(&self, event: &InteractionCreate, rt: Arc<Runtime<T>>) -> EventResult {
|
||||
if let Err(e) = self.registry.execute(event, rt).await {
|
||||
// Only log errors that aren't "not a message command"
|
||||
if !matches!(e, InteractionTypeUnsent) {
|
||||
log::error!("Message command dispatch error: {e:?}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use twilight_model::http::interaction::{
|
||||
InteractionResponse, InteractionResponseData, InteractionResponseType,
|
||||
};
|
||||
|
||||
use crate::framework::{
|
||||
interactions::{InteractionError, context::InteractionContext},
|
||||
runtime::Runtime,
|
||||
};
|
||||
|
||||
pub struct InteractionResponder<T> {
|
||||
ctx: Arc<InteractionContext<T>>,
|
||||
}
|
||||
|
||||
impl<T> InteractionResponder<T> {
|
||||
pub fn new(ctx: Arc<InteractionContext<T>>) -> Self {
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
pub async fn content(&self, content: impl Into<String>) -> 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::RespondingFailed)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
pub mod message_command_runner;
|
||||
pub mod messages;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod components;
|
||||
pub mod context;
|
||||
pub mod message_command;
|
||||
pub mod message_command_registry;
|
||||
pub mod registry;
|
||||
pub mod runner;
|
||||
|
||||
@@ -24,12 +21,6 @@ pub enum InteractionError {
|
||||
#[error("The original message was attempted to be edited, but it failed")]
|
||||
OriginalMessageEditingFailed,
|
||||
|
||||
#[error("Responding to the interaction failed")]
|
||||
RespondingFailed,
|
||||
|
||||
#[error("No prefixes were returned for the event")]
|
||||
PrefixesVectorEmpty,
|
||||
|
||||
#[error(transparent)]
|
||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||
}
|
||||
|
||||
@@ -1,98 +1,14 @@
|
||||
use async_trait::async_trait;
|
||||
use scc::HashSet;
|
||||
use std::sync::Arc;
|
||||
use twilight_model::id::Id;
|
||||
|
||||
use crate::{
|
||||
database::models::{Call, Channel, Message, MessageRow, Report},
|
||||
database::models::{Call, Channel},
|
||||
framework::commands::{Command, CommandError, context::CommandContext},
|
||||
framework::interactions::{
|
||||
InteractionError, context::InteractionContext, message_command::MessageCommand,
|
||||
},
|
||||
state::State,
|
||||
utils::{Bye, get_random_u64},
|
||||
utils::Bye,
|
||||
};
|
||||
|
||||
#[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_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 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
|
||||
{
|
||||
Some(reply_msg_row.data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build the report using the new flat message and replying_to
|
||||
let report = Report {
|
||||
report_id: get_random_u64(),
|
||||
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)],
|
||||
reviewed_by_id: None,
|
||||
resolution: None,
|
||||
};
|
||||
|
||||
// Insert the report
|
||||
report
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(InteractionError::DatabaseError)?;
|
||||
|
||||
// Respond to the user
|
||||
Arc::new(ctx)
|
||||
.send()
|
||||
.content("Message has been reported to the moderators.")
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct CallCommand {
|
||||
/// A set of channel IDs that are currently searching for a call. This works as a lock for two
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use linkify::LinkFinder;
|
||||
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},
|
||||
discord::images,
|
||||
framework::{
|
||||
events::traits::{EventResult, OnMessageHandler},
|
||||
runtime::Runtime,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
slap that ass
|
||||
@@ -62,11 +62,11 @@ pub fn escape_message(event: &MessageCreate) -> String {
|
||||
message = message.replace(url, &"\\#".repeat(url.len()));
|
||||
}
|
||||
|
||||
let normalized = normalize(message.clone());
|
||||
let (censored, rules) = moderate_message(&message);
|
||||
|
||||
log::debug!("Escaped and normalized message: {normalized:?}");
|
||||
log::debug!("Escaped and normalized message: {censored:?}");
|
||||
|
||||
message.sanitize(false)
|
||||
censored
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -113,13 +113,16 @@ pub fn normalize(message: String) -> NormalizedMessage {
|
||||
for char in unidecoded.chars() {
|
||||
if char.is_alphanumeric() {
|
||||
normalized.push(char);
|
||||
mappings.push(i);
|
||||
} else {
|
||||
normalized.push(' ');
|
||||
}
|
||||
|
||||
mappings.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
NormalizedMessage {
|
||||
message: normalized,
|
||||
message: normalized.to_ascii_lowercase(),
|
||||
mappings,
|
||||
}
|
||||
}
|
||||
@@ -127,8 +130,12 @@ pub fn normalize(message: String) -> NormalizedMessage {
|
||||
fn regex_from_phrase(phrase: &str) -> String {
|
||||
let mut pattern = String::new();
|
||||
|
||||
for ch in phrase.replace(" ", "").chars() {
|
||||
pattern.push_str(&format!("{ch}+\\s*"));
|
||||
for (i, ch) in phrase.replace(" ", "").chars().enumerate() {
|
||||
if i == phrase.len() - 1 {
|
||||
pattern.push_str(&format!("{ch}+"));
|
||||
} else {
|
||||
pattern.push_str(&format!("{ch}+\\s*"));
|
||||
}
|
||||
}
|
||||
|
||||
let pattern = format!("\\b{pattern}\\b");
|
||||
@@ -170,17 +177,41 @@ pub fn censor_message(
|
||||
) -> String {
|
||||
let mut censored = original.to_string();
|
||||
|
||||
for (findings, rule) in analysis {
|
||||
// Collect all ranges to censor
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
for (findings, _) in analysis {
|
||||
for finding in findings {
|
||||
while let Some(start) = normalized.message.find(finding) {
|
||||
let end = start + finding.len();
|
||||
for (index, _) in normalized.message.match_indices(finding) {
|
||||
let start_index = normalized.mappings.get(index).cloned().unwrap_or(0);
|
||||
let end_index = normalized.mappings.get(index + finding.len()).cloned().unwrap_or(original.len());
|
||||
|
||||
ranges.push((start_index, end_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort ranges in reverse order to avoid shifting indices
|
||||
ranges.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
|
||||
for (start_index, end_index) in ranges {
|
||||
if start_index < end_index && end_index <= censored.len() {
|
||||
censored.replace_range(start_index..end_index, "[censored]");
|
||||
}
|
||||
}
|
||||
|
||||
censored
|
||||
}
|
||||
|
||||
pub fn moderate_message(original: &str) -> (String, Vec<Arc<dyn Rule>>) {
|
||||
let normalized = normalize(original.to_string());
|
||||
|
||||
let analysis = analyze_message(&normalized.message);
|
||||
let censored = censor_message(original, normalized, &analysis);
|
||||
|
||||
(censored, analysis.iter().map(|(_, r)| r.clone()).collect())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Action {
|
||||
BlockAndPenalty,
|
||||
|
||||
+7
-8
@@ -107,9 +107,15 @@ 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::{
|
||||
cli::{Args, Commands},
|
||||
groups::calls::commands::ReportMessageCommand,
|
||||
framework::bot::Bot,
|
||||
groups::{
|
||||
about::{commands::HelpCommand, components::HelpGoBackButton},
|
||||
@@ -122,12 +128,6 @@ 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,6 @@ async fn start(token: String, database_addr: String) {
|
||||
.command(CallCommand::new())
|
||||
.command(HangupCommand)
|
||||
.component(HelpGoBackButton)
|
||||
.message_command(ReportMessageCommand)
|
||||
.on_ready(ReadyHandler)
|
||||
.on_reconnect(ReconnectHandler)
|
||||
.on_message(MessageRedirector)
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
//! ```
|
||||
|
||||
use scylla::client::session::Session;
|
||||
#[derive(Debug)]
|
||||
|
||||
pub struct State {
|
||||
pub db: Session,
|
||||
|
||||
Reference in New Issue
Block a user