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::{
discover_homeserver::{self, HomeserverInfo},
discover_policy_server,
discover_support::{self, Contact, ContactRole},
discover_support,
},
assign,
};
@@ -67,46 +67,7 @@ pub(crate) async fn well_known_support(
.as_ref()
.map(ToString::to_string);
let email_address = services.config.well_known.support_email.clone();
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()));
}
}
let contacts = services.admin.get_support_contacts().await;
if contacts.is_empty() && support_page.is_none() {
// No admin room, no configured contacts, and no support page
+1
View File
@@ -1,4 +1,5 @@
#![type_length_limit = "16384"] //TODO: reduce me
#![recursion_limit = "256"] // My Giant Async Function
#![allow(clippy::toplevel_ref_arg)]
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 ruma::{
OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId,
api::client::discovery::discover_support::{Contact, ContactRole},
assign,
events::{
Mentions,
room::message::{
@@ -28,7 +30,7 @@ use ruma::{
use tokio::sync::RwLock;
use crate::{
Dep, account_data, globals,
Dep, account_data, config, globals,
media::{MXC_LENGTH, mxc::Mxc},
rooms::{self, state::RoomMutexGuard},
};
@@ -44,6 +46,7 @@ pub struct Service {
struct Services {
server: Arc<Server>,
config: Dep<config::Service>,
globals: Dep<globals::Service>,
alias: Dep<rooms::alias::Service>,
timeline: Dep<rooms::timeline::Service>,
@@ -115,6 +118,7 @@ impl crate::Service for Service {
Ok(Arc::new(Self {
services: Services {
server: args.server.clone(),
config: args.depend::<config::Service>("config"),
globals: args.depend::<globals::Service>("globals"),
alias: args.depend::<rooms::alias::Service>("rooms::alias"),
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
@@ -619,4 +623,52 @@ impl Service {
let weak = services.map(Arc::downgrade);
*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(
"/_continuwuity/",
Router::new()
.nest("/about", about::build())
.nest("/account/", account::build())
.merge(debug::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;
pub(super) mod about;
pub(super) mod account;
mod components;
pub(super) mod debug;
+67 -33
View File
@@ -28,7 +28,7 @@
@media (prefers-color-scheme: dark) {
color-scheme: dark;
--text-color: #f5ebeb;
--secondary: #888;
--secondary: #999;
--bg: oklch(0.15 0.042 317.27);
--panel-bg: oklch(0.24 0.03 317.27);
@@ -94,6 +94,10 @@ p {
}
}
ul {
margin: 1rem 0;
}
section {
margin: 1rem 0;
}
@@ -125,38 +129,6 @@ small.error {
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 {
@media (prefers-color-scheme: dark) {
@@ -262,6 +234,22 @@ h1 {
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 {
width: 100%;
margin-bottom: 0 !important;
@@ -271,6 +259,52 @@ h1 {
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) {
main {
padding-block-start: 2rem;
+6 -6
View File
@@ -34,17 +34,17 @@
.info {
flex: 1 1;
p {
margin: 0;
&.name {
font-weight: 700;
}
.name {
font-weight: bold;
}
.id {
color: var(--secondary);
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 {
margin: 0 !important;
}
@@ -1,32 +1,36 @@
<div class="card">
{{ avatar }}
<div class="info">
<p class="name">
{% if let Some(display_name) = display_name %}
{{ display_name }}
{% else %}
Unknown device
{% endif %}
<div class="name">
<span>
{% if let Some(display_name) = display_name %}
{{ display_name }}
{% else %}
Unknown device
{% endif %}
</span>
{% if style == DeviceCardStyle::Detailed %}
<span class="id">
&bullet;&nbsp;{{ device_id }}
{% if let Some(metadata) = oauth_metadata %}
&bullet;&nbsp;<a href="{{ metadata.client_uri }}">Client website</a>
{% else %}
(legacy)
{% endif %}
<ul class="id bullet-separated">
<li>{{ device_id }}</li>
<li>
{% if let Some(metadata) = oauth_metadata %}
<a href="{{ metadata.client_uri }}">Client website</a>
{% else %}
(legacy)
{% endif %}
</li>
</span>
{% endif %}
</p>
<p>
</div>
<div>
Last active: {{ last_active }}
</p>
<p>
</div>
<div>
{% if style == DeviceCardStyle::Detailed %}
<a href="{{ crate::ROUTE_PREFIX }}/account/device/{{ device_id }}/remove">Remove</a>
{% else %}
<a href="{{ crate::ROUTE_PREFIX }}/account/device/{{ device_id }}/">Details</a>
{% endif %}
</p>
</div>
</div>
</div>
+2 -10
View File
@@ -19,16 +19,8 @@
{%~ block content %}{% endblock ~%}
{%~ block footer ~%}
<footer>
<img class="logo" src="{{ crate::ROUTE_PREFIX }}/resources/logo.svg">
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a> {{ 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>
<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") }} &bullet; <a href="{{ crate::ROUTE_PREFIX }}/about">About</a></p>
</footer>
{%~ endblock ~%}
</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>
Settings here <em class="negative">may affect the integrity of your account</em>.
</p>
<a href="cross_signing_reset">Reset your digital identity</a> &bullet;
<a href="deactivate">Deactivate your account</a>
<ul class="bullet-separated">
<li><a href="cross_signing_reset">Reset your digital identity</a></li>
<li><a href="deactivate">Deactivate your account</a></li>
</ul>
</details>
</section>
{% 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>
{{ device_card }}
{% if let Some((client_metadata, _)) = client_metadata %}
<section>
<p>
{% if let Some(tos_uri) = &client_metadata.tos_uri %}
<a href="{{ tos_uri }}">Terms of service</a>
{% endif %}
{% if let Some(policy_uri) = &client_metadata.policy_uri %}
&bullet;&nbsp;<a href="{{ policy_uri }}">Policies</a>
{% endif %}
</p>
</section>
{% if client_metadata.tos_uri.is_some() || client_metadata.policy_uri.is_some() %}
<section>
<ul class="bullet-separated">
{% if let Some(tos_uri) = &client_metadata.tos_uri %}
<li><a href="{{ tos_uri }}">Terms of service</a></li>
{% endif %}
{% if let Some(policy_uri) = &client_metadata.policy_uri %}
<li><a href="{{ policy_uri }}">Policies</a></li>
{% endif %}
</ul>
</section>
{% endif %}
{% endif %}
<section>
<p>