Add media cache, improve default styles

This commit is contained in:
asonix 2020-03-25 22:26:45 -05:00
parent 0a42450801
commit 9ada30626b
18 changed files with 613 additions and 65 deletions

178
Cargo.lock generated
View file

@ -364,6 +364,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "ammonia"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89eac85170f4b3fb3dc5e442c1cfb036cb8eecf9dbbd431a161ffad15d90ea3b"
dependencies = [
"html5ever",
"lazy_static",
"maplit",
"markup5ever_rcdom",
"matches",
"tendril",
"url",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
@ -930,6 +945,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "futf"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
dependencies = [
"mac",
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.4"
@ -1118,6 +1143,20 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "html5ever"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b"
dependencies = [
"log",
"mac",
"markup5ever",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "http"
version = "0.2.0"
@ -1339,6 +1378,47 @@ dependencies = [
"linked-hash-map 0.5.2",
]
[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markup5ever"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab"
dependencies = [
"log",
"phf",
"phf_codegen",
"serde 1.0.105",
"serde_derive",
"serde_json",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
dependencies = [
"html5ever",
"markup5ever",
"tendril",
"xml5ever",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
@ -1437,6 +1517,12 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nodrop"
version = "0.1.14"
@ -1600,6 +1686,26 @@ dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -1680,6 +1786,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "pretty_env_logger"
version = "0.4.0"
@ -1763,6 +1875,7 @@ dependencies = [
"rand_chacha",
"rand_core",
"rand_hc",
"rand_pcg",
]
[[package]]
@ -1793,6 +1906,15 @@ dependencies = [
"rand_core",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.1.56"
@ -1826,12 +1948,14 @@ dependencies = [
"actix-rt",
"actix-web",
"actix-webfinger",
"ammonia",
"anyhow",
"async-trait",
"background-jobs",
"background-jobs-core",
"base64 0.12.0",
"bb8-postgres",
"bytes",
"config",
"dotenv",
"env_logger",
@ -2246,6 +2370,31 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "string_cache"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a"
dependencies = [
"lazy_static",
"new_debug_unreachable",
"phf_shared",
"precomputed-hash",
"serde 1.0.105",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
]
[[package]]
name = "stringprep"
version = "0.1.2"
@ -2332,6 +2481,17 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tendril"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707feda9f2582d5d680d733e38755547a3e8fb471e7ba11452ecfd9ce93a5d3b"
dependencies = [
"futf",
"mac",
"utf-8",
]
[[package]]
name = "termcolor"
version = "1.1.0"
@ -2636,6 +2796,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf-8"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"
[[package]]
name = "uuid"
version = "0.8.1"
@ -2815,6 +2981,18 @@ dependencies = [
"winapi-build",
]
[[package]]
name = "xml5ever"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59"
dependencies = [
"log",
"mac",
"markup5ever",
"time 0.1.42",
]
[[package]]
name = "yaml-rust"
version = "0.4.3"

View file

@ -19,9 +19,11 @@ actix-rt = "1.0.0"
actix-web = { version = "3.0.0-alpha.1", features = ["rustls"] }
actix-webfinger = "0.3.0-alpha.3"
activitystreams = "0.5.0-alpha.11"
ammonia = "3.1.0"
async-trait = "0.1.24"
background-jobs = { version = "0.8.0-alpha.0", git = "https://git.asonix.dog/Aardwolf/background-jobs", default-features = false, features = ["background-jobs-actix"] }
background-jobs-core = { version = "0.7.0", git = "https://git.asonix.dog/Aardwolf/background-jobs" }
bytes = "0.5.4"
base64 = "0.12"
bb8-postgres = { version = "0.4.0", features = ["with-serde_json-1", "with-uuid-0_8", "with-chrono-0_4"] }
config = "0.10.1"

View file

@ -7,32 +7,69 @@ body {
padding-bottom: 96px;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
body,
body * {
box-sizing: border-box;
}
header {
padding: 32px 0;
background-color: #333;
color: #f5f5f5;
text-align: center;
.header-text {
max-width: 700px;
margin: auto;
padding: 24px 0;
}
h1 {
margin: 0px;
}
p {
margin: 0;
margin-top: 8px;
font-style: italic;
}
}
section {
padding: 24px;
background-color: #fff;
border: 1px solid #e5e5e5;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
border-radius: 3px;
margin: 32px auto 0;
max-width: 700px;
padding-bottom: 32px;
> p:first-child {
margin-top: 0;
}
h3 {
margin-top: 0px;
padding: 24px;
margin: 0px;
}
li {
padding-top: 24px;
padding-bottom: 24px;
border-top: 1px solid #e5e5e5;
}
.padded {
padding: 0 24px;
}
.joining {
padding: 24px;
border-top: 1px solid #e5e5e5;
}
}
@ -57,6 +94,15 @@ pre {
}
}
a {
&,
&:focus,
&:hover,
&:active {
color: #ea7fbc;
}
}
footer {
background-color: #333;
color: #f5f5f5;
@ -67,16 +113,78 @@ footer {
right: 0;
text-align: center;
a {
&,
&:focus,
&:hover,
&:active {
color: #ea7fbc;
}
}
p {
margin: 0;
}
}
.instance,
.info {
h4 {
font-size: 20px;
margin: 0;
}
.instance-info {
background-color: #f5f5f5;
border-top: 1px solid #e5e5e5;
border-bottom: 1px solid #e5e5e5;
padding: 32px;
.instance-description {
margin: 0;
margin-bottom: 24px;
}
}
a {
text-decoration: none;
}
}
.admin {
margin-top: 32px;
display: flex;
align-items: center;
background-color: #fff;
border: 1px solid #e5e5e5;
border-radius: 3px;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
.display-name {
font-weight: 600;
font-size: 16px;
margin: 0;
}
.username {
font-size: 14px;
color: #777;
margin: 0;
margin-top: 8px;
}
}
.avatar {
width: 80px;
height: 80px;
img {
width: 100%;
border-radius: 40px;
border: 1px solid #333;
background-color: #f5f5f5;
box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.1);
}
}
@media(max-width: 700px) {
header .header-text {
padding: 24px;
}
section {
border-left: none;
border-right: none;
}
}

View file

@ -28,6 +28,7 @@ pub enum UrlKind {
Inbox,
Index,
MainKey,
Media(Uuid),
NodeInfo,
Outbox,
}
@ -136,6 +137,7 @@ impl Config {
UrlKind::Inbox => format!("{}://{}/inbox", scheme, self.hostname),
UrlKind::Index => format!("{}://{}/", scheme, self.hostname),
UrlKind::MainKey => format!("{}://{}/actor#main-key", scheme, self.hostname),
UrlKind::Media(uuid) => format!("{}://{}/media/{}", scheme, self.hostname, uuid),
UrlKind::NodeInfo => format!("{}://{}/nodeinfo/2.0.json", scheme, self.hostname),
UrlKind::Outbox => format!("{}://{}/outbox", scheme, self.hostname),
}

60
src/data/media.rs Normal file
View file

@ -0,0 +1,60 @@
use activitystreams::primitives::XsdAnyUri;
use bytes::Bytes;
use lru::LruCache;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::{Mutex, RwLock};
use ttl_cache::TtlCache;
use uuid::Uuid;
static MEDIA_DURATION: Duration = Duration::from_secs(60 * 60 * 24 * 2);
#[derive(Clone)]
pub struct Media {
inverse: Arc<Mutex<HashMap<XsdAnyUri, Uuid>>>,
url_cache: Arc<Mutex<LruCache<Uuid, XsdAnyUri>>>,
byte_cache: Arc<RwLock<TtlCache<Uuid, (String, Bytes)>>>,
}
impl Media {
pub fn new() -> Self {
Media {
inverse: Arc::new(Mutex::new(HashMap::new())),
url_cache: Arc::new(Mutex::new(LruCache::new(128))),
byte_cache: Arc::new(RwLock::new(TtlCache::new(128))),
}
}
pub async fn get_uuid(&self, url: &XsdAnyUri) -> Option<Uuid> {
let uuid = self.inverse.lock().await.get(url).cloned()?;
if self.url_cache.lock().await.contains(&uuid) {
return Some(uuid);
}
self.inverse.lock().await.remove(url);
None
}
pub async fn get_url(&self, uuid: Uuid) -> Option<XsdAnyUri> {
self.url_cache.lock().await.get(&uuid).cloned()
}
pub async fn get_bytes(&self, uuid: Uuid) -> Option<(String, Bytes)> {
self.byte_cache.read().await.get(&uuid).cloned()
}
pub async fn store_url(&self, url: &XsdAnyUri) -> Uuid {
let uuid = Uuid::new_v4();
self.inverse.lock().await.insert(url.clone(), uuid);
self.url_cache.lock().await.put(uuid, url.clone());
uuid
}
pub async fn store_bytes(&self, uuid: Uuid, content_type: String, bytes: Bytes) {
self.byte_cache
.write()
.await
.insert(uuid, (content_type, bytes), MEDIA_DURATION);
}
}

View file

@ -1,9 +1,11 @@
mod actor;
mod media;
mod node;
mod state;
pub use self::{
actor::{Actor, ActorCache},
node::{Node, NodeCache},
media::Media,
node::{Contact, Info, Instance, Node, NodeCache},
state::State,
};

View file

@ -70,6 +70,9 @@ pub enum MyError {
#[error("Hosts don't match, {0}, {1}")]
HostMismatch(String, String),
#[error("Invalid or missing content type")]
ContentType,
#[error("Couldn't flush buffer")]
FlushBuffer,

View file

@ -1,4 +1,4 @@
use crate::jobs::JobState;
use crate::{config::UrlKind, jobs::JobState};
use activitystreams::primitives::XsdAnyUri;
use anyhow::Error;
use background_jobs::{Job, Processor};
@ -44,7 +44,14 @@ impl QueryInstance {
instance.description
};
if let Some(contact) = instance.contact {
if let Some(mut contact) = instance.contact {
if let Some(uuid) = state.media.get_uuid(&contact.avatar).await {
contact.avatar = state.config.generate_url(UrlKind::Media(uuid)).parse()?;
} else {
let uuid = state.media.store_url(&contact.avatar).await;
contact.avatar = state.config.generate_url(UrlKind::Media(uuid)).parse()?;
}
state
.node_cache
.set_contact(
@ -57,6 +64,8 @@ impl QueryInstance {
.await?;
}
let description = ammonia::clean(&description);
state
.node_cache
.set_instance(

View file

@ -9,7 +9,8 @@ pub use self::{
};
use crate::{
data::{ActorCache, NodeCache, State},
config::Config,
data::{ActorCache, Media, NodeCache, State},
db::Db,
error::MyError,
jobs::{
@ -33,10 +34,24 @@ pub fn create_server(db: Db) -> JobServer {
JobServer::new(shared)
}
pub fn create_workers(state: State, actors: ActorCache, job_server: JobServer) {
pub fn create_workers(
state: State,
actors: ActorCache,
job_server: JobServer,
media: Media,
config: Config,
) {
let remote_handle = job_server.remote.clone();
WorkerConfig::new(move || JobState::new(state.clone(), actors.clone(), job_server.clone()))
WorkerConfig::new(move || {
JobState::new(
state.clone(),
actors.clone(),
job_server.clone(),
media.clone(),
config.clone(),
)
})
.register(DeliverProcessor)
.register(DeliverManyProcessor)
.register(NodeinfoProcessor)
@ -51,6 +66,8 @@ pub struct JobState {
requests: Requests,
state: State,
actors: ActorCache,
config: Config,
media: Media,
node_cache: NodeCache,
job_server: JobServer,
}
@ -61,11 +78,19 @@ pub struct JobServer {
}
impl JobState {
fn new(state: State, actors: ActorCache, job_server: JobServer) -> Self {
fn new(
state: State,
actors: ActorCache,
job_server: JobServer,
media: Media,
config: Config,
) -> Self {
JobState {
requests: state.requests(),
node_cache: state.node_cache(),
actors,
config,
media,
state,
job_server,
}

View file

@ -16,7 +16,7 @@ mod routes;
use self::{
args::Args,
config::Config,
data::{ActorCache, State},
data::{ActorCache, Media, State},
db::Db,
jobs::{create_server, create_workers},
middleware::RelayResolver,
@ -62,6 +62,7 @@ async fn main() -> Result<(), anyhow::Error> {
return Ok(());
}
let media = Media::new();
let state = State::hydrate(config.clone(), &db).await?;
let actors = ActorCache::new(db.clone());
let job_server = create_server(db.clone());
@ -84,9 +85,11 @@ async fn main() -> Result<(), anyhow::Error> {
let state = state.clone();
let actors = actors.clone();
let job_server = job_server.clone();
let media = media.clone();
let config = config.clone();
Arbiter::new().exec_fn(move || {
create_workers(state, actors, job_server);
create_workers(state, actors, job_server, media, config);
});
}
actix_rt::signal::ctrl_c().await?;
@ -98,7 +101,13 @@ async fn main() -> Result<(), anyhow::Error> {
let bind_address = config.bind_address();
HttpServer::new(move || {
if !no_jobs {
create_workers(state.clone(), actors.clone(), job_server.clone());
create_workers(
state.clone(),
actors.clone(),
job_server.clone(),
media.clone(),
config.clone(),
);
}
App::new()
@ -109,7 +118,9 @@ async fn main() -> Result<(), anyhow::Error> {
.data(actors.clone())
.data(config.clone())
.data(job_server.clone())
.data(media.clone())
.service(web::resource("/").route(web::get().to(index)))
.service(web::resource("/media/{path}").route(web::get().to(routes::media)))
.service(
web::resource("/inbox")
.wrap(config.digest_middleware())

View file

@ -1,6 +1,7 @@
use crate::error::MyError;
use activitystreams::primitives::XsdAnyUri;
use actix_web::client::Client;
use bytes::Bytes;
use http_signature_normalization_actix::prelude::*;
use log::error;
use rsa::{hash::Hashes, padding::PaddingScheme, RSAPrivateKey};
@ -63,6 +64,55 @@ impl Requests {
})
}
pub async fn fetch_bytes(&self, url: &str) -> Result<(String, Bytes), MyError> {
let mut res = self
.client
.get(url)
.header("Accept", "application/activity+json")
.header("User-Agent", self.user_agent.as_str())
.signature(&self.config, &self.key_id, |signing_string| {
self.sign(signing_string)
})?
.send()
.await
.map_err(|e| {
error!("Couldn't send request to {}, {}", url, e);
MyError::SendRequest
})?;
let content_type = if let Some(content_type) = res.headers().get("content-type") {
if let Ok(s) = content_type.to_str() {
s.to_owned()
} else {
return Err(MyError::ContentType);
}
} else {
return Err(MyError::ContentType);
};
if !res.status().is_success() {
if let Ok(bytes) = res.body().await {
if let Ok(s) = String::from_utf8(bytes.as_ref().to_vec()) {
if !s.is_empty() {
error!("Response, {}", s);
}
}
}
return Err(MyError::Status(res.status()));
}
let bytes = match res.body().limit(1024 * 1024 * 4).await {
Err(e) => {
error!("Coudn't fetch json from {}, {}", url, e);
return Err(MyError::ReceiveResponse);
}
Ok(bytes) => bytes,
};
Ok((content_type, bytes))
}
pub async fn deliver<T>(&self, inbox: XsdAnyUri, item: &T) -> Result<(), MyError>
where
T: serde::ser::Serialize,

View file

@ -8,7 +8,6 @@ pub async fn route(
config: web::Data<Config>,
) -> Result<HttpResponse, MyError> {
let nodes = state.node_cache().nodes().await;
let mut buf = BufWriter::new(Vec::new());
crate::templates::index(&mut buf, &nodes, &config)?;

27
src/routes/media.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::{data::Media, error::MyError, requests::Requests};
use actix_web::{web, HttpResponse};
use uuid::Uuid;
pub async fn route(
media: web::Data<Media>,
requests: web::Data<Requests>,
uuid: web::Path<Uuid>,
) -> Result<HttpResponse, MyError> {
let uuid = uuid.into_inner();
if let Some((content_type, bytes)) = media.get_bytes(uuid).await {
return Ok(HttpResponse::Ok().content_type(content_type).body(bytes));
}
if let Some(url) = media.get_url(uuid).await {
let (content_type, bytes) = requests.fetch_bytes(url.as_str()).await?;
media
.store_bytes(uuid, content_type.clone(), bytes.clone())
.await;
return Ok(HttpResponse::Ok().content_type(content_type).body(bytes));
}
Ok(HttpResponse::NotFound().finish())
}

View file

@ -1,6 +1,7 @@
mod actor;
mod inbox;
mod index;
mod media;
mod nodeinfo;
mod statics;
@ -8,6 +9,7 @@ pub use self::{
actor::route as actor,
inbox::route as inbox,
index::route as index,
media::route as media,
nodeinfo::{route as nodeinfo, well_known as nodeinfo_meta},
statics::route as statics,
};

15
templates/admin.rs.html Normal file
View file

@ -0,0 +1,15 @@
@use crate::data::Contact;
@(contact: &Contact)
<div class="admin">
<div class="left">
<figure class="avatar">
<img src="@contact.avatar" alt="@contact.display_name's avatar">
</figure>
</div>
<div class="right">
<p class="display-name"><a href="@contact.url">@contact.display_name</a></p>
<p class="username">@@@contact.username</p>
</div>
</div>

View file

@ -1,4 +1,8 @@
@use crate::{config::{Config, UrlKind}, templates::statics::index_css, data::Node};
@use crate::{
config::{Config, UrlKind},
data::Node,
templates::{info, instance, statics::index_css},
};
@(nodes: &[Node], config: &Config)
@ -11,28 +15,29 @@
</head>
<body>
<header>
<h1>Welcome to @config.software_name() on @config.hostname()</h1>
<div class="header-text">
<h1>@config.software_name()</h1>
<p>on @config.hostname()</p>
</div>
</header>
<main>
<section>
<h3>Connected Servers:</h3>
<h3>Connected Servers</h3>
@if nodes.is_empty() {
<p>There are no connected servers at this time.</p>
} else {
<ul>
@for node in nodes {
@if let Some(domain) = node.base.as_url().domain() {
@if let Some(inst) = node.instance.as_ref() {
<li>
<p><a href="@node.base">@domain</a></p>
@if let Some(info) = node.info.as_ref() {
<p>Running @info.software version @info.version</p>
@if info.reg {
<p>Registration is open</p>
} else {
<p>Registration is closed</p>
}
}
@:instance(inst, node.info.as_ref().map(|info| { info.software.as_ref() }), node.contact.as_ref(), &node.base)
</li>
} else {
@if let Some(inf) = node.info.as_ref() {
<li>
@:info(&inf, &node.base)
</li>
}
}
}
</ul>
@ -40,6 +45,7 @@
</section>
<section>
<h3>Joining</h3>
<article class="joining">
<p>
If you are the admin of a server that supports activitypub relays, you can add
this relay to your server.
@ -61,6 +67,7 @@
Consult the documentation for your server. It's likely that it follows either
Mastodon or Pleroma's relay formatting.
</p>
</article>
</section>
</main>
<footer>

16
templates/info.rs.html Normal file
View file

@ -0,0 +1,16 @@
@use crate::data::Info;
@use activitystreams::primitives::XsdAnyUri;
@(info: &Info, base: &XsdAnyUri)
<article class="info">
@if let Some(domain) = base.as_url().domain() {
<h4 class="padded"><a href="@base">@domain</a></h4>
}
<p class="padded">
Running @info.software, version @info.version.
@if info.reg {
Registration is open
}
</p>
</article>

View file

@ -0,0 +1,32 @@
@use crate::{data::{Contact, Instance}, templates::admin};
@use activitystreams::primitives::XsdAnyUri;
@(instance: &Instance, software: Option<&str>, contact: Option<&Contact>, base: &XsdAnyUri)
<article class="instance">
<h4 class="padded"><a href="@base">@instance.title</a></h4>
<p class="padded">
@if let Some(software) = software {
Running @software, version @instance.version.
}
@if instance.reg {
<br>Registration is open.
@if instance.requires_approval {
Accounts must be approved by an admin.
}
} else{
Registration is closed
}
</p>
<div class="instance-info">
<h4 class="instance-description">Description:</h4>
<div class="description">
<div class="please-stay">
@Html(&instance.description)
</div>
</div>
@if let Some(contact) = contact {
@:admin(contact)
}
</div>
</article>