From a1495e6731468b4196b822c0aa441547c95e593f Mon Sep 17 00:00:00 2001 From: Condorra Date: Thu, 27 Jul 2023 21:38:29 +1000 Subject: [PATCH] Implement "report abuse" command. --- blastmud_game/src/db.rs | 51 ++++++++++++++++++- .../src/message_handler/new_session.rs | 18 +++++-- .../src/message_handler/user_commands.rs | 3 +- .../message_handler/user_commands/report.rs | 41 +++++++++++++++ schema/schema.sql | 14 ++++- 5 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/report.rs diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 7f6e871..e84d89d 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -193,7 +193,7 @@ impl DBPool { let conn = self.get_conn().await?; Ok(conn .query( - "SELECT item, session, listener, message FROM sendqueue ORDER BY item ASC LIMIT 10", + "SELECT item, session, listener, message FROM sendqueue WHERE sent_at IS NULL ORDER BY item ASC LIMIT 10", &[], ) .await? @@ -204,7 +204,18 @@ impl DBPool { pub async fn delete_from_sendqueue(self: &DBPool, item: &SendqueueItem) -> DResult<()> { let conn = self.get_conn().await?; - conn.execute("DELETE FROM sendqueue WHERE item=$1", &[&item.item]) + conn.execute( + "UPDATE sendqueue SET sent_at = NOW() WHERE item=$1", + &[&item.item], + ) + .await?; + conn.execute("DELETE FROM sendqueue WHERE item IN (\ + WITH item_rows AS (\ + SELECT item, row_number() OVER (ORDER BY sent_at DESC) AS rn FROM sendqueue + WHERE sent_at IS NOT NULL AND \ + session = $1 AND listener = $2) \ + SELECT item FROM item_rows WHERE rn > 80 \ + )", &[&item.session.session, &item.session.listener]) .await?; Ok(()) } @@ -1670,6 +1681,42 @@ impl DBTrans { .collect()) } + pub async fn sendqueue_to_abusereport( + &self, + uuid: &Uuid, + username: &str, + session: &ListenerSession, + ) -> DResult<()> { + self.pg_trans()? + .execute( + "INSERT INTO abuselog (id, triggered_by, logdata, expires) \ + VALUES ($1, $2, (\ + SELECT json_build_object(\ + 'sendqueue', json_build_array(array_agg(json_build_object(\ + 'message', message, 'sent_at', sent_at)))) FROM sendqueue WHERE \ + listener = $3 AND session = $4), \ + NOW() + INTERVAL '60 days')", + &[&uuid, &username, &session.listener, &session.session], + ) + .await?; + Ok(()) + } + + pub async fn clean_and_count_abusereports(&self, username: &str) -> DResult { + let trans = self.pg_trans()?; + trans + .execute("DELETE FROM abuselog WHERE expires < NOW()", &[]) + .await?; + Ok(trans + .query_one( + "SELECT COUNT(*) AS n FROM abuselog \ + WHERE triggered_by = $1", + &[&username], + ) + .await? + .get("n")) + } + pub async fn commit(mut self: Self) -> DResult<()> { let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); if let Some(trans) = trans_opt { diff --git a/blastmud_game/src/message_handler/new_session.rs b/blastmud_game/src/message_handler/new_session.rs index 4e2599c..104ef7b 100644 --- a/blastmud_game/src/message_handler/new_session.rs +++ b/blastmud_game/src/message_handler/new_session.rs @@ -1,9 +1,9 @@ -use crate::message_handler::ListenerSession; -use crate::DResult; use crate::db::DBPool; +use crate::message_handler::ListenerSession; +use crate::models::session::Session; +use crate::DResult; use ansi::ansi; use std::default::Default; -use crate::models::session::Session; // ANSI art version of the symbol we are legally required to display per: // https://www.legislation.gov.au/Details/F2017C00102 @@ -35,7 +35,14 @@ const AUS_RATING_SYMBOL: &'static str = "\x1b[48;5;234m \x1b[38;5;252;48;5;235m \x1b[49;38;5;233m▀▀\x1b[49;38;5;235m▀\x1b[49;38;5;242m▀\x1b[49;38;5;249m▀\x1b[49;38;5;254m▀\x1b[49;38;5;255m▀\x1b[49;38;5;253m▀▀\x1b[49;38;5;255m▀\x1b[49;38;5;254m▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀\x1b[49;38;5;255m▀\x1b[49;38;5;254m▀\x1b[49;38;5;7m▀\x1b[49;38;5;243m▀\x1b[49;38;5;235m▀\x1b[49;38;5;233m▀\x1b[49;38;5;234m▀\x1b[49;38;5;233m▀\x1b[m"; pub async fn handle(session: &ListenerSession, source: String, pool: &DBPool) -> DResult<()> { - pool.start_session(session, &Session { source, ..Default::default() }).await?; + pool.start_session( + session, + &Session { + source, + ..Default::default() + }, + ) + .await?; pool.queue_for_session(&session, Some(&(ansi!("\ Welcome to BlastMud - a text-based post-apocalyptic \ game restricted to adults (18+)\r\n").to_owned() + AUS_RATING_SYMBOL + ansi!("\r\n\ @@ -47,7 +54,8 @@ pub async fn handle(session: &ListenerSession, source: String, pool: &DBPool) -> \thelp to learn more.\r\n\ [Please contact staff@blastmud.org with any feedback or suggestions on how to \r\n\ improve Blastmud, to report any inappropriate user generated content or behaviour, or if you \r\n\ - need any other help from the game's operators].\r\n\ + need any other help from the game's operators; use report abuse immediately after \r\n\ + receiving any inappropriate message to store evidence].\r\n\ Blastmud's privacy policy: https://blastmud.org/privacy/\r\n")))).await?; Ok(()) } diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 030d07d..d75353a 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -52,6 +52,7 @@ mod quit; pub mod register; pub mod remove; pub mod rent; +mod report; pub mod say; mod score; mod sign; @@ -200,9 +201,9 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "reply" => page::VERB, "put" => put::VERB, - "remove" => remove::VERB, "rent" => rent::VERB, + "report" => report::VERB, "\'" => say::VERB, "say" => say::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/report.rs b/blastmud_game/src/message_handler/user_commands/report.rs new file mode 100644 index 0000000..dd6f409 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/report.rs @@ -0,0 +1,41 @@ +use super::{get_user_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext}; +use ansi::ansi; +use async_trait::async_trait; +use uuid::Uuid; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle( + self: &Self, + ctx: &mut VerbContext, + _verb: &str, + remaining: &str, + ) -> UResult<()> { + if remaining != "abuse" { + user_error(ansi!("Try report abuse.").to_owned())?; + } + + let user = get_user_or_fail(ctx)?; + let username = user.username.to_lowercase(); + if ctx.trans.clean_and_count_abusereports(&username).await? > 10 { + user_error( + ansi!( + "You have too many recent abuse reports logged to record any more. \ + Contact staff@blastmud.org for help." + ) + .to_owned(), + )?; + } + + let uuid = Uuid::new_v4(); + ctx.trans + .sendqueue_to_abusereport(&uuid, &username, &ctx.session) + .await?; + ctx.trans.queue_for_session(ctx.session, Some(&format!("Up to the last 80 things we sent you since you logged in will now be retained for at least 60 days under reference {}. Send an email to staff@blastmud.org explaining exactly what happened, and include this reference. We'll be able to look it up and investigate.\n", &uuid))).await?; + + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/schema/schema.sql b/schema/schema.sql index 6342d79..35ec835 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -7,7 +7,7 @@ CREATE DATABASE blast_schemaonly; CREATE TABLE listeners ( listener UUID NOT NULL PRIMARY KEY, - last_seen TIMESTAMP WITH TIME ZONE + last_seen TIMESTAMPTZ ); CREATE TABLE sessions ( @@ -48,9 +48,19 @@ CREATE UNLOGGED TABLE sendqueue ( item BIGSERIAL NOT NULL PRIMARY KEY, session UUID NOT NULL REFERENCES sessions(session), listener UUID REFERENCES listeners(listener), - message TEXT /* Nullable, null means disconnect */ + message TEXT, /* Nullable, null means disconnect */ + sent_at TIMESTAMPTZ ); +CREATE TABLE abuselog ( + id UUID NOT NULL PRIMARY KEY, + triggered_by TEXT NOT NULL, + logdata JSONB NOT NULL, + expires TIMESTAMPTZ NOT NULL +); +CREATE INDEX abuselog_by_triggerer ON abuselog(triggered_by); +CREATE INDEX abuselog_by_expires ON abuselog(expires); + CREATE TABLE tasks ( task_id BIGSERIAL NOT NULL PRIMARY KEY, details JSONB NOT NULL