mirror of
https://gitlab.com/famedly/conduit.git
synced 2025-10-15 19:42:07 +00:00
feat(media): blocking
This commit is contained in:
parent
d76637048a
commit
594fe5f98f
9 changed files with 738 additions and 19 deletions
|
@ -1,10 +1,12 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
collections::BTreeMap,
|
||||
convert::TryFrom,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use chrono::DateTime;
|
||||
use clap::{Args, Parser};
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
|
@ -37,7 +39,7 @@ use crate::{
|
|||
Error, PduEvent, Result,
|
||||
};
|
||||
|
||||
use super::pdu::PduBuilder;
|
||||
use super::{media::BlockedMediaInfo, pdu::PduBuilder};
|
||||
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Parser)]
|
||||
|
@ -180,6 +182,55 @@ enum AdminCommand {
|
|||
force_filehash: bool,
|
||||
},
|
||||
|
||||
/// Prevents the list of media from being accessed, but does not delete the media if it
|
||||
/// is already downloaded. If the media has already been downloaded, the sha256 hash
|
||||
/// is blocked, meaning that any other current or future uploads/downloads of the exact same
|
||||
/// content cannot be accessed either.
|
||||
///
|
||||
/// There should be one MXC URI per line, all contained within a code-block
|
||||
BlockMedia {
|
||||
#[arg(long, short)]
|
||||
/// Prevents the specified media from being downloaded in the future
|
||||
///
|
||||
/// Note: This will also delete identical media uploaded by other users, so
|
||||
/// only use this all the media is known to be undesirable
|
||||
and_purge: bool,
|
||||
#[arg(long, short)]
|
||||
/// Optional reason as to why this media should be blocked
|
||||
reason: Option<String>,
|
||||
},
|
||||
|
||||
/// Prevents all media uploaded by the local users, listed in a code-block, from being accessed
|
||||
///
|
||||
/// Note: This will also block media with the same SHA256 hash, so
|
||||
/// only use this when all media uploaded by the user is undesirable (or if
|
||||
/// you only plan for the bloackage to be temporary)
|
||||
BlockMediaFromUsers {
|
||||
#[arg(
|
||||
long, short,
|
||||
value_parser = humantime::parse_duration
|
||||
)]
|
||||
/// Only block media uploaded in the last {timeframe}
|
||||
///
|
||||
/// Should be in the form specified by humantime::parse_duration
|
||||
/// (e.g. 48h, 60min, 10days etc.)
|
||||
// --help is unformatted
|
||||
#[allow(rustdoc::bare_urls)]
|
||||
/// https://docs.rs/humantime/2.2.0/humantime/fn.parse_duration.html
|
||||
from_last: Option<Duration>,
|
||||
#[arg(long, short)]
|
||||
/// Optional reason as to why this media should be blocked
|
||||
reason: Option<String>,
|
||||
},
|
||||
|
||||
/// Lists all media that is currently blocked
|
||||
ListBlockedMedia,
|
||||
|
||||
/// Allows previously blocked media to be accessed again. Will also unblock media with the
|
||||
/// same SHA256 hash
|
||||
/// There should be one MXC URI per line, all contained within a code-block
|
||||
UnblockMedia,
|
||||
|
||||
/// Get the auth_chain of a PDU
|
||||
GetAuthChain {
|
||||
/// An event ID (the $ character followed by the base64 reference hash)
|
||||
|
@ -986,6 +1037,121 @@ impl Service {
|
|||
))
|
||||
}
|
||||
}
|
||||
AdminCommand::BlockMedia { and_purge, reason } => media_from_body(body).map_or_else(
|
||||
|message| message,
|
||||
|media| {
|
||||
let failed_count = services().media.block(&media, reason).len();
|
||||
let failed_purge_count = if and_purge {
|
||||
services().media.purge(&media, true).len()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
match (failed_count == 0, failed_purge_count == 0) {
|
||||
(true, true) => RoomMessageEventContent::text_plain("Successfully blocked media"),
|
||||
(false, true) => RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to block {failed_count} media, check logs for more details"
|
||||
)),
|
||||
(true, false ) => RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to purge {failed_purge_count} media, check logs for more details"
|
||||
)),
|
||||
(false, false) => RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to block {failed_count}, and purge {failed_purge_count} media, check logs for more details"
|
||||
))
|
||||
}
|
||||
},
|
||||
),
|
||||
AdminCommand::BlockMediaFromUsers { from_last, reason } => {
|
||||
let after = from_last.map(unix_secs_from_duration).transpose()?;
|
||||
|
||||
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
|
||||
{
|
||||
let user_ids = match userids_from_body(&body)? {
|
||||
Ok(v) => v,
|
||||
Err(message) => return Ok(message),
|
||||
};
|
||||
|
||||
let mut failed_count = 0;
|
||||
|
||||
for user_id in user_ids {
|
||||
let reason = reason.as_ref().map_or_else(
|
||||
|| Cow::Owned(format!("uploaded by {user_id}")),
|
||||
Cow::Borrowed,
|
||||
);
|
||||
|
||||
failed_count += services()
|
||||
.media
|
||||
.block_from_user(user_id, &reason, after)
|
||||
.len();
|
||||
}
|
||||
|
||||
if failed_count == 0 {
|
||||
RoomMessageEventContent::text_plain("Successfully blocked media")
|
||||
} else {
|
||||
RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to block {failed_count} media, check logs for more details"
|
||||
))
|
||||
}
|
||||
} else {
|
||||
RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
)
|
||||
}
|
||||
}
|
||||
AdminCommand::ListBlockedMedia => {
|
||||
let mut markdown_message = String::from(
|
||||
"| SHA256 hash | MXC URI | Time Blocked | Reason |\n| --- | --- | --- | --- |",
|
||||
);
|
||||
let mut html_message = String::from(
|
||||
r#"<table><thead><tr><th scope="col">SHA256 hash</th><th scope="col">MXC URI</th><th scope="col">Time Blocked</th><th scope="col">Reason</th></tr></thead><tbody>"#,
|
||||
);
|
||||
|
||||
for media in services().media.list_blocked() {
|
||||
let Ok(BlockedMediaInfo {
|
||||
server_name,
|
||||
media_id,
|
||||
unix_secs,
|
||||
reason,
|
||||
sha256_hex,
|
||||
}) = media else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let sha256_hex = sha256_hex.unwrap_or_default();
|
||||
let reason = reason.unwrap_or_default();
|
||||
|
||||
let time = i64::try_from(unix_secs)
|
||||
.map(|unix_secs| DateTime::from_timestamp(unix_secs, 0))
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Time is valid");
|
||||
|
||||
markdown_message
|
||||
.push_str(&format!("\n| {sha256_hex} | mxc://{server_name}/{media_id} | {time} | {reason} |"));
|
||||
|
||||
html_message.push_str(&format!(
|
||||
"<tr><td>{sha256_hex}</td><td>mxc://{server_name}/{media_id}</td><td>{time}</td><td>{reason}</td></tr>",
|
||||
))
|
||||
}
|
||||
|
||||
html_message.push_str("</tbody></table>");
|
||||
|
||||
RoomMessageEventContent::text_html(markdown_message, html_message)
|
||||
}
|
||||
AdminCommand::UnblockMedia => media_from_body(body).map_or_else(
|
||||
|message| message,
|
||||
|media| {
|
||||
let failed_count = services().media.unblock(&media).len();
|
||||
|
||||
if failed_count == 0 {
|
||||
RoomMessageEventContent::text_plain("Successfully unblocked media")
|
||||
} else {
|
||||
RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to unblock {failed_count} media, check logs for more details"
|
||||
))
|
||||
}
|
||||
},
|
||||
),
|
||||
AdminCommand::SignJson => {
|
||||
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
|
||||
{
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use ruma::{OwnedServerName, ServerName, UserId};
|
||||
use sha2::{digest::Output, Sha256};
|
||||
|
||||
use crate::Result;
|
||||
use crate::{Error, Result};
|
||||
|
||||
use super::BlockedMediaInfo;
|
||||
|
||||
use super::DbFileMeta;
|
||||
|
||||
|
@ -16,6 +18,7 @@ pub trait Data: Send + Sync {
|
|||
filename: Option<&str>,
|
||||
content_type: Option<&str>,
|
||||
user_id: Option<&UserId>,
|
||||
is_blocked_filehash: bool,
|
||||
) -> Result<()>;
|
||||
|
||||
fn search_file_metadata(&self, servername: &ServerName, media_id: &str) -> Result<DbFileMeta>;
|
||||
|
@ -62,4 +65,32 @@ pub trait Data: Send + Sync {
|
|||
force_filehash: bool,
|
||||
after: Option<u64>,
|
||||
) -> Vec<Result<String>>;
|
||||
|
||||
fn is_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<bool>;
|
||||
|
||||
fn block(
|
||||
&self,
|
||||
media: &[(OwnedServerName, String)],
|
||||
unix_secs: u64,
|
||||
reason: Option<String>,
|
||||
) -> Vec<Error>;
|
||||
|
||||
fn block_from_user(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
now: u64,
|
||||
reason: &str,
|
||||
after: Option<u64>,
|
||||
) -> Vec<Error>;
|
||||
|
||||
fn unblock(&self, media: &[(OwnedServerName, String)]) -> Vec<Error>;
|
||||
|
||||
/// Returns a Vec of:
|
||||
/// - The server the media is from
|
||||
/// - The media id
|
||||
/// - The time it was blocked, in unix seconds
|
||||
/// - The optional reason why it was blocked
|
||||
fn list_blocked(&self) -> Vec<Result<BlockedMediaInfo>>;
|
||||
|
||||
fn is_blocked_filehash(&self, sha256_digest: &[u8]) -> Result<bool>;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use tracing::error;
|
|||
|
||||
use crate::{
|
||||
config::{DirectoryStructure, MediaConfig},
|
||||
services, Error, Result,
|
||||
services, utils, Error, Result,
|
||||
};
|
||||
use image::imageops::FilterType;
|
||||
|
||||
|
@ -38,6 +38,14 @@ pub struct Service {
|
|||
pub db: &'static dyn Data,
|
||||
}
|
||||
|
||||
pub struct BlockedMediaInfo {
|
||||
pub server_name: OwnedServerName,
|
||||
pub media_id: String,
|
||||
pub unix_secs: u64,
|
||||
pub reason: Option<String>,
|
||||
pub sha256_hex: Option<String>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Uploads a file.
|
||||
pub async fn create(
|
||||
|
@ -59,9 +67,16 @@ impl Service {
|
|||
filename,
|
||||
content_type,
|
||||
user_id,
|
||||
self.db.is_blocked_filehash(&sha256_digest)?,
|
||||
)?;
|
||||
|
||||
create_file(&sha256_hex, file).await
|
||||
if !self.db.is_blocked_filehash(&sha256_digest)? {
|
||||
create_file(&sha256_hex, file).await
|
||||
} else if user_id.is_none() {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Uploads or replaces a file thumbnail.
|
||||
|
@ -358,6 +373,49 @@ impl Service {
|
|||
|
||||
purge_files(hashes)
|
||||
}
|
||||
|
||||
/// Checks whether the media has been blocked by administrators, returning either
|
||||
/// a database error, or a not found error if it is blocked
|
||||
pub fn check_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<()> {
|
||||
if self.db.is_blocked(server_name, media_id)? {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the specified media as blocked, preventing them from being accessed
|
||||
pub fn block(&self, media: &[(OwnedServerName, String)], reason: Option<String>) -> Vec<Error> {
|
||||
let now = utils::secs_since_unix_epoch();
|
||||
|
||||
self.db.block(media, now, reason)
|
||||
}
|
||||
|
||||
/// Marks the media uploaded by a local user as blocked, preventing it from being accessed
|
||||
pub fn block_from_user(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
reason: &str,
|
||||
after: Option<u64>,
|
||||
) -> Vec<Error> {
|
||||
let now = utils::secs_since_unix_epoch();
|
||||
|
||||
self.db.block_from_user(user_id, now, reason, after)
|
||||
}
|
||||
|
||||
/// Unblocks the specified media, allowing them from being accessed again
|
||||
pub fn unblock(&self, media: &[(OwnedServerName, String)]) -> Vec<Error> {
|
||||
self.db.unblock(media)
|
||||
}
|
||||
|
||||
/// Returns a Vec of:
|
||||
/// - The server the media is from
|
||||
/// - The media id
|
||||
/// - The time it was blocked, in unix seconds
|
||||
/// - The optional reason why it was blocked
|
||||
pub fn list_blocked(&self) -> Vec<Result<BlockedMediaInfo>> {
|
||||
self.db.list_blocked()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the media file, using the configured media backend
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue