add: custom emojis
fix: don't show reposts of posts from blocked users fix: don't show questions when they're from users you've blocked
This commit is contained in:
parent
9f187039e6
commit
275dd0a1eb
25 changed files with 697 additions and 61 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -3599,7 +3599,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "2.3.0"
|
version = "3.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"async-stripe",
|
"async-stripe",
|
||||||
|
@ -3629,7 +3629,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "2.3.0"
|
version = "3.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
@ -3653,7 +3653,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "2.3.0"
|
version = "3.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -3662,7 +3662,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "2.3.0"
|
version = "3.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "2.3.0"
|
version = "3.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -112,6 +112,9 @@ version = "1.0.0"
|
||||||
"communities:action.create_channel" = "Create channel"
|
"communities:action.create_channel" = "Create channel"
|
||||||
"communities:label.chats" = "Chats"
|
"communities:label.chats" = "Chats"
|
||||||
"communities:label.show_community" = "Show community"
|
"communities:label.show_community" = "Show community"
|
||||||
|
"communities:tab.emojis" = "Emojis"
|
||||||
|
"communities:label.upload" = "Upload"
|
||||||
|
"communities:label.file" = "File"
|
||||||
|
|
||||||
"notifs:action.mark_as_read" = "Mark as read"
|
"notifs:action.mark_as_read" = "Mark as read"
|
||||||
"notifs:action.mark_as_unread" = "Mark as unread"
|
"notifs:action.mark_as_unread" = "Mark as unread"
|
||||||
|
|
|
@ -311,6 +311,12 @@ img.contain {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img.emoji {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* avatar/banner */
|
/* avatar/banner */
|
||||||
.avatar {
|
.avatar {
|
||||||
--size: 50px;
|
--size: 50px;
|
||||||
|
|
|
@ -327,7 +327,7 @@ hide_user_menu=true) }}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.grouped {
|
.message.grouped {
|
||||||
padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 39px);
|
padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 31px);
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.sidebars_shown) .sidebar {
|
body:not(.sidebars_shown) .sidebar {
|
||||||
|
|
|
@ -23,6 +23,11 @@
|
||||||
{{ icon "rss" }}
|
{{ icon "rss" }}
|
||||||
<span>{{ text "communities:tab.channels" }}</span>
|
<span>{{ text "communities:tab.channels" }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %} {% if can_manage_emojis %}
|
||||||
|
<a href="#/emojis" data-tab-button="emojis">
|
||||||
|
{{ icon "smile" }}
|
||||||
|
<span>{{ text "communities:tab.emojis" }}</span>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -436,6 +441,167 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{% endif %} {% if can_manage_emojis %}
|
||||||
|
<div
|
||||||
|
class="card tertiary w-full hidden flex flex-col gap-2"
|
||||||
|
data-tab="emojis"
|
||||||
|
>
|
||||||
|
{{ components::supporter_ad(body="Become a supporter to upload GIF
|
||||||
|
animated emojis!") }}
|
||||||
|
|
||||||
|
<div class="card-nest" ui_ident="change_banner">
|
||||||
|
<div class="card small flex items-center gap-2">
|
||||||
|
{{ icon "upload" }}
|
||||||
|
<b>{{ text "communities:label.upload" }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="card flex flex-col gap-2"
|
||||||
|
onsubmit="upload_emoji(event)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="name"
|
||||||
|
>{{ text "communities:label.name" }}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
placeholder="name"
|
||||||
|
required
|
||||||
|
minlength="2"
|
||||||
|
maxlength="32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="file"
|
||||||
|
>{{ text "communities:label.file" }}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="banner_file"
|
||||||
|
name="file"
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg,image/avif,image/webp"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button>{{ text "communities:action.create" }}</button>
|
||||||
|
|
||||||
|
<span class="fade"
|
||||||
|
>Emojis can be a maximum of 256 KiB, or 512x512px (width x
|
||||||
|
height).</span
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for emoji in emojis %}
|
||||||
|
<div
|
||||||
|
class="card secondary flex flex-wrap gap-2 items-center justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<img
|
||||||
|
src="/api/v1/communities/{{ community.id }}/emojis/{{ emoji.name }}"
|
||||||
|
alt="{{ emoji.name }}"
|
||||||
|
class="emoji"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<b>{{ emoji.name }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="quaternary small"
|
||||||
|
onclick="rename_emoji('{{ emoji.id }}')"
|
||||||
|
>
|
||||||
|
{{ icon "pencil" }}
|
||||||
|
<span>{{ text "chats:action.rename" }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="quaternary small red"
|
||||||
|
onclick="remove_emoji('{{ emoji.id }}')"
|
||||||
|
>
|
||||||
|
{{ icon "x" }}
|
||||||
|
<span>{{ text "stacks:label.remove" }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
globalThis.upload_emoji = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.querySelector("button").style.display = "none";
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
`/api/v1/communities/{{ community.id }}/emojis/${e.target.name.value}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: e.target.file.files[0],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
trigger("atto::toast", [
|
||||||
|
res.ok ? "success" : "error",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
e.target.querySelector("button").removeAttribute("style");
|
||||||
|
});
|
||||||
|
|
||||||
|
alert("Emoji upload in progress. Please wait!");
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.rename_emoji = async (id) => {
|
||||||
|
const name = await trigger("atto::prompt", ["New emoji name:"]);
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/v1/emojis_id/${id}/name`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
trigger("atto::toast", [
|
||||||
|
res.ok ? "success" : "error",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.remove_emoji = async (id) => {
|
||||||
|
if (
|
||||||
|
!(await trigger("atto::confirm", [
|
||||||
|
"Are you sure you would like to do this? This action is permanent.",
|
||||||
|
]))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/v1/emojis_id/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
trigger("atto::toast", [
|
||||||
|
res.ok ? "success" : "error",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
@ -179,6 +179,9 @@ and show_community and community.id != config.town_square or question %}
|
||||||
<div
|
<div
|
||||||
class="card flex flex-col gap-2 {% if secondary %}secondary{% endif %}"
|
class="card flex flex-col gap-2 {% if secondary %}secondary{% endif %}"
|
||||||
id="post:{{ post.id }}"
|
id="post:{{ post.id }}"
|
||||||
|
data-community="{{ post.community }}"
|
||||||
|
data-ownsup="{{ owner.permissions|has_supporter }}"
|
||||||
|
hook="verify_emojis"
|
||||||
>
|
>
|
||||||
<div class="w-full flex gap-2">
|
<div class="w-full flex gap-2">
|
||||||
<a href="/@{{ owner.username }}">
|
<a href="/@{{ owner.username }}">
|
||||||
|
@ -1202,6 +1205,17 @@ show_kick=false, secondary=false) -%}
|
||||||
></emoji-picker>
|
></emoji-picker>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
setTimeout(async () => {
|
||||||
|
document.querySelector("emoji-picker").customEmoji =
|
||||||
|
await trigger("me::emojis");
|
||||||
|
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = `.custom-emoji { border-radius: 4px !important; } .category { font-weight: 600; }`;
|
||||||
|
document
|
||||||
|
.querySelector("emoji-picker")
|
||||||
|
.shadowRoot.appendChild(style);
|
||||||
|
}, 150);
|
||||||
|
|
||||||
document
|
document
|
||||||
.querySelector("emoji-picker")
|
.querySelector("emoji-picker")
|
||||||
.addEventListener("emoji-click", async (event) => {
|
.addEventListener("emoji-click", async (event) => {
|
||||||
|
@ -1214,14 +1228,20 @@ show_kick=false, secondary=false) -%}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById(
|
if (event.detail.unicode) {
|
||||||
window.EMOJI_PICKER_TEXT_ID,
|
document.getElementById(
|
||||||
).value += ` :${await (
|
window.EMOJI_PICKER_TEXT_ID,
|
||||||
await fetch("/api/v1/lookup_emoji", {
|
).value += ` :${await (
|
||||||
method: "POST",
|
await fetch("/api/v1/lookup_emoji", {
|
||||||
body: event.detail.unicode,
|
method: "POST",
|
||||||
})
|
body: event.detail.unicode,
|
||||||
).text()}:`;
|
})
|
||||||
|
).text()}:`;
|
||||||
|
} else {
|
||||||
|
document.getElementById(
|
||||||
|
window.EMOJI_PICKER_TEXT_ID,
|
||||||
|
).value += ` :${event.detail.emoji.shortcodes[0]}:`;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("emoji_dialog").close();
|
document.getElementById("emoji_dialog").close();
|
||||||
});
|
});
|
||||||
|
|
|
@ -71,6 +71,10 @@
|
||||||
class="card flex flex-col items-center gap-2"
|
class="card flex flex-col items-center gap-2"
|
||||||
id="social"
|
id="social"
|
||||||
>
|
>
|
||||||
|
{% if profile.settings.status %}
|
||||||
|
<p>{{ profile.settings.status }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="w-full flex">
|
<div class="w-full flex">
|
||||||
<a
|
<a
|
||||||
href="/@{{ profile.username }}/followers"
|
href="/@{{ profile.username }}/followers"
|
||||||
|
|
|
@ -112,6 +112,7 @@ macros -%}
|
||||||
atto["hooks::check_reactions"]();
|
atto["hooks::check_reactions"]();
|
||||||
atto["hooks::tabs"]();
|
atto["hooks::tabs"]();
|
||||||
atto["hooks::spotify_time_text"](); // spotify durations
|
atto["hooks::spotify_time_text"](); // spotify durations
|
||||||
|
atto["hooks::verify_emoji"]();
|
||||||
|
|
||||||
if (document.getElementById("tokens")) {
|
if (document.getElementById("tokens")) {
|
||||||
trigger("me::render_token_picker", [
|
trigger("me::render_token_picker", [
|
||||||
|
|
|
@ -310,7 +310,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`/api/v1/stacks/{{ stack.id }}`, {
|
fetch("/api/v1/stacks/{{ stack.id }}", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
|
|
|
@ -215,6 +215,28 @@ media_theme_pref();
|
||||||
});
|
});
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
|
self.define("hooks::verify_emoji", (_) => {
|
||||||
|
for (const post of document.querySelectorAll("[hook=verify_emojis]")) {
|
||||||
|
const community_id = post.getAttribute("data-community");
|
||||||
|
const owner_is_supporter =
|
||||||
|
post.getAttribute("data-ownsup") === "true";
|
||||||
|
|
||||||
|
if (owner_is_supporter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not supporter; check emojis
|
||||||
|
for (const img of post.querySelectorAll(".emoji")) {
|
||||||
|
if (
|
||||||
|
img.src.split("/api/v1/communities/")[1].split("/")[0] !==
|
||||||
|
community_id
|
||||||
|
) {
|
||||||
|
img.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.define("hooks::scroll", (_, scroll_element, track_element) => {
|
self.define("hooks::scroll", (_, scroll_element, track_element) => {
|
||||||
const goals = [150, 250, 500, 1000];
|
const goals = [150, 250, 500, 1000];
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,9 @@
|
||||||
res.message,
|
res.message,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
delete self.LOGIN_ACCOUNT_TOKENS[res.payload];
|
||||||
|
self.set_login_account_tokens(self.LOGIN_ACCOUNT_TOKENS);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
@ -345,6 +348,26 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.define("emojis", async () => {
|
||||||
|
const payload = (await (await fetch("/api/v1/my_emojis")).json())
|
||||||
|
.payload;
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
for (const [community, [category, emojis]] of Object.entries(payload)) {
|
||||||
|
for (const emoji of emojis) {
|
||||||
|
out.push({
|
||||||
|
category,
|
||||||
|
name: emoji.name,
|
||||||
|
shortcodes: [`${community}.${emoji.name}`],
|
||||||
|
url: `/api/v1/communities/${community}/emojis/${emoji.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
|
||||||
// token switcher
|
// token switcher
|
||||||
self.define(
|
self.define(
|
||||||
"set_login_account_tokens",
|
"set_login_account_tokens",
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
|
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
|
||||||
use tetratto_core::model::{auth::Notification, permissions::FinePermission, ApiReturn, Error};
|
use tetratto_core::model::{
|
||||||
|
auth::{User, Notification},
|
||||||
|
moderation::AuditLogEntry,
|
||||||
|
permissions::FinePermission,
|
||||||
|
ApiReturn, Error,
|
||||||
|
};
|
||||||
use stripe::{EventObject, EventType};
|
use stripe::{EventObject, EventType};
|
||||||
use crate::State;
|
use crate::State;
|
||||||
|
|
||||||
|
@ -70,14 +75,50 @@ pub async fn stripe_webhook(
|
||||||
|
|
||||||
let customer_id = invoice.customer.unwrap().id();
|
let customer_id = invoice.customer.unwrap().id();
|
||||||
|
|
||||||
// allow 30s for everything to finalize
|
|
||||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
|
||||||
|
|
||||||
// pull user and update role
|
// pull user and update role
|
||||||
let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
|
let mut retries: usize = 0;
|
||||||
Ok(ua) => ua,
|
let mut user: Option<User> = None;
|
||||||
Err(e) => return Json(e.into()),
|
|
||||||
};
|
loop {
|
||||||
|
if retries >= 5 {
|
||||||
|
// we've already tried 5 times (10 seconds of waiting)... it's not
|
||||||
|
// going to happen
|
||||||
|
//
|
||||||
|
// we're going to report this error to the audit log so someone can
|
||||||
|
// check manually later
|
||||||
|
if let Err(e) = data
|
||||||
|
.create_audit_log_entry(AuditLogEntry::new(
|
||||||
|
0,
|
||||||
|
format!("invoice tier update failed: stripe {customer_id}"),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(Error::GeneralNotFound("user".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match data.get_user_by_stripe_id(customer_id.as_str()).await {
|
||||||
|
Ok(ua) => {
|
||||||
|
if !user.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
user = Some(ua);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
tracing::info!("checkout session not stored in db yet");
|
||||||
|
retries += 1;
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = user.unwrap();
|
||||||
|
tracing::info!("found subscription user in {retries} tries");
|
||||||
|
|
||||||
if user.permissions.check(FinePermission::SUPPORTER) {
|
if user.permissions.check(FinePermission::SUPPORTER) {
|
||||||
return Json(ApiReturn {
|
return Json(ApiReturn {
|
||||||
|
|
|
@ -216,7 +216,7 @@ pub async fn logout_request(
|
||||||
Json(ApiReturn {
|
Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "Goodbye!".to_string(),
|
message: "Goodbye!".to_string(),
|
||||||
payload: (),
|
payload: Some(user.username.clone()),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,19 @@
|
||||||
use axum::response::IntoResponse;
|
use std::fs::exists;
|
||||||
|
|
||||||
|
use image::ImageFormat;
|
||||||
|
use pathbufd::PathBufD;
|
||||||
|
use crate::{
|
||||||
|
get_user_from_token,
|
||||||
|
image::{save_buffer, Image},
|
||||||
|
routes::api::v1::{auth::images::read_image, UpdateEmojiName},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
use tetratto_core::model::{
|
||||||
|
uploads::{CustomEmoji, MediaType, MediaUpload},
|
||||||
|
ApiReturn, Error,
|
||||||
|
};
|
||||||
|
|
||||||
/// Expand a unicode emoji into its Gemoji shortcode.
|
/// Expand a unicode emoji into its Gemoji shortcode.
|
||||||
pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse {
|
pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse {
|
||||||
|
@ -10,3 +25,209 @@ pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse {
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_request(
|
||||||
|
Path((community, name)): Path<(usize, String)>,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
|
||||||
|
if community == 0 {
|
||||||
|
return Err((
|
||||||
|
[("Content-Type", "image/svg+xml")],
|
||||||
|
Body::from(read_image(PathBufD::current().extend(&[
|
||||||
|
data.0.dirs.media.as_str(),
|
||||||
|
"images",
|
||||||
|
"default-avatar.svg",
|
||||||
|
]))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let emoji = match data.get_emoji_by_community_name(community, &name).await {
|
||||||
|
Ok(ua) => ua,
|
||||||
|
Err(_) => {
|
||||||
|
return Err((
|
||||||
|
[("Content-Type", "image/svg+xml")],
|
||||||
|
Body::from(read_image(PathBufD::current().extend(&[
|
||||||
|
data.0.dirs.media.as_str(),
|
||||||
|
"images",
|
||||||
|
"default-avatar.svg",
|
||||||
|
]))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let upload = data
|
||||||
|
.get_upload_by_id(emoji.0.unwrap().upload_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let path = upload.path(&data.0);
|
||||||
|
|
||||||
|
if !exists(&path).unwrap() {
|
||||||
|
return Err((
|
||||||
|
[("Content-Type", "image/svg+xml")],
|
||||||
|
Body::from(read_image(PathBufD::current().extend(&[
|
||||||
|
data.0.dirs.media.as_str(),
|
||||||
|
"images",
|
||||||
|
"default-avatar.svg",
|
||||||
|
]))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
[("Content-Type", upload.what.mime())],
|
||||||
|
Body::from(read_image(path)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// maximum file dimensions: 512x512px (256KiB)
|
||||||
|
pub const MAXIUMUM_FILE_SIZE: usize = 262144;
|
||||||
|
|
||||||
|
pub async fn create_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path((community, name)): Path<(usize, String)>,
|
||||||
|
img: Image,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// check file size
|
||||||
|
if img.0.len() > MAXIUMUM_FILE_SIZE {
|
||||||
|
return Json(Error::DataTooLong("image".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure emoji doesn't already exist in community
|
||||||
|
if data
|
||||||
|
.get_emoji_by_community_name(community, &name)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Json(
|
||||||
|
Error::MiscError("This emoji name is already in use in this community".to_string())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create upload
|
||||||
|
let upload = match data
|
||||||
|
.create_upload(MediaUpload::new(
|
||||||
|
if img.1 == "image/gif" {
|
||||||
|
MediaType::Gif
|
||||||
|
} else {
|
||||||
|
MediaType::Webp
|
||||||
|
},
|
||||||
|
user.id,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// create emoji
|
||||||
|
let is_animated = img.1 == "image/gif";
|
||||||
|
match data
|
||||||
|
.create_emoji(CustomEmoji::new(
|
||||||
|
user.id,
|
||||||
|
community,
|
||||||
|
upload.id,
|
||||||
|
name,
|
||||||
|
is_animated,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
if let Err(e) = save_buffer(
|
||||||
|
&upload.path(&data.0).to_string(),
|
||||||
|
img.0.to_vec(),
|
||||||
|
if is_animated {
|
||||||
|
ImageFormat::Gif
|
||||||
|
} else {
|
||||||
|
ImageFormat::WebP
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return Json(Error::MiscError(e.to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Emoji created".to_string(),
|
||||||
|
payload: (),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if let Err(e) = upload.remove(&data.0) {
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_name_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
Json(req): Json<UpdateEmojiName>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.update_emoji_name(id, user, &req.name).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Emoji updated".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.delete_emoji(id, &user).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Emoji deleted".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_my_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.get_user_emojis(user.id).await {
|
||||||
|
Ok(d) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: d.len().to_string(),
|
||||||
|
payload: Some(d),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -322,6 +322,23 @@ pub fn routes() -> Router {
|
||||||
"/lookup_emoji",
|
"/lookup_emoji",
|
||||||
post(communities::emojis::get_emoji_shortcode),
|
post(communities::emojis::get_emoji_shortcode),
|
||||||
)
|
)
|
||||||
|
.route("/my_emojis", get(communities::emojis::get_my_request))
|
||||||
|
.route(
|
||||||
|
"/communities/{id}/emojis/{name}",
|
||||||
|
get(communities::emojis::get_request),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/communities/{id}/emojis/{name}",
|
||||||
|
post(communities::emojis::create_request),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/emojis_id/{id}/name",
|
||||||
|
post(communities::emojis::update_name_request),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/emojis_id/{id}",
|
||||||
|
delete(communities::emojis::delete_request),
|
||||||
|
)
|
||||||
// stacks
|
// stacks
|
||||||
.route("/stacks", post(stacks::create_request))
|
.route("/stacks", post(stacks::create_request))
|
||||||
.route("/stacks/{id}/name", post(stacks::update_name_request))
|
.route("/stacks/{id}/name", post(stacks::update_name_request))
|
||||||
|
@ -547,3 +564,8 @@ pub struct UpdateStackSort {
|
||||||
pub struct AddOrRemoveStackUser {
|
pub struct AddOrRemoveStackUser {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateEmojiName {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
|
@ -533,6 +533,14 @@ pub async fn settings_request(
|
||||||
let can_manage_channels = membership.role.check(CommunityPermission::MANAGE_CHANNELS)
|
let can_manage_channels = membership.role.check(CommunityPermission::MANAGE_CHANNELS)
|
||||||
| user.permissions.check(FinePermission::MANAGE_CHANNELS);
|
| user.permissions.check(FinePermission::MANAGE_CHANNELS);
|
||||||
|
|
||||||
|
let emojis = match data.0.get_emojis_by_community(community.id).await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let can_manage_emojis = membership.role.check(CommunityPermission::MANAGE_EMOJIS)
|
||||||
|
| user.permissions.check(FinePermission::MANAGE_EMOJIS);
|
||||||
|
|
||||||
// init context
|
// 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, &Some(user)).await;
|
let mut context = initial_context(&data.0.0, lang, &Some(user)).await;
|
||||||
|
@ -546,6 +554,9 @@ pub async fn settings_request(
|
||||||
context.insert("can_manage_channels", &can_manage_channels);
|
context.insert("can_manage_channels", &can_manage_channels);
|
||||||
context.insert("channels", &channels);
|
context.insert("channels", &channels);
|
||||||
|
|
||||||
|
context.insert("can_manage_emojis", &can_manage_emojis);
|
||||||
|
context.insert("emojis", &emojis);
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
data.1
|
data.1
|
||||||
|
@ -574,11 +585,17 @@ pub async fn post_request(
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ignore_users = if let Some(ref ua) = user {
|
||||||
|
data.0.get_userblocks_receivers(ua.id).await
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
// check repost
|
// check repost
|
||||||
let reposting = data.0.get_post_reposting(&post).await;
|
let reposting = data.0.get_post_reposting(&post, &ignore_users).await;
|
||||||
|
|
||||||
// check question
|
// check question
|
||||||
let question = match data.0.get_post_question(&post).await {
|
let question = match data.0.get_post_question(&post, &ignore_users).await {
|
||||||
Ok(q) => q,
|
Ok(q) => q,
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
};
|
};
|
||||||
|
@ -681,11 +698,17 @@ pub async fn reposts_request(
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ignore_users = if let Some(ref ua) = user {
|
||||||
|
data.0.get_userblocks_receivers(ua.id).await
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
// check repost
|
// check repost
|
||||||
let reposting = data.0.get_post_reposting(&post).await;
|
let reposting = data.0.get_post_reposting(&post, &ignore_users).await;
|
||||||
|
|
||||||
// check question
|
// check question
|
||||||
let question = match data.0.get_post_question(&post).await {
|
let question = match data.0.get_post_question(&post, &ignore_users).await {
|
||||||
Ok(q) => q,
|
Ok(q) => q,
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "2.3.0"
|
version = "3.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::cache::Cache;
|
use crate::cache::Cache;
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
|
@ -55,6 +57,29 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
Ok(res.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all emojis by their community for the communities the given user is in.
|
||||||
|
pub async fn get_user_emojis(
|
||||||
|
&self,
|
||||||
|
id: usize,
|
||||||
|
) -> Result<HashMap<usize, (String, Vec<CustomEmoji>)>> {
|
||||||
|
let memberships = self.get_memberships_by_owner(id).await?;
|
||||||
|
let mut out = HashMap::new();
|
||||||
|
|
||||||
|
for membership in memberships {
|
||||||
|
let community = self.get_community_by_id(membership.community).await?;
|
||||||
|
|
||||||
|
out.insert(
|
||||||
|
community.id,
|
||||||
|
(
|
||||||
|
community.title.clone(),
|
||||||
|
self.get_emojis_by_community(community.id).await?,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get an emoji by community and name.
|
/// Get an emoji by community and name.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -134,8 +159,8 @@ impl DataManager {
|
||||||
"INSERT INTO emojis VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
"INSERT INTO emojis VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||||
params![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.owner as i64),
|
|
||||||
&(data.created as i64),
|
&(data.created as i64),
|
||||||
|
&(data.owner as i64),
|
||||||
&(data.community as i64),
|
&(data.community as i64),
|
||||||
&(data.upload_id as i64),
|
&(data.upload_id as i64),
|
||||||
&data.name,
|
&data.name,
|
||||||
|
@ -176,7 +201,7 @@ impl DataManager {
|
||||||
return Err(Error::DatabaseError(e.to_string()));
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete uploads
|
// delete upload
|
||||||
self.delete_upload(emoji.upload_id).await?;
|
self.delete_upload(emoji.upload_id).await?;
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
|
@ -184,5 +209,5 @@ impl DataManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_method!(update_emoji_name(&str)@get_emoji_by_id:MANAGE_EMOJIS -> "UPDATE emojis SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.emoji:{}");
|
auto_method!(update_emoji_name(&str)@get_emoji_by_id:MANAGE_EMOJIS -> "UPDATE emojis SET name = $1 WHERE id = $2" --cache-key-tmpl="atto.emoji:{}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,11 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the post the given post is reposting (if some).
|
/// Get the post the given post is reposting (if some).
|
||||||
pub async fn get_post_reposting(&self, post: &Post) -> Option<(User, Post)> {
|
pub async fn get_post_reposting(
|
||||||
|
&self,
|
||||||
|
post: &Post,
|
||||||
|
ignore_users: &[usize],
|
||||||
|
) -> Option<(User, Post)> {
|
||||||
if let Some(ref repost) = post.context.repost {
|
if let Some(ref repost) = post.context.repost {
|
||||||
if let Some(reposting) = repost.reposting {
|
if let Some(reposting) = repost.reposting {
|
||||||
let mut x = match self.get_post_by_id(reposting).await {
|
let mut x = match self.get_post_by_id(reposting).await {
|
||||||
|
@ -86,6 +90,10 @@ impl DataManager {
|
||||||
Err(_) => return None,
|
Err(_) => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ignore_users.contains(&x.owner) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
x.mark_as_repost();
|
x.mark_as_repost();
|
||||||
Some((
|
Some((
|
||||||
match self.get_user_by_id(x.owner).await {
|
match self.get_user_by_id(x.owner).await {
|
||||||
|
@ -103,9 +111,18 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the question of a given post.
|
/// Get the question of a given post.
|
||||||
pub async fn get_post_question(&self, post: &Post) -> Result<Option<(Question, User)>> {
|
pub async fn get_post_question(
|
||||||
|
&self,
|
||||||
|
post: &Post,
|
||||||
|
ignore_users: &[usize],
|
||||||
|
) -> Result<Option<(Question, User)>> {
|
||||||
if post.context.answering != 0 {
|
if post.context.answering != 0 {
|
||||||
let question = self.get_question_by_id(post.context.answering).await?;
|
let question = self.get_question_by_id(post.context.answering).await?;
|
||||||
|
|
||||||
|
if ignore_users.contains(&question.owner) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
let user = if question.owner == 0 {
|
let user = if question.owner == 0 {
|
||||||
User::anonymous()
|
User::anonymous()
|
||||||
} else {
|
} else {
|
||||||
|
@ -138,8 +155,8 @@ impl DataManager {
|
||||||
out.push((
|
out.push((
|
||||||
post.clone(),
|
post.clone(),
|
||||||
user.clone(),
|
user.clone(),
|
||||||
self.get_post_reposting(&post).await,
|
self.get_post_reposting(&post, ignore_users).await,
|
||||||
self.get_post_question(&post).await?,
|
self.get_post_question(&post, ignore_users).await?,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
let user = self.get_user_by_id(owner).await?;
|
let user = self.get_user_by_id(owner).await?;
|
||||||
|
@ -147,8 +164,8 @@ impl DataManager {
|
||||||
out.push((
|
out.push((
|
||||||
post.clone(),
|
post.clone(),
|
||||||
user,
|
user,
|
||||||
self.get_post_reposting(&post).await,
|
self.get_post_reposting(&post, ignore_users).await,
|
||||||
self.get_post_question(&post).await?,
|
self.get_post_question(&post, ignore_users).await?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,8 +213,8 @@ impl DataManager {
|
||||||
post.clone(),
|
post.clone(),
|
||||||
user.clone(),
|
user.clone(),
|
||||||
community.to_owned(),
|
community.to_owned(),
|
||||||
self.get_post_reposting(&post).await,
|
self.get_post_reposting(&post, ignore_users).await,
|
||||||
self.get_post_question(&post).await?,
|
self.get_post_question(&post, ignore_users).await?,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
let user = self.get_user_by_id(owner).await?;
|
let user = self.get_user_by_id(owner).await?;
|
||||||
|
@ -235,8 +252,8 @@ impl DataManager {
|
||||||
post.clone(),
|
post.clone(),
|
||||||
user,
|
user,
|
||||||
community,
|
community,
|
||||||
self.get_post_reposting(&post).await,
|
self.get_post_reposting(&post, ignore_users).await,
|
||||||
self.get_post_question(&post).await?,
|
self.get_post_question(&post, ignore_users).await?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
use std::fs::{exists, remove_file};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::cache::Cache;
|
use crate::cache::Cache;
|
||||||
use crate::model::{Error, Result, uploads::MediaUpload};
|
use crate::model::{Error, Result, uploads::MediaUpload};
|
||||||
use crate::{auto_method, execute, get, query_row, query_rows, params};
|
use crate::{auto_method, execute, get, query_row, query_rows, params};
|
||||||
|
|
||||||
use pathbufd::PathBufD;
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
use rusqlite::Row;
|
use rusqlite::Row;
|
||||||
|
|
||||||
|
@ -56,7 +54,7 @@ impl DataManager {
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `data` - a mock [`MediaUpload`] object to insert
|
/// * `data` - a mock [`MediaUpload`] object to insert
|
||||||
pub async fn create_upload(&self, data: MediaUpload) -> Result<()> {
|
pub async fn create_upload(&self, data: MediaUpload) -> Result<MediaUpload> {
|
||||||
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())),
|
||||||
|
@ -78,7 +76,7 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// return
|
// return
|
||||||
Ok(())
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_upload(&self, id: usize) -> Result<()> {
|
pub async fn delete_upload(&self, id: usize) -> Result<()> {
|
||||||
|
@ -91,16 +89,8 @@ impl DataManager {
|
||||||
// if there's an issue in the database
|
// if there's an issue in the database
|
||||||
//
|
//
|
||||||
// the actual file takes up much more space than the database entry.
|
// the actual file takes up much more space than the database entry.
|
||||||
let path = PathBufD::current().extend(&[self.0.dirs.media.as_str(), "uploads"]);
|
let upload = self.get_upload_by_id(id).await?;
|
||||||
if let Ok(exists) = exists(&path) {
|
upload.remove(&self.0)?;
|
||||||
if exists {
|
|
||||||
if let Err(e) = remove_file(&path) {
|
|
||||||
return Err(Error::MiscError(e.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::GeneralNotFound("file".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete from database
|
// delete from database
|
||||||
let conn = match self.connect().await {
|
let conn = match self.connect().await {
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
use pathbufd::PathBufD;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||||
|
use crate::config::Config;
|
||||||
|
use std::fs::{write, exists, remove_file};
|
||||||
|
use super::{Error, Result};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum MediaType {
|
pub enum MediaType {
|
||||||
|
@ -15,6 +19,22 @@ pub enum MediaType {
|
||||||
Gif,
|
Gif,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MediaType {
|
||||||
|
pub fn extension(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Webp => "webp",
|
||||||
|
Self::Avif => "avif",
|
||||||
|
Self::Png => "png",
|
||||||
|
Self::Jpg => "jpg",
|
||||||
|
Self::Gif => "gif",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mime(&self) -> String {
|
||||||
|
format!("image/{}", self.extension())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct MediaUpload {
|
pub struct MediaUpload {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
|
@ -33,6 +53,35 @@ impl MediaUpload {
|
||||||
what,
|
what,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the path to the fs file for this upload.
|
||||||
|
pub fn path(&self, config: &Config) -> PathBufD {
|
||||||
|
PathBufD::current()
|
||||||
|
.extend(&[config.dirs.media.as_str(), "uploads"])
|
||||||
|
.join(format!("{}.{}", self.id, self.what.extension()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write to this upload in the file system.
|
||||||
|
pub fn write(&self, config: &Config, bytes: &[u8]) -> Result<()> {
|
||||||
|
match write(self.path(config), bytes) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete this upload in the file system.
|
||||||
|
pub fn remove(&self, config: &Config) -> Result<()> {
|
||||||
|
let path = self.path(config);
|
||||||
|
|
||||||
|
if !exists(&path).unwrap() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match remove_file(path) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -86,7 +135,7 @@ impl CustomEmoji {
|
||||||
out = out.replace(
|
out = out.replace(
|
||||||
&emoji.0,
|
&emoji.0,
|
||||||
&format!(
|
&format!(
|
||||||
"<img class=\"emoji\" src=\"/api/v1/communities/{}/emoji/{}\" />",
|
"<img class=\"emoji\" src=\"/api/v1/communities/{}/emojis/{}\" />",
|
||||||
emoji.1, emoji.2
|
emoji.1, emoji.2
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "2.3.0"
|
version = "3.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 = "2.3.0"
|
version = "3.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -37,7 +37,10 @@ pub fn render_markdown(input: &str) -> String {
|
||||||
.add_tag_attributes("a", &["href", "target"])
|
.add_tag_attributes("a", &["href", "target"])
|
||||||
.clean(&html)
|
.clean(&html)
|
||||||
.to_string()
|
.to_string()
|
||||||
.replace("src=\"", "loading=\"lazy\" src=\"/api/v1/util/proxy?url=")
|
.replace(
|
||||||
|
"src=\"http",
|
||||||
|
"loading=\"lazy\" src=\"/api/v1/util/proxy?url=",
|
||||||
|
)
|
||||||
.replace("<video loading=", "<video controls loading=")
|
.replace("<video loading=", "<video controls loading=")
|
||||||
.replace("-->", "<align class=\"right\">")
|
.replace("-->", "<align class=\"right\">")
|
||||||
.replace("->", "<align class=\"center\">")
|
.replace("->", "<align class=\"center\">")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue