feat: Add a page with some information about the server

This commit is contained in:
Ginger
2026-05-20 10:40:32 -04:00
parent a0aeebd237
commit 4da00fa28a
14 changed files with 293 additions and 133 deletions
+2 -41
View File
@@ -4,7 +4,7 @@ use ruma::{
api::client::discovery::{ api::client::discovery::{
discover_homeserver::{self, HomeserverInfo}, discover_homeserver::{self, HomeserverInfo},
discover_policy_server, discover_policy_server,
discover_support::{self, Contact, ContactRole}, discover_support,
}, },
assign, assign,
}; };
@@ -67,46 +67,7 @@ pub(crate) async fn well_known_support(
.as_ref() .as_ref()
.map(ToString::to_string); .map(ToString::to_string);
let email_address = services.config.well_known.support_email.clone(); let contacts = services.admin.get_support_contacts().await;
let matrix_id = services.config.well_known.support_mxid.clone();
let pgp_key = services.config.well_known.support_pgp_key.clone();
// TODO: support defining multiple contacts in the config
let mut contacts: Vec<Contact> = vec![];
let role = services
.config
.well_known
.support_role
.clone()
.unwrap_or(ContactRole::Admin);
// Add configured contact if at least one contact method is specified
let configured_contact = match (matrix_id, email_address) {
| (Some(matrix_id), email_address) =>
Some(assign!(Contact::with_matrix_id(role, matrix_id), { email_address })),
| (None, Some(email_address)) => Some(Contact::with_email_address(role, email_address)),
| (None, None) => None,
};
if let Some(mut configured_contact) = configured_contact {
configured_contact.pgp_key = pgp_key;
contacts.push(configured_contact);
}
// Try to add admin users as contacts if no contacts are configured
if contacts.is_empty() {
let admin_users = services.admin.get_admins().await;
for user_id in &admin_users {
if *user_id == services.globals.server_user {
continue;
}
contacts.push(Contact::with_matrix_id(ContactRole::Admin, user_id.to_owned()));
}
}
if contacts.is_empty() && support_page.is_none() { if contacts.is_empty() && support_page.is_none() {
// No admin room, no configured contacts, and no support page // No admin room, no configured contacts, and no support page
+1
View File
@@ -1,4 +1,5 @@
#![type_length_limit = "16384"] //TODO: reduce me #![type_length_limit = "16384"] //TODO: reduce me
#![recursion_limit = "256"] // My Giant Async Function
#![allow(clippy::toplevel_ref_arg)] #![allow(clippy::toplevel_ref_arg)]
extern crate conduwuit_core as conduwuit; extern crate conduwuit_core as conduwuit;
+53 -1
View File
@@ -18,6 +18,8 @@ use futures::{Future, FutureExt, StreamExt, TryFutureExt};
use loole::{Receiver, Sender}; use loole::{Receiver, Sender};
use ruma::{ use ruma::{
OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId,
api::client::discovery::discover_support::{Contact, ContactRole},
assign,
events::{ events::{
Mentions, Mentions,
room::message::{ room::message::{
@@ -28,7 +30,7 @@ use ruma::{
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{ use crate::{
Dep, account_data, globals, Dep, account_data, config, globals,
media::{MXC_LENGTH, mxc::Mxc}, media::{MXC_LENGTH, mxc::Mxc},
rooms::{self, state::RoomMutexGuard}, rooms::{self, state::RoomMutexGuard},
}; };
@@ -44,6 +46,7 @@ pub struct Service {
struct Services { struct Services {
server: Arc<Server>, server: Arc<Server>,
config: Dep<config::Service>,
globals: Dep<globals::Service>, globals: Dep<globals::Service>,
alias: Dep<rooms::alias::Service>, alias: Dep<rooms::alias::Service>,
timeline: Dep<rooms::timeline::Service>, timeline: Dep<rooms::timeline::Service>,
@@ -115,6 +118,7 @@ impl crate::Service for Service {
Ok(Arc::new(Self { Ok(Arc::new(Self {
services: Services { services: Services {
server: args.server.clone(), server: args.server.clone(),
config: args.depend::<config::Service>("config"),
globals: args.depend::<globals::Service>("globals"), globals: args.depend::<globals::Service>("globals"),
alias: args.depend::<rooms::alias::Service>("rooms::alias"), alias: args.depend::<rooms::alias::Service>("rooms::alias"),
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"), timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
@@ -619,4 +623,52 @@ impl Service {
let weak = services.map(Arc::downgrade); let weak = services.map(Arc::downgrade);
*receiver = weak; *receiver = weak;
} }
/// Get the server's configured support contacts.
pub async fn get_support_contacts(&self) -> Vec<Contact> {
let email_address = self.services.config.well_known.support_email.clone();
let matrix_id = self.services.config.well_known.support_mxid.clone();
let pgp_key = self.services.config.well_known.support_pgp_key.clone();
// TODO: support defining multiple contacts in the config
let mut contacts: Vec<Contact> = vec![];
let role = self
.services
.config
.well_known
.support_role
.clone()
.unwrap_or(ContactRole::Admin);
// Add configured contact if at least one contact method is specified
let configured_contact = match (matrix_id, email_address) {
| (Some(matrix_id), email_address) =>
Some(assign!(Contact::with_matrix_id(role, matrix_id), { email_address })),
| (None, Some(email_address)) =>
Some(Contact::with_email_address(role, email_address)),
| (None, None) => None,
};
if let Some(mut configured_contact) = configured_contact {
configured_contact.pgp_key = pgp_key;
contacts.push(configured_contact);
}
// Try to add admin users as contacts if no contacts are configured
if contacts.is_empty() {
let admin_users = self.get_admins().await;
for user_id in &admin_users {
if *user_id == self.services.globals.server_user {
continue;
}
contacts.push(Contact::with_matrix_id(ContactRole::Admin, user_id.to_owned()));
}
}
contacts
}
} }
+1
View File
@@ -129,6 +129,7 @@ pub fn build(services: &Services) -> Router<state::State> {
.nest( .nest(
"/_continuwuity/", "/_continuwuity/",
Router::new() Router::new()
.nest("/about", about::build())
.nest("/account/", account::build()) .nest("/account/", account::build())
.merge(debug::build()) .merge(debug::build())
.nest("/oauth2/", oauth::build()) .nest("/oauth2/", oauth::build())
+38
View File
@@ -0,0 +1,38 @@
use std::collections::BTreeMap;
use axum::{Extension, Router, extract::State, routing::get};
use conduwuit_core::config::TermsDocument;
use ruma::{
OwnedServerName,
api::client::discovery::discover_support::{Contact, ContactRole},
};
use url::Url;
use crate::{
pages::{Result, TemplateContext},
response, template,
};
pub(crate) fn build() -> Router<crate::State> { Router::new().route("/", get(get_about)) }
template! {
struct About use "about.html.j2" {
server_name: OwnedServerName,
support_page: Option<Url>,
contacts: Vec<Contact>,
terms: BTreeMap<String, TermsDocument>
}
}
async fn get_about(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
) -> Result {
response!(About::new(
context,
services.globals.server_name().to_owned(),
services.config.well_known.support_page.clone(),
services.admin.get_support_contacts().await,
services.config.registration_terms.documents.clone()
))
}
+1
View File
@@ -11,6 +11,7 @@ use conduwuit_core::utils;
use crate::WebError; use crate::WebError;
pub(super) mod about;
pub(super) mod account; pub(super) mod account;
mod components; mod components;
pub(super) mod debug; pub(super) mod debug;
+67 -33
View File
@@ -28,7 +28,7 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
color-scheme: dark; color-scheme: dark;
--text-color: #f5ebeb; --text-color: #f5ebeb;
--secondary: #888; --secondary: #999;
--bg: oklch(0.15 0.042 317.27); --bg: oklch(0.15 0.042 317.27);
--panel-bg: oklch(0.24 0.03 317.27); --panel-bg: oklch(0.24 0.03 317.27);
@@ -94,6 +94,10 @@ p {
} }
} }
ul {
margin: 1rem 0;
}
section { section {
margin: 1rem 0; margin: 1rem 0;
} }
@@ -125,38 +129,6 @@ small.error {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.panel {
--preferred-width: 12rem + 40dvw;
--maximum-width: 48rem;
--minimum-width: 32rem;
width: min(clamp(var(--minimum-width), var(--preferred-width), var(--maximum-width)), calc(100dvw - 3rem));
border-radius: var(--border-radius-lg);
background-color: var(--panel-bg);
padding-inline: 1.5rem;
padding-block: 1rem;
margin-top: 1em;
margin-bottom: auto;
box-shadow: 0 0.25em 0.375em hsla(0, 0%, 0%, 0.1);
&.middle {
margin-top: 0;
margin-bottom: 0;
}
&.narrow {
--preferred-width: 12rem + 20dvw;
--maximum-width: 36rem;
input, button, a.button {
width: 100%;
}
}
&:not(.narrow) form p {
margin-bottom: 0;
}
}
img.matrix-icon { img.matrix-icon {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -262,6 +234,22 @@ h1 {
margin-bottom: 0.67em; margin-bottom: 0.67em;
} }
ul.bullet-separated {
display: inline-block;
margin: 0;
padding: 0;
li {
display: inline;
flex: 1;
list-style-type: none;
&:not(:first-child)::before {
content: "• ";
}
}
}
.fullwidth { .fullwidth {
width: 100%; width: 100%;
margin-bottom: 0 !important; margin-bottom: 0 !important;
@@ -271,6 +259,52 @@ h1 {
user-select: all; user-select: all;
} }
.panel {
--preferred-width: 12rem + 40dvw;
--maximum-width: 48rem;
--minimum-width: 32rem;
width: min(clamp(var(--minimum-width), var(--preferred-width), var(--maximum-width)), calc(100dvw - 3rem));
border-radius: var(--border-radius-lg);
background-color: var(--panel-bg);
padding-inline: 1.5rem;
padding-block: 1rem;
margin-top: 1em;
margin-bottom: auto;
box-shadow: 0 0.25em 0.375em hsla(0, 0%, 0%, 0.1);
&.middle {
margin-top: 0;
margin-bottom: 0;
}
&.narrow {
--preferred-width: 12rem + 20dvw;
--maximum-width: 36rem;
input, button, a.button {
width: 100%;
}
}
&:not(.narrow) form p {
margin-bottom: 0;
}
}
.project-name {
font-weight: bold;
text-decoration: none !important;
background: linear-gradient(
130deg,
oklch(from var(--c1) var(--name-lightness) c h),
oklch(from var(--c2) var(--name-lightness) c h)
);
background-clip: text;
color: transparent;
filter: brightness(1.2);
}
@media (max-width: 425px) { @media (max-width: 425px) {
main { main {
padding-block-start: 2rem; padding-block-start: 2rem;
+6 -6
View File
@@ -34,17 +34,17 @@
.info { .info {
flex: 1 1; flex: 1 1;
p { .name {
margin: 0; font-weight: bold;
&.name {
font-weight: 700;
}
} }
.id { .id {
color: var(--secondary); color: var(--secondary);
font-weight: normal; font-weight: normal;
&::before {
content: "•";
}
} }
} }
-12
View File
@@ -1,15 +1,3 @@
.project-name {
text-decoration: none;
background: linear-gradient(
130deg,
oklch(from var(--c1) var(--name-lightness) c h),
oklch(from var(--c2) var(--name-lightness) c h)
);
background-clip: text;
color: transparent;
filter: brightness(1.2);
}
.button { .button {
margin: 0 !important; margin: 0 !important;
} }
@@ -1,32 +1,36 @@
<div class="card"> <div class="card">
{{ avatar }} {{ avatar }}
<div class="info"> <div class="info">
<p class="name"> <div class="name">
{% if let Some(display_name) = display_name %} <span>
{{ display_name }} {% if let Some(display_name) = display_name %}
{% else %} {{ display_name }}
Unknown device {% else %}
{% endif %} Unknown device
{% endif %}
</span>
{% if style == DeviceCardStyle::Detailed %} {% if style == DeviceCardStyle::Detailed %}
<span class="id"> <ul class="id bullet-separated">
&bullet;&nbsp;{{ device_id }} <li>{{ device_id }}</li>
{% if let Some(metadata) = oauth_metadata %} <li>
&bullet;&nbsp;<a href="{{ metadata.client_uri }}">Client website</a> {% if let Some(metadata) = oauth_metadata %}
{% else %} <a href="{{ metadata.client_uri }}">Client website</a>
(legacy) {% else %}
{% endif %} (legacy)
{% endif %}
</li>
</span> </span>
{% endif %} {% endif %}
</p> </div>
<p> <div>
Last active: {{ last_active }} Last active: {{ last_active }}
</p> </div>
<p> <div>
{% if style == DeviceCardStyle::Detailed %} {% if style == DeviceCardStyle::Detailed %}
<a href="{{ crate::ROUTE_PREFIX }}/account/device/{{ device_id }}/remove">Remove</a> <a href="{{ crate::ROUTE_PREFIX }}/account/device/{{ device_id }}/remove">Remove</a>
{% else %} {% else %}
<a href="{{ crate::ROUTE_PREFIX }}/account/device/{{ device_id }}/">Details</a> <a href="{{ crate::ROUTE_PREFIX }}/account/device/{{ device_id }}/">Details</a>
{% endif %} {% endif %}
</p> </div>
</div> </div>
</div> </div>
+2 -10
View File
@@ -19,16 +19,8 @@
{%~ block content %}{% endblock ~%} {%~ block content %}{% endblock ~%}
{%~ block footer ~%} {%~ block footer ~%}
<footer> <footer>
<img class="logo" src="{{ crate::ROUTE_PREFIX }}/resources/logo.svg"> <a href="{{ crate::ROUTE_PREFIX }}/"><img class="logo" src="{{ crate::ROUTE_PREFIX }}/resources/logo.svg"></a>
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a> {{ env!("CARGO_PKG_VERSION") }} <p>Powered by <a href="https://continuwuity.org">Continuwuity</a> {{ env!("CARGO_PKG_VERSION") }} &bullet; <a href="{{ crate::ROUTE_PREFIX }}/about">About</a></p>
{%~ if let Some(version_info) = conduwuit_build_metadata::version_tag() ~%}
{%~ if let Some(url) = conduwuit_build_metadata::GIT_REMOTE_COMMIT_URL.or(conduwuit_build_metadata::GIT_REMOTE_WEB_URL) ~%}
(<a href="{{ url }}">{{ version_info }}</a>)
{%~ else ~%}
({{ version_info }})
{%~ endif ~%}
{%~ endif ~%}
</p>
</footer> </footer>
{%~ endblock ~%} {%~ endblock ~%}
</body> </body>
+84
View File
@@ -0,0 +1,84 @@
{% extends "_layout.html.j2" %}
{%- block title -%}
About server
{%- endblock -%}
{%- block content -%}
<div class="panel">
<h1>About {{ server_name }}</h1>
{% if let Some(support_page) = support_page %}
<p>
Support: <a href="{{ support_page }} target="_blank">{{ support_page }}</a>
</p>
{% endif %}
{% if !contacts.is_empty() %}
<p>
Contact the operators of this server:
</p>
<ul>
{% for contact in contacts %}
<li>
{%- match contact.role -%}
{%- when ContactRole::Admin -%}
Administrator
{%- when ContactRole::Security -%}
Security
{%- when ContactRole::Moderator -%}
Moderator
{%- when _ -%}
Contact
{%- endmatch -%}
: <ul class="bullet-separated">
{%- if let Some(matrix_id) = contact.matrix_id -%}
<li><a href="matrix:u/{{ matrix_id.localpart() }}:{{ matrix_id.server_name() }}?action=chat" target="_blank">{{ matrix_id }}</a></li>
{%- endif -%}
{%- if let Some(email_address) = contact.email_address -%}
<li><a href="mailto:{{ email_address }}" target="_blank">{{ email_address }}</a>
{%- if let Some(pgp_key) = contact.pgp_key -%}
(<a href="{{ pgp_key }}" target="_blank">PGP</a>)
{%- endif -%}
</li>
{%- endif -%}
</ul>
</li>
{% endfor %}
</ul>
{% endif %}
{% if !terms.is_empty() %}
<p>
By using {{ server_name }} you agree to the following policies:
</p>
<ul>
{% for (id, document) in terms %}
<li>
<a target="_blank" href="{{ document.url }}">{{ document.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
<p>
Server version {{ env!("CARGO_PKG_VERSION") }}
{%~ if let Some(version_info) = conduwuit_build_metadata::version_tag() ~%}
{%~ if let Some(url) = conduwuit_build_metadata::GIT_REMOTE_COMMIT_URL.or(conduwuit_build_metadata::GIT_REMOTE_WEB_URL) ~%}
(<a href="{{ url }}">{{ version_info }}</a>)
{%~ else ~%}
({{ version_info }})
{%~ endif ~%}
{%~ endif ~%}
</p>
<hr>
<p>
{{ server_name }} is powered by <a class="project-name" href="https://continuwuity.org">Continuwuity</a>,
a high-performance and community-driven <a href="https://matrix.org">Matrix</a> homeserver
maintained as an open source project by volunteers from around the world.
</p>
<p>
<ul class="bullet-separated">
<li><a href="https://forgejo.ellis.link/continuwuation/continuwuity">Source code</a></li>
<li><a href="https://matrix.to/#/#continuwuity:continuwuity.org">Official Matrix chatroom</a></li>
<li><span class="project-name">❤</span>&nbsp;<a href="https://opencollective.com/continuwuity">Support the project</a></li>
</ul>
</p>
</div>
{% endblock %}
+4 -2
View File
@@ -58,8 +58,10 @@ Your account
<p> <p>
Settings here <em class="negative">may affect the integrity of your account</em>. Settings here <em class="negative">may affect the integrity of your account</em>.
</p> </p>
<a href="cross_signing_reset">Reset your digital identity</a> &bullet; <ul class="bullet-separated">
<a href="deactivate">Deactivate your account</a> <li><a href="cross_signing_reset">Reset your digital identity</a></li>
<li><a href="deactivate">Deactivate your account</a></li>
</ul>
</details> </details>
</section> </section>
{% when AccountBody::Locked %} {% when AccountBody::Locked %}
+12 -10
View File
@@ -9,16 +9,18 @@ Device information
<h1>About device <a class="back" href="{{ crate::ROUTE_PREFIX }}/account/">Back</a></h1> <h1>About device <a class="back" href="{{ crate::ROUTE_PREFIX }}/account/">Back</a></h1>
{{ device_card }} {{ device_card }}
{% if let Some((client_metadata, _)) = client_metadata %} {% if let Some((client_metadata, _)) = client_metadata %}
<section> {% if client_metadata.tos_uri.is_some() || client_metadata.policy_uri.is_some() %}
<p> <section>
{% if let Some(tos_uri) = &client_metadata.tos_uri %} <ul class="bullet-separated">
<a href="{{ tos_uri }}">Terms of service</a> {% if let Some(tos_uri) = &client_metadata.tos_uri %}
{% endif %} <li><a href="{{ tos_uri }}">Terms of service</a></li>
{% if let Some(policy_uri) = &client_metadata.policy_uri %} {% endif %}
&bullet;&nbsp;<a href="{{ policy_uri }}">Policies</a> {% if let Some(policy_uri) = &client_metadata.policy_uri %}
{% endif %} <li><a href="{{ policy_uri }}">Policies</a></li>
</p> {% endif %}
</section> </ul>
</section>
{% endif %}
{% endif %} {% endif %}
<section> <section>
<p> <p>