mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-12 19:56:48 +00:00
feat: Allow room moderators to view redacted event content (MSC2815)
This commit is contained in:
1 parent
2c605151eb
commit
98fe280de4
9 files changed
+133
-3
No files matched your search
@@ -0,0 +1 @@
|
||||
Added [MSC2815](https://github.com/matrix-org/matrix-spec-proposals/pull/2815), letting room moderators fetch the content of redacted events, behind the `allow_moderators_to_view_redacted_content` config option. Contributed by @eleboucher.
|
||||
@@ -1664,6 +1664,15 @@
|
||||
# env: CONTINUWUITY_ALLOW_STICKY_EVENTS
|
||||
#allow_sticky_events = false
|
||||
|
||||
# Allow room moderators to view redacted event content ([MSC2815]).
|
||||
#
|
||||
# Redacted content is kept in the database indefinitely, and readable by
|
||||
# users at or above the room's `redact` power level, and server admins.
|
||||
#
|
||||
# MSC2815: https://github.com/matrix-org/matrix-spec-proposals/pull/2815
|
||||
#
|
||||
#allow_moderators_to_view_redacted_content = false
|
||||
|
||||
# Maximum time federation user can indicate typing.
|
||||
#
|
||||
# env: CONTINUWUITY_TYPING_FEDERATION_TIMEOUT_S
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Event, Result, debug_warn, err};
|
||||
use ruma::api::client::room::get_room_event;
|
||||
use axum::extract::{Query, State};
|
||||
use conduwuit::{Err, Error, Event, PduEvent, Result, debug_warn, err};
|
||||
use futures::{FutureExt, TryFutureExt, future::try_join};
|
||||
use http::StatusCode;
|
||||
use ruma::{
|
||||
UserId,
|
||||
api::{client::room::get_room_event, error::ErrorKind},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Ruma, client::is_ignored_pdu};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct Msc2815Params {
|
||||
#[serde(rename = "fi.mau.msc2815.include_unredacted_content", default)]
|
||||
include_unredacted_content: Option<String>,
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
|
||||
///
|
||||
/// Gets a single event.
|
||||
pub(crate) async fn get_room_event_route(
|
||||
State(ref services): State<crate::State>,
|
||||
Query(params): Query<Msc2815Params>,
|
||||
ref body: Ruma<get_room_event::v3::Request>,
|
||||
) -> Result<get_room_event::v3::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
@@ -54,6 +68,13 @@ pub(crate) async fn get_room_event_route(
|
||||
return Err!(Request(Forbidden("You don't have permission to view this event.")));
|
||||
}
|
||||
|
||||
if services.config.allow_moderators_to_view_redacted_content
|
||||
&& params.include_unredacted_content.as_deref() == Some("true")
|
||||
&& event.is_redacted()
|
||||
{
|
||||
unredact(services, sender_user, &mut event).await?;
|
||||
}
|
||||
|
||||
if let Err(e) = services
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
@@ -72,3 +93,49 @@ pub(crate) async fn get_room_event_route(
|
||||
|
||||
Ok(get_room_event::v3::Response::new(event.into_format()))
|
||||
}
|
||||
|
||||
async fn unredact(services: &crate::State, sender_user: &UserId, event: &mut PduEvent) -> Result {
|
||||
let power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_power_levels(&event.room_id_or_hash())
|
||||
.await;
|
||||
|
||||
if power_levels.for_user(sender_user) < power_levels.redact
|
||||
&& !services.users.is_admin(sender_user).await
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"You don't have permission to view redacted content in this room."
|
||||
)));
|
||||
}
|
||||
|
||||
let Ok(pdu_id) = services.rooms.timeline.get_pdu_id(event.event_id()).await else {
|
||||
return Err(content_unavailable());
|
||||
};
|
||||
|
||||
let Ok(content) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_unredacted_content(&pdu_id)
|
||||
.await
|
||||
else {
|
||||
return Err(content_unavailable());
|
||||
};
|
||||
|
||||
event.content = content;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn content_unavailable() -> Error {
|
||||
let kind = serde_json::from_value::<ErrorKind>(
|
||||
json!({ "errcode": "FI.MAU.MSC2815_UNREDACTED_CONTENT_DELETED" }),
|
||||
)
|
||||
.expect("errcode is a valid custom error kind");
|
||||
|
||||
Error::Request(
|
||||
kind,
|
||||
"The unredacted content of this event is not available.".into(),
|
||||
StatusCode::NOT_FOUND,
|
||||
)
|
||||
}
|
||||
@@ -34,6 +34,11 @@ pub(crate) async fn get_supported_versions_route(
|
||||
unstable_features.insert("org.matrix.msc4354".to_owned(), true);
|
||||
}
|
||||
|
||||
if services.config.allow_moderators_to_view_redacted_content {
|
||||
// moderators can view redacted content (https://github.com/matrix-org/matrix-spec-proposals/pull/2815)
|
||||
unstable_features.insert("fi.mau.msc2815".to_owned(), true);
|
||||
}
|
||||
|
||||
Ok(assign!(get_supported_versions::Response::new(versions()), {
|
||||
unstable_features
|
||||
}))
|
||||
|
||||
@@ -1776,6 +1776,17 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub allow_sticky_events: bool,
|
||||
|
||||
/// Allow room moderators to view redacted event content ([MSC2815]).
|
||||
///
|
||||
/// Redacted content is kept in the database indefinitely, and readable by
|
||||
/// users at or above the room's `redact` power level, and server admins.
|
||||
///
|
||||
/// MSC2815: https://github.com/matrix-org/matrix-spec-proposals/pull/2815
|
||||
///
|
||||
/// default: false
|
||||
#[serde(default)]
|
||||
pub allow_moderators_to_view_redacted_content: bool,
|
||||
|
||||
/// Maximum time federation user can indicate typing.
|
||||
///
|
||||
/// default: 30
|
||||
|
||||
@@ -153,6 +153,11 @@ pub(super) static MAPS: &[Descriptor] = &[
|
||||
index_size: 512,
|
||||
..descriptor::SEQUENTIAL
|
||||
},
|
||||
Descriptor {
|
||||
name: "pduid_unredactedcontent",
|
||||
key_size_hint: Some(16),
|
||||
..descriptor::SEQUENTIAL
|
||||
},
|
||||
Descriptor {
|
||||
name: "publicroomids",
|
||||
..descriptor::RANDOM_SMALL
|
||||
|
||||
@@ -8,6 +8,7 @@ use conduwuit::{
|
||||
use database::{Database, Deserialized, Json, KeyVal, Map};
|
||||
use futures::{FutureExt, Stream, TryFutureExt, TryStreamExt, future::select_ok, pin_mut};
|
||||
use ruma::{CanonicalJsonObject, EventId, OwnedUserId, RoomId, api::Direction};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
use super::{PduId, RawPduId};
|
||||
use crate::{Dep, rooms, rooms::short::ShortRoomId};
|
||||
@@ -16,6 +17,7 @@ pub(super) struct Data {
|
||||
eventid_outlierpdu: Arc<Map>,
|
||||
eventid_pduid: Arc<Map>,
|
||||
pduid_pdu: Arc<Map>,
|
||||
pduid_unredactedcontent: Arc<Map>,
|
||||
roomid_lasttimelinecount: Arc<Map>,
|
||||
userroomid_highlightcount: Arc<Map>,
|
||||
userroomid_notificationcount: Arc<Map>,
|
||||
@@ -36,6 +38,7 @@ impl Data {
|
||||
eventid_outlierpdu: db["eventid_outlierpdu"].clone(),
|
||||
eventid_pduid: db["eventid_pduid"].clone(),
|
||||
pduid_pdu: db["pduid_pdu"].clone(),
|
||||
pduid_unredactedcontent: db["pduid_unredactedcontent"].clone(),
|
||||
roomid_lasttimelinecount: db["roomid_lasttimelinecount"].clone(),
|
||||
userroomid_highlightcount: db["userroomid_highlightcount"].clone(),
|
||||
userroomid_notificationcount: db["userroomid_notificationcount"].clone(),
|
||||
@@ -234,6 +237,20 @@ impl Data {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn save_unredacted_content(&self, pdu_id: &RawPduId, content: &RawJsonValue) {
|
||||
self.pduid_unredactedcontent.raw_put(pdu_id, Json(content));
|
||||
}
|
||||
|
||||
pub(super) async fn get_unredacted_content(
|
||||
&self,
|
||||
pdu_id: &RawPduId,
|
||||
) -> Result<Box<RawJsonValue>> {
|
||||
self.pduid_unredactedcontent
|
||||
.get(pdu_id)
|
||||
.await
|
||||
.deserialized()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all events and their tokens in a room that
|
||||
/// happened before the event with id `until` in reverse-chronological
|
||||
/// order.
|
||||
|
||||
@@ -25,6 +25,7 @@ use ruma::{
|
||||
events::room::encrypted::Relation,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
use self::data::Data;
|
||||
pub use self::{create::pdu_fits, data::PdusIterItem};
|
||||
@@ -242,6 +243,11 @@ impl Service {
|
||||
self.db.replace_pdu(pdu_id, pdu_json).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn get_unredacted_content(&self, pdu_id: &RawPduId) -> Result<Box<RawJsonValue>> {
|
||||
self.db.get_unredacted_content(pdu_id).await
|
||||
}
|
||||
|
||||
/// Returns an iterator over all PDUs in a room. Unknown rooms produce no
|
||||
/// items.
|
||||
#[inline]
|
||||
|
||||
@@ -52,6 +52,15 @@ impl super::Service {
|
||||
.get_room_version(&pdu.room_id_or_hash())
|
||||
.await?;
|
||||
|
||||
if !pdu.is_redacted()
|
||||
&& self
|
||||
.services
|
||||
.config
|
||||
.allow_moderators_to_view_redacted_content
|
||||
{
|
||||
self.db.save_unredacted_content(&pdu_id, &pdu.content);
|
||||
}
|
||||
|
||||
pdu.redact(&room_version_id, Some(reason.event_id()))?;
|
||||
|
||||
let obj = utils::to_canonical_object(&pdu).map_err(|e| {
|
||||
|
||||
Reference in new issue
Block a user