add: communities list page

TODO(ui): implement community creation, community viewing, community posting
TODO(ui): implement profile following, followers, and posts feed
This commit is contained in:
trisua 2025-03-27 18:10:47 -04:00
parent 5cfca49793
commit d6fbfc3cd6
28 changed files with 497 additions and 313 deletions

View file

@ -40,6 +40,8 @@ pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.html")
pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.html"); pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.html");
pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.html"); pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.html");
pub const COMMUNITIES_LIST: &str = include_str!("./public/html/communities/list.html");
// langs // langs
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
@ -145,6 +147,8 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config); write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config);
write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config); write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config);
write_template!(html_path->"communities/list.html"(crate::assets::COMMUNITIES_LIST) -d "communities" --config=config);
html_path html_path
} }
@ -179,7 +183,11 @@ pub(crate) async fn init_dirs(config: &Config) {
pub(crate) static CACHE_BREAKER: LazyLock<String> = LazyLock::new(|| salt()); pub(crate) static CACHE_BREAKER: LazyLock<String> = LazyLock::new(|| salt());
/// Create the initial template context. /// Create the initial template context.
pub(crate) fn initial_context(config: &Config, lang: &LangFile, user: &Option<User>) -> Context { pub(crate) async fn initial_context(
config: &Config,
lang: &LangFile,
user: &Option<User>,
) -> Context {
let mut ctx = Context::new(); let mut ctx = Context::new();
ctx.insert("config", &config); ctx.insert("config", &config);
ctx.insert("user", &user); ctx.insert("user", &user);

View file

@ -3,6 +3,7 @@ version = "1.0.0"
[data] [data]
"general:link.home" = "Home" "general:link.home" = "Home"
"general:link.communities" = "Communities"
"dialog:action.okay" = "Ok" "dialog:action.okay" = "Ok"
"dialog:action.continue" = "Continue" "dialog:action.continue" = "Continue"
@ -13,8 +14,14 @@ version = "1.0.0"
"auth:action.login" = "Login" "auth:action.login" = "Login"
"auth:action.register" = "Register" "auth:action.register" = "Register"
"auth:action.logout" = "Logout" "auth:action.logout" = "Logout"
"auto:action.follow" = "Follow"
"auto:action.unfollow" = "Unfollow"
"auth:link.my_profile" = "My profile" "auth:link.my_profile" = "My profile"
"auth:link.settings" = "Settings" "auth:link.settings" = "Settings"
"auth:label.followers" = "Followers" "auth:label.followers" = "Followers"
"auth:label.following" = "Following" "auth:label.following" = "Following"
"auth:label.joined_journals" = "Joined Journals" "auth:label.joined_journals" = "Joined Journals"
"communities:action.create" = "Create"
"communities:label.create_new" = "Create new community"
"communities:label.name" = "Name"

View file

@ -0,0 +1,30 @@
{% import "macros.html" as macros %} {% extends "root.html" %} {% block head %}
<title>My communities - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav(selected="communities") }}
<main class="flex flex-col gap-2">
<div class="card-nest">
<div class="card">
<b>{{ text "communities:label.create_new" }}</b>
</div>
<form class="card flex flex-col gap-2">
<div class="flex flex-col gap-1">
<label for="">{{ text "communities:label.name" }}</label>
<input
type="text"
name="title"
id="title"
placeholder="name"
required
minlength="2"
maxlength="32"
/>
</div>
<button class="primary">
{{ text "communities:action.create" }}
</button>
</form>
</div>
</main>
{% endblock %}

View file

@ -14,11 +14,29 @@
{{ icon "house" }} {{ icon "house" }}
<span class="desktop">{{ text "general:link.home" }}</span> <span class="desktop">{{ text "general:link.home" }}</span>
</a> </a>
<a
href="/communities"
class="button {% if selected == 'communities' %}active{% endif %}"
>
{{ icon "book-heart" }}
<span class="desktop"
>{{ text "general:link.communities" }}</span
>
</a>
{% endif %} {% endif %}
</div> </div>
<div class="flex nav_side"> <div class="flex nav_side">
{% if user %} {% if user %}
<a href="/notifs" class="button" title="Notifications">
{{ icon "bell" }} {% if user.notification_count > 0 %}
<span class="notification tr"
>{{ user.notification_count }}</span
>
{% endif %}
</a>
<div class="dropdown"> <div class="dropdown">
<!-- prettier-ignore --> <!-- prettier-ignore -->
<button <button
@ -34,7 +52,7 @@
<div class="inner"> <div class="inner">
<b class="title">{{ user.username }}</b> <b class="title">{{ user.username }}</b>
<a href="/user/{{ user.username }}"> <a href="/user/{{ user.username }}">
{{ icon "book-heart" }} {{ icon "circle-user-round" }}
<span>{{ text "auth:link.my_profile" }}</span> <span>{{ text "auth:link.my_profile" }}</span>
</a> </a>

View file

@ -1,6 +1,6 @@
{% import "macros.html" as macros %} {% extends "root.html" %} {% block head %} {% import "macros.html" as macros %} {% extends "root.html" %} {% block head %}
<title>{{ error_text }} - Tetratto</title> <title>{{ error_text }} - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav(selected="home") }} {% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2"> <main class="flex flex-col gap-2">
<div class="card-nest"> <div class="card-nest">
<div class="card"> <div class="card">

View file

@ -1,11 +1,11 @@
use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ApiReturn, Error, journal::Journal}; use tetratto_core::model::{ApiReturn, Error, communities::Community};
use crate::{ use crate::{
State, get_user_from_token, State, get_user_from_token,
routes::api::v1::{ routes::api::v1::{
CreateJournal, UpdateJournalPrompt, UpdateJournalReadAccess, UpdateJournalTitle, CreateCommunity, UpdateCommunityContext, UpdateJournalReadAccess, UpdateJournalTitle,
UpdateJournalWriteAccess, UpdateJournalWriteAccess,
}, },
}; };
@ -13,7 +13,7 @@ use crate::{
pub async fn create_request( pub async fn create_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
Json(req): Json<CreateJournal>, Json(req): Json<CreateCommunity>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let data = &(data.read().await).0; let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) { let user = match get_user_from_token!(jar, data) {
@ -22,12 +22,12 @@ pub async fn create_request(
}; };
match data match data
.create_page(Journal::new(req.title, req.prompt, user.id)) .create_community(Community::new(req.title, user.id))
.await .await
{ {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Page created".to_string(), message: "Community created".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
@ -45,10 +45,10 @@ pub async fn delete_request(
None => return Json(Error::NotAllowed.into()), None => return Json(Error::NotAllowed.into()),
}; };
match data.delete_page(id, user).await { match data.delete_community(id, user).await {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Page deleted".to_string(), message: "Community deleted".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
@ -67,21 +67,21 @@ pub async fn update_title_request(
None => return Json(Error::NotAllowed.into()), None => return Json(Error::NotAllowed.into()),
}; };
match data.update_page_title(id, user, req.title).await { match data.update_community_title(id, user, req.title).await {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Page updated".to_string(), message: "Community updated".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
} }
} }
pub async fn update_prompt_request( pub async fn update_context_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
Path(id): Path<usize>, Path(id): Path<usize>,
Json(req): Json<UpdateJournalPrompt>, Json(req): Json<UpdateCommunityContext>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let data = &(data.read().await).0; let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) { let user = match get_user_from_token!(jar, data) {
@ -89,10 +89,10 @@ pub async fn update_prompt_request(
None => return Json(Error::NotAllowed.into()), None => return Json(Error::NotAllowed.into()),
}; };
match data.update_page_prompt(id, user, req.prompt).await { match data.update_community_context(id, user, req.context).await {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Page updated".to_string(), message: "Community updated".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
@ -111,10 +111,13 @@ pub async fn update_read_access_request(
None => return Json(Error::NotAllowed.into()), None => return Json(Error::NotAllowed.into()),
}; };
match data.update_page_read_access(id, user, req.access).await { match data
.update_community_read_access(id, user, req.access)
.await
{
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Page updated".to_string(), message: "Community updated".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
@ -133,10 +136,13 @@ pub async fn update_write_access_request(
None => return Json(Error::NotAllowed.into()), None => return Json(Error::NotAllowed.into()),
}; };
match data.update_page_write_access(id, user, req.access).await { match data
.update_community_write_access(id, user, req.access)
.await
{
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Page updated".to_string(), message: "Community updated".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),

View file

@ -0,0 +1,2 @@
pub mod communities;
pub mod posts;

View file

@ -1,6 +1,6 @@
use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ApiReturn, Error, journal::JournalPost}; use tetratto_core::model::{ApiReturn, Error, communities::Post};
use crate::{ use crate::{
State, get_user_from_token, State, get_user_from_token,
@ -19,7 +19,7 @@ pub async fn create_request(
}; };
match data match data
.create_post(JournalPost::new( .create_post(Post::new(
req.content, req.content,
req.journal, req.journal,
req.replying_to, req.replying_to,
@ -29,7 +29,7 @@ pub async fn create_request(
{ {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Entry created".to_string(), message: "Post created".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
@ -50,7 +50,7 @@ pub async fn delete_request(
match data.delete_post(id, user).await { match data.delete_post(id, user).await {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Entry deleted".to_string(), message: "Post deleted".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
@ -72,7 +72,7 @@ pub async fn update_content_request(
match data.update_post_content(id, user, req.content).await { match data.update_post_content(id, user, req.content).await {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Entry updated".to_string(), message: "Post updated".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
@ -94,7 +94,7 @@ pub async fn update_context_request(
match data.update_post_context(id, user, req.context).await { match data.update_post_context(id, user, req.context).await {
Ok(_) => Json(ApiReturn { Ok(_) => Json(ApiReturn {
ok: true, ok: true,
message: "Entry updated".to_string(), message: "Post updated".to_string(),
payload: (), payload: (),
}), }),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),

View file

@ -1,2 +0,0 @@
pub mod journals;
pub mod posts;

View file

@ -1,5 +1,5 @@
pub mod auth; pub mod auth;
pub mod journal; pub mod communities;
pub mod reactions; pub mod reactions;
use axum::{ use axum::{
@ -8,7 +8,7 @@ use axum::{
}; };
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::model::{ use tetratto_core::model::{
journal::{JournalPostContext, JournalReadAccess, JournalWriteAccess}, communities::{CommunityContext, CommunityReadAccess, CommunityWriteAccess, PostContext},
reactions::AssetType, reactions::AssetType,
}; };
@ -19,34 +19,40 @@ pub fn routes() -> Router {
.route("/reactions/{id}", get(reactions::get_request)) .route("/reactions/{id}", get(reactions::get_request))
.route("/reactions/{id}", delete(reactions::delete_request)) .route("/reactions/{id}", delete(reactions::delete_request))
// journal journals // journal journals
.route("/journals", post(journal::journals::create_request))
.route("/journals/{id}", delete(journal::journals::delete_request))
.route( .route(
"/journals/{id}/title", "/communities",
post(journal::journals::update_title_request), post(communities::communities::create_request),
) )
.route( .route(
"/journals/{id}/prompt", "/communities/{id}",
post(journal::journals::update_prompt_request), delete(communities::communities::delete_request),
)
.route(
"/communities/{id}/title",
post(communities::communities::update_title_request),
)
.route(
"/communities/{id}/context",
post(communities::communities::update_context_request),
) )
.route( .route(
"/journals/{id}/access/read", "/journals/{id}/access/read",
post(journal::journals::update_read_access_request), post(communities::communities::update_read_access_request),
) )
.route( .route(
"/journals/{id}/access/write", "/journals/{id}/access/write",
post(journal::journals::update_write_access_request), post(communities::communities::update_write_access_request),
) )
// journal posts // posts
.route("/posts", post(journal::posts::create_request)) .route("/posts", post(communities::posts::create_request))
.route("/posts/{id}", delete(journal::posts::delete_request)) .route("/posts/{id}", delete(communities::posts::delete_request))
.route( .route(
"/posts/{id}/content", "/posts/{id}/content",
post(journal::posts::update_content_request), post(communities::posts::update_content_request),
) )
.route( .route(
"/posts/{id}/context", "/posts/{id}/context",
post(journal::posts::update_context_request), post(communities::posts::update_context_request),
) )
// auth // auth
// global // global
@ -99,9 +105,8 @@ pub struct AuthProps {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CreateJournal { pub struct CreateCommunity {
pub title: String, pub title: String,
pub prompt: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -110,18 +115,18 @@ pub struct UpdateJournalTitle {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateJournalPrompt { pub struct UpdateCommunityContext {
pub prompt: String, pub context: CommunityContext,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateJournalReadAccess { pub struct UpdateJournalReadAccess {
pub access: JournalReadAccess, pub access: CommunityReadAccess,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateJournalWriteAccess { pub struct UpdateJournalWriteAccess {
pub access: JournalWriteAccess, pub access: CommunityWriteAccess,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -139,7 +144,7 @@ pub struct UpdateJournalEntryContent {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateJournalEntryContext { pub struct UpdateJournalEntryContext {
pub context: JournalPostContext, pub context: PostContext,
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View file

@ -15,7 +15,7 @@ pub async fn login_request(jar: CookieJar, Extension(data): Extension<State>) ->
} }
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0, lang, &user); let mut context = initial_context(&data.0.0, lang, &user).await;
Ok(Html( Ok(Html(
data.1.render("auth/login.html", &mut context).unwrap(), data.1.render("auth/login.html", &mut context).unwrap(),
@ -35,7 +35,7 @@ pub async fn register_request(
} }
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0, lang, &user); let mut context = initial_context(&data.0.0, lang, &user).await;
Ok(Html( Ok(Html(
data.1.render("auth/register.html", &mut context).unwrap(), data.1.render("auth/register.html", &mut context).unwrap(),

View file

@ -0,0 +1,37 @@
use super::render_error;
use crate::{State, assets::initial_context, get_lang, get_user_from_token};
use axum::{
Extension,
response::{Html, IntoResponse},
};
use axum_extra::extract::CookieJar;
use tetratto_core::model::Error;
/// `/communities`
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
let data = data.read().await;
let user = match get_user_from_token!(jar, data.0) {
Some(ua) => ua,
None => {
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &None).await,
));
}
};
let posts = match data.0.get_memberships_by_owner(user.id).await {
Ok(p) => p,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
};
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0, lang, &Some(user)).await;
context.insert("posts", &posts);
// return
Ok(Html(
data.1
.render("communities/list.html", &mut context)
.unwrap(),
))
}

View file

@ -11,7 +11,7 @@ pub async fn index_request(jar: CookieJar, Extension(data): Extension<State>) ->
let user = get_user_from_token!(jar, data.0); let user = get_user_from_token!(jar, data.0);
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0, lang, &user); let mut context = initial_context(&data.0.0, lang, &user).await;
Html(data.1.render("misc/index.html", &mut context).unwrap()) Html(data.1.render("misc/index.html", &mut context).unwrap())
} }

View file

@ -1,4 +1,5 @@
pub mod auth; pub mod auth;
pub mod communities;
pub mod misc; pub mod misc;
pub mod profile; pub mod profile;
@ -21,16 +22,18 @@ pub fn routes() -> Router {
.route("/auth/login", get(auth::login_request)) .route("/auth/login", get(auth::login_request))
// profile // profile
.route("/user/{username}", get(profile::posts_request)) .route("/user/{username}", get(profile::posts_request))
// communities
.route("/communities", get(communities::list_request))
} }
pub fn render_error( pub async fn render_error(
e: Error, e: Error,
jar: &CookieJar, jar: &CookieJar,
data: &(DataManager, tera::Tera), data: &(DataManager, tera::Tera),
user: &Option<User>, user: &Option<User>,
) -> String { ) -> String {
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0, lang, &user); let mut context = initial_context(&data.0.0, lang, &user).await;
context.insert("error_text", &e.to_string()); context.insert("error_text", &e.to_string());
data.1.render("misc/error.html", &mut context).unwrap() data.1.render("misc/error.html", &mut context).unwrap()
} }

View file

@ -6,6 +6,14 @@ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
}; };
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use tera::Context;
use tetratto_core::model::{Error, auth::User};
pub fn profile_context(context: &mut Context, profile: &User, is_self: bool, is_following: bool) {
context.insert("profile", &profile);
context.insert("is_self", &is_self);
context.insert("is_following", &is_following);
}
/// `/user/{username}` /// `/user/{username}`
pub async fn posts_request( pub async fn posts_request(
@ -19,7 +27,7 @@ pub async fn posts_request(
let other_user = match data.0.get_user_by_username(&username).await { let other_user = match data.0.get_user_by_username(&username).await {
Ok(ua) => ua, Ok(ua) => ua,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user))), Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
}; };
let posts = match data let posts = match data
@ -28,15 +36,46 @@ pub async fn posts_request(
.await .await
{ {
Ok(p) => p, Ok(p) => p,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user))), Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
}; };
// check if we're blocked
if let Some(ref ua) = user {
if data
.0
.get_userblock_by_initiator_receiver(other_user.id, ua.id)
.await
.is_ok()
{
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &user).await,
));
}
}
// init context
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0, lang, &user); let mut context = initial_context(&data.0.0, lang, &user).await;
let is_self = if let Some(ref ua) = user {
ua.id == other_user.id
} else {
false
};
let is_following = if let Some(ref ua) = user {
data.0
.get_userfollow_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
context.insert("profile", &other_user);
context.insert("posts", &posts); context.insert("posts", &posts);
profile_context(&mut context, &other_user, is_self, is_following);
// return
Ok(Html( Ok(Html(
data.1.render("profile/posts.html", &mut context).unwrap(), data.1.render("profile/posts.html", &mut context).unwrap(),
)) ))

View file

@ -1,12 +1,12 @@
use super::*; use super::*;
use crate::cache::Cache; use crate::cache::Cache;
use crate::model::journal::JournalMembership; use crate::model::communities::{CommunityContext, CommunityMembership};
use crate::model::journal_permissions::JournalPermission; use crate::model::communities_permissions::CommunityPermission;
use crate::model::{ use crate::model::{
Error, Result, Error, Result,
auth::User, auth::User,
journal::Journal, communities::Community,
journal::{JournalReadAccess, JournalWriteAccess}, communities::{CommunityReadAccess, CommunityWriteAccess},
permissions::FinePermission, permissions::FinePermission,
}; };
use crate::{auto_method, execute, get, query_row}; use crate::{auto_method, execute, get, query_row};
@ -18,32 +18,32 @@ use rusqlite::Row;
use tokio_postgres::Row; use tokio_postgres::Row;
impl DataManager { impl DataManager {
/// Get a [`Journal`] from an SQL row. /// Get a [`Community`] from an SQL row.
pub(crate) fn get_page_from_row( pub(crate) fn get_community_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>, #[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row, #[cfg(feature = "postgres")] x: &Row,
) -> Journal { ) -> Community {
Journal { Community {
id: get!(x->0(i64)) as usize, id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize, created: get!(x->1(i64)) as usize,
title: get!(x->2(String)), title: get!(x->2(String)),
prompt: get!(x->3(String)), context: serde_json::from_str(&get!(x->3(String))).unwrap(),
owner: get!(x->4(i64)) as usize, owner: get!(x->4(i64)) as usize,
read_access: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(), read_access: serde_json::from_str(&get!(x->5(String))).unwrap(),
write_access: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(), write_access: serde_json::from_str(&get!(x->6(String))).unwrap(),
// likes // likes
likes: get!(x->6(i64)) as isize, likes: get!(x->6(i64)) as isize,
dislikes: get!(x->7(i64)) as isize, dislikes: get!(x->7(i64)) as isize,
} }
} }
auto_method!(get_page_by_id()@get_page_from_row -> "SELECT * FROM journals WHERE id = $1" --name="journal" --returns=Journal --cache-key-tmpl="atto.journal:{}"); auto_method!(get_page_by_id()@get_community_from_row -> "SELECT * FROM journals WHERE id = $1" --name="journal" --returns=Community --cache-key-tmpl="atto.journal:{}");
/// Create a new journal page in the database. /// Create a new journal page in the database.
/// ///
/// # Arguments /// # Arguments
/// * `data` - a mock [`Journal`] object to insert /// * `data` - a mock [`Journal`] object to insert
pub async fn create_page(&self, data: Journal) -> Result<()> { pub async fn create_community(&self, data: Community) -> Result<()> {
// check values // check values
if data.title.len() < 2 { if data.title.len() < 2 {
return Err(Error::DataTooShort("title".to_string())); return Err(Error::DataTooShort("title".to_string()));
@ -51,12 +51,6 @@ impl DataManager {
return Err(Error::DataTooLong("title".to_string())); return Err(Error::DataTooLong("title".to_string()));
} }
if data.prompt.len() < 2 {
return Err(Error::DataTooShort("prompt".to_string()));
} else if data.prompt.len() > 2048 {
return Err(Error::DataTooLong("prompt".to_string()));
}
// ... // ...
let conn = match self.connect().await { let conn = match self.connect().await {
Ok(c) => c, Ok(c) => c,
@ -70,7 +64,7 @@ impl DataManager {
&data.id.to_string().as_str(), &data.id.to_string().as_str(),
&data.created.to_string().as_str(), &data.created.to_string().as_str(),
&data.title.as_str(), &data.title.as_str(),
&data.prompt.as_str(), &serde_json::to_string(&data.context).unwrap().as_str(),
&data.owner.to_string().as_str(), &data.owner.to_string().as_str(),
&serde_json::to_string(&data.read_access).unwrap().as_str(), &serde_json::to_string(&data.read_access).unwrap().as_str(),
&serde_json::to_string(&data.write_access).unwrap().as_str(), &serde_json::to_string(&data.write_access).unwrap().as_str(),
@ -82,10 +76,10 @@ impl DataManager {
} }
// add journal page owner as admin // add journal page owner as admin
self.create_membership(JournalMembership::new( self.create_membership(CommunityMembership::new(
data.owner, data.owner,
data.id, data.id,
JournalPermission::ADMINISTRATOR, CommunityPermission::ADMINISTRATOR,
)) ))
.await .await
.unwrap(); .unwrap();
@ -94,11 +88,11 @@ impl DataManager {
Ok(()) Ok(())
} }
auto_method!(delete_page()@get_page_by_id:MANAGE_JOURNAL_PAGES -> "DELETE journals pages WHERE id = $1" --cache-key-tmpl="atto.journal:{}"); auto_method!(delete_community()@get_page_by_id:MANAGE_JOURNAL_PAGES -> "DELETE journals pages WHERE id = $1" --cache-key-tmpl="atto.journal:{}");
auto_method!(update_page_title(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE journals SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.journal:{}"); auto_method!(update_community_title(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE journals SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.journal:{}");
auto_method!(update_page_prompt(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE journals SET prompt = $1 WHERE id = $2" --cache-key-tmpl="atto.journal:{}"); auto_method!(update_community_context(CommunityContext)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE journals SET prompt = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}");
auto_method!(update_page_read_access(JournalReadAccess)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE journals SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}"); auto_method!(update_community_read_access(CommunityReadAccess)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE journals SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}");
auto_method!(update_page_write_access(JournalWriteAccess)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE journals SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}"); auto_method!(update_community_write_access(CommunityWriteAccess)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE journals SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}");
auto_method!(incr_page_likes() -> "UPDATE journals SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.journal:{}" --incr); auto_method!(incr_page_likes() -> "UPDATE journals SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.journal:{}" --incr);
auto_method!(incr_page_dislikes() -> "UPDATE journals SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.journal:{}" --incr); auto_method!(incr_page_dislikes() -> "UPDATE journals SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.journal:{}" --incr);

View file

@ -2,6 +2,6 @@ CREATE TABLE IF NOT EXISTS memberships (
id INTEGER NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
created INTEGER NOT NULL, created INTEGER NOT NULL,
owner INTEGER NOT NULL, owner INTEGER NOT NULL,
journal INTEGER NOT NULL, community INTEGER NOT NULL,
role INTEGER NOT NULL role INTEGER NOT NULL
) )

View file

@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS posts (
created INTEGER NOT NULL, created INTEGER NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
owner INTEGER NOT NULL, owner INTEGER NOT NULL,
journal INTEGER NOT NULL, community INTEGER NOT NULL,
context TEXT NOT NULL, context TEXT NOT NULL,
replying_to INTEGER, -- the ID of the post this is a comment on... NULL means it isn't a reply replying_to INTEGER, -- the ID of the post this is a comment on... NULL means it isn't a reply
-- likes -- likes

View file

@ -1,10 +1,10 @@
use super::*; use super::*;
use crate::cache::Cache; use crate::cache::Cache;
use crate::model::{ use crate::model::{
Error, Result, auth::User, journal::JournalMembership, journal_permissions::JournalPermission, Error, Result, auth::User, communities::CommunityMembership,
permissions::FinePermission, communities_permissions::CommunityPermission, permissions::FinePermission,
}; };
use crate::{auto_method, execute, get, query_row}; use crate::{auto_method, execute, get, query_row, query_rows};
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
use rusqlite::Row; use rusqlite::Row;
@ -17,24 +17,24 @@ impl DataManager {
pub(crate) fn get_membership_from_row( pub(crate) fn get_membership_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>, #[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row, #[cfg(feature = "postgres")] x: &Row,
) -> JournalMembership { ) -> CommunityMembership {
JournalMembership { CommunityMembership {
id: get!(x->0(i64)) as usize, id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize, created: get!(x->1(i64)) as usize,
owner: get!(x->2(i64)) as usize, owner: get!(x->2(i64)) as usize,
journal: get!(x->3(i64)) as usize, community: get!(x->3(i64)) as usize,
role: JournalPermission::from_bits(get!(x->4(u32))).unwrap(), role: CommunityPermission::from_bits(get!(x->4(u32))).unwrap(),
} }
} }
auto_method!(get_membership_by_id()@get_membership_from_row -> "SELECT * FROM memberships WHERE id = $1" --name="journal membership" --returns=JournalMembership --cache-key-tmpl="atto.membership:{}"); auto_method!(get_membership_by_id()@get_membership_from_row -> "SELECT * FROM memberships WHERE id = $1" --name="journal membership" --returns=CommunityMembership --cache-key-tmpl="atto.membership:{}");
/// Get a journal membership by `owner` and `journal`. /// Get a community membership by `owner` and `community`.
pub async fn get_membership_by_owner_journal( pub async fn get_membership_by_owner_community(
&self, &self,
owner: usize, owner: usize,
journal: usize, community: usize,
) -> Result<JournalMembership> { ) -> Result<CommunityMembership> {
let conn = match self.connect().await { let conn = match self.connect().await {
Ok(c) => c, Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())), Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -42,23 +42,44 @@ impl DataManager {
let res = query_row!( let res = query_row!(
&conn, &conn,
"SELECT * FROM memberships WHERE owner = $1 AND journal = $2", "SELECT * FROM memberships WHERE owner = $1 AND community = $2",
&[&(owner as i64), &(journal as i64)], &[&(owner as i64), &(community as i64)],
|x| { Ok(Self::get_membership_from_row(x)) } |x| { Ok(Self::get_membership_from_row(x)) }
); );
if res.is_err() { if res.is_err() {
return Err(Error::GeneralNotFound("journal membership".to_string())); return Err(Error::GeneralNotFound("community membership".to_string()));
} }
Ok(res.unwrap()) Ok(res.unwrap())
} }
/// Create a new journal membership in the database. /// Get all community memberships by `owner`.
pub async fn get_memberships_by_owner(&self, owner: usize) -> Result<Vec<CommunityMembership>> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM memberships WHERE owner = $1",
&[&(owner as i64)],
|x| { Self::get_membership_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("community membership".to_string()));
}
Ok(res.unwrap())
}
/// Create a new community membership in the database.
/// ///
/// # Arguments /// # Arguments
/// * `data` - a mock [`JournalMembership`] object to insert /// * `data` - a mock [`CommunityMembership`] object to insert
pub async fn create_membership(&self, data: JournalMembership) -> Result<()> { pub async fn create_membership(&self, data: CommunityMembership) -> Result<()> {
let conn = match self.connect().await { let conn = match self.connect().await {
Ok(c) => c, Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())), Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -71,7 +92,7 @@ impl DataManager {
&data.id.to_string().as_str(), &data.id.to_string().as_str(),
&data.created.to_string().as_str(), &data.created.to_string().as_str(),
&data.owner.to_string().as_str(), &data.owner.to_string().as_str(),
&data.journal.to_string().as_str(), &data.community.to_string().as_str(),
&(data.role.bits()).to_string().as_str(), &(data.role.bits()).to_string().as_str(),
] ]
); );
@ -91,8 +112,8 @@ impl DataManager {
// pull other user's membership status // pull other user's membership status
if let Ok(z) = self.get_membership_by_id(user.id).await { if let Ok(z) = self.get_membership_by_id(user.id).await {
// somebody with MANAGE_ROLES _and_ a higher role number can remove us // somebody with MANAGE_ROLES _and_ a higher role number can remove us
if (!z.role.check(JournalPermission::MANAGE_ROLES) | (z.role < y.role)) if (!z.role.check(CommunityPermission::MANAGE_ROLES) | (z.role < y.role))
&& !z.role.check(JournalPermission::ADMINISTRATOR) && !z.role.check(CommunityPermission::ADMINISTRATOR)
{ {
return Err(Error::NotAllowed); return Err(Error::NotAllowed);
} }
@ -125,7 +146,7 @@ impl DataManager {
pub async fn update_membership_role( pub async fn update_membership_role(
&self, &self,
id: usize, id: usize,
new_role: JournalPermission, new_role: CommunityPermission,
) -> Result<()> { ) -> Result<()> {
let conn = match self.connect().await { let conn = match self.connect().await {
Ok(c) => c, Ok(c) => c,

View file

@ -1,8 +1,8 @@
mod auth; mod auth;
mod common; mod common;
mod communities;
mod drivers; mod drivers;
mod ipbans; mod ipbans;
mod journals;
mod memberships; mod memberships;
mod notifications; mod notifications;
mod posts; mod posts;

View file

@ -26,7 +26,7 @@ impl DataManager {
auto_method!(get_notification_by_id()@get_notification_from_row -> "SELECT * FROM notifications WHERE id = $1" --name="notification" --returns=Notification --cache-key-tmpl="atto.notification:{}"); auto_method!(get_notification_by_id()@get_notification_from_row -> "SELECT * FROM notifications WHERE id = $1" --name="notification" --returns=Notification --cache-key-tmpl="atto.notification:{}");
/// Get a reaction by `owner` and `asset`. /// Get all notifications by `owner`.
pub async fn get_notifications_by_owner(&self, owner: usize) -> Result<Vec<Notification>> { pub async fn get_notifications_by_owner(&self, owner: usize) -> Result<Vec<Notification>> {
let conn = match self.connect().await { let conn = match self.connect().await {
Ok(c) => c, Ok(c) => c,
@ -107,9 +107,9 @@ impl DataManager {
self.2.remove(format!("atto.notification:{}", id)).await; self.2.remove(format!("atto.notification:{}", id)).await;
// decr notification count // decr notification count
// self.decr_user_notifications(notification.owner) self.decr_user_notifications(notification.owner)
// .await .await
// .unwrap(); .unwrap();
// return // return
Ok(()) Ok(())

View file

@ -1,8 +1,10 @@
use super::*; use super::*;
use crate::cache::Cache; use crate::cache::Cache;
use crate::model::journal::JournalPostContext; use crate::model::communities::PostContext;
use crate::model::{ use crate::model::{
Error, Result, auth::User, journal::JournalPost, journal::JournalWriteAccess, Error, Result,
auth::User,
communities::{CommunityWriteAccess, Post},
permissions::FinePermission, permissions::FinePermission,
}; };
use crate::{auto_method, execute, get, query_row, query_rows}; use crate::{auto_method, execute, get, query_row, query_rows};
@ -14,17 +16,17 @@ use rusqlite::Row;
use tokio_postgres::Row; use tokio_postgres::Row;
impl DataManager { impl DataManager {
/// Get a [`JournalEntry`] from an SQL row. /// Get a [`Post`] from an SQL row.
pub(crate) fn get_post_from_row( pub(crate) fn get_post_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>, #[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row, #[cfg(feature = "postgres")] x: &Row,
) -> JournalPost { ) -> Post {
JournalPost { Post {
id: get!(x->0(i64)) as usize, id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize, created: get!(x->1(i64)) as usize,
content: get!(x->2(String)), content: get!(x->2(String)),
owner: get!(x->3(i64)) as usize, owner: get!(x->3(i64)) as usize,
journal: get!(x->4(i64)) as usize, community: get!(x->4(i64)) as usize,
context: serde_json::from_str(&get!(x->5(String))).unwrap(), context: serde_json::from_str(&get!(x->5(String))).unwrap(),
replying_to: if let Some(id) = get!(x->6(Option<i64>)) { replying_to: if let Some(id) = get!(x->6(Option<i64>)) {
Some(id as usize) Some(id as usize)
@ -39,7 +41,7 @@ impl DataManager {
} }
} }
auto_method!(get_post_by_id()@get_post_from_row -> "SELECT * FROM posts WHERE id = $1" --name="post" --returns=JournalPost --cache-key-tmpl="atto.post:{}"); auto_method!(get_post_by_id()@get_post_from_row -> "SELECT * FROM posts WHERE id = $1" --name="post" --returns=Post --cache-key-tmpl="atto.post:{}");
/// Get all posts which are comments on the given post by ID. /// Get all posts which are comments on the given post by ID.
/// ///
@ -47,12 +49,7 @@ impl DataManager {
/// * `id` - the ID of the post the requested posts are commenting on /// * `id` - the ID of the post the requested posts are commenting on
/// * `batch` - the limit of posts in each page /// * `batch` - the limit of posts in each page
/// * `page` - the page number /// * `page` - the page number
pub async fn get_post_comments( pub async fn get_post_comments(&self, id: usize, batch: usize, page: usize) -> Result<Post> {
&self,
id: usize,
batch: usize,
page: usize,
) -> Result<JournalPost> {
let conn = match self.connect().await { let conn = match self.connect().await {
Ok(c) => c, Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())), Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -83,7 +80,7 @@ impl DataManager {
id: usize, id: usize,
batch: usize, batch: usize,
page: usize, page: usize,
) -> Result<Vec<JournalPost>> { ) -> Result<Vec<Post>> {
let conn = match self.connect().await { let conn = match self.connect().await {
Ok(c) => c, Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())), Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -107,7 +104,7 @@ impl DataManager {
/// ///
/// # Arguments /// # Arguments
/// * `data` - a mock [`JournalEntry`] object to insert /// * `data` - a mock [`JournalEntry`] object to insert
pub async fn create_post(&self, data: JournalPost) -> Result<()> { pub async fn create_post(&self, data: Post) -> Result<()> {
// check values // check values
if data.content.len() < 2 { if data.content.len() < 2 {
return Err(Error::DataTooShort("content".to_string())); return Err(Error::DataTooShort("content".to_string()));
@ -116,20 +113,20 @@ impl DataManager {
} }
// check permission in page // check permission in page
let page = match self.get_page_by_id(data.journal).await { let page = match self.get_page_by_id(data.community).await {
Ok(p) => p, Ok(p) => p,
Err(e) => return Err(e), Err(e) => return Err(e),
}; };
match page.write_access { match page.write_access {
JournalWriteAccess::Owner => { CommunityWriteAccess::Owner => {
if data.owner != page.owner { if data.owner != page.owner {
return Err(Error::NotAllowed); return Err(Error::NotAllowed);
} }
} }
JournalWriteAccess::Joined => { CommunityWriteAccess::Joined => {
if let Err(_) = self if let Err(_) = self
.get_membership_by_owner_journal(data.owner, page.id) .get_membership_by_owner_community(data.owner, page.id)
.await .await
{ {
return Err(Error::NotAllowed); return Err(Error::NotAllowed);
@ -152,7 +149,7 @@ impl DataManager {
&Some(data.created.to_string()), &Some(data.created.to_string()),
&Some(data.content), &Some(data.content),
&Some(data.owner.to_string()), &Some(data.owner.to_string()),
&Some(data.journal.to_string()), &Some(data.community.to_string()),
&Some(serde_json::to_string(&data.context).unwrap()), &Some(serde_json::to_string(&data.context).unwrap()),
&if let Some(id) = data.replying_to { &if let Some(id) = data.replying_to {
Some(id.to_string()) Some(id.to_string())
@ -180,7 +177,7 @@ impl DataManager {
auto_method!(delete_post()@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "DELETE FROM posts WHERE id = $1" --cache-key-tmpl="atto.post:{}"); auto_method!(delete_post()@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "DELETE FROM posts WHERE id = $1" --cache-key-tmpl="atto.post:{}");
auto_method!(update_post_content(String)@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "UPDATE posts SET content = $1 WHERE id = $2" --cache-key-tmpl="atto.post:{}"); auto_method!(update_post_content(String)@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "UPDATE posts SET content = $1 WHERE id = $2" --cache-key-tmpl="atto.post:{}");
auto_method!(update_post_context(JournalPostContext)@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "UPDATE posts SET context = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.post:{}"); auto_method!(update_post_context(PostContext)@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "UPDATE posts SET context = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.post:{}");
auto_method!(incr_post_likes() -> "UPDATE posts SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr); auto_method!(incr_post_likes() -> "UPDATE posts SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
auto_method!(incr_post_dislikes() -> "UPDATE posts SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr); auto_method!(incr_post_dislikes() -> "UPDATE posts SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);

View file

@ -38,7 +38,7 @@ impl DataManager {
let res = query_row!( let res = query_row!(
&conn, &conn,
"SELECT * FROM userblocks WHERE initator = $1 AND receiver = $2", "SELECT * FROM userblocks WHERE initiator = $1 AND receiver = $2",
&[&(initiator as i64), &(receiver as i64)], &[&(initiator as i64), &(receiver as i64)],
|x| { Ok(Self::get_userblock_from_row(x)) } |x| { Ok(Self::get_userblock_from_row(x)) }
); );

View file

@ -38,7 +38,7 @@ impl DataManager {
let res = query_row!( let res = query_row!(
&conn, &conn,
"SELECT * FROM userfollows WHERE initator = $1 AND receiver = $2", "SELECT * FROM userfollows WHERE initiator = $1 AND receiver = $2",
&[&(initiator as i64), &(receiver as i64)], &[&(initiator as i64), &(receiver as i64)],
|x| { Ok(Self::get_userfollow_from_row(x)) } |x| { Ok(Self::get_userfollow_from_row(x)) }
); );

View file

@ -0,0 +1,174 @@
use serde::{Deserialize, Serialize};
use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp};
use super::communities_permissions::CommunityPermission;
#[derive(Serialize, Deserialize)]
pub struct Community {
pub id: usize,
pub created: usize,
pub title: String,
pub context: CommunityContext,
/// The ID of the owner of the community.
pub owner: usize,
/// Who can read the community page.
pub read_access: CommunityReadAccess,
/// Who can write to the community page (create posts belonging to it).
///
/// The owner of the community page (and moderators) are the ***only*** people
/// capable of removing posts.
pub write_access: CommunityWriteAccess,
pub likes: isize,
pub dislikes: isize,
}
impl Community {
/// Create a new [`Community`].
pub fn new(title: String, owner: usize) -> Self {
Self {
id: AlmostSnowflake::new(1234567890)
.to_string()
.parse::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
title,
context: CommunityContext::default(),
owner,
read_access: CommunityReadAccess::default(),
write_access: CommunityWriteAccess::default(),
likes: 0,
dislikes: 0,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct CommunityContext {
pub description: String,
}
impl Default for CommunityContext {
fn default() -> Self {
Self {
description: String::new(),
}
}
}
/// Who can read a [`Community`].
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum CommunityReadAccess {
/// Everybody can view the community.
Everybody,
/// Only people with the link to the community.
Unlisted,
/// Only the owner of the community.
Private,
}
impl Default for CommunityReadAccess {
fn default() -> Self {
Self::Everybody
}
}
/// Who can write to a [`Community`].
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum CommunityWriteAccess {
/// Everybody.
Everybody,
/// Only people who joined the community can write to it.
///
/// Memberships can be managed by the owner of the community.
Joined,
/// Only the owner of the community.
Owner,
}
impl Default for CommunityWriteAccess {
fn default() -> Self {
Self::Joined
}
}
#[derive(Serialize, Deserialize)]
pub struct CommunityMembership {
pub id: usize,
pub created: usize,
pub owner: usize,
pub community: usize,
pub role: CommunityPermission,
}
impl CommunityMembership {
/// Create a new [`CommunityMembership`].
pub fn new(owner: usize, community: usize, role: CommunityPermission) -> Self {
Self {
id: AlmostSnowflake::new(1234567890)
.to_string()
.parse::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
owner,
community,
role,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct PostContext {
pub comments_enabled: bool,
}
impl Default for PostContext {
fn default() -> Self {
Self {
comments_enabled: true,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct Post {
pub id: usize,
pub created: usize,
pub content: String,
/// The ID of the owner of this post.
pub owner: usize,
/// The ID of the [`Community`] this post belongs to.
pub community: usize,
/// Extra information about the post.
pub context: PostContext,
/// The ID of the post this post is a comment on.
pub replying_to: Option<usize>,
pub likes: isize,
pub dislikes: isize,
pub comment_count: usize,
}
impl Post {
/// Create a new [`Post`].
pub fn new(
content: String,
community: usize,
replying_to: Option<usize>,
owner: usize,
) -> Self {
Self {
id: AlmostSnowflake::new(1234567890)
.to_string()
.parse::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
content,
owner,
community,
context: PostContext::default(),
replying_to,
likes: 0,
dislikes: 0,
comment_count: 0,
}
}
}

View file

@ -7,7 +7,7 @@ use serde::{
bitflags! { bitflags! {
/// Fine-grained journal permissions built using bitwise operations. /// Fine-grained journal permissions built using bitwise operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JournalPermission: u32 { pub struct CommunityPermission: u32 {
const DEFAULT = 1 << 0; const DEFAULT = 1 << 0;
const ADMINISTRATOR = 1 << 1; const ADMINISTRATOR = 1 << 1;
const MEMBER = 1 << 2; const MEMBER = 1 << 2;
@ -18,7 +18,7 @@ bitflags! {
} }
} }
impl Serialize for JournalPermission { impl Serialize for CommunityPermission {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
@ -29,7 +29,7 @@ impl Serialize for JournalPermission {
struct JournalPermissionVisitor; struct JournalPermissionVisitor;
impl<'de> Visitor<'de> for JournalPermissionVisitor { impl<'de> Visitor<'de> for JournalPermissionVisitor {
type Value = JournalPermission; type Value = CommunityPermission;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("u32") formatter.write_str("u32")
@ -39,10 +39,10 @@ impl<'de> Visitor<'de> for JournalPermissionVisitor {
where where
E: DeError, E: DeError,
{ {
if let Some(permission) = JournalPermission::from_bits(value) { if let Some(permission) = CommunityPermission::from_bits(value) {
Ok(permission) Ok(permission)
} else { } else {
Ok(JournalPermission::from_bits_retain(value)) Ok(CommunityPermission::from_bits_retain(value))
} }
} }
@ -50,10 +50,10 @@ impl<'de> Visitor<'de> for JournalPermissionVisitor {
where where
E: DeError, E: DeError,
{ {
if let Some(permission) = JournalPermission::from_bits(value as u32) { if let Some(permission) = CommunityPermission::from_bits(value as u32) {
Ok(permission) Ok(permission)
} else { } else {
Ok(JournalPermission::from_bits_retain(value as u32)) Ok(CommunityPermission::from_bits_retain(value as u32))
} }
} }
@ -61,15 +61,15 @@ impl<'de> Visitor<'de> for JournalPermissionVisitor {
where where
E: DeError, E: DeError,
{ {
if let Some(permission) = JournalPermission::from_bits(value as u32) { if let Some(permission) = CommunityPermission::from_bits(value as u32) {
Ok(permission) Ok(permission)
} else { } else {
Ok(JournalPermission::from_bits_retain(value as u32)) Ok(CommunityPermission::from_bits_retain(value as u32))
} }
} }
} }
impl<'de> Deserialize<'de> for JournalPermission { impl<'de> Deserialize<'de> for CommunityPermission {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
@ -78,15 +78,15 @@ impl<'de> Deserialize<'de> for JournalPermission {
} }
} }
impl JournalPermission { impl CommunityPermission {
/// Join two [`JournalPermission`]s into a single `u32`. /// Join two [`JournalPermission`]s into a single `u32`.
pub fn join(lhs: JournalPermission, rhs: JournalPermission) -> JournalPermission { pub fn join(lhs: CommunityPermission, rhs: CommunityPermission) -> CommunityPermission {
lhs | rhs lhs | rhs
} }
/// Check if the given `input` contains the given [`JournalPermission`]. /// Check if the given `input` contains the given [`JournalPermission`].
pub fn check(self, permission: JournalPermission) -> bool { pub fn check(self, permission: CommunityPermission) -> bool {
if (self & JournalPermission::ADMINISTRATOR) == JournalPermission::ADMINISTRATOR { if (self & CommunityPermission::ADMINISTRATOR) == CommunityPermission::ADMINISTRATOR {
// has administrator permission, meaning everything else is automatically true // has administrator permission, meaning everything else is automatically true
return true; return true;
} }
@ -96,16 +96,16 @@ impl JournalPermission {
/// Check if the given [`JournalPermission`] qualifies as "Member" status. /// Check if the given [`JournalPermission`] qualifies as "Member" status.
pub fn check_member(self) -> bool { pub fn check_member(self) -> bool {
self.check(JournalPermission::MEMBER) self.check(CommunityPermission::MEMBER)
} }
/// Check if the given [`JournalPermission`] qualifies as "Moderator" status. /// Check if the given [`JournalPermission`] qualifies as "Moderator" status.
pub fn check_moderator(self) -> bool { pub fn check_moderator(self) -> bool {
self.check(JournalPermission::MANAGE_POSTS) self.check(CommunityPermission::MANAGE_POSTS)
} }
} }
impl Default for JournalPermission { impl Default for CommunityPermission {
fn default() -> Self { fn default() -> Self {
Self::DEFAULT Self::DEFAULT
} }

View file

@ -1,155 +0,0 @@
use serde::{Deserialize, Serialize};
use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp};
use super::journal_permissions::JournalPermission;
#[derive(Serialize, Deserialize)]
pub struct Journal {
pub id: usize,
pub created: usize,
pub title: String,
pub prompt: String,
/// The ID of the owner of the journal page.
pub owner: usize,
/// Who can read the journal page.
pub read_access: JournalReadAccess,
/// Who can write to the journal page (create journal entries belonging to it).
///
/// The owner of the journal page (and moderators) are the ***only*** people
/// capable of removing entries.
pub write_access: JournalWriteAccess,
pub likes: isize,
pub dislikes: isize,
}
impl Journal {
/// Create a new [`Journal`].
pub fn new(title: String, prompt: String, owner: usize) -> Self {
Self {
id: AlmostSnowflake::new(1234567890)
.to_string()
.parse::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
title,
prompt,
owner,
read_access: JournalReadAccess::default(),
write_access: JournalWriteAccess::default(),
likes: 0,
dislikes: 0,
}
}
}
/// Who can read a [`Journal`].
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum JournalReadAccess {
/// Everybody can view the journal page from the owner's profile.
Everybody,
/// Only people with the link to the journal page.
Unlisted,
/// Only the owner of the journal page.
Private,
}
impl Default for JournalReadAccess {
fn default() -> Self {
Self::Everybody
}
}
/// Who can write to a [`Journal`].
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum JournalWriteAccess {
/// Everybody (authenticated users only still).
Everybody,
/// Only people who joined the journal page can write to it.
///
/// Memberships can be managed by the owner of the journal page.
Joined,
/// Only the owner of the journal page.
Owner,
}
impl Default for JournalWriteAccess {
fn default() -> Self {
Self::Joined
}
}
#[derive(Serialize, Deserialize)]
pub struct JournalMembership {
pub id: usize,
pub created: usize,
pub owner: usize,
pub journal: usize,
pub role: JournalPermission,
}
impl JournalMembership {
pub fn new(owner: usize, journal: usize, role: JournalPermission) -> Self {
Self {
id: AlmostSnowflake::new(1234567890)
.to_string()
.parse::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
owner,
journal,
role,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct JournalPostContext {
pub comments_enabled: bool,
}
impl Default for JournalPostContext {
fn default() -> Self {
Self {
comments_enabled: true,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct JournalPost {
pub id: usize,
pub created: usize,
pub content: String,
/// The ID of the owner of this entry.
pub owner: usize,
/// The ID of the [`Journal`] this entry belongs to.
pub journal: usize,
/// Extra information about the journal entry.
pub context: JournalPostContext,
/// The ID of the post this post is a comment on.
pub replying_to: Option<usize>,
pub likes: isize,
pub dislikes: isize,
pub comment_count: usize,
}
impl JournalPost {
/// Create a new [`JournalEntry`].
pub fn new(content: String, journal: usize, replying_to: Option<usize>, owner: usize) -> Self {
Self {
id: AlmostSnowflake::new(1234567890)
.to_string()
.parse::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
content,
owner,
journal,
context: JournalPostContext::default(),
replying_to,
likes: 0,
dislikes: 0,
comment_count: 0,
}
}
}

View file

@ -1,6 +1,6 @@
pub mod auth; pub mod auth;
pub mod journal; pub mod communities;
pub mod journal_permissions; pub mod communities_permissions;
pub mod permissions; pub mod permissions;
pub mod reactions; pub mod reactions;