add: littleweb full
This commit is contained in:
parent
3fc0872867
commit
d67e7c9c33
32 changed files with 1699 additions and 71 deletions
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tetratto-core"
|
||||
version = "10.0.0"
|
||||
version = "11.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
@ -19,4 +19,8 @@ base16ct = { version = "0.2.0", features = ["alloc"] }
|
|||
base64 = "0.22.1"
|
||||
emojis = "0.7.0"
|
||||
regex = "1.11.1"
|
||||
oiseau = { version = "0.1.2", default-features = false, features = ["postgres", "redis"] }
|
||||
oiseau = { version = "0.1.2", default-features = false, features = [
|
||||
"postgres",
|
||||
"redis",
|
||||
] }
|
||||
paste = "1.0.15"
|
||||
|
|
|
@ -361,6 +361,9 @@ fn default_banned_usernames() -> Vec<String> {
|
|||
"search".to_string(),
|
||||
"journals".to_string(),
|
||||
"links".to_string(),
|
||||
"app".to_string(),
|
||||
"services".to_string(),
|
||||
"domains".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use crate::model::{
|
||||
auth::User,
|
||||
littleweb::{Domain, DomainData, DomainTld},
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
Error, Result,
|
||||
use crate::{
|
||||
database::NAME_REGEX,
|
||||
model::{
|
||||
auth::User,
|
||||
littleweb::{Domain, DomainData, DomainTld},
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
Error, Result,
|
||||
},
|
||||
};
|
||||
use crate::{auto_method, DataManager};
|
||||
use oiseau::{cache::Cache, execute, get, params, query_row, query_rows, PostgresRow};
|
||||
|
@ -71,6 +74,8 @@ impl DataManager {
|
|||
Ok(res.unwrap())
|
||||
}
|
||||
|
||||
const MAXIMUM_FREE_DOMAINS: usize = 5;
|
||||
|
||||
/// Create a new domain in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -83,6 +88,31 @@ impl DataManager {
|
|||
return Err(Error::DataTooLong("name".to_string()));
|
||||
}
|
||||
|
||||
// check number of domains
|
||||
let owner = self.get_user_by_id(data.owner).await?;
|
||||
|
||||
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||
let domains = self.get_domains_by_user(data.owner).await?;
|
||||
|
||||
if domains.len() >= Self::MAXIMUM_FREE_DOMAINS {
|
||||
return Err(Error::MiscError(
|
||||
"You already have the maximum number of domains you can have".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// check name
|
||||
let regex = regex::RegexBuilder::new(NAME_REGEX)
|
||||
.multi_line(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
if regex.captures(&data.name).is_some() {
|
||||
return Err(Error::MiscError(
|
||||
"Domain name contains invalid characters".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// check for existing
|
||||
if self
|
||||
.get_domain_by_name_tld(&data.name, &data.tld)
|
||||
|
|
|
@ -126,5 +126,6 @@ impl DataManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(update_service_files(Vec<ServiceFsEntry>)@get_service_by_id:FinePermission::MANAGE_USERS; -> "UPDATE services SET data = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.service:{}");
|
||||
auto_method!(update_service_name(&str)@get_service_by_id:FinePermission::MANAGE_USERS; -> "UPDATE services SET name = $1 WHERE id = $2" --cache-key-tmpl="atto.service:{}");
|
||||
auto_method!(update_service_files(Vec<ServiceFsEntry>)@get_service_by_id:FinePermission::MANAGE_USERS; -> "UPDATE services SET files = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.service:{}");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::fmt::Display;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||
use paste::paste;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Service {
|
||||
|
@ -24,18 +26,24 @@ impl Service {
|
|||
}
|
||||
|
||||
/// Resolve a file from the virtual file system.
|
||||
pub fn file(&self, path: &str) -> Option<ServiceFsEntry> {
|
||||
///
|
||||
/// # Returns
|
||||
/// `(file, id path)`
|
||||
pub fn file(&self, path: &str) -> Option<(ServiceFsEntry, Vec<String>)> {
|
||||
let segments = path.chars().filter(|x| x == &'/').count();
|
||||
|
||||
let mut path = path.split("/");
|
||||
let mut path_segment = path.next().unwrap();
|
||||
let mut ids = Vec::new();
|
||||
let mut i = 0;
|
||||
|
||||
let mut f = &self.files;
|
||||
|
||||
while let Some(nf) = f.iter().find(|x| x.name == path_segment) {
|
||||
if i == segments - 1 {
|
||||
return Some(nf.to_owned());
|
||||
ids.push(nf.id.clone());
|
||||
|
||||
if i == segments {
|
||||
return Some((nf.to_owned(), ids));
|
||||
}
|
||||
|
||||
f = &nf.children;
|
||||
|
@ -45,6 +53,31 @@ impl Service {
|
|||
|
||||
None
|
||||
}
|
||||
|
||||
/// Resolve a file from the virtual file system (mutable).
|
||||
///
|
||||
/// # Returns
|
||||
/// `&mut file`
|
||||
pub fn file_mut(&mut self, id_path: Vec<String>) -> Option<&mut ServiceFsEntry> {
|
||||
let total_segments = id_path.len();
|
||||
let mut i = 0;
|
||||
|
||||
let mut f = &mut self.files;
|
||||
for segment in id_path {
|
||||
if let Some(nf) = f.iter_mut().find(|x| (**x).id == segment) {
|
||||
if i == total_segments - 1 {
|
||||
return Some(nf);
|
||||
}
|
||||
|
||||
f = &mut nf.children;
|
||||
i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A file type for [`ServiceFsEntry`] structs.
|
||||
|
@ -77,36 +110,92 @@ impl Display for ServiceFsMime {
|
|||
/// A single entry in the file system of [`Service`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceFsEntry {
|
||||
/// Files use a UUID since they're generated on the client.
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub mime: ServiceFsMime,
|
||||
pub children: Vec<ServiceFsEntry>,
|
||||
pub content: String,
|
||||
/// SHA-256 checksum of the entry's content.
|
||||
pub checksum: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum DomainTld {
|
||||
Bunny,
|
||||
}
|
||||
|
||||
impl Display for DomainTld {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Bunny => "bunny",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for DomainTld {
|
||||
fn from(value: &str) -> Self {
|
||||
match value {
|
||||
"bunny" => Self::Bunny,
|
||||
_ => Self::Bunny,
|
||||
macro_rules! domain_tld_display_match {
|
||||
($self:ident, $($tld:ident),+ $(,)?) => {
|
||||
match $self {
|
||||
$(
|
||||
Self::$tld => stringify!($tld).to_lowercase(),
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! domain_tld_strings {
|
||||
($($tld:ident),+ $(,)?) => {
|
||||
$(
|
||||
paste! {
|
||||
/// Constant from macro.
|
||||
const [<TLD_ $tld:snake:upper>]: LazyLock<String> = LazyLock::new(|| stringify!($tld).to_lowercase());
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! domain_tld_from_match {
|
||||
($value:ident, $($tld:ident),+ $(,)?) => {
|
||||
{
|
||||
$(
|
||||
paste! {
|
||||
let [<$tld:snake:lower>] = &*[<TLD_ $tld:snake:upper>];
|
||||
}
|
||||
)+;
|
||||
|
||||
// can't use match here, the expansion is going to look really ugly
|
||||
$(
|
||||
if $value == paste!{ [<$tld:snake:lower>] } {
|
||||
return Self::$tld;
|
||||
}
|
||||
)+
|
||||
|
||||
return Self::Bunny;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! define_domain_tlds {
|
||||
($($tld:ident),+ $(,)?) => {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum DomainTld {
|
||||
$($tld),+
|
||||
}
|
||||
|
||||
domain_tld_strings!($($tld),+);
|
||||
|
||||
impl From<&str> for DomainTld {
|
||||
fn from(value: &str) -> Self {
|
||||
domain_tld_from_match!(
|
||||
value, $($tld),+
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DomainTld {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// using this macro allows us to just copy and paste the enum variants
|
||||
f.write_str(&domain_tld_display_match!(
|
||||
self, $($tld),+
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// This is VERY important so that I don't have to manually type them all for the UI dropdown.
|
||||
pub const TLDS_VEC: LazyLock<Vec<&str>> = LazyLock::new(|| vec![$(stringify!($tld)),+]);
|
||||
}
|
||||
}
|
||||
|
||||
define_domain_tlds!(
|
||||
Bunny, Tet, Cool, Qwerty, Boy, Girl, Them, Quack, Bark, Meow, Silly, Wow, Neko, Yay, Lol, Love,
|
||||
Fun, Gay, City, Woah, Clown, Apple, Yaoi, Yuri, World, Wav, Zero, Evil, Dragon, Yum, Site
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Domain {
|
||||
pub id: usize,
|
||||
|
@ -142,12 +231,12 @@ impl Domain {
|
|||
|
||||
// we're reversing this so it's predictable, as there might not always be a subdomain
|
||||
// (we shouldn't have the variable entry be first, there is always going to be a tld)
|
||||
let mut s: Vec<&str> = no_protocol.split(".").collect();
|
||||
let mut s: Vec<&str> = no_protocol.split("/").next().unwrap().split(".").collect();
|
||||
s.reverse();
|
||||
let mut s = s.into_iter();
|
||||
|
||||
let tld = DomainTld::from(s.next().unwrap());
|
||||
let domain = s.next().unwrap();
|
||||
let domain = s.next().unwrap_or("default.bunny");
|
||||
let subdomain = s.next().unwrap_or("@");
|
||||
|
||||
// get path
|
||||
|
@ -157,7 +246,7 @@ impl Domain {
|
|||
while char != '/' {
|
||||
// we need to keep eating characters until we reach the first /
|
||||
// (marking the start of the path)
|
||||
char = chars.next().unwrap();
|
||||
char = chars.next().unwrap_or('/');
|
||||
}
|
||||
|
||||
let path: String = chars.collect();
|
||||
|
@ -183,7 +272,10 @@ impl Domain {
|
|||
pub fn service(&self, subdomain: &str) -> Option<usize> {
|
||||
let s = self.data.iter().find(|x| x.0 == subdomain)?;
|
||||
match s.1 {
|
||||
DomainData::Service(id) => Some(id),
|
||||
DomainData::Service(ref id) => Some(match id.parse::<usize>() {
|
||||
Ok(id) => id,
|
||||
Err(_) => return None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +285,7 @@ impl Domain {
|
|||
pub enum DomainData {
|
||||
/// The ID of the service this domain points to. The first service found will
|
||||
/// always be used. This means having multiple service entires will be useless.
|
||||
Service(usize),
|
||||
Service(String),
|
||||
/// A text entry with a maximum of 512 characters.
|
||||
Text(String),
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue