Compare commits

..

2 Commits

Author SHA1 Message Date
Jade Ellis
4a7ad1350b WIP 2025-06-14 20:14:09 +01:00
Jade Ellis
d0f00e6f5c feat: Allow mentioning @room in an admin announcement 2025-06-14 19:09:54 +01:00
51 changed files with 355 additions and 3026 deletions

View File

@@ -1,2 +0,0 @@
[alias]
xtask = "run --package xtask --"

134
Cargo.lock generated
View File

@@ -47,56 +47,12 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.98"
@@ -761,25 +717,14 @@ dependencies = [
"clap_derive",
]
[[package]]
name = "clap-markdown"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2a2617956a06d4885b490697b5307ebb09fec10b088afc18c81762d848c2339"
dependencies = [
"clap",
]
[[package]]
name = "clap_builder"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
@@ -800,16 +745,6 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clap_mangen"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc33c849748320656a90832f54a5eeecaa598e92557fb5dedebc3355746d31e4"
dependencies = [
"clap",
"roff",
]
[[package]]
name = "cmake"
version = "0.1.54"
@@ -825,12 +760,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -865,7 +794,6 @@ dependencies = [
"tokio-metrics",
"tracing",
"tracing-flame",
"tracing-journald",
"tracing-opentelemetry",
"tracing-subscriber",
]
@@ -1059,6 +987,7 @@ dependencies = [
"base64 0.22.1",
"blurhash",
"bytes",
"conduwuit_build_metadata",
"conduwuit_core",
"conduwuit_database",
"const-str",
@@ -2406,12 +2335,6 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.12.1"
@@ -3002,12 +2925,6 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "openssl-probe"
version = "0.1.6"
@@ -3776,12 +3693,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "roff"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
[[package]]
name = "ruma"
version = "0.10.1"
@@ -4614,12 +4525,6 @@ dependencies = [
"quote",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subslice"
version = "0.2.3"
@@ -5163,17 +5068,6 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "tracing-journald"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657"
dependencies = [
"libc",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
@@ -5351,12 +5245,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.16.0"
@@ -5956,26 +5844,6 @@ dependencies = [
"markup5ever",
]
[[package]]
name = "xtask"
version = "0.5.0-rc.5"
dependencies = [
"clap",
"serde",
"serde_json",
]
[[package]]
name = "xtask-admin-command"
version = "0.5.0-rc.5"
dependencies = [
"clap-markdown",
"clap_builder",
"clap_mangen",
"conduwuit",
"conduwuit_admin",
]
[[package]]
name = "yansi"
version = "1.0.1"

View File

@@ -2,7 +2,7 @@
[workspace]
resolver = "2"
members = ["src/*", "xtask/*"]
members = ["src/*"]
default-members = ["src/*"]
[workspace.package]
@@ -213,8 +213,6 @@ default-features = false
version = "0.3.19"
default-features = false
features = ["env-filter", "std", "tracing", "tracing-log", "ansi", "fmt"]
[workspace.dependencies.tracing-journald]
version = "0.3.1"
[workspace.dependencies.tracing-core]
version = "0.1.33"
default-features = false
@@ -383,7 +381,7 @@ features = [
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
"unstable-extensible-events",
"unstable-pdu",
@@ -639,11 +637,6 @@ package = "conduwuit_build_metadata"
path = "src/build_metadata"
default-features = false
[workspace.dependencies.conduwuit]
package = "conduwuit"
path = "src/main"
###############################################################################
#
# Release profiles

View File

@@ -16,10 +16,6 @@ DeviceAllow=char-tty
StandardInput=tty-force
StandardOutput=tty
StandardError=journal+console
Environment="CONTINUWUITY_LOG_TO_JOURNALD=1"
Environment="CONTINUWUITY_JOURNALD_IDENTIFIER=%N"
TTYReset=yes
# uncomment to allow buffer to be cleared every restart
TTYVTDisallocate=no

View File

@@ -119,6 +119,15 @@
#
#allow_announcements_check = true
# If enabled, continuwuity will send anonymous analytics data periodically
# to help improve development. This includes basic server metadata like
# version, commit hash, and federation status. All requests are signed
# with the server's federation signing key. Data is sent on startup (with
# up to 5 minutes jitter) and every 12 hours thereafter (with up to 30
# minutes jitter) to distribute load.
#
#allow_analytics = true
# Set this to any float value to multiply continuwuity's in-memory LRU
# caches with such as "auth_chain_cache_capacity".
#
@@ -660,21 +669,6 @@
#
#log_thread_ids = false
# Enable journald logging on Unix platforms
#
# When enabled, log output will be sent to the systemd journal
# This is only supported on Unix platforms
#
#log_to_journald = false
# The syslog identifier to use with journald logging
#
# Only used when journald logging is enabled
#
# Defaults to the binary name
#
#journald_identifier =
# OpenID token expiration/TTL in seconds.
#
# These are the OpenID tokens that are primarily used for Matrix account

View File

@@ -14,9 +14,6 @@ Type=notify
Environment="CONTINUWUITY_CONFIG=/etc/conduwuit/conduwuit.toml"
Environment="CONTINUWUITY_LOG_TO_JOURNALD=1"
Environment="CONTINUWUITY_JOURNALD_IDENTIFIER=%N"
ExecStart=/usr/sbin/conduwuit
ReadWritePaths=/var/lib/conduwuit /etc/conduwuit

View File

@@ -3,7 +3,7 @@
"$id": "https://continwuity.org/schema/announcements.schema.json",
"type": "object",
"properties": {
"updates": {
"announcements": {
"type": "array",
"items": {
"type": "object",
@@ -16,6 +16,10 @@
},
"date": {
"type": "string"
},
"mention_room": {
"type": "boolean",
"description": "Whether to mention the room (@room) when posting this announcement"
}
},
"required": [
@@ -26,6 +30,6 @@
}
},
"required": [
"updates"
"announcements"
]
}

View File

@@ -10,7 +10,7 @@
#[derive(Debug, Parser)]
#[command(name = "conduwuit", version = conduwuit::version())]
pub enum AdminCommand {
pub(super) enum AdminCommand {
#[command(subcommand)]
/// - Commands for managing appservices
Appservices(AppserviceCommand),

View File

@@ -7,7 +7,7 @@
#[derive(Debug, Subcommand)]
#[admin_command_dispatch]
pub enum AppserviceCommand {
pub(super) enum AppserviceCommand {
/// - Register an appservice using its registration YAML
///
/// This command needs a YAML generated by an appservice (such as a bridge),

View File

@@ -7,6 +7,6 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum CheckCommand {
pub(super) enum CheckCommand {
CheckAllUsers,
}

View File

@@ -11,7 +11,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum DebugCommand {
pub(super) enum DebugCommand {
/// - Echo input of admin command
Echo {
message: Vec<String>,

View File

@@ -4,7 +4,7 @@
#[admin_command_dispatch]
#[derive(Debug, clap::Subcommand)]
pub enum TesterCommand {
pub(crate) enum TesterCommand {
Panic,
Failure,
Tester,

View File

@@ -8,7 +8,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum FederationCommand {
pub(super) enum FederationCommand {
/// - List all rooms we are currently handling an incoming pdu from
IncomingFederation,

View File

@@ -9,7 +9,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum MediaCommand {
pub(super) enum MediaCommand {
/// - Deletes a single media file from our database and on the filesystem
/// via a single MXC URL or event ID (not redacted)
Delete {
@@ -90,10 +90,10 @@ pub enum MediaCommand {
#[arg(short, long, default_value("10000"))]
timeout: u32,
#[arg(long, default_value("800"))]
#[arg(short, long, default_value("800"))]
width: u32,
#[arg(long, default_value("800"))]
#[arg(short, long, default_value("800"))]
height: u32,
},
}

View File

@@ -33,8 +33,6 @@
conduwuit::mod_dtor! {}
conduwuit::rustc_flags_capture! {}
pub use crate::admin::AdminCommand;
/// Install the admin command processor
pub async fn init(admin_service: &service::admin::Service) {
_ = admin_service

View File

@@ -8,7 +8,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/account_data.rs
pub enum AccountDataCommand {
pub(crate) enum AccountDataCommand {
/// - Returns all changes to the account data that happened after `since`.
ChangesSince {
/// Full user ID

View File

@@ -6,7 +6,7 @@
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/appservice.rs
pub enum AppserviceCommand {
pub(crate) enum AppserviceCommand {
/// - Gets the appservice registration info/details from the ID as a string
GetRegistration {
/// Appservice registration ID

View File

@@ -6,7 +6,7 @@
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/globals.rs
pub enum GlobalsCommand {
pub(crate) enum GlobalsCommand {
DatabaseVersion,
CurrentCount,

View File

@@ -27,7 +27,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
/// Query tables from database
pub enum QueryCommand {
pub(super) enum QueryCommand {
/// - account_data.rs iterators and getters
#[command(subcommand)]
AccountData(AccountDataCommand),

View File

@@ -7,7 +7,7 @@
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/presence.rs
pub enum PresenceCommand {
pub(crate) enum PresenceCommand {
/// - Returns the latest presence event for the given user.
GetPresence {
/// Full user ID

View File

@@ -5,7 +5,7 @@
use crate::Context;
#[derive(Debug, Subcommand)]
pub enum PusherCommand {
pub(crate) enum PusherCommand {
/// - Returns all the pushers for the user.
GetPushers {
/// Full user ID

View File

@@ -19,7 +19,7 @@
#[derive(Debug, Subcommand)]
#[allow(clippy::enum_variant_names)]
/// Query tables from database
pub enum RawCommand {
pub(crate) enum RawCommand {
/// - List database maps
RawMaps,

View File

@@ -8,7 +8,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
/// Resolver service and caches
pub enum ResolverCommand {
pub(crate) enum ResolverCommand {
/// Query the destinations cache
DestinationsCache {
server_name: Option<OwnedServerName>,

View File

@@ -7,7 +7,7 @@
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/rooms/alias.rs
pub enum RoomAliasCommand {
pub(crate) enum RoomAliasCommand {
ResolveLocalAlias {
/// Full room alias
alias: OwnedRoomAliasId,

View File

@@ -6,7 +6,7 @@
use crate::Context;
#[derive(Debug, Subcommand)]
pub enum RoomStateCacheCommand {
pub(crate) enum RoomStateCacheCommand {
ServerInRoom {
server: OwnedServerName,
room_id: OwnedRoomId,

View File

@@ -8,7 +8,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
/// Query tables from database
pub enum RoomTimelineCommand {
pub(crate) enum RoomTimelineCommand {
Pdus {
room_id: OwnedRoomOrAliasId,

View File

@@ -8,7 +8,7 @@
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/sending.rs
pub enum SendingCommand {
pub(crate) enum SendingCommand {
/// - Queries database for all `servercurrentevent_data`
ActiveRequests,

View File

@@ -7,7 +7,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
/// Query tables from database
pub enum ShortCommand {
pub(crate) enum ShortCommand {
ShortEventId {
event_id: OwnedEventId,
},

View File

@@ -8,7 +8,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/users.rs
pub enum UsersCommand {
pub(crate) enum UsersCommand {
CountUsers,
IterUsers,

View File

@@ -8,7 +8,7 @@
use crate::Context;
#[derive(Debug, Subcommand)]
pub enum RoomAliasCommand {
pub(crate) enum RoomAliasCommand {
/// - Make an alias point to a room.
Set {
#[arg(short, long)]

View File

@@ -6,7 +6,7 @@
use crate::{Context, PAGE_SIZE, get_room_info};
#[derive(Debug, Subcommand)]
pub enum RoomDirectoryCommand {
pub(crate) enum RoomDirectoryCommand {
/// - Publish a room to the room directory
Publish {
/// The room id of the room to publish

View File

@@ -7,7 +7,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum RoomInfoCommand {
pub(crate) enum RoomInfoCommand {
/// - List joined members in a room
ListJoinedMembers {
room_id: OwnedRoomId,

View File

@@ -16,7 +16,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum RoomCommand {
pub(super) enum RoomCommand {
/// - List all rooms the server knows about
#[clap(alias = "list")]
ListRooms {

View File

@@ -12,7 +12,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum RoomModerationCommand {
pub(crate) enum RoomModerationCommand {
/// - Bans a room from local users joining and evicts all our local users
/// (including server
/// admins)

View File

@@ -145,6 +145,16 @@ pub(super) async fn restart(&self, force: bool) -> Result {
self.write_str("Restarting server...").await
}
#[admin_command]
pub(super) async fn upload_analytics(&self) -> Result {
match self.services.analytics.force_upload().await {
| Ok(()) => self.write_str("Analytics uploaded successfully.").await,
| Err(e) =>
self.write_str(&format!("Failed to upload analytics: {e}"))
.await,
}
}
#[admin_command]
pub(super) async fn shutdown(&self) -> Result {
warn!("shutdown command");

View File

@@ -9,7 +9,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum ServerCommand {
pub(super) enum ServerCommand {
/// - Time elapsed since startup
Uptime,
@@ -64,4 +64,7 @@ pub enum ServerCommand {
/// - Shutdown the server
Shutdown,
/// - Upload analytics
UploadAnalytics,
}

View File

@@ -8,7 +8,7 @@
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum UserCommand {
pub(super) enum UserCommand {
/// - Create a new user
#[clap(alias = "create")]
CreateUser {

View File

@@ -169,6 +169,18 @@ pub struct Config {
#[serde(alias = "allow_check_for_updates", default = "true_fn")]
pub allow_announcements_check: bool,
/// If enabled, continuwuity will send anonymous analytics data periodically
/// to help improve development. This includes basic server metadata like
/// version, build information and federation status. All requests are
/// signed with the server's federation signing key.
///
/// This is also used to warn about potential problems with federation, if
/// federation is enabled.
///
/// default: true
#[serde(default = "true_fn")]
pub allow_analytics: bool,
/// Set this to any float value to multiply continuwuity's in-memory LRU
/// caches with such as "auth_chain_cache_capacity".
///
@@ -795,24 +807,6 @@ pub struct Config {
#[serde(default)]
pub log_thread_ids: bool,
/// Enable journald logging on Unix platforms
///
/// When enabled, log output will be sent to the systemd journal
/// This is only supported on Unix platforms
///
/// default: false
#[cfg(target_family = "unix")]
#[serde(default)]
pub log_to_journald: bool,
/// The syslog identifier to use with journald logging
///
/// Only used when journald logging is enabled
///
/// Defaults to the binary name
#[cfg(target_family = "unix")]
pub journald_identifier: Option<String>,
/// OpenID token expiration/TTL in seconds.
///
/// These are the OpenID tokens that are primarily used for Matrix account

View File

@@ -43,7 +43,6 @@ default = [
"io_uring",
"jemalloc",
"jemalloc_conf",
"journald",
"media_thumbnail",
"release_max_log_level",
"systemd",
@@ -131,11 +130,6 @@ sentry_telemetry = [
systemd = [
"conduwuit-router/systemd",
]
journald = [ # This is a stub on non-unix platforms
"dep:tracing-journald",
]
# enable the tokio_console server ncompatible with release_max_log_level
tokio_console = [
"dep:console-subscriber",
@@ -189,7 +183,6 @@ tracing-opentelemetry.optional = true
tracing-opentelemetry.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracing-journald = { workspace = true, optional = true }
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
hardened_malloc-rs.workspace = true

View File

@@ -43,16 +43,6 @@ pub(crate) fn init(
.with(console_layer.with_filter(console_reload_filter))
.with(cap_layer);
// If journald logging is enabled on Unix platforms, create a separate
// subscriber for it
#[cfg(all(target_family = "unix", feature = "journald"))]
if config.log_to_journald {
println!("Initialising journald logging");
if let Err(e) = init_journald_logging(config) {
eprintln!("Failed to initialize journald logging: {e}");
}
}
#[cfg(feature = "sentry_telemetry")]
let subscriber = {
let sentry_filter = EnvFilter::try_new(&config.sentry_filter)
@@ -132,28 +122,6 @@ pub(crate) fn init(
Ok(ret)
}
#[cfg(all(target_family = "unix", feature = "journald"))]
fn init_journald_logging(config: &Config) -> Result<()> {
use tracing_journald::Layer as JournaldLayer;
let journald_filter =
EnvFilter::try_new(&config.log).map_err(|e| err!(Config("log", "{e}.")))?;
let mut journald_layer = JournaldLayer::new()
.map_err(|e| err!(Config("journald", "Failed to initialize journald layer: {e}.")))?;
if let Some(ref identifier) = config.journald_identifier {
journald_layer = journald_layer.with_syslog_identifier(identifier.to_owned());
}
let journald_subscriber =
Registry::default().with(journald_layer.with_filter(journald_filter));
let _guard = tracing::subscriber::set_default(journald_subscriber);
Ok(())
}
fn tokio_console_enabled(config: &Config) -> (bool, &'static str) {
if !cfg!(all(feature = "tokio_console", tokio_unstable)) {
return (false, "");
@@ -173,10 +141,7 @@ fn tokio_console_enabled(config: &Config) -> (bool, &'static str) {
(true, "")
}
fn set_global_default<S>(subscriber: S)
where
S: tracing::Subscriber + Send + Sync + 'static,
{
fn set_global_default<S: SubscriberExt + Send + Sync>(subscriber: S) {
tracing::subscriber::set_global_default(subscriber)
.expect("the global default tracing subscriber failed to be initialized");
}

View File

@@ -78,6 +78,7 @@ zstd_compression = [
async-trait.workspace = true
base64.workspace = true
bytes.workspace = true
conduwuit-build-metadata.workspace = true
conduwuit-core.workspace = true
conduwuit-database.workspace = true
const-str.workspace = true

View File

@@ -0,0 +1,245 @@
//! # Analytics service
//!
//! This service is responsible for collecting and uploading anonymous server
//! metadata to help improve continuwuity development.
//!
//! All requests are signed with the server's federation signing key for
//! authentication. This service respects the `allow_analytics` configuration
//! option and is enabled by default.
//!
//! Analytics are sent on startup (with up to 5 minutes jitter) and every 12
//! hours thereafter (with up to 30 minutes jitter) to distribute load.
use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use conduwuit::{
Result, Server, debug, err, info,
version::{self, user_agent},
warn,
};
use database::{Deserialized, Map};
use rand::Rng;
use ruma::ServerName;
use serde::{Deserialize, Serialize};
use tokio::{
sync::Notify,
time::{MissedTickBehavior, interval},
};
use crate::{Dep, client, config, federation, globals, server_keys, users};
extern crate conduwuit_build_metadata as build_metadata;
pub struct Service {
interval: Duration,
jitter: Duration,
startup_jitter: Duration,
interrupt: Notify,
db: Arc<Map>,
services: Services,
}
struct Services {
client: Dep<client::Service>,
globals: Dep<globals::Service>,
server_keys: Dep<server_keys::Service>,
federation: Dep<federation::Service>,
users: Dep<users::Service>,
server: Arc<Server>,
config: Dep<config::Service>,
}
#[derive(Debug, Serialize)]
struct AnalyticsPayload {
server_name: String,
version: &'static str,
commit_hash: Option<&'static str>,
user_count: usize,
federation_enabled: bool,
room_creation_allowed: bool,
public_room_directory_over_federation: bool,
build_profile: &'static str,
opt_level: &'static str,
rustc_version: &'static str,
features: Vec<&'static str>,
host: &'static str,
target: &'static str,
// the following can all be derived from the target
target_arch: &'static str,
target_os: &'static str,
target_env: &'static str,
target_family: &'static str,
}
#[derive(Debug, Deserialize)]
struct AnalyticsResponse {
success: bool,
message: Option<String>,
}
const ANALYTICS_URL: &str = "https://analytics.continuwuity.org/api/v1/metrics";
const ANALYTICS_SERVERNAME: &str = "analytics.continuwuity.org";
const ANALYTICS_INTERVAL: u64 = 43200; // 12 hours in seconds
const ANALYTICS_JITTER: u64 = 1800; // 30 minutes in seconds
const ANALYTICS_STARTUP_JITTER: u64 = 300; // 5 minutes in seconds
const LAST_ANALYTICS_TIMESTAMP: &[u8; 21] = b"last_analytics_upload";
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
let mut rng = rand::thread_rng();
let jitter_seconds = rng.gen_range(0..=ANALYTICS_JITTER);
let startup_jitter_seconds = rng.gen_range(0..=ANALYTICS_STARTUP_JITTER);
Ok(Arc::new(Self {
interval: Duration::from_secs(ANALYTICS_INTERVAL),
jitter: Duration::from_secs(jitter_seconds),
startup_jitter: Duration::from_secs(startup_jitter_seconds),
interrupt: Notify::new(),
db: args.db["global"].clone(),
services: Services {
globals: args.depend::<globals::Service>("globals"),
client: args.depend::<client::Service>("client"),
config: args.depend::<config::Service>("config"),
server_keys: args.depend::<server_keys::Service>("server_keys"),
users: args.depend::<users::Service>("users"),
federation: args.depend::<federation::Service>("federation"),
server: args.server.clone(),
},
}))
}
#[tracing::instrument(skip_all, name = "analytics", level = "debug")]
async fn worker(self: Arc<Self>) -> Result<()> {
if !self.services.server.config.allow_analytics {
debug!("Analytics collection is disabled");
return Ok(());
}
// Send initial analytics on startup (with shorter jitter)
tokio::time::sleep(self.startup_jitter).await;
if let Err(e) = self.upload_analytics().await {
warn!(%e, "Failed to upload initial analytics");
}
let mut i = interval(self.interval);
i.set_missed_tick_behavior(MissedTickBehavior::Delay);
i.reset_after(self.interval + self.jitter);
loop {
tokio::select! {
() = self.interrupt.notified() => break,
_ = i.tick() => {
if let Err(e) = self.upload_analytics().await {
warn!(%e, "Failed to upload analytics");
}
}
}
}
Ok(())
}
fn interrupt(&self) { self.interrupt.notify_waiters(); }
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
#[tracing::instrument(skip_all)]
async fn upload_analytics(&self) -> Result<()> {
let payload = self.collect_metadata().await;
let json_payload = serde_json::to_vec(&payload)?;
// Create HTTP request
let request = http::Request::builder()
.method("POST")
.uri(ANALYTICS_URL)
.header("Content-Type", "application/json")
.header("User-Agent", user_agent())
.body(json_payload)?;
// Sign the request using federation signing
let reqwest_request = self.services.federation.sign_non_federation_request(
ServerName::parse(ANALYTICS_SERVERNAME).unwrap(),
request,
)?;
// self.sign_analytics_request(&mut request).await?;
let response = self
.services
.client
.default
.execute(reqwest_request)
.await?;
let status = response.status();
if let Ok(analytics_response) =
serde_json::from_str::<AnalyticsResponse>(&response.text().await?)
{
if analytics_response.success {
debug!("Analytics uploaded successfully");
self.update_last_upload_timestamp().await;
}
let msg = analytics_response.message.unwrap_or_default();
warn!("Analytics upload warning: {}", msg);
} else if status.is_success() {
info!("Analytics uploaded successfully (no structured response)");
self.update_last_upload_timestamp().await;
} else {
warn!("Analytics upload failed (no structured response) with status: {}", status);
}
Ok(())
}
async fn collect_metadata(&self) -> AnalyticsPayload {
let config = &self.services.config;
AnalyticsPayload {
server_name: self.services.globals.server_name().to_string(),
version: version::version(),
commit_hash: build_metadata::GIT_COMMIT_HASH,
user_count: self.services.users.count().await,
federation_enabled: config.allow_federation,
room_creation_allowed: config.allow_room_creation,
public_room_directory_over_federation: config
.allow_public_room_directory_over_federation,
build_profile: build_metadata::built::PROFILE,
opt_level: build_metadata::built::OPT_LEVEL,
rustc_version: build_metadata::built::RUSTC_VERSION,
features: build_metadata::built::FEATURES.to_vec(),
host: build_metadata::built::HOST,
target: build_metadata::built::TARGET,
target_arch: build_metadata::built::CFG_TARGET_ARCH,
target_os: build_metadata::built::CFG_OS,
target_env: build_metadata::built::CFG_ENV,
target_family: build_metadata::built::CFG_FAMILY,
}
}
async fn update_last_upload_timestamp(&self) {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.db.raw_put(LAST_ANALYTICS_TIMESTAMP, timestamp);
}
pub async fn last_upload_timestamp(&self) -> u64 {
self.db
.get(LAST_ANALYTICS_TIMESTAMP)
.await
.deserialized()
.unwrap_or(0_u64)
}
pub async fn force_upload(&self) -> Result<()> {
if !self.services.config.allow_analytics {
return Err(err!(Config("allow_analytics", "Analytics collection is disabled")));
}
self.upload_analytics().await
}
}

View File

@@ -20,7 +20,7 @@
use async_trait::async_trait;
use conduwuit::{Result, Server, debug, info, warn};
use database::{Deserialized, Map};
use ruma::events::room::message::RoomMessageEventContent;
use ruma::events::{Mentions, room::message::RoomMessageEventContent};
use serde::Deserialize;
use tokio::{
sync::Notify,
@@ -53,6 +53,8 @@ struct CheckForAnnouncementsResponseEntry {
id: u64,
date: Option<String>,
message: String,
#[serde(default, skip_serializing_if = "bool::not")]
mention_room: bool,
}
const CHECK_FOR_ANNOUNCEMENTS_URL: &str =
@@ -139,19 +141,20 @@ async fn handle(&self, announcement: &CheckForAnnouncementsResponseEntry) {
} else {
info!("[announcements] {:#}", announcement.message);
}
let mut message = RoomMessageEventContent::text_markdown(format!(
"### New announcement{}\n\n{}",
announcement
.date
.as_ref()
.map_or_else(String::new, |date| format!(" - `{date}`")),
announcement.message
));
self.services
.admin
.send_message(RoomMessageEventContent::text_markdown(format!(
"### New announcement{}\n\n{}",
announcement
.date
.as_ref()
.map_or_else(String::new, |date| format!(" - `{date}`")),
announcement.message
)))
.await
.ok();
if announcement.mention_room {
message = message.add_mentions(Mentions::with_room_mention());
}
self.services.admin.send_message(message).await.ok();
}
#[inline]

View File

@@ -107,6 +107,20 @@ fn prepare(&self, dest: &ServerName, mut request: http::Request<Vec<u8>>) -> Res
Ok(request)
}
#[implement(super::Service)]
pub fn sign_non_federation_request(
&self,
dest: &ServerName,
mut request: http::Request<Vec<u8>>,
) -> Result<Request> {
self.sign_request(&mut request, dest);
let request = Request::try_from(request)?;
self.services.server.check_running()?;
Ok(request)
}
#[implement(super::Service)]
fn validate_url(&self, url: &Url) -> Result<()> {
if let Some(url_host) = url.host_str() {

View File

@@ -9,6 +9,7 @@
pub mod account_data;
pub mod admin;
pub mod analytics;
pub mod announcements;
pub mod appservice;
pub mod client;

View File

@@ -10,8 +10,8 @@
use tokio::sync::Mutex;
use crate::{
account_data, admin, announcements, appservice, client, config, emergency, federation,
globals, key_backups,
account_data, admin, analytics, announcements, appservice, client, config, emergency,
federation, globals, key_backups,
manager::Manager,
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
service::{Args, Map, Service},
@@ -21,6 +21,7 @@
pub struct Services {
pub account_data: Arc<account_data::Service>,
pub admin: Arc<admin::Service>,
pub analytics: Arc<analytics::Service>,
pub appservice: Arc<appservice::Service>,
pub config: Arc<config::Service>,
pub client: Arc<client::Service>,
@@ -68,6 +69,7 @@ macro_rules! build {
Ok(Arc::new(Self {
account_data: build!(account_data::Service),
admin: build!(admin::Service),
analytics: build!(analytics::Service),
appservice: build!(appservice::Service),
resolver: build!(resolver::Service),
client: build!(client::Service),

View File

@@ -1,26 +0,0 @@
[package]
name = "xtask-admin-command"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]
clap-markdown = "0.1.5"
clap_builder = { version = "4.5.38", default-features = false }
clap_mangen = "0.2"
conduwuit-admin.workspace = true
# Hack to prevent rebuilds
conduwuit.workspace = true
[lints]
workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,58 +0,0 @@
use std::{
fs::{self, File},
io,
path::Path,
};
use clap_builder::{Command, CommandFactory};
use conduwuit_admin::AdminCommand;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let task = args.next();
match task {
| None => todo!(),
| Some(t) => match t.as_str() {
| "man" => {
let dir = Path::new("./admin-man");
gen_manpages(dir)?;
},
| "md" => {
let command = AdminCommand::command().name("admin");
let res = clap_markdown::help_markdown_command_custom(
&command,
&clap_markdown::MarkdownOptions::default(),
);
println!("{res}");
},
| invalid => return Err(format!("Invalid task name: {invalid}").into()),
},
}
Ok(())
}
fn gen_manpages(dir: &Path) -> Result<(), io::Error> {
fn r#gen(dir: &Path, c: &Command, prefix: Option<&str>) -> Result<(), io::Error> {
fs::create_dir_all(dir)?;
let sub_name = c.get_display_name().unwrap_or_else(|| c.get_name());
let name = if let Some(prefix) = prefix {
format!("{prefix}-{sub_name}")
} else {
sub_name.to_owned()
};
let mut out = File::create(dir.join(format!("{name}.1")))?;
let clap_mangen = clap_mangen::Man::new(c.to_owned().disable_help_flag(true));
clap_mangen.render(&mut out)?;
for sub in c.get_subcommands() {
r#gen(&dir.join(sub_name), sub, Some(&name))?;
}
Ok(())
}
r#gen(dir, &AdminCommand::command().name("admin"), None)
}

View File

@@ -1,22 +0,0 @@
[package]
name = "xtask"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]
clap.workspace = true
# Required for working with JSON output from cargo metadata
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[lints]
workspace = true

View File

@@ -1,11 +0,0 @@
use std::{env, process::Command};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut child = Command::new("cargo").args(["run", "--package", "xtask-admin-command", "--"].into_iter().map(ToOwned::to_owned).chain(env::args().skip(2)))
// .stdout(Stdio::piped())
// .stderr(Stdio::piped())
.spawn()
.expect("failed to execute child");
child.wait()?;
Ok(())
}