add: circle stacks
This commit is contained in:
parent
50704d27a9
commit
56cea83933
27 changed files with 419 additions and 107 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -2621,9 +2621,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.19"
|
version = "0.12.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
|
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -2638,12 +2638,10 @@ dependencies = [
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
"hyper-tls 0.6.0",
|
"hyper-tls 0.6.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"ipnet",
|
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
|
@ -3233,7 +3231,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"async-stripe",
|
"async-stripe",
|
||||||
|
@ -3264,7 +3262,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
@ -3286,7 +3284,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -3295,7 +3293,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -10,7 +10,7 @@ Make sure you have AT LEAST rustc version 1.89.0-nightly.
|
||||||
Everything Tetratto needs will be built into the main binary. You can build Tetratto with the following command:
|
Everything Tetratto needs will be built into the main binary. You can build Tetratto with the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build
|
cargo build -r
|
||||||
```
|
```
|
||||||
|
|
||||||
Tetratto **requires** a PostgreSQL server, as well as a Redis (or Redis fork) instance.
|
Tetratto **requires** a PostgreSQL server, as well as a Redis (or Redis fork) instance.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -19,7 +19,7 @@ tetratto-core = { path = "../core" }
|
||||||
tetratto-l10n = { path = "../l10n" }
|
tetratto-l10n = { path = "../l10n" }
|
||||||
|
|
||||||
image = "0.25.6"
|
image = "0.25.6"
|
||||||
reqwest = { version = "0.12.19", features = ["json", "stream"] }
|
reqwest = { version = "0.12.20", features = ["json", "stream"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
|
|
|
@ -127,11 +127,8 @@ where
|
||||||
Err(_) => return Err((StatusCode::BAD_REQUEST, "json data isn't utf8".to_string())),
|
Err(_) => return Err((StatusCode::BAD_REQUEST, "json data isn't utf8".to_string())),
|
||||||
}) {
|
}) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return Err((
|
return Err((StatusCode::BAD_REQUEST, e.to_string()));
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"could not parse json data as json".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,13 @@
|
||||||
("value" "{{ community.id }}")
|
("value" "{{ community.id }}")
|
||||||
("selected" "{% if selected_community == community.id -%}true{% else %}false{%- endif %}")
|
("selected" "{% if selected_community == community.id -%}true{% else %}false{%- endif %}")
|
||||||
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))
|
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))
|
||||||
|
(text "{% endfor %}")
|
||||||
|
(text "{% for stack in stacks %}")
|
||||||
|
(option
|
||||||
|
("value" "{{ stack.id }}")
|
||||||
|
("selected" "{% if selected_stack == stack.id -%}true{% else %}false{%- endif %}")
|
||||||
|
("is_stack" "true")
|
||||||
|
(text "{{ stack.name }} (circle)"))
|
||||||
(text "{% endfor %}")))
|
(text "{% endfor %}")))
|
||||||
(form
|
(form
|
||||||
("class" "card flex flex-col gap-2")
|
("class" "card flex flex-col gap-2")
|
||||||
|
@ -184,13 +191,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const is_selected_stack = document.getElementById(
|
||||||
|
\"community_to_post_to\",
|
||||||
|
).selectedOptions[0].getAttribute(\"is_stack\") === \"true\";
|
||||||
|
const selected_community = document.getElementById(
|
||||||
|
\"community_to_post_to\",
|
||||||
|
).selectedOptions[0].value;
|
||||||
|
|
||||||
body.append(
|
body.append(
|
||||||
\"body\",
|
\"body\",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
content: e.target.content.value,
|
content: e.target.content.value,
|
||||||
community: document.getElementById(
|
community: !is_selected_stack ? selected_community : \"0\",
|
||||||
\"community_to_post_to\",
|
stack: is_selected_stack ? selected_community : \"0\",
|
||||||
).selectedOptions[0].value,
|
|
||||||
poll: poll_data[1],
|
poll: poll_data[1],
|
||||||
title: e.target.title.value,
|
title: e.target.title.value,
|
||||||
}),
|
}),
|
||||||
|
@ -316,12 +329,15 @@
|
||||||
(text "{% else %}")
|
(text "{% else %}")
|
||||||
(script
|
(script
|
||||||
(text "async function create_post_from_form(e) {
|
(text "async function create_post_from_form(e) {
|
||||||
|
e.preventDefault();
|
||||||
const id = await trigger(\"me::repost\", [
|
const id = await trigger(\"me::repost\", [
|
||||||
\"{{ quoting[1].id }}\",
|
\"{{ quoting[1].id }}\",
|
||||||
e.target.content.value,
|
e.target.content.value,
|
||||||
document.getElementById(\"community_to_post_to\")
|
document.getElementById(\"community_to_post_to\")
|
||||||
.selectedOptions[0].value,
|
.selectedOptions[0].value,
|
||||||
false,
|
false,
|
||||||
|
document.getElementById(\"community_to_post_to\")
|
||||||
|
.selectedOptions[0].getAttribute(\"is_stack\") === \"true\",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// update settings
|
// update settings
|
||||||
|
@ -394,27 +410,34 @@
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
|
|
||||||
(script
|
(script
|
||||||
(text "const town_square = \"{{ config.town_square }}\";
|
(text "(() => {const town_square = \"{{ config.town_square }}\";
|
||||||
const user_id = \"{{ user.id }}\";
|
const user_id = \"{{ user.id }}\";
|
||||||
|
|
||||||
function update_community_avatar(e) {
|
window.update_community_avatar = (e) => {
|
||||||
const element = e.target.parentElement.querySelector(\".avatar\");
|
const element = e.target.parentElement.querySelector(\".avatar\");
|
||||||
|
const is_stack = e.target.selectedOptions[0].getAttribute(\"is_stack\") === \"true\";
|
||||||
const id = e.target.selectedOptions[0].value;
|
const id = e.target.selectedOptions[0].value;
|
||||||
|
|
||||||
element.setAttribute(\"title\", id);
|
element.setAttribute(\"title\", id);
|
||||||
element.setAttribute(\"alt\", `${id}'s avatar`);
|
element.setAttribute(\"alt\", `${id}'s avatar`);
|
||||||
|
|
||||||
if (id === town_square) {
|
if (id === town_square || is_stack) {
|
||||||
element.src = `/api/v1/auth/user/${user_id}/avatar?selector_type=id`;
|
element.src = `/api/v1/auth/user/${user_id}/avatar?selector_type=id`;
|
||||||
} else {
|
} else {
|
||||||
element.src = `/api/v1/communities/${id}/avatar`;
|
element.src = `/api/v1/communities/${id}/avatar`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_community_supports_title(e) {
|
window.check_community_supports_title = async (e) => {
|
||||||
const element = document.getElementById(\"title_field\");
|
const element = document.getElementById(\"title_field\");
|
||||||
|
const is_stack = e.target.selectedOptions[0].getAttribute(\"is_stack\") === \"true\";
|
||||||
const id = e.target.selectedOptions[0].value;
|
const id = e.target.selectedOptions[0].value;
|
||||||
|
|
||||||
|
if (is_stack) {
|
||||||
|
element.classList.add(\"hidden\");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetch(`/api/v1/communities/${id}/supports_titles`)
|
fetch(`/api/v1/communities/${id}/supports_titles`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
@ -436,7 +459,7 @@
|
||||||
});
|
});
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
async function cancel_create_post() {
|
window.cancel_create_post = async () => {
|
||||||
if (
|
if (
|
||||||
!(await trigger(\"atto::confirm\", [
|
!(await trigger(\"atto::confirm\", [
|
||||||
\"Are you sure you would like to do this? Your post content will be lost.\",
|
\"Are you sure you would like to do this? Your post content will be lost.\",
|
||||||
|
@ -446,6 +469,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
window.history.back();
|
window.history.back();
|
||||||
}"))
|
}})();"))
|
||||||
|
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -173,8 +173,13 @@
|
||||||
("class" "flex items-center")
|
("class" "flex items-center")
|
||||||
("style" "color: var(--color-primary)")
|
("style" "color: var(--color-primary)")
|
||||||
(text "{{ icon \"square-asterisk\" }}"))
|
(text "{{ icon \"square-asterisk\" }}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %} {% if post.stack -%}")
|
||||||
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
|
(span
|
||||||
|
("title" "Posted to a stack you're in")
|
||||||
|
("class" "flex items-center")
|
||||||
|
("style" "color: var(--color-primary)")
|
||||||
|
(text "{{ icon \"layers\" }}"))
|
||||||
|
(text "{%- endif %} {% if community and community.is_forge -%} {% if post.is_open -%}")
|
||||||
(span
|
(span
|
||||||
("title" "Open")
|
("title" "Open")
|
||||||
("class" "flex items-center green")
|
("class" "flex items-center green")
|
||||||
|
|
|
@ -298,6 +298,7 @@
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
content: e.target.content.value,
|
content: e.target.content.value,
|
||||||
community: \"{{ community.id }}\",
|
community: \"{{ community.id }}\",
|
||||||
|
stack: \"{{ post.stack }}\",
|
||||||
replying_to: \"{{ post.id }}\",
|
replying_to: \"{{ post.id }}\",
|
||||||
poll: poll_data[1],
|
poll: poll_data[1],
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -581,7 +581,9 @@
|
||||||
(li
|
(li
|
||||||
(text "Ability to create more than 1 app"))
|
(text "Ability to create more than 1 app"))
|
||||||
(li
|
(li
|
||||||
(text "Create up to 10 stack blocks")))
|
(text "Create up to 10 stack blocks"))
|
||||||
|
(li
|
||||||
|
(text "Add unlimited users to stacks")))
|
||||||
(a
|
(a
|
||||||
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
|
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
|
||||||
("class" "button")
|
("class" "button")
|
||||||
|
|
|
@ -17,14 +17,27 @@
|
||||||
(text "{{ components::avatar(username=stack.owner, selector_type=\"id\") }}"))
|
(text "{{ components::avatar(username=stack.owner, selector_type=\"id\") }}"))
|
||||||
(span
|
(span
|
||||||
(text "{{ stack.name }}")))
|
(text "{{ stack.name }}")))
|
||||||
|
(div
|
||||||
|
("class" "flex gap-2")
|
||||||
|
(text "{% if stack.mode == 'Circle' -%}")
|
||||||
|
; post button for circle stacks
|
||||||
|
(a
|
||||||
|
("href" "/communities/intents/post?stack={{ stack.id }}")
|
||||||
|
("class" "button lowered small")
|
||||||
|
(text "{{ icon \"plus\" }}")
|
||||||
|
(span
|
||||||
|
(text "{{ text \"general:action.post\" }}")))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
(text "{% if user and user.id == stack.owner -%}")
|
(text "{% if user and user.id == stack.owner -%}")
|
||||||
|
; manage button for stack owner only
|
||||||
(a
|
(a
|
||||||
("href" "/stacks/{{ stack.id }}/manage")
|
("href" "/stacks/{{ stack.id }}/manage")
|
||||||
("class" "button lowered small")
|
("class" "button lowered small")
|
||||||
(text "{{ icon \"pencil\" }}")
|
(text "{{ icon \"pencil\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.manage\" }}")))
|
(text "{{ text \"general:action.manage\" }}")))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}")))
|
||||||
(div
|
(div
|
||||||
("class" "card w-full flex flex-col gap-2")
|
("class" "card w-full flex flex-col gap-2")
|
||||||
(text "{% if list|length == 0 -%}")
|
(text "{% if list|length == 0 -%}")
|
||||||
|
@ -37,6 +50,7 @@
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
|
|
||||||
(text "{% if stack.mode == 'BlockList' -%}")
|
(text "{% if stack.mode == 'BlockList' -%}")
|
||||||
|
; block button + user list for blocklist only
|
||||||
(text "{% if not is_blocked -%}")
|
(text "{% if not is_blocked -%}")
|
||||||
(button
|
(button
|
||||||
("onclick" "block_all()")
|
("onclick" "block_all()")
|
||||||
|
@ -50,6 +64,12 @@
|
||||||
("class" "flex gap-2 flex-wrap w-full")
|
("class" "flex gap-2 flex-wrap w-full")
|
||||||
(text "{% for user in list %} {{ components::user_plate(user=user, secondary=true) }} {% endfor %}"))
|
(text "{% for user in list %} {{ components::user_plate(user=user, secondary=true) }} {% endfor %}"))
|
||||||
(text "{% else %}")
|
(text "{% else %}")
|
||||||
|
; user icons for circle stack
|
||||||
|
(text "{% if stack.mode == 'Circle' -%} {% for user in stack.users %}")
|
||||||
|
(text "{{ components::avatar(username=user, selector_type=\"id\", size=\"24px\") }}")
|
||||||
|
(text "{% endfor %} {%- endif %}")
|
||||||
|
|
||||||
|
; posts for all stacks except blocklist
|
||||||
(text "{% for post in list %}
|
(text "{% for post in list %}
|
||||||
{% if post[2].read_access == \"Everybody\" -%}
|
{% if post[2].read_access == \"Everybody\" -%}
|
||||||
{% if post[0].context.repost and post[0].context.repost.reposting -%}
|
{% if post[0].context.repost and post[0].context.repost.reposting -%}
|
||||||
|
|
|
@ -67,7 +67,11 @@
|
||||||
(option
|
(option
|
||||||
("value" "BlockList")
|
("value" "BlockList")
|
||||||
("selected" "{% if stack.mode == 'BlockList' -%}true{% else %}false{%- endif %}")
|
("selected" "{% if stack.mode == 'BlockList' -%}true{% else %}false{%- endif %}")
|
||||||
(text "Block list")))))
|
(text "Block list"))
|
||||||
|
(option
|
||||||
|
("value" "Circle")
|
||||||
|
("selected" "{% if stack.mode == 'Circle' -%}true{% else %}false{%- endif %}")
|
||||||
|
(text "Circle")))))
|
||||||
(div
|
(div
|
||||||
("class" "card-nest")
|
("class" "card-nest")
|
||||||
("ui_ident" "sort")
|
("ui_ident" "sort")
|
||||||
|
|
|
@ -259,7 +259,14 @@
|
||||||
|
|
||||||
self.define(
|
self.define(
|
||||||
"repost",
|
"repost",
|
||||||
(_, id, content, community, do_not_redirect = false) => {
|
(
|
||||||
|
_,
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
community,
|
||||||
|
do_not_redirect = false,
|
||||||
|
is_stack = false,
|
||||||
|
) => {
|
||||||
return new Promise((resolve, _) => {
|
return new Promise((resolve, _) => {
|
||||||
fetch(`/api/v1/posts/${id}/repost`, {
|
fetch(`/api/v1/posts/${id}/repost`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -268,7 +275,8 @@
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content,
|
content,
|
||||||
community,
|
community: !is_stack ? community : "0",
|
||||||
|
stack: is_stack ? community : "0",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
|
|
|
@ -124,6 +124,10 @@ pub async fn create_request(
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
props.title = req.title;
|
props.title = req.title;
|
||||||
|
props.stack = match req.stack.parse::<usize>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// check sizes
|
// check sizes
|
||||||
|
@ -197,8 +201,7 @@ pub async fn create_repost_request(
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
match data
|
let mut props = Post::repost(
|
||||||
.create_post(Post::repost(
|
|
||||||
req.content,
|
req.content,
|
||||||
match req.community.parse::<usize>() {
|
match req.community.parse::<usize>() {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
|
@ -206,9 +209,15 @@ pub async fn create_repost_request(
|
||||||
},
|
},
|
||||||
user.id,
|
user.id,
|
||||||
id,
|
id,
|
||||||
))
|
);
|
||||||
.await
|
|
||||||
{
|
props.stack = match req.stack.parse::<usize>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...
|
||||||
|
match data.create_post(props).await {
|
||||||
Ok(id) => Json(ApiReturn {
|
Ok(id) => Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "Post reposted".to_string(),
|
message: "Post reposted".to_string(),
|
||||||
|
|
|
@ -616,12 +616,15 @@ pub struct CreatePost {
|
||||||
pub poll: Option<CreatePostPoll>,
|
pub poll: Option<CreatePostPoll>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub stack: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateRepost {
|
pub struct CreateRepost {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub community: String,
|
pub community: String,
|
||||||
|
pub stack: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -5,11 +5,14 @@ use axum::{
|
||||||
Extension, Json,
|
Extension, Json,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use tetratto_core::model::{
|
use tetratto_core::{
|
||||||
|
model::{
|
||||||
oauth,
|
oauth,
|
||||||
permissions::FinePermission,
|
permissions::FinePermission,
|
||||||
stacks::{StackBlock, StackPrivacy, UserStack},
|
stacks::{StackBlock, StackMode, StackPrivacy, UserStack},
|
||||||
ApiReturn, Error,
|
ApiReturn, Error,
|
||||||
|
},
|
||||||
|
DataManager,
|
||||||
};
|
};
|
||||||
use super::{
|
use super::{
|
||||||
AddOrRemoveStackUser, CreateStack, UpdateStackMode, UpdateStackName, UpdateStackPrivacy,
|
AddOrRemoveStackUser, CreateStack, UpdateStackMode, UpdateStackName, UpdateStackPrivacy,
|
||||||
|
@ -161,6 +164,25 @@ pub async fn add_user_request(
|
||||||
};
|
};
|
||||||
|
|
||||||
stack.users.push(other_user.id);
|
stack.users.push(other_user.id);
|
||||||
|
|
||||||
|
// check number of stacks
|
||||||
|
let owner = match data.get_user_by_id(stack.owner).await {
|
||||||
|
Ok(ua) => ua,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||||
|
if stack.users.len() >= DataManager::MAXIMUM_FREE_STACK_USERS {
|
||||||
|
return Json(
|
||||||
|
Error::MiscError(
|
||||||
|
"This stack already has the maximum users it can have".to_string(),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
match data.update_stack_users(id, &user, stack.users).await {
|
match data.update_stack_users(id, &user, stack.users).await {
|
||||||
Ok(_) => Json(ApiReturn {
|
Ok(_) => Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -250,6 +272,7 @@ pub async fn get_users_request(
|
||||||
|
|
||||||
if stack.privacy == StackPrivacy::Private
|
if stack.privacy == StackPrivacy::Private
|
||||||
&& user.id != stack.owner
|
&& user.id != stack.owner
|
||||||
|
&& !(stack.mode == StackMode::Circle && stack.users.contains(&user.id))
|
||||||
&& !user.permissions.check(FinePermission::MANAGE_STACKS)
|
&& !user.permissions.check(FinePermission::MANAGE_STACKS)
|
||||||
{
|
{
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
|
|
|
@ -11,8 +11,12 @@ use axum_extra::extract::CookieJar;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
use tetratto_core::model::{
|
use tetratto_core::model::{
|
||||||
auth::User, communities::Community, communities_permissions::CommunityPermission,
|
auth::User,
|
||||||
permissions::FinePermission, Error,
|
communities::Community,
|
||||||
|
communities_permissions::CommunityPermission,
|
||||||
|
permissions::FinePermission,
|
||||||
|
stacks::{StackMode, UserStack},
|
||||||
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
@ -245,6 +249,8 @@ pub struct CreatePostProps {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub community: usize,
|
pub community: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub stack: usize,
|
||||||
|
#[serde(default)]
|
||||||
pub from_draft: usize,
|
pub from_draft: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub quote: usize,
|
pub quote: usize,
|
||||||
|
@ -286,6 +292,16 @@ pub async fn create_post_request(
|
||||||
communities.push(community)
|
communities.push(community)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let stacks = match data.0.get_stacks_by_user(user.id).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let stacks: Vec<&UserStack> = stacks
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.mode == StackMode::Circle)
|
||||||
|
.collect();
|
||||||
|
|
||||||
// get draft
|
// get draft
|
||||||
let draft = if props.from_draft != 0 {
|
let draft = if props.from_draft != 0 {
|
||||||
match data.0.get_draft_by_id(props.from_draft).await {
|
match data.0.get_draft_by_id(props.from_draft).await {
|
||||||
|
@ -326,8 +342,10 @@ pub async fn create_post_request(
|
||||||
|
|
||||||
context.insert("draft", &draft);
|
context.insert("draft", &draft);
|
||||||
context.insert("drafts", &drafts);
|
context.insert("drafts", &drafts);
|
||||||
|
context.insert("stacks", &stacks);
|
||||||
context.insert("quoting", "ing);
|
context.insert("quoting", "ing);
|
||||||
context.insert("communities", &communities);
|
context.insert("communities", &communities);
|
||||||
|
context.insert("selected_stack", &props.stack);
|
||||||
context.insert("selected_community", &props.community);
|
context.insert("selected_community", &props.community);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
|
@ -663,6 +681,28 @@ pub async fn post_request(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check stack
|
||||||
|
if post.stack != 0 {
|
||||||
|
let stack = match data.0.get_stack_by_id(post.stack).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref ua) = user {
|
||||||
|
if (stack.owner != ua.id) && !stack.users.contains(&ua.id) {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we MUST be authenticated to view posts in a stack
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &user).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
let community = match data.0.get_community_by_id(post.community).await {
|
let community = match data.0.get_community_by_id(post.community).await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
|
|
|
@ -50,7 +50,7 @@ pub async fn settings_request(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let stacks = match data.0.get_stacks_by_owner(profile.id).await {
|
let stacks = match data.0.get_stacks_by_user(profile.id).await {
|
||||||
Ok(ua) => ua,
|
Ok(ua) => ua,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(Html(render_error(e, &jar, &data, &None).await));
|
return Err(Html(render_error(e, &jar, &data, &None).await));
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) ->
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let list = match data.0.get_stacks_by_owner(user.id).await {
|
let list = match data.0.get_stacks_by_user(user.id).await {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
};
|
};
|
||||||
|
@ -63,6 +63,7 @@ pub async fn feed_request(
|
||||||
|
|
||||||
if stack.privacy == StackPrivacy::Private
|
if stack.privacy == StackPrivacy::Private
|
||||||
&& user.id != stack.owner
|
&& user.id != stack.owner
|
||||||
|
&& !(stack.mode == StackMode::Circle && stack.users.contains(&user.id))
|
||||||
&& !user.permissions.check(FinePermission::MANAGE_STACKS)
|
&& !user.permissions.check(FinePermission::MANAGE_STACKS)
|
||||||
{
|
{
|
||||||
return Err(Html(
|
return Err(Html(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -11,7 +11,7 @@ tetratto-shared = { path = "../shared" }
|
||||||
tetratto-l10n = { path = "../l10n" }
|
tetratto-l10n = { path = "../l10n" }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] }
|
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] }
|
||||||
reqwest = { version = "0.12.19", features = ["json"] }
|
reqwest = { version = "0.12.20", features = ["json"] }
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.1"
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
md-5 = "0.10.6"
|
md-5 = "0.10.6"
|
||||||
|
|
|
@ -95,7 +95,7 @@ impl DataManager {
|
||||||
let owner = self.get_user_by_id(data.owner).await?;
|
let owner = self.get_user_by_id(data.owner).await?;
|
||||||
|
|
||||||
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||||
let stacks = self.get_stacks_by_owner(data.owner).await?;
|
let stacks = self.get_stacks_by_user(data.owner).await?;
|
||||||
|
|
||||||
if stacks.len() >= Self::MAXIMUM_FREE_DRAFTS {
|
if stacks.len() >= Self::MAXIMUM_FREE_DRAFTS {
|
||||||
return Err(Error::MiscError(
|
return Err(Error::MiscError(
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::model::auth::Notification;
|
||||||
use crate::model::communities::{Poll, Question};
|
use crate::model::communities::{Poll, Question};
|
||||||
use crate::model::communities_permissions::CommunityPermission;
|
use crate::model::communities_permissions::CommunityPermission;
|
||||||
use crate::model::moderation::AuditLogEntry;
|
use crate::model::moderation::AuditLogEntry;
|
||||||
use crate::model::stacks::StackSort;
|
use crate::model::stacks::{StackMode, StackSort, UserStack};
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
Error, Result,
|
Error, Result,
|
||||||
auth::User,
|
auth::User,
|
||||||
|
@ -25,6 +25,7 @@ pub type FullPost = (
|
||||||
Option<(User, Post)>,
|
Option<(User, Post)>,
|
||||||
Option<(Question, User)>,
|
Option<(Question, User)>,
|
||||||
Option<(Poll, bool, bool)>,
|
Option<(Poll, bool, bool)>,
|
||||||
|
Option<UserStack>,
|
||||||
);
|
);
|
||||||
|
|
||||||
macro_rules! private_post_replying {
|
macro_rules! private_post_replying {
|
||||||
|
@ -114,7 +115,7 @@ impl DataManager {
|
||||||
poll_id: get!(x->13(i64)) as usize,
|
poll_id: get!(x->13(i64)) as usize,
|
||||||
title: get!(x->14(String)),
|
title: get!(x->14(String)),
|
||||||
is_open: get!(x->15(i32)) as i8 == 1,
|
is_open: get!(x->15(i32)) as i8 == 1,
|
||||||
circle: get!(x->16(i64)) as usize,
|
stack: get!(x->16(i64)) as usize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,6 +276,39 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the stack of the given post (if some).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `(can view post, stack)`
|
||||||
|
pub async fn get_post_stack(
|
||||||
|
&self,
|
||||||
|
seen_stacks: &mut HashMap<usize, UserStack>,
|
||||||
|
post: &Post,
|
||||||
|
as_user_id: usize,
|
||||||
|
) -> (bool, Option<UserStack>) {
|
||||||
|
if post.stack != 0 {
|
||||||
|
if let Some(s) = seen_stacks.get(&post.stack) {
|
||||||
|
(
|
||||||
|
(s.owner == as_user_id) | s.users.contains(&as_user_id),
|
||||||
|
Some(s.to_owned()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let s = match self.get_stack_by_id(post.stack).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return (true, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
seen_stacks.insert(s.id, s.to_owned());
|
||||||
|
(
|
||||||
|
(s.owner == as_user_id) | s.users.contains(&as_user_id),
|
||||||
|
Some(s.to_owned()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(true, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Complete a vector of just posts with their owner as well.
|
/// Complete a vector of just posts with their owner as well.
|
||||||
pub async fn fill_posts(
|
pub async fn fill_posts(
|
||||||
&self,
|
&self,
|
||||||
|
@ -288,12 +322,14 @@ impl DataManager {
|
||||||
Option<(User, Post)>,
|
Option<(User, Post)>,
|
||||||
Option<(Question, User)>,
|
Option<(Question, User)>,
|
||||||
Option<(Poll, bool, bool)>,
|
Option<(Poll, bool, bool)>,
|
||||||
|
Option<UserStack>,
|
||||||
)>,
|
)>,
|
||||||
> {
|
> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
let mut users: HashMap<usize, User> = HashMap::new();
|
let mut users: HashMap<usize, User> = HashMap::new();
|
||||||
let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
|
let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
|
||||||
|
let mut seen_stacks: HashMap<usize, UserStack> = HashMap::new();
|
||||||
let mut replying_posts: HashMap<usize, Post> = HashMap::new();
|
let mut replying_posts: HashMap<usize, Post> = HashMap::new();
|
||||||
|
|
||||||
for post in posts {
|
for post in posts {
|
||||||
|
@ -304,12 +340,25 @@ impl DataManager {
|
||||||
let owner = post.owner;
|
let owner = post.owner;
|
||||||
|
|
||||||
if let Some(ua) = users.get(&owner) {
|
if let Some(ua) = users.get(&owner) {
|
||||||
|
let (can_view, stack) = self
|
||||||
|
.get_post_stack(
|
||||||
|
&mut seen_stacks,
|
||||||
|
&post,
|
||||||
|
if let Some(ua) = user { ua.id } else { 0 },
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if !can_view {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
out.push((
|
out.push((
|
||||||
post.clone(),
|
post.clone(),
|
||||||
ua.clone(),
|
ua.clone(),
|
||||||
self.get_post_reposting(&post, ignore_users, user).await,
|
self.get_post_reposting(&post, ignore_users, user).await,
|
||||||
self.get_post_question(&post, ignore_users).await?,
|
self.get_post_question(&post, ignore_users).await?,
|
||||||
self.get_post_poll(&post, user).await?,
|
self.get_post_poll(&post, user).await?,
|
||||||
|
stack,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
let ua = self.get_user_by_id(owner).await?;
|
let ua = self.get_user_by_id(owner).await?;
|
||||||
|
@ -357,6 +406,18 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (can_view, stack) = self
|
||||||
|
.get_post_stack(
|
||||||
|
&mut seen_stacks,
|
||||||
|
&post,
|
||||||
|
if let Some(ua) = user { ua.id } else { 0 },
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if !can_view {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
users.insert(owner, ua.clone());
|
users.insert(owner, ua.clone());
|
||||||
out.push((
|
out.push((
|
||||||
|
@ -365,6 +426,7 @@ impl DataManager {
|
||||||
self.get_post_reposting(&post, ignore_users, user).await,
|
self.get_post_reposting(&post, ignore_users, user).await,
|
||||||
self.get_post_question(&post, ignore_users).await?,
|
self.get_post_question(&post, ignore_users).await?,
|
||||||
self.get_post_poll(&post, user).await?,
|
self.get_post_poll(&post, user).await?,
|
||||||
|
stack,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,6 +446,7 @@ impl DataManager {
|
||||||
|
|
||||||
let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new();
|
let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new();
|
||||||
let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
|
let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
|
||||||
|
let mut seen_stacks: HashMap<usize, UserStack> = HashMap::new();
|
||||||
let mut replying_posts: HashMap<usize, Post> = HashMap::new();
|
let mut replying_posts: HashMap<usize, Post> = HashMap::new();
|
||||||
|
|
||||||
for post in posts {
|
for post in posts {
|
||||||
|
@ -395,6 +458,18 @@ impl DataManager {
|
||||||
let community = post.community;
|
let community = post.community;
|
||||||
|
|
||||||
if let Some((ua, community)) = seen_before.get(&(owner, community)) {
|
if let Some((ua, community)) = seen_before.get(&(owner, community)) {
|
||||||
|
let (can_view, stack) = self
|
||||||
|
.get_post_stack(
|
||||||
|
&mut seen_stacks,
|
||||||
|
&post,
|
||||||
|
if let Some(ua) = user { ua.id } else { 0 },
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if !can_view {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
out.push((
|
out.push((
|
||||||
post.clone(),
|
post.clone(),
|
||||||
ua.clone(),
|
ua.clone(),
|
||||||
|
@ -402,6 +477,7 @@ impl DataManager {
|
||||||
self.get_post_reposting(&post, ignore_users, user).await,
|
self.get_post_reposting(&post, ignore_users, user).await,
|
||||||
self.get_post_question(&post, ignore_users).await?,
|
self.get_post_question(&post, ignore_users).await?,
|
||||||
self.get_post_poll(&post, user).await?,
|
self.get_post_poll(&post, user).await?,
|
||||||
|
stack,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
let ua = self.get_user_by_id(owner).await?;
|
let ua = self.get_user_by_id(owner).await?;
|
||||||
|
@ -440,6 +516,18 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (can_view, stack) = self
|
||||||
|
.get_post_stack(
|
||||||
|
&mut seen_stacks,
|
||||||
|
&post,
|
||||||
|
if let Some(ua) = user { ua.id } else { 0 },
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if !can_view {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
let community = self.get_community_by_id(community).await?;
|
let community = self.get_community_by_id(community).await?;
|
||||||
seen_before.insert((owner, community.id), (ua.clone(), community.clone()));
|
seen_before.insert((owner, community.id), (ua.clone(), community.clone()));
|
||||||
|
@ -450,6 +538,7 @@ impl DataManager {
|
||||||
self.get_post_reposting(&post, ignore_users, user).await,
|
self.get_post_reposting(&post, ignore_users, user).await,
|
||||||
self.get_post_question(&post, ignore_users).await?,
|
self.get_post_question(&post, ignore_users).await?,
|
||||||
self.get_post_poll(&post, user).await?,
|
self.get_post_poll(&post, user).await?,
|
||||||
|
stack,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -933,6 +1022,37 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
Ok(res.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all posts from the given stack (from most recent).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `id` - the ID of the stack the requested posts belong to
|
||||||
|
/// * `batch` - the limit of posts in each page
|
||||||
|
/// * `page` - the page number
|
||||||
|
pub async fn get_posts_by_stack(
|
||||||
|
&self,
|
||||||
|
id: usize,
|
||||||
|
batch: usize,
|
||||||
|
page: usize,
|
||||||
|
) -> Result<Vec<Post>> {
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = query_rows!(
|
||||||
|
&conn,
|
||||||
|
"SELECT * FROM posts WHERE stack = $1 AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
|
||||||
|
&[&(id as i64), &(batch as i64), &((page * batch) as i64)],
|
||||||
|
|x| { Self::get_post_from_row(x) }
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(Error::GeneralNotFound("post".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all pinned posts from the given community (from most recent).
|
/// Get all pinned posts from the given community (from most recent).
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -1370,7 +1490,30 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let community = self.get_community_by_id(data.community).await?;
|
// check stack
|
||||||
|
if data.stack != 0 {
|
||||||
|
let stack = self.get_stack_by_id(data.stack).await?;
|
||||||
|
|
||||||
|
if stack.mode != StackMode::Circle {
|
||||||
|
return Err(Error::MiscError(
|
||||||
|
"You must use a \"Circle\" stack for this".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if stack.owner != data.owner && !stack.users.contains(&data.owner) {
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let community = if data.stack != 0 {
|
||||||
|
// if we're posting to a stack, the community should always be the town square
|
||||||
|
data.community = self.0.0.town_square;
|
||||||
|
self.get_community_by_id(self.0.0.town_square).await?
|
||||||
|
} else {
|
||||||
|
// otherwise, load whatever community the post is requesting
|
||||||
|
self.get_community_by_id(data.community).await?
|
||||||
|
};
|
||||||
|
|
||||||
// check values (if this isn't reposting something else)
|
// check values (if this isn't reposting something else)
|
||||||
let is_reposting = if let Some(ref repost) = data.context.repost {
|
let is_reposting = if let Some(ref repost) = data.context.repost {
|
||||||
|
@ -1466,6 +1609,10 @@ impl DataManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref rt) = reposting {
|
if let Some(ref rt) = reposting {
|
||||||
|
if rt.stack != data.stack && rt.stack != 0 {
|
||||||
|
return Err(Error::MiscError("Cannot repost out of stack".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
if data.content.is_empty() {
|
if data.content.is_empty() {
|
||||||
// reposting but NOT quoting... we shouldn't be able to repost a direct repost
|
// reposting but NOT quoting... we shouldn't be able to repost a direct repost
|
||||||
data.context.reposts_enabled = false;
|
data.context.reposts_enabled = false;
|
||||||
|
@ -1507,7 +1654,7 @@ impl DataManager {
|
||||||
|
|
||||||
// send notification
|
// send notification
|
||||||
// this would look better if rustfmt didn't give up on this line
|
// this would look better if rustfmt didn't give up on this line
|
||||||
if owner.id != rt.owner && !owner.settings.private_profile {
|
if owner.id != rt.owner && !owner.settings.private_profile && data.stack == 0 {
|
||||||
self.create_notification(
|
self.create_notification(
|
||||||
Notification::new(
|
Notification::new(
|
||||||
format!(
|
format!(
|
||||||
|
@ -1631,7 +1778,7 @@ impl DataManager {
|
||||||
&(data.poll_id as i64),
|
&(data.poll_id as i64),
|
||||||
&data.title,
|
&data.title,
|
||||||
&{ if data.is_open { 1 } else { 0 } },
|
&{ if data.is_open { 1 } else { 0 } },
|
||||||
&(data.circle as i64),
|
&(data.stack as i64),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1781,7 +1928,7 @@ impl DataManager {
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&conn,
|
||||||
"UPDATE posts SET is_deleted = $1 WHERE id = $2",
|
"UPDATE posts SET is_deleted = $1 WHERE id = $2",
|
||||||
params![if is_deleted { 1 } else { 0 }, &(id as i64)]
|
params![&if is_deleted { 1 } else { 0 }, &(id as i64)]
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
|
@ -1793,8 +1940,10 @@ impl DataManager {
|
||||||
if is_deleted {
|
if is_deleted {
|
||||||
// decr parent comment count
|
// decr parent comment count
|
||||||
if let Some(replying_to) = y.replying_to {
|
if let Some(replying_to) = y.replying_to {
|
||||||
|
if replying_to != 0 {
|
||||||
self.decr_post_comments(replying_to).await.unwrap();
|
self.decr_post_comments(replying_to).await.unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// decr user post count
|
// decr user post count
|
||||||
let owner = self.get_user_by_id(y.owner).await?;
|
let owner = self.get_user_by_id(y.owner).await?;
|
||||||
|
@ -1893,7 +2042,7 @@ impl DataManager {
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&conn,
|
||||||
"UPDATE posts SET is_open = $1 WHERE id = $2",
|
"UPDATE posts SET is_open = $1 WHERE id = $2",
|
||||||
params![if is_open { 1 } else { 0 }, &(id as i64)]
|
params![&if is_open { 1 } else { 0 }, &(id as i64)]
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
|
@ -2091,5 +2240,5 @@ impl DataManager {
|
||||||
auto_method!(decr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
|
auto_method!(decr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
|
||||||
|
|
||||||
auto_method!(incr_post_comments() -> "UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
|
auto_method!(incr_post_comments() -> "UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
|
||||||
auto_method!(decr_post_comments() -> "UPDATE posts SET comment_count = comment_count - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
|
auto_method!(decr_post_comments()@get_post_by_id -> "UPDATE posts SET comment_count = comment_count - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr=comment_count);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use oiseau::cache::Cache;
|
use oiseau::cache::Cache;
|
||||||
use crate::model::{
|
use crate::{
|
||||||
Error, Result,
|
database::posts::FullPost,
|
||||||
|
model::{
|
||||||
auth::User,
|
auth::User,
|
||||||
permissions::FinePermission,
|
permissions::FinePermission,
|
||||||
stacks::{StackPrivacy, UserStack, StackMode, StackSort},
|
stacks::{StackMode, StackPrivacy, StackSort, UserStack},
|
||||||
communities::{Community, Poll, Post, Question},
|
Error, Result,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use crate::{auto_method, DataManager};
|
use crate::{auto_method, DataManager};
|
||||||
|
|
||||||
|
@ -37,16 +39,7 @@ impl DataManager {
|
||||||
page: usize,
|
page: usize,
|
||||||
ignore_users: &Vec<usize>,
|
ignore_users: &Vec<usize>,
|
||||||
user: &Option<User>,
|
user: &Option<User>,
|
||||||
) -> Result<
|
) -> Result<Vec<FullPost>> {
|
||||||
Vec<(
|
|
||||||
Post,
|
|
||||||
User,
|
|
||||||
Community,
|
|
||||||
Option<(User, Post)>,
|
|
||||||
Option<(Question, User)>,
|
|
||||||
Option<(Poll, bool, bool)>,
|
|
||||||
)>,
|
|
||||||
> {
|
|
||||||
let stack = self.get_stack_by_id(id).await?;
|
let stack = self.get_stack_by_id(id).await?;
|
||||||
|
|
||||||
Ok(match stack.mode {
|
Ok(match stack.mode {
|
||||||
|
@ -89,6 +82,19 @@ impl DataManager {
|
||||||
"You should use `get_stack_users` for this type".to_string(),
|
"You should use `get_stack_users` for this type".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
StackMode::Circle => {
|
||||||
|
if !stack.users.contains(&as_user_id) && as_user_id != stack.owner {
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fill_posts_with_community(
|
||||||
|
self.get_posts_by_stack(stack.id, batch, page).await?,
|
||||||
|
as_user_id,
|
||||||
|
&ignore_users,
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,9 +125,11 @@ impl DataManager {
|
||||||
|
|
||||||
/// Get all stacks by user.
|
/// Get all stacks by user.
|
||||||
///
|
///
|
||||||
|
/// Also pulls stacks that are of "Circle" type AND the user is added to the `users` list.
|
||||||
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `id` - the ID of the user to fetch stacks for
|
/// * `id` - the ID of the user to fetch stacks for
|
||||||
pub async fn get_stacks_by_owner(&self, id: usize) -> Result<Vec<UserStack>> {
|
pub async fn get_stacks_by_user(&self, id: usize) -> Result<Vec<UserStack>> {
|
||||||
let conn = match self.0.connect().await {
|
let conn = match self.0.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())),
|
||||||
|
@ -129,8 +137,8 @@ impl DataManager {
|
||||||
|
|
||||||
let res = query_rows!(
|
let res = query_rows!(
|
||||||
&conn,
|
&conn,
|
||||||
"SELECT * FROM stacks WHERE owner = $1 ORDER BY name ASC",
|
"SELECT * FROM stacks WHERE owner = $1 OR (mode = '\"Circle\"' AND users LIKE $2) ORDER BY name ASC",
|
||||||
&[&(id as i64)],
|
&[&(id as i64), &format!("%{id}%")],
|
||||||
|x| { Self::get_stack_from_row(x) }
|
|x| { Self::get_stack_from_row(x) }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -142,6 +150,7 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAXIMUM_FREE_STACKS: usize = 5;
|
const MAXIMUM_FREE_STACKS: usize = 5;
|
||||||
|
pub const MAXIMUM_FREE_STACK_USERS: usize = 50;
|
||||||
|
|
||||||
/// Create a new stack in the database.
|
/// Create a new stack in the database.
|
||||||
///
|
///
|
||||||
|
@ -159,7 +168,7 @@ impl DataManager {
|
||||||
let owner = self.get_user_by_id(data.owner).await?;
|
let owner = self.get_user_by_id(data.owner).await?;
|
||||||
|
|
||||||
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||||
let stacks = self.get_stacks_by_owner(data.owner).await?;
|
let stacks = self.get_stacks_by_user(data.owner).await?;
|
||||||
|
|
||||||
if stacks.len() >= Self::MAXIMUM_FREE_STACKS {
|
if stacks.len() >= Self::MAXIMUM_FREE_STACKS {
|
||||||
return Err(Error::MiscError(
|
return Err(Error::MiscError(
|
||||||
|
@ -216,6 +225,25 @@ impl DataManager {
|
||||||
return Err(Error::DatabaseError(e.to_string()));
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete stackblocks
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"DELETE FROM stackblocks WHERE stack = $1",
|
||||||
|
&[&(id as i64)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete posts
|
||||||
|
let res = execute!(&conn, "DELETE FROM posts WHERE stack = $1", &[&(id as i64)]);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
self.0.1.remove(format!("atto.stack:{}", id)).await;
|
self.0.1.remove(format!("atto.stack:{}", id)).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -260,10 +260,10 @@ pub struct Post {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// If the post is "open". Posts can act as tickets in a forge community.
|
/// If the post is "open". Posts can act as tickets in a forge community.
|
||||||
pub is_open: bool,
|
pub is_open: bool,
|
||||||
/// The ID of the circle this post belongs to. 0 means no circle is connected.
|
/// The ID of the stack this post belongs to. 0 means no stack is connected.
|
||||||
///
|
///
|
||||||
/// If circle is not 0, community should be 0 (and vice versa).
|
/// If stack is not 0, community should be 0 (and vice versa).
|
||||||
pub circle: usize,
|
pub stack: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Post {
|
impl Post {
|
||||||
|
@ -291,7 +291,7 @@ impl Post {
|
||||||
poll_id,
|
poll_id,
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
is_open: true,
|
is_open: true,
|
||||||
circle: 0,
|
stack: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,8 +76,6 @@ pub enum AppScope {
|
||||||
UserCreateCommunities,
|
UserCreateCommunities,
|
||||||
/// Create stacks on behalf of the user.
|
/// Create stacks on behalf of the user.
|
||||||
UserCreateStacks,
|
UserCreateStacks,
|
||||||
/// Create circles on behalf of the user.
|
|
||||||
UserCreateCircles,
|
|
||||||
/// Delete posts owned by the user.
|
/// Delete posts owned by the user.
|
||||||
UserDeletePosts,
|
UserDeletePosts,
|
||||||
/// Delete messages owned by the user.
|
/// Delete messages owned by the user.
|
||||||
|
@ -108,8 +106,6 @@ pub enum AppScope {
|
||||||
UserManageRequests,
|
UserManageRequests,
|
||||||
/// Manage the user's uploads.
|
/// Manage the user's uploads.
|
||||||
UserManageUploads,
|
UserManageUploads,
|
||||||
/// Manage the user's circles (add/remove users or delete).
|
|
||||||
UserManageCircles,
|
|
||||||
/// Edit posts created by the user.
|
/// Edit posts created by the user.
|
||||||
UserEditPosts,
|
UserEditPosts,
|
||||||
/// Edit drafts created by the user.
|
/// Edit drafts created by the user.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum StackPrivacy {
|
pub enum StackPrivacy {
|
||||||
/// Can be viewed by anyone.
|
/// Can be viewed by anyone.
|
||||||
Public,
|
Public,
|
||||||
|
@ -15,7 +15,7 @@ impl Default for StackPrivacy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum StackMode {
|
pub enum StackMode {
|
||||||
/// `users` vec contains ID of users to INCLUDE into the timeline;
|
/// `users` vec contains ID of users to INCLUDE into the timeline;
|
||||||
/// every other user is excluded
|
/// every other user is excluded
|
||||||
|
@ -28,6 +28,8 @@ pub enum StackMode {
|
||||||
///
|
///
|
||||||
/// Other users can block the entire list (creating a `StackBlock`, not a `UserBlock`).
|
/// Other users can block the entire list (creating a `StackBlock`, not a `UserBlock`).
|
||||||
BlockList,
|
BlockList,
|
||||||
|
/// `users` vec contains ID of users who are allowed to view posts posted to the stack.
|
||||||
|
Circle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StackMode {
|
impl Default for StackMode {
|
||||||
|
@ -36,7 +38,7 @@ impl Default for StackMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum StackSort {
|
pub enum StackSort {
|
||||||
Created,
|
Created,
|
||||||
Likes,
|
Likes,
|
||||||
|
@ -48,7 +50,7 @@ impl Default for StackSort {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct UserStack {
|
pub struct UserStack {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub created: usize,
|
pub created: usize,
|
||||||
|
@ -76,7 +78,7 @@ impl UserStack {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct StackBlock {
|
pub struct StackBlock {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub created: usize,
|
pub created: usize,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
ALTER TABLE posts
|
ALTER TABLE posts
|
||||||
ADD COLUMN circle BIGINT NOT NULL DEFAULT 0;
|
ADD COLUMN circle BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE posts
|
||||||
|
ADD COLUMN stack BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue