Compare commits

...

33 Commits

Author SHA1 Message Date
timedout 3feb32572c fix: Use server runtime instead of tokio 2026-01-25 04:29:24 +00:00
timedout ca51993ee3 fix: Deserialisation error breaks logins 2026-01-25 04:22:43 +00:00
timedout 655eacfa7b feat: Mark remote servers as "interested" when returning media 2026-01-25 03:58:07 +00:00
timedout 3825bff733 feat: Support federating out media redactions 2026-01-25 03:56:40 +00:00
timedout d90d22c917 feat: Add local media self-redaction 2026-01-25 03:13:12 +00:00
Ginger 6cb9d50383 chore: News fragment 2026-01-21 12:27:13 -05:00
Ginger 77c0f6e0c6 fix: Add a code path for clients trying to use fallback auth 2026-01-21 12:27:13 -05:00
Jade Ellis c85e710760 fix: Add option to mark certain config sections as optional
Fixes #1290
2026-01-20 17:36:22 +00:00
Renovate Bot 59346fc766 chore(deps): update pre-commit hook crate-ci/committed to v1.1.10 2026-01-20 16:25:19 +00:00
Renovate Bot 9c5e735888 chore(deps): update dependency cargo-bins/cargo-binstall to v1.16.7 2026-01-20 16:24:46 +00:00
Ginger fe74e82318 chore: Formatting 2026-01-20 10:00:26 -05:00
K900 cb79a3b9d7 refactor(treewide): get rid of compile time build environment introspection
It's cursed and not very useful. Still a few uses of ctor left, but oh well.
2026-01-19 19:44:28 +00:00
timedout ebc8df1c4d feat: Add endpoints required for API-based takedowns and room bans 2026-01-18 18:47:15 +00:00
nex b667a963cf chore: Fixup typos 2026-01-18 15:22:14 +00:00
timedout 5a6b909b37 fix: Remove homebrewed error mangling for correctness 2026-01-18 15:22:14 +00:00
timedout dba9cf0ad2 chore: Add news fragment 2026-01-18 15:22:14 +00:00
timedout 287ddd9bc5 fix: Only fall back to legacy media when response is M_UNRECOGNIZED
https://spec.matrix.org/v1.17/server-server-api/#content-repository
Previously we would fall back for ALL
auth media errors.
2026-01-18 15:22:14 +00:00
Jason Volk 79a278b9e8 Fix verification loss; workaround Nheko-Reborn/nheko#1908 (closes #146)
Signed-off-by: Jason Volk <jason@zemos.net>
2026-01-18 14:41:01 +00:00
Ginger 6c5d658ef2 fix: Fix explosions with new tracing 2026-01-15 09:28:26 -05:00
Renovate Bot 70c43abca8 chore(deps): update rust-patch-updates 2026-01-15 09:28:26 -05:00
Renovate Bot 6a9b47c52e chore(deps): update rust-patch-updates 2026-01-15 05:03:40 +00:00
Ginger c042de96f8 chore(deps): Update rspress to 2.0.0-rc.5 2026-01-14 09:35:20 -05:00
Jade Ellis 7a6acd1c82 chore: Changelog 2026-01-13 20:29:30 +00:00
Jade Ellis d260c4fcc2 style: Fix yo unused variables 2026-01-13 20:29:30 +00:00
Jade Ellis fa15de9764 feat: Admin announce improvements
- Check announcements on first start
- Print out any fetch errors on first start in the admin room
- Randomly jitter the next check
2026-01-13 20:29:30 +00:00
Jade Ellis e6c7a4ae60 docs: Changelog 2026-01-13 00:05:20 +00:00
Jade Ellis 5bed4ad81d chore: Admin announcement 2026-01-13 00:01:28 +00:00
Jade Ellis 587abe9d14 chore: Release 2026-01-12 23:47:37 +00:00
Jade Ellis c499042a76 docs: Changelog 2026-01-12 23:45:42 +00:00
timedout 86e450a835 fix: M_BAD_JSON in send_join and send_knock 2026-01-12 17:53:37 +00:00
Jade Ellis 4c796029bb chore: Add correct configuration for cargo release 2026-01-12 16:20:38 +00:00
Jade Ellis fc3615c46b docs: Changelog 2026-01-12 16:20:38 +00:00
Jade Ellis 7375f7a68e feat: Improve the display of the configuration in the admin room 2026-01-12 16:20:38 +00:00
63 changed files with 1129 additions and 922 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ repos:
stages: [commit-msg]
- repo: https://github.com/crate-ci/committed
rev: v1.1.9
rev: v1.1.10
hooks:
- id: committed
+21
View File
@@ -1,3 +1,24 @@
# Continuwuity 0.5.3 (2026-01-12)
## Features
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by @Jade (#1279)
## Bugfixes
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by @nex (#1286)
## Docs
- Improve admin command documentation generation. Contributed by @ginger (#1280)
## Misc
- Improve timeout-related code for federation and URL previews. Contributed by @Jade (#1278)
# Continuwuity 0.5.2 (2026-01-09)
## Features
Generated
+556 -406
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.2"
version = "0.5.3"
[workspace.metadata.crane]
name = "conduwuit"
@@ -158,7 +158,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.10"
version = "0.0.14"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -342,7 +342,7 @@ version = "0.1.2"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
rev = "f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
rev = "db86e37d602cc4935853bf14d5aace47d22358ed"
features = [
"compat",
"rand",
+1
View File
@@ -0,0 +1 @@
The announcement checker will now announce errors it encounters in the first run to the admin room, plus a few other misc improvements. Contributed by @Jade
+1
View File
@@ -0,0 +1 @@
Fix the generated configuration containing uncommented optional sections. Contributed by @Jade
+1
View File
@@ -0,0 +1 @@
Fixed specification non-compliance when handling remote media errors. Contributed by @nex.
+1
View File
@@ -0,0 +1 @@
UIAA requests which check for out-of-band success (sent by matrix-js-sdk) will no longer create unhelpful errors in the logs.
+4 -6
View File
@@ -1759,10 +1759,6 @@
#
#config_reload_signal = true
# This item is undocumented. Please contribute documentation for it.
#
#ldap = false
[global.tls]
# Path to a valid TLS certificate file.
@@ -1930,7 +1926,9 @@
#
#admin_filter = ""
[global.antispam.meowlnir]
#[global.antispam]
#[global.antispam.meowlnir]
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
#
@@ -1955,7 +1953,7 @@
#
#check_all_joins = false
[global.antispam.draupnir]
#[global.antispam.draupnir]
# The base URL on which to contact Draupnir (before /api/).
#
+1 -1
View File
@@ -48,7 +48,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.16.6
ENV BINSTALL_VERSION=1.16.7
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+1 -1
View File
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.16.6
ENV BINSTALL_VERSION=1.16.7
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
@@ -6,10 +6,10 @@
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 7,
"mention_room": true,
"date": "2025-12-30",
"message": "Continuwuity v0.5.1 has been released. **The release contains a fix for the critical vulnerability [GHSA-m5p2-vccg-8c9v](https://github.com/continuwuity/continuwuity/security/advisories/GHSA-m5p2-vccg-8c9v) (embargoed) affecting all Conduit-derived servers. Update as soon as possible.**\n\nThis has been *actively exploited* to attempt account takeover and forge events bricking the Continuwuity rooms. The new space is accessible at [Continuwuity (room list)](https://matrix.to/#/!8cR4g-i9ucof69E4JHNg9LbPVkGprHb3SzcrGBDDJgk?via=continuwuity.org&via=starstruck.systems&via=gingershaped.computer)\n"
"id": 8,
"mention_room": false,
"date": "2026-01-12",
"message": "Hey everyone!\n\nJust letting you know we've released [v0.5.3](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.3) - this one is a bit of a hotfix for an issue with inviting and allowing others to join rooms.\n\nIf you appreceate the round-the-clock work we've been doing to keep your servers secure over this holiday period, we'd really appreciate your support - you can sponsor individuals on our team using the 'sponsor' button at the top of [our GitHub repository](https://github.com/continuwuity/continuwuity). If you can't do that, even a star helps - spreading the word and advocating for our project helps keep it going.\n\nHave a lovely rest of your year \\\n[Jade \\(she/her\\)](https://matrix.to/#/%40jade%3Aellis.link) \n🩵"
}
]
}
-4
View File
@@ -118,10 +118,6 @@ Print detailed tokio runtime metrics accumulated since last command invocation
Print the current time
## `!admin debug list-dependencies`
List dependencies
## `!admin debug database-stats`
Get database statistics
-4
View File
@@ -16,10 +16,6 @@ Show configuration values
Reload configuration values
## `!admin server list-features`
List the features built into the server
## `!admin server memory-usage`
Print database memory usage statistics
+15 -15
View File
@@ -713,9 +713,9 @@
}
},
"node_modules/@remix-run/router": {
"version": "1.23.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3244,9 +3244,9 @@
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"version": "13.2.1",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4285,13 +4285,13 @@
}
},
"node_modules/react-router": {
"version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
"version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.1"
"@remix-run/router": "1.23.2"
},
"engines": {
"node": ">=14.0.0"
@@ -4301,14 +4301,14 @@
}
},
"node_modules/react-router-dom": {
"version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
"version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.1",
"react-router": "6.30.2"
"@remix-run/router": "1.23.2",
"react-router": "6.30.3"
},
"engines": {
"node": ">=14.0.0"
+7
View File
@@ -1 +1,8 @@
tag-message = "chore: Release v{{version}}"
tag-prefix = ""
shared-version = true
publish = false
sign-commit = true
sign-tag = true
-1
View File
@@ -87,7 +87,6 @@ serde-saphyr.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
ctor.workspace = true
[lints]
workspace = true
-26
View File
@@ -819,32 +819,6 @@ pub(super) async fn time(&self) -> Result {
self.write_str(&now).await
}
#[admin_command]
pub(super) async fn list_dependencies(&self, names: bool) -> Result {
if names {
let out = info::cargo::dependencies_names().join(" ");
return self.write_str(&out).await;
}
let mut out = String::new();
let deps = info::cargo::dependencies();
writeln!(out, "| name | version | features |")?;
writeln!(out, "| ---- | ------- | -------- |")?;
for (name, dep) in deps {
let version = dep.try_req().unwrap_or("*");
let feats = dep.req_features();
let feats = if !feats.is_empty() {
feats.join(" ")
} else {
String::new()
};
writeln!(out, "| {name} | {version} | {feats} |")?;
}
self.write_str(&out).await
}
#[admin_command]
pub(super) async fn database_stats(
&self,
-6
View File
@@ -206,12 +206,6 @@ pub enum DebugCommand {
/// Print the current time
Time,
/// List dependencies
ListDependencies {
#[arg(short, long)]
names: bool,
},
/// Get database statistics
DatabaseStats {
property: Option<String>,
-3
View File
@@ -30,11 +30,8 @@ pub(crate) use crate::{context::Context, utils::get_room_info};
pub(crate) const PAGE_SIZE: usize = 100;
use ctor::{ctor, dtor};
conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}
pub use crate::admin::AdminCommand;
+2 -30
View File
@@ -1,7 +1,7 @@
use std::{fmt::Write, path::PathBuf, sync::Arc};
use std::{path::PathBuf, sync::Arc};
use conduwuit::{
Err, Result, info,
Err, Result,
utils::{stream::IterStream, time},
warn,
};
@@ -59,34 +59,6 @@ pub(super) async fn reload_config(&self, path: Option<PathBuf>) -> Result {
.await
}
#[admin_command]
pub(super) async fn list_features(&self, available: bool, enabled: bool, comma: bool) -> Result {
let delim = if comma { "," } else { " " };
if enabled && !available {
let features = info::rustc::features().join(delim);
let out = format!("`\n{features}\n`");
return self.write_str(&out).await;
}
if available && !enabled {
let features = info::cargo::features().join(delim);
let out = format!("`\n{features}\n`");
return self.write_str(&out).await;
}
let mut features = String::new();
let enabled = info::rustc::features();
let available = info::cargo::features();
for feature in available {
let active = enabled.contains(&feature.as_str());
let emoji = if active { "" } else { "" };
let remark = if active { "[enabled]" } else { "" };
writeln!(features, "{emoji} {feature} {remark}")?;
}
self.write_str(&features).await
}
#[admin_command]
pub(super) async fn memory_usage(&self) -> Result {
let services_usage = self.services.memory_usage().await?;
-12
View File
@@ -21,18 +21,6 @@ pub enum ServerCommand {
path: Option<PathBuf>,
},
/// List the features built into the server
ListFeatures {
#[arg(short, long)]
available: bool,
#[arg(short, long)]
enabled: bool,
#[arg(short, long)]
comma: bool,
},
/// Print database memory usage statistics
MemoryUsage,
-1
View File
@@ -91,7 +91,6 @@ serde.workspace = true
sha1.workspace = true
tokio.workspace = true
tracing.workspace = true
ctor.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
pub mod rooms;
+132
View File
@@ -0,0 +1,132 @@
use axum::extract::State;
use conduwuit::{Err, Result, info, utils::ReadyExt, warn};
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedRoomAliasId, continuwuity_admin_api::rooms,
events::room::message::RoomMessageEventContent,
};
use crate::{Ruma, client::leave_room};
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban`
///
/// Bans or unbans a room.
pub(crate) async fn ban_room(
State(services): State<crate::State>,
body: Ruma<rooms::ban::v1::Request>,
) -> Result<rooms::ban::v1::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
if body.banned {
// Don't ban again if already banned
if services.rooms.metadata.is_banned(&body.room_id).await {
return Err!(Request(InvalidParam("Room is already banned")));
}
info!(%sender_user, "Banning room {}", body.room_id);
services
.admin
.notice(&format!("{sender_user} banned {} (ban in progress)", body.room_id))
.await;
let mut users = services
.rooms
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| services.globals.user_is_local(user))
.boxed();
let mut evicted = Vec::new();
let mut failed_evicted = Vec::new();
while let Some(ref user_id) = users.next().await {
info!("Evicting user {} from room {}", user_id, body.room_id);
match leave_room(&services, user_id, &body.room_id, None)
.boxed()
.await
{
| Ok(()) => {
services.rooms.state_cache.forget(&body.room_id, user_id);
evicted.push(user_id.clone());
},
| Err(e) => {
warn!("Failed to evict user {} from room {}: {}", user_id, body.room_id, e);
failed_evicted.push(user_id.clone());
},
}
}
let aliases: Vec<OwnedRoomAliasId> = services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await;
for alias in &aliases {
info!("Removing alias {} for banned room {}", alias, body.room_id);
services
.rooms
.alias
.remove_alias(alias, &services.globals.server_user)
.await?;
}
services.rooms.directory.set_not_public(&body.room_id); // remove from the room directory
services.rooms.metadata.ban_room(&body.room_id, true); // prevent further joins
services.rooms.metadata.disable_room(&body.room_id, true); // disable federation
services
.admin
.notice(&format!(
"Finished banning {}: Removed {} users ({} failed) and {} aliases",
body.room_id,
evicted.len(),
failed_evicted.len(),
aliases.len()
))
.await;
if !evicted.is_empty() || !failed_evicted.is_empty() || !aliases.is_empty() {
let msg = services
.admin
.text_or_file(RoomMessageEventContent::text_markdown(format!(
"Removed users:\n{}\n\nFailed to remove users:\n{}\n\nRemoved aliases: {}",
evicted
.iter()
.map(|u| u.as_str())
.collect::<Vec<_>>()
.join("\n"),
failed_evicted
.iter()
.map(|u| u.as_str())
.collect::<Vec<_>>()
.join("\n"),
aliases
.iter()
.map(|a| a.as_str())
.collect::<Vec<_>>()
.join(", "),
)))
.await;
services.admin.send_message(msg).await.ok();
}
Ok(rooms::ban::v1::Response::new(evicted, failed_evicted, aliases))
} else {
// Don't unban if not banned
if !services.rooms.metadata.is_banned(&body.room_id).await {
return Err!(Request(InvalidParam("Room is not banned")));
}
info!(%sender_user, "Unbanning room {}", body.room_id);
services.rooms.metadata.disable_room(&body.room_id, false);
services.rooms.metadata.ban_room(&body.room_id, false);
services
.admin
.notice(&format!("{sender_user} unbanned {}", body.room_id))
.await;
Ok(rooms::ban::v1::Response::new(Vec::new(), Vec::new(), Vec::new()))
}
}
+35
View File
@@ -0,0 +1,35 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::{OwnedRoomId, continuwuity_admin_api::rooms};
use crate::Ruma;
/// # `GET /_continuwuity/admin/rooms/list`
///
/// Lists all rooms known to this server, excluding banned ones.
pub(crate) async fn list_rooms(
State(services): State<crate::State>,
body: Ruma<rooms::list::v1::Request>,
) -> Result<rooms::list::v1::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
let mut rooms: Vec<OwnedRoomId> = services
.rooms
.metadata
.iter_ids()
.filter_map(|room_id| async move {
if !services.rooms.metadata.is_banned(room_id).await {
Some(room_id.to_owned())
} else {
None
}
})
.collect()
.await;
rooms.sort();
Ok(rooms::list::v1::Response::new(rooms))
}
+2
View File
@@ -0,0 +1,2 @@
pub mod ban;
pub mod list;
+4 -1
View File
@@ -91,8 +91,11 @@ pub(crate) async fn upload_keys_route(
.users
.get_device_keys(sender_user, sender_device)
.await
.and_then(|keys| keys.deserialize().map_err(Into::into))
{
if existing_keys.json().get() == device_keys.json().get() {
// NOTE: also serves as a workaround for a nheko bug which omits cross-signing
// NOTE: signatures when re-uploading the same DeviceKeys.
if existing_keys.keys == deser_device_keys.keys {
debug!(
%sender_user,
%sender_device,
+73 -2
View File
@@ -3,8 +3,11 @@ use std::time::Duration;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, err,
Err, Result,
debug::DebugInspect,
debug_info, err,
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
warn,
};
use conduwuit_service::{
Services,
@@ -12,8 +15,9 @@ use conduwuit_service::{
};
use reqwest::Url;
use ruma::{
Mxc, UserId,
Mxc, OwnedServerName, UserId,
api::client::{
authenticated_media,
authenticated_media::{
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
get_media_preview,
@@ -244,6 +248,73 @@ pub(crate) async fn get_media_preview_route(
})
}
async fn dispatch_redaction(
server_name: OwnedServerName,
media_id: String,
servers: Vec<OwnedServerName>,
services: crate::State,
) {
for server in servers {
if services.globals.server_is_ours(&server) {
continue;
}
debug_info!("Asking {server} to redact media mxc://{server_name}/{media_id}");
services
.federation
.execute(&server, authenticated_media::redact::unstable::Request {
server_name: server_name.clone(),
media_id: media_id.clone(),
})
.await
.debug_inspect(|_| {
debug_info!("Asked {server} to redact media mxc://{server_name}/{media_id}");
})
.inspect_err(|e| {
warn!(
"Failed to ask {server} to redact media mxc://{server_name}/{media_id}: {e}"
);
})
.ok();
}
}
#[tracing::instrument(
name = "media_redact",
level = "debug",
skip_all,
fields(%_client),
)]
pub(crate) async fn redact_media_route(
State(services): State<crate::State>,
InsecureClientIp(_client): InsecureClientIp,
body: Ruma<authenticated_media::redact::unstable::Request>,
) -> Result<authenticated_media::redact::unstable::Response> {
let user = body.sender_user();
let mxc = Mxc {
server_name: &body.server_name,
media_id: &body.media_id,
};
if !services.media.user_owns(user, &mxc).await {
return Err!(Request(Forbidden("You do not have permission to redact this attachment.")));
}
services.media.redact(&mxc).await?;
// TODO: This should be a persistent background task
let servers = services.media.get_interested_servers(&mxc).await;
services.server.runtime().spawn(dispatch_redaction(
mxc.server_name.to_owned(),
mxc.media_id.to_owned(),
servers,
services,
));
Ok(authenticated_media::redact::unstable::Response {})
}
async fn fetch_thumbnail(
services: &Services,
mxc: &Mxc<'_>,
-8
View File
@@ -371,11 +371,3 @@ pub(crate) async fn is_ignored_invite(
.invite_filter_level(&sender_user, recipient_user)
.await == FilterLevel::Ignore
}
#[cfg_attr(debug_assertions, ctor::ctor)]
fn _is_sorted() {
debug_assert!(
IGNORED_MESSAGE_TYPES.is_sorted(),
"IGNORED_MESSAGE_TYPES must be sorted by the developer"
);
}
+3 -2
View File
@@ -1,12 +1,13 @@
#![type_length_limit = "16384"] //TODO: reduce me
#![allow(clippy::toplevel_ref_arg)]
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_service as service;
pub mod client;
pub mod router;
pub mod server;
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_service as service;
pub mod admin;
pub(crate) use self::router::{Ruma, RumaResponse, State};
+5 -2
View File
@@ -17,7 +17,7 @@ use http::{Uri, uri};
use self::handler::RouterExt;
pub(super) use self::{args::Args as Ruma, response::RumaResponse};
use crate::{client, server};
use crate::{admin, client, server};
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
let config = &server.config;
@@ -154,6 +154,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::get_content_route)
.ruma_route(&client::get_content_as_filename_route)
.ruma_route(&client::get_media_preview_route)
.ruma_route(&client::redact_media_route)
.ruma_route(&client::get_media_config_route)
.ruma_route(&client::get_devices_route)
.ruma_route(&client::get_device_route)
@@ -187,7 +188,9 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
.ruma_route(&client::room_initial_sync_route)
.route("/client/server.json", get(client::syncv3_client_server_json));
.route("/client/server.json", get(client::syncv3_client_server_json))
.ruma_route(&admin::rooms::ban::ban_room)
.ruma_route(&admin::rooms::list::list_rooms);
if config.allow_federation {
router = router
+2
View File
@@ -46,6 +46,7 @@ pub(crate) async fn get_content_route(
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
};
services.media.mark_server_interested(&mxc, body.origin());
Ok(get_content::v1::Response {
content: FileOrLocation::File(content),
@@ -89,6 +90,7 @@ pub(crate) async fn get_content_thumbnail_route(
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
};
services.media.mark_server_interested(&mxc, body.origin());
Ok(get_content_thumbnail::v1::Response {
content: FileOrLocation::File(content),
+2 -12
View File
@@ -13,8 +13,7 @@ use conduwuit::{
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{
CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
ServerName,
CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName,
api::federation::membership::create_join_event,
events::{
StateEventType,
@@ -178,15 +177,6 @@ async fn create_join_event(
}
}
let origin: OwnedServerName = serde_json::from_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event does not have an origin server name."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson("Event has an invalid origin server name: {e}"))))?;
trace!("Signing send_join event");
services
.server_keys
@@ -204,7 +194,7 @@ async fn create_join_event(
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, room_id, &event_id, value.clone(), true)
.handle_incoming_pdu(sender.server_name(), room_id, &event_id, value.clone(), true)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
+2 -11
View File
@@ -6,7 +6,7 @@ use conduwuit::{
};
use futures::FutureExt;
use ruma::{
OwnedServerName, OwnedUserId,
OwnedUserId,
RoomVersionId::*,
api::federation::knock::send_knock,
events::{
@@ -136,15 +136,6 @@ pub(crate) async fn create_knock_event_v1_route(
return Err!(Request(InvalidParam("state_key does not match sender user of event.")));
}
let origin: OwnedServerName = serde_json::from_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event does not have an origin server name."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson("Event has an invalid origin server name: {e}"))))?;
let mut event: JsonObject = serde_json::from_str(body.pdu.get())
.map_err(|e| err!(Request(InvalidParam("Invalid knock event PDU: {e}"))))?;
@@ -163,7 +154,7 @@ pub(crate) async fn create_knock_event_v1_route(
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, &body.room_id, &event_id, value.clone(), true)
.handle_incoming_pdu(sender.server_name(), &body.room_id, &event_id, value.clone(), true)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
+1 -1
View File
@@ -47,7 +47,7 @@ type Key = ArrayVec<usize, KEY_SEGS>;
const NAME_MAX: usize = 128;
const KEY_SEGS: usize = 8;
#[crate::ctor]
#[ctor::ctor]
fn _static_initialization() {
acq_epoch().expect("pre-initialization of jemalloc failed");
acq_epoch().expect("pre-initialization of jemalloc failed");
+19 -8
View File
@@ -53,8 +53,7 @@ use crate::{Result, err, error::Error, utils::sys};
### For more information, see:
### https://continuwuity.org/configuration.html
"#,
ignore = "config_paths catchall well_known tls blurhashing \
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure antispam"
ignore = "config_paths catchall"
)]
pub struct Config {
// Paths to config file(s). Not supposed to be set manually in the config file,
@@ -105,7 +104,7 @@ pub struct Config {
#[serde(default = "default_port")]
port: ListeningPort,
// external structure; separate section
/// display: nested
#[serde(default)]
pub tls: TlsConfig,
@@ -724,7 +723,7 @@ pub struct Config {
#[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId,
// external structure; separate section
/// display: nested
#[serde(default)]
pub well_known: WellKnownConfig,
@@ -2030,19 +2029,22 @@ pub struct Config {
/// etc. This is a hidden argument that should NOT be used in production as
/// it is highly insecure and I will personally yell at you if I catch you
/// using this.
///
/// display: hidden
#[serde(default)]
pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure:
bool,
// external structure; separate section
/// display: nested
#[serde(default)]
pub ldap: LdapConfig,
/// Configuration for antispam support
/// display: nested
#[serde(default)]
pub antispam: Option<Antispam>,
// external structure; separate section
/// display: nested
#[serde(default)]
pub blurhashing: BlurhashConfig,
#[serde(flatten)]
@@ -2259,15 +2261,23 @@ struct ListeningAddr {
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam",
optional = "true"
)]
pub struct Antispam {
/// display: nested
pub meowlnir: Option<MeowlnirConfig>,
/// display: nested
pub draupnir: Option<DraupnirConfig>,
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.meowlnir"
section = "global.antispam.meowlnir",
optional = "true"
)]
pub struct MeowlnirConfig {
/// The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
@@ -2296,7 +2306,8 @@ pub struct MeowlnirConfig {
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.draupnir"
section = "global.antispam.draupnir",
optional = "true"
)]
pub struct DraupnirConfig {
/// The base URL on which to contact Draupnir (before /api/).
+1 -1
View File
@@ -62,7 +62,7 @@ pub const INFO_SPAN_LEVEL: Level = if cfg!(debug_assertions) {
pub static DEBUGGER: LazyLock<bool> =
LazyLock::new(|| env::var("_").unwrap_or_default().ends_with("gdb"));
#[cfg_attr(debug_assertions, crate::ctor)]
#[cfg_attr(debug_assertions, ctor::ctor)]
#[cfg_attr(not(debug_assertions), allow(dead_code))]
fn set_panic_trap() {
if !*DEBUGGER {
+2 -2
View File
@@ -115,7 +115,7 @@ macro_rules! err {
macro_rules! err_log {
($out:ident, $level:ident, $($fields:tt)+) => {{
use $crate::tracing::{
callsite, callsite2, metadata, valueset, Callsite,
callsite, callsite2, metadata, valueset_all, Callsite,
Level,
};
@@ -133,7 +133,7 @@ macro_rules! err_log {
fields: $($fields)+,
};
($crate::error::visit)(&mut $out, LEVEL, &__CALLSITE, &mut valueset!(__CALLSITE.metadata().fields(), $($fields)+));
($crate::error::visit)(&mut $out, LEVEL, &__CALLSITE, &mut valueset_all!(__CALLSITE.metadata().fields(), $($fields)+));
($out).into()
}}
}
-95
View File
@@ -1,95 +0,0 @@
//! Information about the build related to Cargo. This is a frontend interface
//! informed by proc-macros that capture raw information at build time which is
//! further processed at runtime either during static initialization or as
//! necessary.
use std::sync::OnceLock;
use cargo_toml::{DepsSet, Manifest};
use conduwuit_macros::cargo_manifest;
use crate::Result;
// Raw captures of the cargo manifest for each crate. This is provided by a
// proc-macro at build time since the source directory and the cargo toml's may
// not be present during execution.
#[cargo_manifest]
const WORKSPACE_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "macros")]
const MACROS_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "core")]
const CORE_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "database")]
const DATABASE_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "service")]
const SERVICE_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "admin")]
const ADMIN_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "router")]
const ROUTER_MANIFEST: &'static str = ();
#[cargo_manifest(crate = "main")]
const MAIN_MANIFEST: &'static str = ();
/// Processed list of features across all project crates. This is generated from
/// the data in the MANIFEST strings and contains all possible project features.
/// For *enabled* features see the info::rustc module instead.
static FEATURES: OnceLock<Vec<String>> = OnceLock::new();
/// Processed list of dependencies. This is generated from the data captured in
/// the MANIFEST.
static DEPENDENCIES: OnceLock<DepsSet> = OnceLock::new();
#[must_use]
pub fn dependencies_names() -> Vec<&'static str> {
dependencies().keys().map(String::as_str).collect()
}
pub fn dependencies() -> &'static DepsSet {
DEPENDENCIES.get_or_init(|| {
init_dependencies().unwrap_or_else(|e| panic!("Failed to initialize dependencies: {e}"))
})
}
/// List of all possible features for the project. For *enabled* features in
/// this build see the companion function in info::rustc.
pub fn features() -> &'static Vec<String> {
FEATURES.get_or_init(|| {
init_features().unwrap_or_else(|e| panic!("Failed initialize features: {e}"))
})
}
fn init_features() -> Result<Vec<String>> {
let mut features = Vec::new();
append_features(&mut features, WORKSPACE_MANIFEST)?;
append_features(&mut features, MACROS_MANIFEST)?;
append_features(&mut features, CORE_MANIFEST)?;
append_features(&mut features, DATABASE_MANIFEST)?;
append_features(&mut features, SERVICE_MANIFEST)?;
append_features(&mut features, ADMIN_MANIFEST)?;
append_features(&mut features, ROUTER_MANIFEST)?;
append_features(&mut features, MAIN_MANIFEST)?;
features.sort();
features.dedup();
Ok(features)
}
fn append_features(features: &mut Vec<String>, manifest: &str) -> Result<()> {
let manifest = Manifest::from_str(manifest)?;
features.extend(manifest.features.keys().cloned());
Ok(())
}
fn init_dependencies() -> Result<DepsSet> {
let manifest = Manifest::from_str(WORKSPACE_MANIFEST)?;
let deps_set = manifest
.workspace
.as_ref()
.expect("manifest has workspace section")
.dependencies
.clone();
Ok(deps_set)
}
-7
View File
@@ -1,12 +1,5 @@
//! Information about the project. This module contains version, build, system,
//! etc information which can be queried by admins or used by developers.
pub mod cargo;
pub mod room_version;
pub mod rustc;
pub mod version;
pub use conduwuit_macros::rustc_flags_capture;
pub const MODULE_ROOT: &str = const_str::split!(std::module_path!(), "::")[0];
pub const CRATE_PREFIX: &str = const_str::split!(MODULE_ROOT, '_')[0];
-54
View File
@@ -1,54 +0,0 @@
//! Information about the build related to rustc. This is a frontend interface
//! informed by proc-macros at build time. Since the project is split into
//! several crates, lower-level information is supplied from each crate during
//! static initialization.
use std::{collections::BTreeMap, sync::OnceLock};
use crate::utils::exchange;
/// Raw capture of rustc flags used to build each crate in the project. Informed
/// by rustc_flags_capture macro (one in each crate's mod.rs). This is
/// done during static initialization which is why it's mutex-protected and pub.
/// Should not be written to by anything other than our macro.
///
/// We specifically use a std mutex here because parking_lot cannot be used
/// after thread local storage is destroyed on MacOS.
pub static FLAGS: std::sync::Mutex<BTreeMap<&str, &[&str]>> =
std::sync::Mutex::new(BTreeMap::new());
/// Processed list of enabled features across all project crates. This is
/// generated from the data in FLAGS.
static FEATURES: OnceLock<Vec<&'static str>> = OnceLock::new();
/// List of features enabled for the project.
pub fn features() -> &'static Vec<&'static str> { FEATURES.get_or_init(init_features) }
fn init_features() -> Vec<&'static str> {
let mut features = Vec::new();
FLAGS
.lock()
.expect("locked")
.iter()
.for_each(|(_, flags)| append_features(&mut features, flags));
features.sort_unstable();
features.dedup();
features
}
fn append_features(features: &mut Vec<&'static str>, flags: &[&'static str]) {
let mut next_is_cfg = false;
for flag in flags {
let is_cfg = *flag == "--cfg";
let is_feature = flag.starts_with("feature=");
if exchange(&mut next_is_cfg, is_cfg) && is_feature {
if let Some(feature) = flag
.split_once('=')
.map(|(_, feature)| feature.trim_matches('"'))
{
features.push(feature);
}
}
}
}
+2 -4
View File
@@ -22,7 +22,7 @@ pub use ::tracing;
pub use config::Config;
pub use error::Error;
pub use info::{
rustc_flags_capture, version,
version,
version::{name, version},
};
pub use matrix::{
@@ -30,12 +30,10 @@ pub use matrix::{
};
pub use parking_lot::{Mutex as SyncMutex, RwLock as SyncRwLock};
pub use server::Server;
pub use utils::{ctor, dtor, implement, result, result::Result};
pub use utils::{implement, result, result::Result};
pub use crate as conduwuit_core;
rustc_flags_capture! {}
#[cfg(any(not(conduwuit_mods), not(feature = "conduwuit_mods")))]
pub mod mods {
#[macro_export]
-1
View File
@@ -22,7 +22,6 @@ pub mod time;
pub mod with_lock;
pub use ::conduwuit_macros::implement;
pub use ::ctor::{ctor, dtor};
pub use self::{
arrayvec::ArrayVecExt,
-1
View File
@@ -64,7 +64,6 @@ serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true
ctor.workspace = true
[lints]
workspace = true
+8
View File
@@ -108,6 +108,14 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "mediaid_user",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "mediaid_redacted",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "mediaid_interestedservername",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "onetimekeyid_onetimekeys",
..descriptor::RANDOM_SMALL
-3
View File
@@ -3,11 +3,8 @@
extern crate conduwuit_core as conduwuit;
extern crate rust_rocksdb as rocksdb;
use ctor::{ctor, dtor};
conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}
#[cfg(test)]
mod benches;
-47
View File
@@ -1,47 +0,0 @@
use std::{fs::read_to_string, path::PathBuf};
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::{Error, ItemConst, Meta};
use crate::{Result, utils};
pub(super) fn manifest(item: ItemConst, args: &[Meta]) -> Result<TokenStream> {
let member = utils::get_named_string(args, "crate");
let path = manifest_path(member.as_deref())?;
let manifest = read_to_string(&path).unwrap_or_default();
let val = manifest.as_str();
let name = item.ident;
let ret = quote! {
const #name: &'static str = #val;
};
Ok(ret.into())
}
#[allow(clippy::option_env_unwrap)]
fn manifest_path(member: Option<&str>) -> Result<PathBuf> {
let Some(path) = option_env!("CARGO_MANIFEST_DIR") else {
return Err(Error::new(
Span::call_site().into(),
"missing CARGO_MANIFEST_DIR in environment",
));
};
let mut path: PathBuf = path.into();
// conduwuit/src/macros/ -> conduwuit/src/
path.pop();
if let Some(member) = member {
// conduwuit/$member/Cargo.toml
path.push(member);
} else {
// conduwuit/src/ -> conduwuit/
path.pop();
}
path.push("Cargo.toml");
Ok(path)
}
+76 -36
View File
@@ -73,11 +73,19 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
.expect("written to config file");
}
file.write_fmt(format_args!("\n[{section}]\n"))
let optional = settings.get("optional").is_some_and(|v| v == "true");
let section_header = if optional {
format!("\n#[{section}]\n")
} else {
format!("\n[{section}]\n")
};
file.write_fmt(format_args!("{section_header}"))
.expect("written to config file");
}
let mut summary: Vec<TokenStream2> = Vec::new();
let mut nested_displays: Vec<TokenStream2> = Vec::new();
if let Fields::Named(FieldsNamed { named, .. }) = &input.fields {
for field in named {
let Some(ident) = &field.ident else {
@@ -92,35 +100,6 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
continue;
};
let doc = get_doc_comment(field)
.unwrap_or_else(|| undocumented.into())
.trim_end()
.to_owned();
let doc = if doc.ends_with('#') {
format!("{doc}\n")
} else {
format!("{doc}\n#\n")
};
let default = get_doc_comment_line(field, "default")
.or_else(|| get_default(field))
.unwrap_or_default();
let default = if !default.is_empty() {
format!(" {default}")
} else {
default
};
if let Some(file) = file.as_mut() {
file.write_fmt(format_args!("\n{doc}"))
.expect("written to config file");
file.write_fmt(format_args!("#{ident} ={default}\n"))
.expect("written to config file");
}
let display = get_doc_comment_line(field, "display");
let display_directive = |key| {
display
@@ -129,17 +108,77 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
.flat_map(|display| display.split(' '))
.any(|directive| directive == key)
};
let is_nested = display_directive("nested");
let is_hidden = display_directive("hidden");
if !display_directive("hidden") {
let value = if display_directive("sensitive") {
quote! { "***********" }
// Only generate config file entries for non-nested, visible types
if !is_nested && !is_hidden {
let doc = get_doc_comment(field)
.unwrap_or_else(|| undocumented.into())
.trim_end()
.to_owned();
let doc = if doc.ends_with('#') {
format!("{doc}\n")
} else {
quote! { format_args!("{:?}", self.#ident) }
format!("{doc}\n#\n")
};
let name = ident.to_string();
let default = get_doc_comment_line(field, "default")
.or_else(|| get_default(field))
.unwrap_or_default();
let default = if !default.is_empty() {
format!(" {default}")
} else {
default
};
if let Some(file) = file.as_mut() {
file.write_fmt(format_args!("\n{doc}"))
.expect("written to config file");
file.write_fmt(format_args!("#{ident} ={default}\n"))
.expect("written to config file");
}
}
// Generate Display implementation for all fields
let name = ident.to_string();
if display_directive("sensitive") {
summary.push(quote! {
writeln!(out, "| {} | {} |", #name, #value)?;
writeln!(out, "| {} | {} |", #name, "***********")?;
});
} else if is_nested {
let is_option = matches!(type_name.as_str(), "Option");
if is_option {
summary.push(quote! {
writeln!(out, "| {} | {} |", #name,
if self.#ident.is_some() { "[configured]" } else { "None" })?;
});
nested_displays.push(quote! {
if let Some(nested) = &self.#ident {
writeln!(out)?;
writeln!(out, "## {}", #name)?;
write!(out, "{}", nested)?;
}
});
} else {
summary.push(quote! {
writeln!(out, "| {} | [configured] |", #name)?;
});
nested_displays.push(quote! {
writeln!(out)?;
writeln!(out, "## {}", #name)?;
write!(out, "{}", &self.#ident)?;
});
}
} else {
summary.push(quote! {
writeln!(out, "| {} | {:?} |", #name, self.#ident)?;
});
}
}
@@ -159,6 +198,7 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
writeln!(out, "| name | value |")?;
writeln!(out, "| :--- | :--- |")?;
#( #summary )*
#( #nested_displays )*
Ok(())
}
}
+1 -11
View File
@@ -1,15 +1,13 @@
mod admin;
mod cargo;
mod config;
mod debug;
mod implement;
mod refutable;
mod rustc;
mod utils;
use proc_macro::TokenStream;
use syn::{
Error, Item, ItemConst, ItemEnum, ItemFn, ItemStruct, Meta,
Error, Item, ItemEnum, ItemFn, ItemStruct, Meta,
parse::{Parse, Parser},
parse_macro_input,
};
@@ -26,19 +24,11 @@ pub fn admin_command_dispatch(args: TokenStream, input: TokenStream) -> TokenStr
attribute_macro::<ItemEnum, _>(args, input, admin::command_dispatch)
}
#[proc_macro_attribute]
pub fn cargo_manifest(args: TokenStream, input: TokenStream) -> TokenStream {
attribute_macro::<ItemConst, _>(args, input, cargo::manifest)
}
#[proc_macro_attribute]
pub fn recursion_depth(args: TokenStream, input: TokenStream) -> TokenStream {
attribute_macro::<Item, _>(args, input, debug::recursion_depth)
}
#[proc_macro]
pub fn rustc_flags_capture(args: TokenStream) -> TokenStream { rustc::flags_capture(args) }
#[proc_macro_attribute]
pub fn refutable(args: TokenStream, input: TokenStream) -> TokenStream {
attribute_macro::<ItemFn, _>(args, input, refutable::refutable)
-29
View File
@@ -1,29 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
pub(super) fn flags_capture(args: TokenStream) -> TokenStream {
let cargo_crate_name = std::env::var("CARGO_CRATE_NAME");
let crate_name = match cargo_crate_name.as_ref() {
| Err(_) => return args,
| Ok(crate_name) => crate_name.trim_start_matches("conduwuit_"),
};
let flag = std::env::args().collect::<Vec<_>>();
let flag_len = flag.len();
let ret = quote! {
pub static RUSTC_FLAGS: [&str; #flag_len] = [#( #flag ),*];
#[ctor]
fn _set_rustc_flags() {
conduwuit_core::info::rustc::FLAGS.lock().expect("locked").insert(#crate_name, &RUSTC_FLAGS);
}
// static strings have to be yanked on module unload
#[dtor]
fn _unset_rustc_flags() {
conduwuit_core::info::rustc::FLAGS.lock().expect("locked").remove(#crate_name);
}
};
ret.into()
}
-1
View File
@@ -207,7 +207,6 @@ clap.workspace = true
console-subscriber.optional = true
console-subscriber.workspace = true
const-str.workspace = true
ctor.workspace = true
log.workspace = true
opentelemetry.optional = true
opentelemetry.workspace = true
+2 -6
View File
@@ -2,7 +2,7 @@
use std::sync::{Arc, atomic::Ordering};
use conduwuit_core::{debug_info, error, rustc_flags_capture};
use conduwuit_core::{debug_info, error};
mod clap;
mod logging;
@@ -13,12 +13,8 @@ mod sentry;
mod server;
mod signal;
use ctor::{ctor, dtor};
use server::Server;
rustc_flags_capture! {}
pub use conduwuit_core::{Error, Result};
use server::Server;
pub use crate::clap::Args;
-1
View File
@@ -122,7 +122,6 @@ tokio.workspace = true
tower.workspace = true
tower-http.workspace = true
tracing.workspace = true
ctor.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true
-2
View File
@@ -12,12 +12,10 @@ use std::{panic::AssertUnwindSafe, pin::Pin, sync::Arc};
use conduwuit::{Error, Result, Server};
use conduwuit_service::Services;
use ctor::{ctor, dtor};
use futures::{Future, FutureExt, TryFutureExt};
conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}
#[unsafe(no_mangle)]
pub extern "Rust" fn start(
-1
View File
@@ -118,7 +118,6 @@ webpage.optional = true
blurhash.workspace = true
blurhash.optional = true
recaptcha-verify = { version = "0.1.5", default-features = false }
ctor.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true
+21 -2
View File
@@ -18,8 +18,9 @@
use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use conduwuit::{Result, Server, debug, info, warn};
use conduwuit::{Result, Server, debug, error, info, warn};
use database::{Deserialized, Map};
use rand::Rng;
use ruma::events::{Mentions, room::message::RoomMessageEventContent};
use serde::Deserialize;
use tokio::{
@@ -86,9 +87,27 @@ impl crate::Service for Service {
return Ok(());
}
// Run the first check immediately and send errors to admin room
if let Err(e) = self.check().await {
error!(?e, "Failed to check for announcements on startup");
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Failed to check for announcements on startup: {e}"
)))
.await
.ok();
}
let first_check_jitter = {
let mut rng = rand::thread_rng();
let jitter_percent = rng.gen_range(-50.0..=10.0);
self.interval.mul_f64(1.0 + jitter_percent / 100.0)
};
let mut i = interval(self.interval);
i.set_missed_tick_behavior(MissedTickBehavior::Delay);
i.reset_after(self.interval);
i.reset_after(first_check_jitter);
loop {
tokio::select! {
() = self.interrupt.notified() => break,
+53 -2
View File
@@ -1,4 +1,4 @@
use std::{sync::Arc, time::Duration};
use std::{str::FromStr, sync::Arc, time::Duration};
use conduwuit::{
Err, Result, debug, debug_info, err,
@@ -6,13 +6,15 @@ use conduwuit::{
};
use database::{Database, Interfix, Map};
use futures::StreamExt;
use ruma::{Mxc, OwnedMxcUri, UserId, http_headers::ContentDisposition};
use ruma::{Mxc, OwnedMxcUri, OwnedServerName, UserId, http_headers::ContentDisposition};
use super::{preview::UrlPreviewData, thumbnail::Dim};
pub(crate) struct Data {
mediaid_file: Arc<Map>,
mediaid_user: Arc<Map>,
mediaid_redacted: Arc<Map>,
mediaid_interestedservername: Arc<Map>,
url_previews: Arc<Map>,
}
@@ -28,6 +30,8 @@ impl Data {
Self {
mediaid_file: db["mediaid_file"].clone(),
mediaid_user: db["mediaid_user"].clone(),
mediaid_redacted: db["mediaid_redacted"].clone(),
mediaid_interestedservername: db["mediaid_interestedservername"].clone(),
url_previews: db["url_previews"].clone(),
}
}
@@ -77,6 +81,22 @@ impl Data {
self.mediaid_user.remove(key);
})
.await;
self.mediaid_interestedservername
.stream_prefix_raw(&prefix)
.ignore_err()
.ready_for_each(|(key, _)| {
debug_assert!(
key.starts_with(mxc.to_string().as_bytes()),
"key should start with the mxc"
);
debug_info!("Deleting interested server name key {key:?}");
self.mediaid_interestedservername.remove(key);
})
.await;
// NOTE: Redaction status is kept even after deletion
}
/// Searches for all files with the given MXC
@@ -275,4 +295,35 @@ impl Data {
image_height,
})
}
/// Marks a media item as redacted, preventing it from being served or
/// re-used.
pub(super) fn mark_redacted(&self, media_id: &str) {
self.mediaid_redacted.insert(media_id, []);
}
/// Checks if a media item is redacted.
pub(super) async fn is_redacted(&self, media_id: &str) -> bool {
self.mediaid_redacted.contains(media_id).await
}
pub(super) fn add_interested_server_name(&self, media_id: &str, server_name: &str) {
let key = (media_id, server_name);
self.mediaid_interestedservername
.insert(&database::serialize_key(key).expect("key must be serializable"), []);
}
pub(super) async fn interested_server_names(&self, media_id: &str) -> Vec<OwnedServerName> {
let prefix = (media_id, Interfix);
self.mediaid_interestedservername
.stream_prefix_raw(&prefix)
.ignore_err()
.map(|(key, _)| {
let parts: Vec<&[u8]> = key.rsplit(|&b| b == 0xFF).collect();
OwnedServerName::parse(string_from_bytes(parts[0]).unwrap_or_default())
.unwrap_or_else(|_| OwnedServerName::from_str("invalid.server").unwrap())
})
.collect()
.await
}
}
+37 -1
View File
@@ -17,7 +17,9 @@ use conduwuit::{
},
warn,
};
use ruma::{Mxc, OwnedMxcUri, UserId, http_headers::ContentDisposition};
use ruma::{
Mxc, OwnedMxcUri, OwnedServerName, ServerName, UserId, http_headers::ContentDisposition,
};
use tokio::{
fs,
io::{AsyncReadExt, AsyncWriteExt, BufReader},
@@ -139,6 +141,28 @@ impl Service {
}
}
/// Marks a media ID as redacted, and deletes the associated file.
pub async fn redact(&self, mxc: &Mxc<'_>) -> Result<()> {
self.db.mark_redacted(mxc.media_id);
self.delete(mxc).await
}
/// Checks if a media ID is redacted.
pub async fn is_redacted(&self, mxc: &Mxc<'_>) -> bool {
self.db.is_redacted(mxc.media_id).await
}
/// Marks a server as "interested" (i.e. has downloaded this media from us).
pub fn mark_server_interested(&self, mxc: &Mxc<'_>, server_name: &ServerName) {
self.db
.add_interested_server_name(mxc.media_id, server_name.as_str());
}
/// Gets all servers interested in this media ID.
pub async fn get_interested_servers(&self, mxc: &Mxc<'_>) -> Vec<OwnedServerName> {
self.db.interested_server_names(mxc.media_id).await
}
/// Deletes all media by the specified user
///
/// currently, this is only practical for local users
@@ -229,6 +253,18 @@ impl Service {
Ok(mxcs)
}
/// Checks if a user owns a given MXC URI
pub async fn user_owns(&self, user: &UserId, mxc: &Mxc<'_>) -> bool {
self
.db
.get_all_user_mxcs(user) // TODO: this can be more efficient.
.await
.iter()
.any(|v| {
v.parts().map(|x|(x.server_name, x.media_id)) == Ok((mxc.server_name, mxc.media_id))
})
}
/// Deletes all media files in the given time frame.
/// Returns a usize with the amount of media files deleted.
pub async fn delete_all_media_within_timeframe(
+15 -41
View File
@@ -56,9 +56,18 @@ pub async fn fetch_remote_content(
let result = self
.fetch_content_authenticated(mxc, user, server, timeout_ms)
.await;
.await
.inspect_err(|error| {
debug_warn!(
%mxc,
?user,
?server,
?error,
"Authenticated fetch of remote content failed"
);
});
if let Err(Error::Request(NotFound, ..)) = &result {
if let Err(Error::Request(Unrecognized, ..)) = &result {
return self
.fetch_content_unauthenticated(mxc, user, server, timeout_ms)
.await;
@@ -87,7 +96,7 @@ async fn fetch_thumbnail_authenticated(
timeout_ms,
};
let Response { content, .. } = self.federation_request(mxc, user, server, request).await?;
let Response { content, .. } = self.federation_request(mxc, server, request).await?;
match content {
| FileOrLocation::File(content) =>
@@ -111,7 +120,7 @@ async fn fetch_content_authenticated(
timeout_ms,
};
let Response { content, .. } = self.federation_request(mxc, user, server, request).await?;
let Response { content, .. } = self.federation_request(mxc, server, request).await?;
match content {
| FileOrLocation::File(content) => self.handle_content_file(mxc, user, content).await,
@@ -145,7 +154,7 @@ async fn fetch_thumbnail_unauthenticated(
let Response {
file, content_type, content_disposition, ..
} = self.federation_request(mxc, user, server, request).await?;
} = self.federation_request(mxc, server, request).await?;
let content = Content { file, content_type, content_disposition };
@@ -173,7 +182,7 @@ async fn fetch_content_unauthenticated(
let Response {
file, content_type, content_disposition, ..
} = self.federation_request(mxc, user, server, request).await?;
} = self.federation_request(mxc, server, request).await?;
let content = Content { file, content_type, content_disposition };
@@ -296,7 +305,6 @@ async fn location_request(&self, location: &str) -> Result<FileMeta> {
async fn federation_request<Request>(
&self,
mxc: &Mxc<'_>,
user: Option<&UserId>,
server: Option<&ServerName>,
request: Request,
) -> Result<Request::IncomingResponse>
@@ -307,40 +315,6 @@ where
.sending
.send_federation_request(server.unwrap_or(mxc.server_name), request)
.await
.map_err(|error| handle_federation_error(mxc, user, server, error))
}
// Handles and adjusts the error for the caller to determine if they should
// request the fallback endpoint or give up.
fn handle_federation_error(
mxc: &Mxc<'_>,
user: Option<&UserId>,
server: Option<&ServerName>,
error: Error,
) -> Error {
let fallback = || {
err!(Request(NotFound(
debug_error!(%mxc, user = user.map(tracing::field::display), server = server.map(tracing::field::display), ?error, "Remote media not found")
)))
};
// Matrix server responses for fallback always taken.
if error.kind() == NotFound || error.kind() == Unrecognized {
return fallback();
}
// If we get these from any middleware we'll try the other endpoint rather than
// giving up too early.
if error.status_code().is_redirection()
|| error.status_code().is_client_error()
|| error.status_code().is_server_error()
{
return fallback();
}
// Reached for 5xx errors. This is where we don't fallback given the likelihood
// the other endpoint will also be a 5xx and we're wasting time.
error
}
#[implement(super::Service)]
-2
View File
@@ -34,11 +34,9 @@ pub mod transaction_ids;
pub mod uiaa;
pub mod users;
use ctor::{ctor, dtor};
pub(crate) use service::{Args, Dep, Service};
pub use crate::services::Services;
conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}
+10
View File
@@ -233,6 +233,16 @@ pub async fn try_auth(
| AuthData::Dummy(_) => {
uiaainfo.completed.push(AuthType::Dummy);
},
| AuthData::FallbackAcknowledgement(_) => {
// The client is checking if authentication has succeeded out-of-band. This is
// possible if the client is using "fallback auth" (see spec section
// 4.9.1.4), which we don't support (and probably never will, because it's a
// disgusting hack).
// Return early to tell the client that no, authentication did not succeed while
// it wasn't looking.
return Ok((false, uiaainfo));
},
| k => error!("type not supported: {:?}", k),
}
+1 -1
View File
@@ -304,7 +304,7 @@ impl Service {
pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); }
pub async fn is_login_disabled(&self, user_id: &UserId) -> bool {
self.db.userid_logindisabled.contains(user_id).await
self.db.userid_logindisabled.exists(user_id).await.is_ok()
}
/// Check if account is active, infallible