add: store avatars and banners in uploads
This commit is contained in:
parent
1e50ace8b2
commit
dbed2b2457
36 changed files with 211 additions and 363 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -385,9 +385,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "buckets-core"
|
name = "buckets-core"
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "536e476a5181a9f8a12d65be91615f036a000a1b1a2eaacde1be78be866940fd"
|
checksum = "e6df107757f765b92fc260dd4b7c2df6c4e4646f79a4f4020c5ca5f249f52dcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"oiseau",
|
"oiseau",
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
("class" "card_nest")
|
("class" "card_nest")
|
||||||
(div
|
(div
|
||||||
("class" "card small flex flex_row gap_2 items_center")
|
("class" "card small flex flex_row gap_2 items_center")
|
||||||
(text "{{ components::avatar(username=user.id, size=\"32px\", selector_type=\"id\") }}")
|
(text "{{ components::avatar(id=user.id, size=\"32px\") }}")
|
||||||
(select
|
(select
|
||||||
("id" "community_to_post_to")
|
("id" "community_to_post_to")
|
||||||
("onchange" "update_community_avatar(event); check_community_supports_title(event)")
|
("onchange" "update_community_avatar(event); check_community_supports_title(event)")
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
(text "{% macro avatar(username, size=\"24px\", selector_type=\"username\") -%}")
|
(text "{% macro avatar(id, size=\"24px\") -%}")
|
||||||
(img
|
(img
|
||||||
("title" "{{ username }}'s avatar")
|
("title" "User avatar")
|
||||||
("src" "/api/v1/auth/user/{{ username }}/avatar?selector_type={{ selector_type }}")
|
("src" "{{ config.service_hosts.buckets }}/avatars/{{ id }}")
|
||||||
("alt" "@{{ username }}")
|
("alt" "User avatar")
|
||||||
("class" "avatar shadow")
|
("class" "avatar shadow")
|
||||||
("loading" "lazy")
|
("loading" "lazy")
|
||||||
("style" "--size: {{ size }}"))
|
("style" "--size: {{ size }}"))
|
||||||
|
@ -20,11 +20,11 @@
|
||||||
("class" "avatar shadow")
|
("class" "avatar shadow")
|
||||||
("loading" "lazy")
|
("loading" "lazy")
|
||||||
("style" "--size: {{ size }}"))
|
("style" "--size: {{ size }}"))
|
||||||
(text "{%- endif %} {%- endmacro %} {% macro banner(username, border_radius=\"var(--radius)\") -%}")
|
(text "{%- endif %} {%- endmacro %} {% macro banner(id, border_radius=\"var(--radius)\") -%}")
|
||||||
(img
|
(img
|
||||||
("title" "{{ username }}'s banner")
|
("title" "User banner")
|
||||||
("src" "/api/v1/auth/user/{{ username }}/banner")
|
("src" "{{ config.service_hosts.buckets }}/banners/{{ id }}")
|
||||||
("alt" "@{{ username }}'s banner")
|
("alt" "User banner")
|
||||||
("class" "banner shadow w_full")
|
("class" "banner shadow w_full")
|
||||||
("loading" "lazy")
|
("loading" "lazy")
|
||||||
("style" "border-radius: {{ border_radius }};"))
|
("style" "border-radius: {{ border_radius }};"))
|
||||||
|
@ -372,7 +372,7 @@
|
||||||
(text "{% if not expect_repost -%}")
|
(text "{% if not expect_repost -%}")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ self::avatar(username=owner.username, size=\"52px\", selector_type=\"username\") }}"))
|
(text "{{ self::avatar(id=owner.id, size=\"52px\") }}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_col w_full gap_1 post_right {% if expect_repost -%}repost{%- endif %}")
|
("class" "flex flex_col w_full gap_1 post_right {% if expect_repost -%}repost{%- endif %}")
|
||||||
|
@ -381,7 +381,7 @@
|
||||||
(text "{% if expect_repost -%}")
|
(text "{% if expect_repost -%}")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
|
(text "{{ self::avatar(id=owner.id, size=\"24px\") }}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(span
|
(span
|
||||||
; ("class" "name")
|
; ("class" "name")
|
||||||
|
@ -542,7 +542,7 @@
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
("class" "mobile")
|
("class" "mobile")
|
||||||
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
|
(text "{{ self::avatar(id=owner.id, size=\"24px\") }}"))
|
||||||
(span
|
(span
|
||||||
("class" "name")
|
("class" "name")
|
||||||
(text "{{ self::full_username(user=owner) }}"))
|
(text "{{ self::full_username(user=owner) }}"))
|
||||||
|
@ -561,7 +561,7 @@
|
||||||
("style" "min-width: 200px")
|
("style" "min-width: 200px")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ self::avatar(username=owner.username, size=\"64px\", selector_type=\"username\") }}"))
|
(text "{{ self::avatar(id=owner.id, size=\"64px\") }}"))
|
||||||
(div
|
(div
|
||||||
("class" "fade flex flex_col")
|
("class" "fade flex flex_col")
|
||||||
("style" "font-size: 12px")
|
("style" "font-size: 12px")
|
||||||
|
@ -617,10 +617,10 @@
|
||||||
(div
|
(div
|
||||||
("class" "card small")
|
("class" "card small")
|
||||||
("style" "padding: 0")
|
("style" "padding: 0")
|
||||||
(text "{{ self::banner(username=user.username, border_radius=\"0px\") }}"))
|
(text "{{ self::banner(id=user.id, border_radius=\"0px\") }}"))
|
||||||
(div
|
(div
|
||||||
("class" "card secondary flex items_center gap_4")
|
("class" "card secondary flex items_center gap_4")
|
||||||
(text "{{ self::avatar(username=user.username, size=\"24px\") }}")
|
(text "{{ self::avatar(id=user.id, size=\"24px\") }}")
|
||||||
(b
|
(b
|
||||||
("class" "flex items_center gap_2")
|
("class" "flex items_center gap_2")
|
||||||
(text "{{ self::username(user=user) }}")
|
(text "{{ self::username(user=user) }}")
|
||||||
|
@ -768,11 +768,11 @@
|
||||||
("class" "avatar shadow")
|
("class" "avatar shadow")
|
||||||
("loading" "lazy")
|
("loading" "lazy")
|
||||||
("style" "--size: 52px"))
|
("style" "--size: 52px"))
|
||||||
(text "{% else %} {{ self::avatar(username=\"anonymous\", selector_type=\"username\", size=\"52px\") }} {%- endif %}"))
|
(text "{% else %} {{ self::avatar(id=\"0\", size=\"52px\") }} {%- endif %}"))
|
||||||
(text "{% else %}")
|
(text "{% else %}")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ self::avatar(username=owner.username, selector_type=\"username\", size=\"52px\") }}"))
|
(text "{{ self::avatar(id=owner.id, size=\"52px\") }}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_col gap_1 w_full")
|
("class" "flex flex_col gap_1 w_full")
|
||||||
|
@ -1186,7 +1186,7 @@
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ user.username }}")
|
("href" "/@{{ user.username }}")
|
||||||
("target" "_top")
|
("target" "_top")
|
||||||
(text "{{ self::avatar(username=user.username, size=\"42px\") }}"))
|
(text "{{ self::avatar(id=user.id, size=\"42px\") }}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_col gap_1 w_full")
|
("class" "flex flex_col gap_1 w_full")
|
||||||
|
@ -1354,7 +1354,7 @@
|
||||||
("class" "flex gap_2 items_center card tiny user_plate {% if secondary -%}secondary{%- endif %} {% if full -%} w_full {%- endif %}")
|
("class" "flex gap_2 items_center card tiny user_plate {% if secondary -%}secondary{%- endif %} {% if full -%} w_full {%- endif %}")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ user.username }}")
|
("href" "/@{{ user.username }}")
|
||||||
(text "{{ self::avatar(username=user.username, size=\"42px\", selector_type=\"username\") }}"))
|
(text "{{ self::avatar(id=user.id, size=\"42px\") }}"))
|
||||||
(div
|
(div
|
||||||
("class" "flex justify_center flex_col")
|
("class" "flex justify_center flex_col")
|
||||||
("style" "{% if show_menu or show_kick -%}width: 60%{% else %}max-width: calc(100% - 42px - var(--pad-4)){%- endif %}")
|
("style" "{% if show_menu or show_kick -%}width: 60%{% else %}max-width: calc(100% - 42px - var(--pad-4)){%- endif %}")
|
||||||
|
@ -2127,7 +2127,7 @@
|
||||||
; user info
|
; user info
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
|
(text "{{ self::avatar(id=owner.id, size=\"24px\") }}"))
|
||||||
(span
|
(span
|
||||||
("class" "name")
|
("class" "name")
|
||||||
(text "{{ self::full_username(user=owner) }}"))
|
(text "{{ self::full_username(user=owner) }}"))
|
||||||
|
@ -2571,7 +2571,7 @@
|
||||||
("class" "card lowered flex gap_2 flex_row")
|
("class" "card lowered flex gap_2 flex_row")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
|
(text "{{ self::avatar(id=owner.username, size=\"32px\") }}"))
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_col")
|
("class" "flex flex_col")
|
||||||
(text "{{ self::full_username(user=owner) }}")
|
(text "{{ self::full_username(user=owner) }}")
|
||||||
|
@ -2596,7 +2596,7 @@
|
||||||
("class" "card flex gap_2 flex_row")
|
("class" "card flex gap_2 flex_row")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
|
(text "{{ self::avatar(id=owner.username, size=\"32px\") }}"))
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_col")
|
("class" "flex flex_col")
|
||||||
(text "{{ self::full_username(user=owner) }}")
|
(text "{{ self::full_username(user=owner) }}")
|
||||||
|
@ -2612,14 +2612,14 @@
|
||||||
(text "{% for receiver in letter.receivers %}")
|
(text "{% for receiver in letter.receivers %}")
|
||||||
(a
|
(a
|
||||||
("href" "/api/v1/auth/user/find/{{ receiver }}")
|
("href" "/api/v1/auth/user/find/{{ receiver }}")
|
||||||
(text "{{ components::avatar(username=receiver, selector_type=\"id\", size=\"18px\") }}"))
|
(text "{{ components::avatar(id=receiver, size=\"18px\") }}"))
|
||||||
(text "{%- endfor %}"))))
|
(text "{%- endfor %}"))))
|
||||||
(text "{% else %}")
|
(text "{% else %}")
|
||||||
(div
|
(div
|
||||||
("class" "card small flex gap_2 flex_row")
|
("class" "card small flex gap_2 flex_row")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ self::avatar(username=owner.username, size=\"24px\") }}"))
|
(text "{{ self::avatar(id=owner.username, size=\"24px\") }}"))
|
||||||
(text "{{ self::full_username(user=owner) }}"))
|
(text "{{ self::full_username(user=owner) }}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
|
|
@ -45,11 +45,11 @@
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "og:image")
|
("name" "og:image")
|
||||||
("content" "{{ config.host|safe }}/api/v1/auth/user/{{ owner.username }}/avatar?selector_type=username"))
|
("content" "{{ config.service_hosts.buckets|safe }}/avatars/{{ owner.id }}"))
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "twitter:image")
|
("name" "twitter:image")
|
||||||
("content" "{{ config.host|safe }}/api/v1/auth/user/{{ owner.username }}/avatar?selector_type=usernamev"))
|
("content" "{{ config.service_hosts.buckets|safe }}/avatars/{{ owner.id }}"))
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "twitter:card")
|
("name" "twitter:card")
|
||||||
|
@ -148,7 +148,7 @@
|
||||||
(a
|
(a
|
||||||
("class" "flex items_center")
|
("class" "flex items_center")
|
||||||
("href" "/@{{ owner.username }}")
|
("href" "/@{{ owner.username }}")
|
||||||
(text "{{ components::avatar(username=owner.username, selector_type=\"username\", size=\"18px\") }}"))
|
(text "{{ components::avatar(id=owner.id, size=\"18px\") }}"))
|
||||||
|
|
||||||
(text "{% if (view_mode and owner) or not view_mode -%}")
|
(text "{% if (view_mode and owner) or not view_mode -%}")
|
||||||
(a
|
(a
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
||||||
("exclude" "dropdown")
|
("exclude" "dropdown")
|
||||||
("style" "gap: var(--pad-1) !important")
|
("style" "gap: var(--pad-1) !important")
|
||||||
(text "{{ components::avatar(username=user.username, size=\"24px\") }}")
|
(text "{{ components::avatar(id=user.id, size=\"24px\") }}")
|
||||||
(icon_class (text "chevron-down") (text "dropdown_arrow")))
|
(icon_class (text "chevron-down") (text "dropdown_arrow")))
|
||||||
|
|
||||||
(text "{{ components::user_menu() }}"))
|
(text "{{ components::user_menu() }}"))
|
||||||
|
|
|
@ -118,7 +118,7 @@
|
||||||
("exclude" "dropdown")
|
("exclude" "dropdown")
|
||||||
("style" "gap: var(--pad-1) !important")
|
("style" "gap: var(--pad-1) !important")
|
||||||
("title" "Account options")
|
("title" "Account options")
|
||||||
(text "{{ components::avatar(username=user.username, size=\"24px\") }}")
|
(text "{{ components::avatar(id=user.id, size=\"24px\") }}")
|
||||||
(icon_class (text "chevron-down") (text "dropdown_arrow")))
|
(icon_class (text "chevron-down") (text "dropdown_arrow")))
|
||||||
|
|
||||||
(text "{{ components::user_menu() }}"))
|
(text "{{ components::user_menu() }}"))
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
const is_id = receiver.startsWith(\"id:\");
|
const is_id = receiver.startsWith(\"id:\");
|
||||||
receiver = receiver.replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replace(\"id:\", \"\");
|
receiver = receiver.replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replace(\"id:\", \"\");
|
||||||
element.innerHTML += `<button class=\"small lowered\" onclick=\"remove_receiver('${receiver}')\" type=\"button\">
|
element.innerHTML += `<button class=\"small lowered\" onclick=\"remove_receiver('${receiver}')\" type=\"button\">
|
||||||
<img class=\"avatar\" style=\"--size: 18px\" src=\"/api/v1/auth/user/${receiver}/avatar?selector_type=${is_id ? \"id\" : \"username\"}\" />
|
<img class=\"avatar\" style=\"--size: 18px\" src=\"${_app_base.service_hosts.buckets}/avatars/${receiver}\" />
|
||||||
<span>${is_id ? \"...\" : receiver}</span>
|
<span>${is_id ? \"...\" : receiver}</span>
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
("class" "card small flex items_center gap_2")
|
("class" "card small flex items_center gap_2")
|
||||||
(a
|
(a
|
||||||
("href" "/api/v1/auth/user/find/{{ request.id }}")
|
("href" "/api/v1/auth/user/find/{{ request.id }}")
|
||||||
(text "{{ components::avatar(username=request.id, selector_type=\"id\") }}"))
|
(text "{{ components::avatar(id=request.id) }}"))
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"requests:label.user_follow_request\" }}")))
|
(text "{{ text \"requests:label.user_follow_request\" }}")))
|
||||||
(div
|
(div
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
(a
|
(a
|
||||||
("class" "card small flex items_center gap_2 flush")
|
("class" "card small flex items_center gap_2 flush")
|
||||||
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
|
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
|
||||||
(text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}")
|
(text "{{ components::avatar(id=item.moderator) }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ item.moderator }}"))
|
(text "{{ item.moderator }}"))
|
||||||
(span
|
(span
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
(a
|
(a
|
||||||
("class" "card small flex items_center gap_2 flush")
|
("class" "card small flex items_center gap_2 flush")
|
||||||
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
|
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
|
||||||
(text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}")
|
(text "{{ components::avatar(id=item.moderator) }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ item.moderator }}"))
|
(text "{{ item.moderator }}"))
|
||||||
(span
|
(span
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
(a
|
(a
|
||||||
("class" "card small flex items_center gap_2 flush")
|
("class" "card small flex items_center gap_2 flush")
|
||||||
("href" "/api/v1/auth/user/find/{{ item.owner }}")
|
("href" "/api/v1/auth/user/find/{{ item.owner }}")
|
||||||
(text "{{ components::avatar(username=item.owner, selector_type=\"id\") }}")
|
(text "{{ components::avatar(id=item.owner) }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ item.owner }}"))
|
(text "{{ item.owner }}"))
|
||||||
(span
|
(span
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
("class" "flex items_center gap_2 flush")
|
("class" "flex items_center gap_2 flush")
|
||||||
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
|
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
|
||||||
("title" "Moderator")
|
("title" "Moderator")
|
||||||
(text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}")
|
(text "{{ components::avatar(id=item.moderator) }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ item.moderator }}"))
|
(text "{{ item.moderator }}"))
|
||||||
(span
|
(span
|
||||||
|
|
|
@ -20,11 +20,11 @@
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "og:image")
|
("name" "og:image")
|
||||||
("content" "{{ config.host|safe }}/api/v1/auth/user/{{ owner.username }}/avatar?selector_type=username"))
|
("content" "{{ config.service_hosts.buckets|safe }}/avatars/{{ owner.id }}"))
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "twitter:image")
|
("name" "twitter:image")
|
||||||
("content" "{{ config.host|safe }}/api/v1/auth/user/{{ owner.username }}/avatar?selector_type=usernamev"))
|
("content" "{{ config.service_hosts.buckets|safe }}/avatars/{{ owner.id }}"))
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "twitter:card")
|
("name" "twitter:card")
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
("class" "card flex items_center gap_2")
|
("class" "card flex items_center gap_2")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ post[1].username }}")
|
("href" "/@{{ post[1].username }}")
|
||||||
(text "{{ components::avatar(username=post[1].username, size=\"24px\", selector_type=\"username\") }}"))
|
(text "{{ components::avatar(id=post[1].id, size=\"24px\") }}"))
|
||||||
(div
|
(div
|
||||||
("class" "name")
|
("class" "name")
|
||||||
(text "{{ components::full_username(user=post[1]) }}")))
|
(text "{{ components::full_username(user=post[1]) }}")))
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
("class" "card small flex items_center justify_between gap_2")
|
("class" "card small flex items_center justify_between gap_2")
|
||||||
(div
|
(div
|
||||||
("class" "flex items_center gap_2")
|
("class" "flex items_center gap_2")
|
||||||
(text "{{ components::avatar(username=profile.username, size=\"24px\") }}")
|
(text "{{ components::avatar(id=profile.id, size=\"24px\") }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ profile.username }}")))
|
(text "{{ profile.username }}")))
|
||||||
(b
|
(b
|
||||||
|
|
|
@ -24,11 +24,11 @@
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "og:image")
|
("name" "og:image")
|
||||||
("content" "{{ config.host|safe }}/api/v1/auth/user/{{ profile.username }}/avatar?selector_type=username"))
|
("content" "{{ config.service_hosts.buckets|safe }}/avatars/{{ profile.id }}"))
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "twitter:image")
|
("name" "twitter:image")
|
||||||
("content" "{{ config.host|safe }}/api/v1/auth/user/{{ profile.username }}/avatar?selector_type=username"))
|
("content" "{{ config.service_hosts.buckets|safe }}/avatars/{{ profile.id }}"))
|
||||||
|
|
||||||
(meta
|
(meta
|
||||||
("name" "twitter:card")
|
("name" "twitter:card")
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
(article
|
(article
|
||||||
(div
|
(div
|
||||||
("class" "content_container flex flex_col gap_4")
|
("class" "content_container flex flex_col gap_4")
|
||||||
(text "{{ components::banner(username=profile.username) }}")
|
(text "{{ components::banner(id=profile.id) }}")
|
||||||
(div
|
(div
|
||||||
("class" "w_full flex gap_4 flex_collapse")
|
("class" "w_full flex gap_4 flex_collapse")
|
||||||
(div
|
(div
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
(div
|
(div
|
||||||
("class" "card flex gap_2")
|
("class" "card flex gap_2")
|
||||||
("id" "user_avatar_and_name")
|
("id" "user_avatar_and_name")
|
||||||
(text "{{ components::avatar(username=profile.username,size=\"72px\") }}")
|
(text "{{ components::avatar(id=profile.id,size=\"72px\") }}")
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_col")
|
("class" "flex flex_col")
|
||||||
(h3
|
(h3
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
("class" "card small flex items_center justify_between gap_2")
|
("class" "card small flex items_center justify_between gap_2")
|
||||||
(div
|
(div
|
||||||
("class" "flex items_center gap_2")
|
("class" "flex items_center gap_2")
|
||||||
(text "{{ components::avatar(username=profile.username, size=\"24px\") }}")
|
(text "{{ components::avatar(id=profile.id, size=\"24px\") }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ profile.username }}")))
|
(text "{{ profile.username }}")))
|
||||||
(b
|
(b
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
("class" "flex items_center gap_2 flush")
|
("class" "flex items_center gap_2 flush")
|
||||||
("href" "/api/v1/auth/user/find/{{ question[0].receiver }}")
|
("href" "/api/v1/auth/user/find/{{ question[0].receiver }}")
|
||||||
(icon (text "send"))
|
(icon (text "send"))
|
||||||
(text "{{ components::avatar(username=question[0].receiver, selector_type='id') }}"))
|
(text "{{ components::avatar(id=question[0].receiver) }}"))
|
||||||
|
|
||||||
; show button to delete question
|
; show button to delete question
|
||||||
(button
|
(button
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
("class" "card small flex items_center justify_between gap_2")
|
("class" "card small flex items_center justify_between gap_2")
|
||||||
(div
|
(div
|
||||||
("class" "flex items_center gap_2")
|
("class" "flex items_center gap_2")
|
||||||
(text "{{ components::avatar(username=profile.username, size=\"24px\") }}")
|
(text "{{ components::avatar(id=profile.id, size=\"24px\") }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ profile.username }}")))
|
(text "{{ profile.username }}")))
|
||||||
(b
|
(b
|
||||||
|
|
|
@ -466,7 +466,7 @@
|
||||||
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}"))
|
(text "{{ components::avatar(id=user.id) }} {{ components::full_username(user=user) }}"))
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(button
|
(button
|
||||||
|
@ -522,7 +522,7 @@
|
||||||
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}"))
|
(text "{{ components::avatar(id=user.id) }} {{ components::full_username(user=user) }}"))
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(button
|
(button
|
||||||
|
@ -594,7 +594,7 @@
|
||||||
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}"))
|
(text "{{ components::avatar(id=user.id) }} {{ components::full_username(user=user) }}"))
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(a
|
(a
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
("class" "card small flex items_center justify_between gap_2")
|
("class" "card small flex items_center justify_between gap_2")
|
||||||
(div
|
(div
|
||||||
("class" "flex items_center gap_2")
|
("class" "flex items_center gap_2")
|
||||||
(text "{{ components::avatar(username=profile.username, size=\"24px\") }}")
|
(text "{{ components::avatar(id=profile.id, size=\"24px\") }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ profile.username }}")))
|
(text "{{ profile.username }}")))
|
||||||
(b
|
(b
|
||||||
|
|
|
@ -31,6 +31,9 @@
|
||||||
name: \"tetratto\",
|
name: \"tetratto\",
|
||||||
ns_store: {},
|
ns_store: {},
|
||||||
classes: {},
|
classes: {},
|
||||||
|
service_hosts: {
|
||||||
|
buckets: \"{{ config.service_hosts.buckets }}\",
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
globalThis.no_policy = false;
|
globalThis.no_policy = false;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
("class" "card_nest")
|
("class" "card_nest")
|
||||||
(div
|
(div
|
||||||
("class" "card small flex items_center gap_2")
|
("class" "card small flex items_center gap_2")
|
||||||
(text "{{ components::avatar(username=add_user.username, size=\"24px\") }}")
|
(text "{{ components::avatar(id=add_user.username, size=\"24px\") }}")
|
||||||
(text "{{ components::full_username(user=add_user) }}"))
|
(text "{{ components::full_username(user=add_user) }}"))
|
||||||
(div
|
(div
|
||||||
("class" "card flex flex_col gap_2")
|
("class" "card flex flex_col gap_2")
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
("class" "flex items_center gap_2")
|
("class" "flex items_center gap_2")
|
||||||
(a
|
(a
|
||||||
("href" "/api/v1/auth/user/find/{{ stack.owner }}")
|
("href" "/api/v1/auth/user/find/{{ stack.owner }}")
|
||||||
(text "{{ components::avatar(username=stack.owner, selector_type=\"id\") }}"))
|
(text "{{ components::avatar(id=stack.owner) }}"))
|
||||||
(span
|
(span
|
||||||
(text "{{ stack.name }}")))
|
(text "{{ stack.name }}")))
|
||||||
(div
|
(div
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
(a
|
(a
|
||||||
("href" "/api/v1/auth/user/find/{{ user }}")
|
("href" "/api/v1/auth/user/find/{{ user }}")
|
||||||
("class" "flush")
|
("class" "flush")
|
||||||
(text "{{ components::avatar(username=user, selector_type=\"id\", size=\"24px\") }}"))
|
(text "{{ components::avatar(id=user, size=\"24px\") }}"))
|
||||||
(text "{% endfor %}"))
|
(text "{% endfor %}"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,7 @@
|
||||||
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
|
||||||
(div
|
(div
|
||||||
("class" "flex gap_2")
|
("class" "flex gap_2")
|
||||||
(text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}"))
|
(text "{{ components::avatar(id=user.id) }} {{ components::full_username(user=user) }}"))
|
||||||
(button
|
(button
|
||||||
("class" "lowered small red")
|
("class" "lowered small red")
|
||||||
("onclick" "remove_user('{{ user.username }}')")
|
("onclick" "remove_user('{{ user.username }}')")
|
||||||
|
|
|
@ -1155,7 +1155,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
|
||||||
const data = await (await fetch(`${src}/json`)).json();
|
const data = await (await fetch(`${src}/json`)).json();
|
||||||
document
|
document
|
||||||
.getElementById("lightbox_img")
|
.getElementById("lightbox_img")
|
||||||
.setAttribute("alt", data.payload.alt);
|
.setAttribute("alt", data.payload.metadata.alt || "Image upload");
|
||||||
document.getElementById("lightbox_img").title = data.payload.alt;
|
document.getElementById("lightbox_img").title = data.payload.alt;
|
||||||
|
|
||||||
document.getElementById("lightbox_img_a").href = src;
|
document.getElementById("lightbox_img_a").href = src;
|
||||||
|
|
|
@ -503,7 +503,7 @@
|
||||||
new Notification(inner_data.title, {
|
new Notification(inner_data.title, {
|
||||||
body: inner_data.content,
|
body: inner_data.content,
|
||||||
icon: matches[1]
|
icon: matches[1]
|
||||||
? `/api/v1/auth/user/${matches[1]}/avatar?selector_type=id`
|
? `${_app_base.service_hosts.buckets}/avatars/${matches[1]}`
|
||||||
: "/public/favicon.svg",
|
: "/public/favicon.svg",
|
||||||
lang: "en-US",
|
lang: "en-US",
|
||||||
});
|
});
|
||||||
|
@ -727,14 +727,6 @@
|
||||||
for (const token of Object.entries($.LOGIN_ACCOUNT_TOKENS)) {
|
for (const token of Object.entries($.LOGIN_ACCOUNT_TOKENS)) {
|
||||||
element.innerHTML += `<div class="flex gap_2 flex_row">
|
element.innerHTML += `<div class="flex gap_2 flex_row">
|
||||||
<button class="lowered w_full justify_start" onclick="trigger('me::login', ['${token[0]}'])">
|
<button class="lowered w_full justify_start" onclick="trigger('me::login', ['${token[0]}'])">
|
||||||
<img
|
|
||||||
title="${token[0]}'s avatar"
|
|
||||||
src="/api/v1/auth/user/${token[0]}/avatar?selector_type=username"
|
|
||||||
alt="Avatar image"
|
|
||||||
class="avatar"
|
|
||||||
style="--size: 24px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span>${token[0]}</span>
|
<span>${token[0]}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
use axum::{
|
use axum::{Extension, Json, response::IntoResponse};
|
||||||
Extension, Json,
|
|
||||||
body::Body,
|
|
||||||
extract::{Path, Query},
|
|
||||||
response::IntoResponse,
|
|
||||||
};
|
|
||||||
use crate::cookie::CookieJar;
|
use crate::cookie::CookieJar;
|
||||||
use pathbufd::{PathBufD, pathd};
|
use pathbufd::PathBufD;
|
||||||
use serde::Deserialize;
|
use std::{fs::File, io::Read};
|
||||||
use std::{
|
use tetratto_core::model::{
|
||||||
fs::{File, exists},
|
permissions::FinePermission,
|
||||||
io::Read,
|
uploads::{MediaType, MediaUpload},
|
||||||
|
ApiReturn, Error,
|
||||||
};
|
};
|
||||||
use tetratto_core::model::{permissions::FinePermission, ApiReturn, Error};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
State,
|
State,
|
||||||
|
@ -29,139 +24,6 @@ pub fn read_image(path: PathBufD) -> Vec<u8> {
|
||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, PartialEq, Eq)]
|
|
||||||
pub enum AvatarSelectorType {
|
|
||||||
#[serde(alias = "username")]
|
|
||||||
Username,
|
|
||||||
#[serde(alias = "id")]
|
|
||||||
Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct AvatarSelectorQuery {
|
|
||||||
pub selector_type: AvatarSelectorType,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a profile's avatar image
|
|
||||||
/// `/api/v1/auth/user/{id}/avatar`
|
|
||||||
pub async fn avatar_request(
|
|
||||||
Path(selector): Path<String>,
|
|
||||||
Extension(data): Extension<State>,
|
|
||||||
Query(req): Query<AvatarSelectorQuery>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let data = &(data.read().await).0;
|
|
||||||
|
|
||||||
let user = match if req.selector_type == AvatarSelectorType::Id {
|
|
||||||
data.get_user_by_id(match selector.parse::<usize>() {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => {
|
|
||||||
return Err((
|
|
||||||
[("Content-Type", "image/svg+xml")],
|
|
||||||
Body::from(read_image(PathBufD::current().extend(&[
|
|
||||||
data.0.0.dirs.media.as_str(),
|
|
||||||
"images",
|
|
||||||
"default-avatar.svg",
|
|
||||||
]))),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
data.get_user_by_username(&selector).await
|
|
||||||
} {
|
|
||||||
Ok(ua) => ua,
|
|
||||||
Err(_) => {
|
|
||||||
return Err((
|
|
||||||
[("Content-Type", "image/svg+xml")],
|
|
||||||
Body::from(read_image(PathBufD::current().extend(&[
|
|
||||||
data.0.0.dirs.media.as_str(),
|
|
||||||
"images",
|
|
||||||
"default-avatar.svg",
|
|
||||||
]))),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mime = if user.settings.avatar_mime.is_empty() {
|
|
||||||
"image/avif"
|
|
||||||
} else {
|
|
||||||
&user.settings.avatar_mime
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = PathBufD::current().extend(&[
|
|
||||||
data.0.0.dirs.media.as_str(),
|
|
||||||
"avatars",
|
|
||||||
&format!("{}.{}", &(user.id as i64), mime.replace("image/", "")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if !exists(&path).unwrap() {
|
|
||||||
return Err((
|
|
||||||
[("Content-Type", "image/svg+xml")],
|
|
||||||
Body::from(read_image(PathBufD::current().extend(&[
|
|
||||||
data.0.0.dirs.media.as_str(),
|
|
||||||
"images",
|
|
||||||
"default-avatar.svg",
|
|
||||||
]))),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
[("Content-Type".to_string(), mime.to_owned())],
|
|
||||||
Body::from(read_image(path)),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a profile's banner image
|
|
||||||
/// `/api/v1/auth/user/{id}/banner`
|
|
||||||
pub async fn banner_request(
|
|
||||||
Path(username): Path<String>,
|
|
||||||
Extension(data): Extension<State>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let data = &(data.read().await).0;
|
|
||||||
|
|
||||||
let user = match data.get_user_by_username(&username).await {
|
|
||||||
Ok(ua) => ua,
|
|
||||||
Err(_) => {
|
|
||||||
return Err((
|
|
||||||
[("Content-Type", "image/svg+xml")],
|
|
||||||
Body::from(read_image(PathBufD::current().extend(&[
|
|
||||||
data.0.0.dirs.media.as_str(),
|
|
||||||
"images",
|
|
||||||
"default-banner.svg",
|
|
||||||
]))),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mime = if user.settings.banner_mime.is_empty() {
|
|
||||||
"image/avif"
|
|
||||||
} else {
|
|
||||||
&user.settings.banner_mime
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = PathBufD::current().extend(&[
|
|
||||||
data.0.0.dirs.media.as_str(),
|
|
||||||
"banners",
|
|
||||||
&format!("{}.{}", &(user.id as i64), mime.replace("image/", "")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if !exists(&path).unwrap() {
|
|
||||||
return Err((
|
|
||||||
[("Content-Type", "image/svg+xml")],
|
|
||||||
Body::from(read_image(PathBufD::current().extend(&[
|
|
||||||
data.0.0.dirs.media.as_str(),
|
|
||||||
"images",
|
|
||||||
"default-banner.svg",
|
|
||||||
]))),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
[("Content-Type".to_string(), mime.to_owned())],
|
|
||||||
Body::from(read_image(path)),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MAXIMUM_FILE_SIZE: usize = 8388608;
|
pub const MAXIMUM_FILE_SIZE: usize = 8388608;
|
||||||
pub const MAXIMUM_GIF_FILE_SIZE: usize = 2097152;
|
pub const MAXIMUM_GIF_FILE_SIZE: usize = 2097152;
|
||||||
|
|
||||||
|
@ -173,44 +35,57 @@ pub async fn upload_avatar_request(
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// get user from token
|
// get user from token
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let mut auth_user = match get_user_from_token!(jar, data) {
|
let auth_user = match get_user_from_token!(jar, data) {
|
||||||
Some(ua) => ua,
|
Some(ua) => ua,
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if img.1 == "image/gif" && !auth_user.permissions.check(FinePermission::SUPPORTER) {
|
if img.1 == "image/gif" {
|
||||||
|
if !auth_user.permissions.check(FinePermission::SUPPORTER) {
|
||||||
return Json(Error::RequiresSupporter.into());
|
return Json(Error::RequiresSupporter.into());
|
||||||
|
} else {
|
||||||
|
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
||||||
|
return Json(Error::FileTooLarge.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||||
|
return Json(Error::FileTooLarge.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mime = if img.1 == "image/gif" {
|
// delete old upload
|
||||||
"image/gif"
|
if let Ok(u) = data
|
||||||
|
.2
|
||||||
|
.get_upload_by_id_bucket(auth_user.id, "avatars")
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if let Err(e) = data.2.delete_upload_with_bucket(u.id, "avatars").await {
|
||||||
|
return Json(Error::MiscError(e.to_string()).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new upload
|
||||||
|
let mut new_upload = MediaUpload::new(
|
||||||
|
if img.1 == "image/gif" {
|
||||||
|
MediaType::Gif
|
||||||
} else {
|
} else {
|
||||||
"image/avif"
|
MediaType::Avif
|
||||||
|
},
|
||||||
|
auth_user.id,
|
||||||
|
"avatars".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
new_upload.id = auth_user.id;
|
||||||
|
|
||||||
|
let upload = match data.2.create_upload(new_upload).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if auth_user.settings.avatar_mime != mime {
|
|
||||||
// mime changed; delete old image
|
|
||||||
let path = pathd!(
|
|
||||||
"{}/avatars/{}.{}",
|
|
||||||
data.0.0.dirs.media,
|
|
||||||
&auth_user.id,
|
|
||||||
auth_user.settings.avatar_mime.replace("image/", "")
|
|
||||||
);
|
|
||||||
|
|
||||||
if std::fs::exists(&path).unwrap() {
|
|
||||||
std::fs::remove_file(path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = pathd!(
|
|
||||||
"{}/avatars/{}.{}",
|
|
||||||
data.0.0.dirs.media,
|
|
||||||
&auth_user.id,
|
|
||||||
mime.replace("image/", "")
|
|
||||||
);
|
|
||||||
|
|
||||||
// upload image (gif)
|
// upload image (gif)
|
||||||
if mime == "image/gif" {
|
let path = upload.path(&data.2.0.0.directory);
|
||||||
|
if img.1 == "image/gif" {
|
||||||
// gif image, don't encode
|
// gif image, don't encode
|
||||||
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
||||||
return Json(Error::FileTooLarge.into());
|
return Json(Error::FileTooLarge.into());
|
||||||
|
@ -218,15 +93,6 @@ pub async fn upload_avatar_request(
|
||||||
|
|
||||||
std::fs::write(&path, img.0).unwrap();
|
std::fs::write(&path, img.0).unwrap();
|
||||||
|
|
||||||
// update user settings
|
|
||||||
auth_user.settings.avatar_mime = "image/gif".to_string();
|
|
||||||
if let Err(e) = data
|
|
||||||
.update_user_settings(auth_user.id, auth_user.settings)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Json(e.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
return Json(ApiReturn {
|
return Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -235,28 +101,8 @@ pub async fn upload_avatar_request(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// check file size
|
|
||||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
|
||||||
return Json(Error::FileTooLarge.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// update user settings
|
|
||||||
auth_user.settings.avatar_mime = "image/avif".to_string();
|
|
||||||
if let Err(e) = data
|
|
||||||
.update_user_settings(auth_user.id, auth_user.settings)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Json(e.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// upload image
|
// upload image
|
||||||
let mut bytes = Vec::new();
|
match save_buffer(&path.to_string(), img.0.to_vec(), image::ImageFormat::Avif) {
|
||||||
|
|
||||||
for byte in img.0 {
|
|
||||||
bytes.push(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
match save_buffer(&path, bytes, image::ImageFormat::Avif) {
|
|
||||||
Ok(_) => Json(ApiReturn {
|
Ok(_) => Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "Avatar uploaded. It might take a bit to update".to_string(),
|
message: "Avatar uploaded. It might take a bit to update".to_string(),
|
||||||
|
@ -274,44 +120,57 @@ pub async fn upload_banner_request(
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// get user from token
|
// get user from token
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let mut auth_user = match get_user_from_token!(jar, data) {
|
let auth_user = match get_user_from_token!(jar, data) {
|
||||||
Some(ua) => ua,
|
Some(ua) => ua,
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if img.1 == "image/gif" && !auth_user.permissions.check(FinePermission::SUPPORTER) {
|
if img.1 == "image/gif" {
|
||||||
|
if !auth_user.permissions.check(FinePermission::SUPPORTER) {
|
||||||
return Json(Error::RequiresSupporter.into());
|
return Json(Error::RequiresSupporter.into());
|
||||||
|
} else {
|
||||||
|
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
||||||
|
return Json(Error::FileTooLarge.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if img.0.len() > MAXIMUM_FILE_SIZE {
|
||||||
|
return Json(Error::FileTooLarge.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mime = if img.1 == "image/gif" {
|
// delete old upload
|
||||||
"image/gif"
|
if let Ok(u) = data
|
||||||
|
.2
|
||||||
|
.get_upload_by_id_bucket(auth_user.id, "banners")
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if let Err(e) = data.2.delete_upload_with_bucket(u.id, "banners").await {
|
||||||
|
return Json(Error::MiscError(e.to_string()).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new upload
|
||||||
|
let mut new_upload = MediaUpload::new(
|
||||||
|
if img.1 == "image/gif" {
|
||||||
|
MediaType::Gif
|
||||||
} else {
|
} else {
|
||||||
"image/avif"
|
MediaType::Avif
|
||||||
|
},
|
||||||
|
auth_user.id,
|
||||||
|
"banners".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
new_upload.id = auth_user.id;
|
||||||
|
|
||||||
|
let upload = match data.2.create_upload(new_upload).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if auth_user.settings.banner_mime != mime {
|
|
||||||
// mime changed; delete old image
|
|
||||||
let path = pathd!(
|
|
||||||
"{}/banners/{}.{}",
|
|
||||||
data.0.0.dirs.media,
|
|
||||||
&auth_user.id,
|
|
||||||
auth_user.settings.banner_mime.replace("image/", "")
|
|
||||||
);
|
|
||||||
|
|
||||||
if std::fs::exists(&path).unwrap() {
|
|
||||||
std::fs::remove_file(path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = pathd!(
|
|
||||||
"{}/banners/{}.{}",
|
|
||||||
data.0.0.dirs.media,
|
|
||||||
&auth_user.id,
|
|
||||||
mime.replace("image/", "")
|
|
||||||
);
|
|
||||||
|
|
||||||
// upload image (gif)
|
// upload image (gif)
|
||||||
if mime == "image/gif" {
|
let path = upload.path(&data.2.0.0.directory);
|
||||||
|
if img.1 == "image/gif" {
|
||||||
// gif image, don't encode
|
// gif image, don't encode
|
||||||
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
if img.0.len() > MAXIMUM_GIF_FILE_SIZE {
|
||||||
return Json(Error::FileTooLarge.into());
|
return Json(Error::FileTooLarge.into());
|
||||||
|
@ -319,15 +178,6 @@ pub async fn upload_banner_request(
|
||||||
|
|
||||||
std::fs::write(&path, img.0).unwrap();
|
std::fs::write(&path, img.0).unwrap();
|
||||||
|
|
||||||
// update user settings
|
|
||||||
auth_user.settings.banner_mime = "image/gif".to_string();
|
|
||||||
if let Err(e) = data
|
|
||||||
.update_user_settings(auth_user.id, auth_user.settings)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Json(e.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
return Json(ApiReturn {
|
return Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -336,28 +186,8 @@ pub async fn upload_banner_request(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// check file size
|
|
||||||
if img.0.len() > MAXIMUM_FILE_SIZE {
|
|
||||||
return Json(Error::FileTooLarge.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// update user settings
|
|
||||||
auth_user.settings.avatar_mime = "image/avif".to_string();
|
|
||||||
if let Err(e) = data
|
|
||||||
.update_user_settings(auth_user.id, auth_user.settings)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Json(e.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// upload image
|
// upload image
|
||||||
let mut bytes = Vec::new();
|
match save_buffer(&path.to_string(), img.0.to_vec(), image::ImageFormat::Avif) {
|
||||||
|
|
||||||
for byte in img.0 {
|
|
||||||
bytes.push(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
match save_buffer(&path, bytes, image::ImageFormat::Avif) {
|
|
||||||
Ok(_) => Json(ApiReturn {
|
Ok(_) => Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "Banner uploaded. It might take a bit to update".to_string(),
|
message: "Banner uploaded. It might take a bit to update".to_string(),
|
||||||
|
|
|
@ -306,8 +306,6 @@ pub fn routes() -> Router {
|
||||||
"/auth/user/me/policy_consent",
|
"/auth/user/me/policy_consent",
|
||||||
post(auth::profile::policy_consent_request),
|
post(auth::profile::policy_consent_request),
|
||||||
)
|
)
|
||||||
.route("/auth/user/{id}/avatar", get(auth::images::avatar_request))
|
|
||||||
.route("/auth/user/{id}/banner", get(auth::images::banner_request))
|
|
||||||
.route("/auth/user/{id}/follow", post(auth::social::follow_request))
|
.route("/auth/user/{id}/follow", post(auth::social::follow_request))
|
||||||
.route(
|
.route(
|
||||||
"/auth/user/{id}/follow/toggle",
|
"/auth/user/{id}/follow/toggle",
|
||||||
|
|
|
@ -49,4 +49,4 @@ oiseau = { version = "0.1.2", default-features = false, features = [
|
||||||
], optional = true }
|
], optional = true }
|
||||||
paste = { version = "1.0.15", optional = true }
|
paste = { version = "1.0.15", optional = true }
|
||||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||||
buckets-core = "1.0.1"
|
buckets-core = "1.0.4"
|
||||||
|
|
|
@ -11,8 +11,6 @@ use crate::model::{
|
||||||
ACHIEVEMENTS, AppliedConfigType,
|
ACHIEVEMENTS, AppliedConfigType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use pathbufd::PathBufD;
|
|
||||||
use std::fs::{exists, remove_file};
|
|
||||||
use tetratto_shared::{
|
use tetratto_shared::{
|
||||||
hash::{hash_salted, salt},
|
hash::{hash_salted, salt},
|
||||||
unix_epoch_timestamp,
|
unix_epoch_timestamp,
|
||||||
|
@ -618,35 +616,6 @@ impl DataManager {
|
||||||
self.delete_app(app.id, &user).await?;
|
self.delete_app(app.id, &user).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove images
|
|
||||||
let avatar = PathBufD::current().extend(&[
|
|
||||||
self.0.0.dirs.media.as_str(),
|
|
||||||
"avatars",
|
|
||||||
&format!(
|
|
||||||
"{}.{}",
|
|
||||||
&(user.id as i64),
|
|
||||||
user.settings.avatar_mime.replace("image/", "")
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let banner = PathBufD::current().extend(&[
|
|
||||||
self.0.0.dirs.media.as_str(),
|
|
||||||
"banners",
|
|
||||||
&format!(
|
|
||||||
"{}.{}",
|
|
||||||
&(user.id as i64),
|
|
||||||
user.settings.banner_mime.replace("image/", "")
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if exists(&avatar).unwrap() {
|
|
||||||
remove_file(avatar).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if exists(&banner).unwrap() {
|
|
||||||
remove_file(banner).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete uploads
|
// delete uploads
|
||||||
for upload in match self.2.get_uploads_by_owner_all(user.id).await {
|
for upload in match self.2.get_uploads_by_owner_all(user.id).await {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
|
|
|
@ -18,6 +18,7 @@ impl DataManager {
|
||||||
pub async fn new(config: Config) -> Result<Self> {
|
pub async fn new(config: Config) -> Result<Self> {
|
||||||
let buckets_manager = BucketsManager::new(BucketsConfig {
|
let buckets_manager = BucketsManager::new(BucketsConfig {
|
||||||
directory: format!("{}/{}", config.dirs.media, "uploads"),
|
directory: format!("{}/{}", config.dirs.media, "uploads"),
|
||||||
|
bucket_defaults: HashMap::new(),
|
||||||
database: config.database.clone(),
|
database: config.database.clone(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -1657,7 +1657,7 @@ impl DataManager {
|
||||||
let res = query_rows!(
|
let res = query_rows!(
|
||||||
&conn,
|
&conn,
|
||||||
&format!(
|
&format!(
|
||||||
"SELECT * FROM posts WHERE (owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
|
"SELECT * FROM posts WHERE (owner = {id} OR owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
|
||||||
first.receiver
|
first.receiver
|
||||||
),
|
),
|
||||||
&[&(batch as i64), &((page * batch) as i64)],
|
&[&(batch as i64), &((page * batch) as i64)],
|
||||||
|
|
|
@ -298,9 +298,6 @@ pub struct UserSettings {
|
||||||
/// The user's status. Shows over connection info.
|
/// The user's status. Shows over connection info.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub status: String,
|
pub status: String,
|
||||||
/// The mime type of the user's avatar.
|
|
||||||
#[serde(default = "mime_avif")]
|
|
||||||
pub avatar_mime: String,
|
|
||||||
/// The mime type of the user's banner.
|
/// The mime type of the user's banner.
|
||||||
#[serde(default = "mime_avif")]
|
#[serde(default = "mime_avif")]
|
||||||
pub banner_mime: String,
|
pub banner_mime: String,
|
||||||
|
|
54
manual_migrations/avatars.js
Normal file
54
manual_migrations/avatars.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import postgres from "npm:postgres";
|
||||||
|
import { parse } from "npm:smol-toml";
|
||||||
|
import { readdir, rename } from "node:fs/promises";
|
||||||
|
|
||||||
|
const config = parse(await Deno.readTextFile(Deno.cwd() + "/tetratto.toml"), {
|
||||||
|
integersAsBigInt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = postgres({
|
||||||
|
user: config.database.user,
|
||||||
|
password: config.database.password,
|
||||||
|
database: config.database.name,
|
||||||
|
hostname: config.database.url.split(":")[0],
|
||||||
|
port: config.database.url.split(":")[1],
|
||||||
|
});
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for (const fname of await readdir(Deno.cwd() + "/media/avatars")) {
|
||||||
|
const [uid, type] = fname.split(".");
|
||||||
|
|
||||||
|
await db`INSERT INTO uploads VALUES (${BigInt(uid)}, ${BigInt(new Date().getTime())}, ${BigInt(uid)}, 'avatars', ${JSON.stringify(
|
||||||
|
{
|
||||||
|
what: type.charAt(0).toUpperCase() + type.slice(1),
|
||||||
|
},
|
||||||
|
)});`;
|
||||||
|
|
||||||
|
await rename(
|
||||||
|
Deno.cwd() + "/media/avatars/" + fname,
|
||||||
|
Deno.cwd() + "/media/uploads/avatars." + fname,
|
||||||
|
);
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
console.log(`done ${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fname of await readdir(Deno.cwd() + "/media/banners")) {
|
||||||
|
const [uid, type] = fname.split(".");
|
||||||
|
|
||||||
|
await db`INSERT INTO uploads VALUES (${BigInt(uid)}, ${BigInt(new Date().getTime())}, ${BigInt(uid)}, 'banners', ${JSON.stringify(
|
||||||
|
{
|
||||||
|
what: type.charAt(0).toUpperCase() + type.slice(1),
|
||||||
|
},
|
||||||
|
)});`;
|
||||||
|
|
||||||
|
await rename(
|
||||||
|
Deno.cwd() + "/media/banners/" + fname,
|
||||||
|
Deno.cwd() + "/media/uploads/banners." + fname,
|
||||||
|
);
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
console.log(`done ${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.end();
|
4
manual_migrations/uploads_pkey.sql
Normal file
4
manual_migrations/uploads_pkey.sql
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
ALTER TABLE uploads
|
||||||
|
DROP CONSTRAINT uploads_pkey;
|
||||||
|
|
||||||
|
ALTER TABLE uploads ADD CONSTRAINT uploads_pkey PRIMARY KEY (id, bucket);
|
Loading…
Add table
Add a link
Reference in a new issue