mirror of
https://gitlab.com/famedly/conduit.git
synced 2025-04-22 14:10:16 +03:00
feat(media): blocking
TODO: Logs (for previous commit as well)
This commit is contained in:
parent
5c941ed72c
commit
6562f319c4
9 changed files with 541 additions and 36 deletions
69
Cargo.lock
generated
69
Cargo.lock
generated
|
@ -38,6 +38,21 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.7"
|
||||
|
@ -430,6 +445,20 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
|
@ -495,6 +524,7 @@ dependencies = [
|
|||
"axum-server",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"directories",
|
||||
"figment",
|
||||
|
@ -1296,6 +1326,30 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.4.0"
|
||||
|
@ -3558,6 +3612,21 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
|
|
@ -123,6 +123,7 @@ thread_local = "1.1.7"
|
|||
hmac = "0.12.1"
|
||||
sha-1 = "0.10.1"
|
||||
# used for conduit's CLI and admin room command parsing
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.3.0", default-features = false, features = [
|
||||
"derive",
|
||||
"error-context",
|
||||
|
|
|
@ -199,6 +199,8 @@ async fn get_content(
|
|||
allow_remote: bool,
|
||||
authenticated: bool,
|
||||
) -> Result<get_content::v1::Response, Error> {
|
||||
services().media.check_blocked(server_name, &media_id)?;
|
||||
|
||||
if let Ok(Some(FileMeta {
|
||||
content_disposition,
|
||||
content_type,
|
||||
|
@ -278,6 +280,8 @@ async fn get_content_as_filename(
|
|||
allow_remote: bool,
|
||||
authenticated: bool,
|
||||
) -> Result<get_content_as_filename::v1::Response, Error> {
|
||||
services().media.check_blocked(server_name, &media_id)?;
|
||||
|
||||
if let Ok(Some(FileMeta {
|
||||
file, content_type, ..
|
||||
})) = services()
|
||||
|
@ -371,6 +375,8 @@ async fn get_content_thumbnail(
|
|||
allow_remote: bool,
|
||||
authenticated: bool,
|
||||
) -> Result<get_content_thumbnail::v1::Response, Error> {
|
||||
services().media.check_blocked(server_name, &media_id)?;
|
||||
|
||||
if let Some(FileMeta {
|
||||
file,
|
||||
content_type,
|
||||
|
|
|
@ -2221,6 +2221,10 @@ pub async fn create_invite_route(
|
|||
pub async fn get_content_route(
|
||||
body: Ruma<get_content::v1::Request>,
|
||||
) -> Result<get_content::v1::Response> {
|
||||
services()
|
||||
.media
|
||||
.check_blocked(services().globals.server_name(), &body.media_id)?;
|
||||
|
||||
if let Some(FileMeta {
|
||||
content_disposition,
|
||||
content_type,
|
||||
|
@ -2249,6 +2253,10 @@ pub async fn get_content_route(
|
|||
pub async fn get_content_thumbnail_route(
|
||||
body: Ruma<get_content_thumbnail::v1::Request>,
|
||||
) -> Result<get_content_thumbnail::v1::Response> {
|
||||
services()
|
||||
.media
|
||||
.check_blocked(services().globals.server_name(), &body.media_id)?;
|
||||
|
||||
let Some(FileMeta {
|
||||
file,
|
||||
content_type,
|
||||
|
|
|
@ -2,7 +2,11 @@ use ruma::{api::client::error::ErrorKind, OwnedServerName, ServerName, UserId};
|
|||
use sha2::digest::Output;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::{database::KeyValueDatabase, service, utils, Error, Result};
|
||||
use crate::{
|
||||
database::KeyValueDatabase,
|
||||
service::{self, media::BlockedMediaInfo},
|
||||
utils, Error, Result,
|
||||
};
|
||||
|
||||
impl service::media::Data for KeyValueDatabase {
|
||||
fn create_file_metadata(
|
||||
|
@ -225,9 +229,9 @@ impl service::media::Data for KeyValueDatabase {
|
|||
))
|
||||
}
|
||||
|
||||
fn purge_and_get_hashes(&self, media: Vec<(OwnedServerName, String)>) -> Vec<Result<String>> {
|
||||
fn purge_and_get_hashes(&self, media: &[(OwnedServerName, String)]) -> Vec<Result<String>> {
|
||||
media
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|(server_name, media_id)| {
|
||||
let mut key = server_name.as_bytes().to_vec();
|
||||
key.push(0xff);
|
||||
|
@ -280,6 +284,7 @@ impl service::media::Data for KeyValueDatabase {
|
|||
prefix.extend_from_slice(user_id.localpart().as_bytes());
|
||||
prefix.push(0xff);
|
||||
|
||||
//TODO: rename this
|
||||
let stuff = self.purge_and_get_hashes(prefix, user_id.server_name(), after);
|
||||
|
||||
stuff
|
||||
|
@ -320,6 +325,205 @@ impl service::media::Data for KeyValueDatabase {
|
|||
.map(|r| r.map(|(digest, _)| hex::encode(digest)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<bool> {
|
||||
let mut key = server_name.as_bytes().to_vec();
|
||||
key.push(0xff);
|
||||
key.extend_from_slice(media_id.as_bytes());
|
||||
|
||||
self.blocked_servername_mediaid
|
||||
.get(&key)
|
||||
.map(|x| x.is_some())
|
||||
}
|
||||
|
||||
fn block(
|
||||
&self,
|
||||
media: &[(OwnedServerName, String)],
|
||||
unix_secs: u64,
|
||||
reason: Option<String>,
|
||||
) -> Vec<Error> {
|
||||
let reason = reason.unwrap_or_default();
|
||||
let unix_secs = unix_secs.to_be_bytes();
|
||||
|
||||
media
|
||||
.iter()
|
||||
.map(|(server_name, media_id)| {
|
||||
let mut key = server_name.as_bytes().to_vec();
|
||||
key.push(0xff);
|
||||
key.extend_from_slice(media_id.as_bytes());
|
||||
|
||||
let mut value = unix_secs.to_vec();
|
||||
value.push(0xff);
|
||||
value.extend_from_slice(reason.as_bytes());
|
||||
self.blocked_servername_mediaid.insert(&key, &value)
|
||||
})
|
||||
.filter_map(Result::err)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn block_from_user(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
now: u64,
|
||||
reason: &str,
|
||||
after: Option<u64>,
|
||||
) -> Vec<Error> {
|
||||
let mut prefix = user_id.server_name().as_bytes().to_vec();
|
||||
prefix.push(0xff);
|
||||
prefix.extend_from_slice(user_id.localpart().as_bytes());
|
||||
prefix.push(0xff);
|
||||
|
||||
let mut value = now.to_be_bytes().to_vec();
|
||||
value.push(0xff);
|
||||
value.extend_from_slice(reason.as_bytes());
|
||||
|
||||
let media = self.servername_userlocalpart_mediaid.scan_prefix(prefix);
|
||||
|
||||
media
|
||||
.map(|(k, _)| {
|
||||
let parts = k.split(|&b| b == 0xff);
|
||||
|
||||
let media_id = parts.last().ok_or_else(|| {
|
||||
Error::bad_database("Invalid format of key in blocked_servername_mediaid")
|
||||
})?;
|
||||
|
||||
let mut key = user_id.server_name().as_bytes().to_vec();
|
||||
key.push(0xff);
|
||||
key.extend_from_slice(media_id);
|
||||
|
||||
if after
|
||||
.map(|after| {
|
||||
let metadata =
|
||||
self.servernamemediaid_metadata.get(&key)?.ok_or_else(|| {
|
||||
Error::bad_database(
|
||||
"Missing metadata for local media in servernamemediaid_metadata",
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut parts = metadata.split(|&b| b == 0xff);
|
||||
|
||||
let sha256_digest = parts.next().ok_or_else(|| {
|
||||
Error::bad_database(
|
||||
"Invalid format of metadata in servernamemediaid_metadata",
|
||||
)
|
||||
})?;
|
||||
|
||||
let metadata =
|
||||
self.filehash_metadata.get(sha256_digest)?.ok_or_else(|| {
|
||||
Error::bad_database(
|
||||
"Missing metadata for local media in filehash_metadata",
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut parts = metadata.split(|&b| b == 0xff);
|
||||
|
||||
let creation_time = u64::from_be_bytes(
|
||||
parts
|
||||
.nth(1)
|
||||
.ok_or_else(|| {
|
||||
Error::bad_database(
|
||||
"Invalid format of metadata in filehash_metadata",
|
||||
)
|
||||
})?
|
||||
.try_into()
|
||||
.map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid creation time in filehash_metadata",
|
||||
)
|
||||
})?,
|
||||
);
|
||||
|
||||
Ok::<bool, Error>(creation_time > after)
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(true)
|
||||
{
|
||||
self.blocked_servername_mediaid.insert(&key, &value)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.filter_map(Result::err)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unblock(&self, media: &[(OwnedServerName, String)]) -> Vec<Error> {
|
||||
media
|
||||
.iter()
|
||||
.map(|(server_name, media_id)| {
|
||||
let mut key = server_name.as_bytes().to_vec();
|
||||
key.push(0xff);
|
||||
key.extend_from_slice(media_id.as_bytes());
|
||||
|
||||
self.blocked_servername_mediaid.remove(&key)
|
||||
})
|
||||
.filter_map(Result::err)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_blocked(&self) -> Vec<Result<BlockedMediaInfo>> {
|
||||
self.blocked_servername_mediaid
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let mut parts = k.split(|&b| b == 0xff);
|
||||
|
||||
let server_name = OwnedServerName::try_from(
|
||||
utils::string_from_bytes(parts.next().ok_or_else(|| {
|
||||
Error::bad_database("Invalid format of key in blocked_servername_mediaid")
|
||||
})?)
|
||||
.map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid server_name String in blocked_servername_mediaid",
|
||||
)
|
||||
})?,
|
||||
)
|
||||
.map_err(|_| {
|
||||
Error::bad_database("Invalid ServerName in blocked_servername_mediaid")
|
||||
})?;
|
||||
|
||||
let media_id = utils::string_from_bytes(parts.next().ok_or_else(|| {
|
||||
Error::bad_database("Invalid format of key in blocked_servername_mediaid")
|
||||
})?)
|
||||
.map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid media_id string in servername_userlocalpart_mediaid",
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut parts = v.split(|&b| b == 0xff);
|
||||
|
||||
let unix_secs = u64::from_be_bytes(
|
||||
parts
|
||||
.next()
|
||||
.map(|bytes| {
|
||||
bytes.try_into().map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid block time in servername_userlocalpart_mediaid ",
|
||||
)
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
Error::bad_database(
|
||||
"Invalid format of value in blocked_servername_mediaid",
|
||||
)
|
||||
})??,
|
||||
);
|
||||
|
||||
let reason = utils::string_from_bytes(parts.next().ok_or_else(|| {
|
||||
Error::bad_database("Invalid format of value in blocked_servername_mediaid")
|
||||
})?)
|
||||
.map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid media_id string in servername_userlocalpart_mediaid",
|
||||
)
|
||||
})?;
|
||||
|
||||
let reason = (!reason.is_empty()).then_some(reason);
|
||||
|
||||
Ok((server_name, media_id, unix_secs, reason))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyValueDatabase {
|
||||
|
|
|
@ -158,6 +158,7 @@ pub struct KeyValueDatabase {
|
|||
pub(super) servernamemediaid_metadata: Arc<dyn KvTree>, // Servername + MediaID -> content sha256 + Filename + ContentType + extra 0xff byte if media is allowed on unauthenticated endpoints
|
||||
pub(super) filehash_servername_mediaid: Arc<dyn KvTree>, // sha256 of content + Servername + MediaID, used to delete dangling references to filehashes from servernamemediaid
|
||||
pub(super) filehash_metadata: Arc<dyn KvTree>, // sha256 of content -> file size + creation time + last access time
|
||||
pub(super) blocked_servername_mediaid: Arc<dyn KvTree>, // Servername + MediaID of blocked media
|
||||
pub(super) servername_userlocalpart_mediaid: Arc<dyn KvTree>, // Servername + User Localpart + MediaID
|
||||
pub(super) servernamemediaid_userlocalpart: Arc<dyn KvTree>, // Servername + MediaID -> User Localpart, used to remove keys from above when files are deleted by unrelated means
|
||||
pub(super) thumbnailid_metadata: Arc<dyn KvTree>, // ThumbnailId = Servername + MediaID + width + height -> Filename + ContentType + extra 0xff byte if media is allowed on unauthenticated endpoints
|
||||
|
@ -370,6 +371,7 @@ impl KeyValueDatabase {
|
|||
servernamemediaid_metadata: builder.open_tree("servernamemediaid_metadata")?,
|
||||
filehash_servername_mediaid: builder.open_tree("filehash_servername_mediaid")?,
|
||||
filehash_metadata: builder.open_tree("filehash_metadata")?,
|
||||
blocked_servername_mediaid: builder.open_tree("blocked_servername_mediaid")?,
|
||||
servername_userlocalpart_mediaid: builder
|
||||
.open_tree("servername_userlocalpart_mediaid")?,
|
||||
servernamemediaid_userlocalpart: builder
|
||||
|
|
|
@ -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::{
|
||||
|
@ -24,8 +26,8 @@ use ruma::{
|
|||
},
|
||||
TimelineEventType,
|
||||
},
|
||||
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedRoomAliasId, OwnedRoomId, RoomAliasId,
|
||||
RoomId, RoomVersionId, ServerName, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName,
|
||||
RoomAliasId, RoomId, RoomVersionId, ServerName, UserId,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||
|
@ -150,6 +152,38 @@ enum AdminCommand {
|
|||
from_last: Option<Duration>,
|
||||
},
|
||||
|
||||
/// Prevents the list of media from being accessed, but does not delete the media if it
|
||||
/// is already downloaded.
|
||||
/// 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
|
||||
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
|
||||
/// Optionally, only block media uploaded in the last <time-frame> with --from-last
|
||||
BlockMediaFromUsers {
|
||||
#[arg(
|
||||
long, short,
|
||||
value_parser = humantime::parse_duration
|
||||
)]
|
||||
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.
|
||||
/// 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)
|
||||
|
@ -857,27 +891,10 @@ impl Service {
|
|||
)
|
||||
}
|
||||
}
|
||||
AdminCommand::PurgeMedia => {
|
||||
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
|
||||
{
|
||||
let mut invalid = Vec::new();
|
||||
|
||||
let media = body
|
||||
.clone()
|
||||
.drain(1..body.len() - 1)
|
||||
.map(<Box<MxcUri>>::from)
|
||||
.filter_map(|mxc| match mxc.parts() {
|
||||
Ok((server_name, media_id)) => {
|
||||
Some((server_name.to_owned(), media_id.to_owned()))
|
||||
}
|
||||
Err(e) => {
|
||||
invalid.push(e);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let failed_count = services().media.purge(media).len();
|
||||
AdminCommand::PurgeMedia => media_from_body(body).map_or_else(
|
||||
|message| message,
|
||||
|media| {
|
||||
let failed_count = services().media.purge(&media).len();
|
||||
|
||||
if failed_count == 0 {
|
||||
RoomMessageEventContent::text_plain("Successfully purged media")
|
||||
|
@ -886,12 +903,7 @@ impl Service {
|
|||
"Failed to delete {failed_count} media, check logs for more details"
|
||||
))
|
||||
}
|
||||
} else {
|
||||
RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}),
|
||||
AdminCommand::PurgeMediaFromUsers { from_last } => {
|
||||
let after = from_last.map(unix_secs_from_duration).transpose()?;
|
||||
|
||||
|
@ -948,6 +960,114 @@ 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).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(
|
||||
"| Server | Media ID | Time Blocked | Reason |\n| --- | --- | --- | --- |",
|
||||
);
|
||||
let mut html_message = String::from(
|
||||
r#"<table><thead><tr><th scope="col">Server</th><th scope="col">Media ID</th><th scope="col">Time Blocked</th><th scope="col">Reason</th></tr></thead><tbody>"#,
|
||||
);
|
||||
|
||||
for media in services().media.get_blocked() {
|
||||
let Ok((server, media_id, unix_secs, reason)) = media else {
|
||||
continue;
|
||||
};
|
||||
|
||||
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| {server} | {media_id} | {time} | {reason} |"));
|
||||
|
||||
html_message.push_str(&format!(
|
||||
"<tr><td>{server}</td><td>{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() == "```"
|
||||
{
|
||||
|
@ -1639,6 +1759,7 @@ fn userids_from_body<'a>(
|
|||
html_message.push_str("</pre>\n\n");
|
||||
}
|
||||
if !markdown_message.is_empty() {
|
||||
//TODO: This should be Err(Err(_))
|
||||
return Ok(Err(RoomMessageEventContent::text_html(
|
||||
markdown_message,
|
||||
html_message,
|
||||
|
@ -1648,6 +1769,27 @@ fn userids_from_body<'a>(
|
|||
Ok(Ok(user_ids))
|
||||
}
|
||||
|
||||
fn media_from_body(
|
||||
body: Vec<&str>,
|
||||
) -> Result<Vec<(OwnedServerName, String)>, RoomMessageEventContent> {
|
||||
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" {
|
||||
Ok(body
|
||||
.clone()
|
||||
.drain(1..body.len() - 1)
|
||||
.map(<Box<MxcUri>>::from)
|
||||
.filter_map(|mxc| {
|
||||
mxc.parts()
|
||||
.map(|(server_name, media_id)| (server_name.to_owned(), media_id.to_owned()))
|
||||
.ok()
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
} else {
|
||||
Err(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn unix_secs_from_duration(duration: Duration) -> Result<u64> {
|
||||
SystemTime::now()
|
||||
.checked_sub(duration).ok_or_else(||Error::AdminCommand("Given timeframe cannot be represented as system time, please try again with a shorter time-frame"))
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use ruma::{OwnedServerName, ServerName, UserId};
|
||||
use sha2::{digest::Output, Sha256};
|
||||
|
||||
use crate::Result;
|
||||
use crate::{Error, Result};
|
||||
|
||||
use super::BlockedMediaInfo;
|
||||
|
||||
pub trait Data: Send + Sync {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -47,7 +49,7 @@ pub trait Data: Send + Sync {
|
|||
height: u32,
|
||||
) -> Result<(String, Option<String>, Option<String>, bool)>;
|
||||
|
||||
fn purge_and_get_hashes(&self, media: Vec<(OwnedServerName, String)>) -> Vec<Result<String>>;
|
||||
fn purge_and_get_hashes(&self, media: &[(OwnedServerName, String)]) -> Vec<Result<String>>;
|
||||
|
||||
fn purge_and_get_hashes_from_user(
|
||||
&self,
|
||||
|
@ -60,4 +62,30 @@ pub trait Data: Send + Sync {
|
|||
server_name: &ServerName,
|
||||
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 get_blocked(&self) -> Vec<Result<BlockedMediaInfo>>;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use ruma::{
|
|||
};
|
||||
use sha2::{digest::Output, Digest, Sha256};
|
||||
|
||||
use crate::{config::MediaConfig, services, Error, Result};
|
||||
use crate::{config::MediaConfig, services, utils, Error, Result};
|
||||
use image::imageops::FilterType;
|
||||
|
||||
use tokio::{
|
||||
|
@ -27,6 +27,8 @@ pub struct Service {
|
|||
pub db: &'static dyn Data,
|
||||
}
|
||||
|
||||
pub type BlockedMediaInfo = (OwnedServerName, String, u64, Option<String>);
|
||||
|
||||
impl Service {
|
||||
/// Uploads a file.
|
||||
pub async fn create(
|
||||
|
@ -274,7 +276,7 @@ impl Service {
|
|||
/// Purges all of the specified media.
|
||||
///
|
||||
/// Returns errors for all the files that were failed to be deleted, if any.
|
||||
pub fn purge(&self, media: Vec<(OwnedServerName, String)>) -> Vec<Error> {
|
||||
pub fn purge(&self, media: &[(OwnedServerName, String)]) -> Vec<Error> {
|
||||
let hashes = self.db.purge_and_get_hashes(media);
|
||||
|
||||
purge_files(hashes)
|
||||
|
@ -302,6 +304,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 get_blocked(&self) -> Vec<Result<BlockedMediaInfo>> {
|
||||
self.db.get_blocked()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the media file, using the configured media backend
|
||||
|
|
Loading…
Reference in a new issue