diff --git a/Cargo.toml b/Cargo.toml index c25b56f..b5beca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,9 @@ package.homepage = "https://tetratto.com" incremental = true [profile.release] -opt-level = 3 +opt-level = "z" lto = true -codegen-units = 2 +codegen-units = 1 # panic = "abort" panic = "unwind" strip = true diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 393fe54..29ff4e6 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -38,7 +38,6 @@ pub const STREAMS_JS: &str = include_str!("./public/js/streams.js"); pub const CARP_JS: &str = include_str!("./public/js/carp.js"); pub const PROTO_LINKS_JS: &str = include_str!("./public/js/proto_links.js"); pub const APP_SDK_JS: &str = include_str!("./public/js/app_sdk.js"); -pub const ADS_JS: &str = include_str!("./public/js/ads.js"); // html pub const BODY: &str = include_str!("./public/html/body.lisp"); @@ -153,8 +152,6 @@ pub const ECONOMY_WALLET: &str = include_str!("./public/html/economy/wallet.lisp pub const ECONOMY_PRODUCTS: &str = include_str!("./public/html/economy/products.lisp"); pub const ECONOMY_EDIT: &str = include_str!("./public/html/economy/edit.lisp"); pub const ECONOMY_PRODUCT: &str = include_str!("./public/html/economy/product.lisp"); -pub const ECONOMY_EDIT_AD: &str = include_str!("./public/html/economy/edit_ad.lisp"); -pub const ECONOMY_AD: &str = include_str!("./public/html/economy/ad.lisp"); // langs pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); @@ -393,8 +390,6 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"economy/products.html"(crate::assets::ECONOMY_PRODUCTS) --config=config --lisp plugins); write_template!(html_path->"economy/edit.html"(crate::assets::ECONOMY_EDIT) --config=config --lisp plugins); write_template!(html_path->"economy/product.html"(crate::assets::ECONOMY_PRODUCT) --config=config --lisp plugins); - write_template!(html_path->"economy/edit_ad.html"(crate::assets::ECONOMY_EDIT_AD) --config=config --lisp plugins); - write_template!(html_path->"economy/ad.html"(crate::assets::ECONOMY_AD) --config=config --lisp plugins); html_path } @@ -469,15 +464,10 @@ pub(crate) async fn initial_context( .check(SecondaryPermission::DEVELOPER_PASS), ); ctx.insert("home", &ua.settings.default_timeline.relative_url()); - ctx.insert( - "renew_policy_consent", - &(ua.last_policy_consent < config.policies.last_updated), - ); } else { ctx.insert("is_helper", &false); ctx.insert("is_manager", &false); ctx.insert("home", &DefaultTimelineChoice::default().relative_url()); - ctx.insert("renew_policy_consent", &false); } ctx.insert("lang", &lang.data); diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index e06e6ed..73507f4 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -208,7 +208,6 @@ version = "1.0.0" "settings:tab.billing" = "Billing" "settings:tab.uploads" = "Uploads" "settings:tab.invites" = "Invites" -"setttings:label.applied_configurations" = "Applied configurations" "mod_panel:label.open_reported_content" = "Open reported content" "mod_panel:label.manage_profile" = "Manage profile" @@ -338,22 +337,9 @@ version = "1.0.0" "economy:label.create_new" = "Create new product" "economy:label.price" = "Price" "economy:label.on_sale" = "On sale" -"economy:label.single_use" = "Only allow users to purchase once" "economy:label.stock" = "Stock" "economy:label.unlimited" = "Unlimited" "economy:label.fulfillment_style" = "Fulfillment style" "economy:label.use_automail" = "Use automail" "economy:label.automail_message" = "Automail message" "economy:action.buy" = "Buy" -"economy:label.already_purchased" = "Already purchased" -"economy:label.snippet_data" = "Snippet data" -"economy:action.apply" = "Apply" -"economy:action.unapply" = "Unapply" -"economy:label.thumbnails" = "Thumbnails" -"economy:label.my_ads" = "My ads" -"economy:label.create_new_ad" = "Create new advertisement" -"economy:label.target" = "Target URL" -"economy:label.image" = "Image" -"economy:label.size_base" = "Size base" -"economy:label.running" = "Running" -"economy:label.embed_ads_on_my_site" = "Embed ads on my site" diff --git a/crates/app/src/public/css/root.css b/crates/app/src/public/css/root.css index 1a53a2e..686cd49 100644 --- a/crates/app/src/public/css/root.css +++ b/crates/app/src/public/css/root.css @@ -201,7 +201,8 @@ p { .name { max-width: 250px; overflow: hidden; - white-space: nowrap; + /* overflow-wrap: break-word; */ + overflow-wrap: anywhere; text-overflow: ellipsis; } diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 52f3464..a3145db 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -1,10 +1,5 @@ @import url("root.css"); -/* ads */ -.tetratto_ad iframe { - border-radius: var(--radius); -} - /* media gallery */ .media_gallery { display: grid; @@ -17,9 +12,7 @@ @media screen and (max-width: 900px) { .media_gallery { - /* grid-auto-flow: row dense; */ /* safari is the most shit browser ever dude, this property causes safari to make images overlap for lord knows why */ - display: flex; - flex-direction: column; + grid-auto-flow: row dense; } } @@ -563,10 +556,6 @@ input[type="checkbox"]:checked { background-image: url("/icons/check.svg"); } -label { - cursor: pointer; -} - /* pillmenu */ .pillmenu { display: flex; diff --git a/crates/app/src/public/html/body.lisp b/crates/app/src/public/html/body.lisp index 599106e..705b14c 100644 --- a/crates/app/src/public/html/body.lisp +++ b/crates/app/src/public/html/body.lisp @@ -1,10 +1,5 @@ (div ("id" "toast_zone")) -; ads -(script ("src" "/js/ads.js?v=tetratto-{{ random_cache_breaker }}")) -(script - (text "TetrattoAds.init();")) - ; large text (text "{% if user and user.settings.large_text -%}") (style @@ -81,8 +76,6 @@ return; } - TetrattoAds.render_ads(\"{{ config.system_user }}\", \"\"); - atto.disconnect_observers(); atto.remove_false_options(); atto.clean_date_codes(); diff --git a/crates/app/src/public/html/chats/channels.lisp b/crates/app/src/public/html/chats/channels.lisp index ced2f93..fb61be0 100644 --- a/crates/app/src/public/html/chats/channels.lisp +++ b/crates/app/src/public/html/chats/channels.lisp @@ -24,18 +24,32 @@ (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") + (text "{% if user.id == channel.owner -%} {% if selected_community == 0 %}") (button - ("onclick" "trigger('atto::copy_text', ['{{ channel.id }}'])") - (icon (text "copy")) - (str (text "general:action.copy_id"))) - (text "{% if user.id == channel.owner or can_manage_channels -%}") - ; owner/manager controls - (button + ("class" "lowered small") ("onclick" "add_member('{{ channel.id }}')") (text "{{ icon \"user-plus\" }}") (span (text "{{ text \"chats:action.add_someone\" }}"))) + ; mute/unmute (button + ("class" "lowered small {% if channel.id in user.channel_mutes -%} hidden {%- endif %}") + ("ui_ident" "channel.mute:{{ channel.id }}") + ("onclick" "mute_channel('{{ channel.id }}')") + (icon (text "bell-off")) + (span + (str (text "chats:action.mute")))) + (button + ("class" "lowered small {% if not channel.id in user.channel_mutes -%} hidden {%- endif %}") + ("ui_ident" "channel.unmute:{{ channel.id }}") + ("onclick" "mute_channel('{{ channel.id }}', false)") + (icon (text "bell-ring")) + (span + (str (text "chats:action.unmute")))) + ; ... + (text "{%- endif %}") + (button + ("class" "lowered small") ("onclick" "update_channel_title('{{ channel.id }}')") (text "{{ icon \"pencil\" }}") (span @@ -46,32 +60,14 @@ (text "{{ icon \"trash\" }}") (span (text "{{ text \"general:action.delete\" }}"))) - (text "{%- endif %} {% if selected_community == 0 %}") - ; mute/unmute - (button - ("class" "{% if channel.id in user.channel_mutes -%} hidden {%- endif %}") - ("ui_ident" "channel.mute:{{ channel.id }}") - ("onclick" "mute_channel('{{ channel.id }}')") - (icon (text "bell-off")) - (span - (str (text "chats:action.mute")))) - (button - ("class" "{% if not channel.id in user.channel_mutes -%} hidden {%- endif %}") - ("ui_ident" "channel.unmute:{{ channel.id }}") - ("onclick" "mute_channel('{{ channel.id }}', false)") - (icon (text "bell-ring")) - (span - (str (text "chats:action.unmute")))) - ; ... - (text "{% if user.id != channel.owner -%}") - ; group chat member controls + (text "{% elif selected_community == 0 %}") (button ("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')") ("class" "red") (text "{{ icon \"door-open\" }}") (span (text "{{ text \"chats:action.leave\" }}"))) - (text "{%- endif %} {%- endif %}")))) + (text "{%- endif %}")))) (text "{% endfor %}")) (text "{% if selected_community == 0 and selected_channel -%}") (div diff --git a/crates/app/src/public/html/communities/create_post.lisp b/crates/app/src/public/html/communities/create_post.lisp index 62a1c00..454a868 100644 --- a/crates/app/src/public/html/communities/create_post.lisp +++ b/crates/app/src/public/html/communities/create_post.lisp @@ -182,7 +182,7 @@ (text "{%- endif %} {%- endif %}") (button ("class" "primary") - (str (text "communities:action.create"))))))) + (text "{{ text \"communities:action.create\" }}")))))) (text "{% if not quoting -%}") (script (text "async function create_post_from_form(e) { diff --git a/crates/app/src/public/html/communities/list.lisp b/crates/app/src/public/html/communities/list.lisp index e65cd8a..bb243c8 100644 --- a/crates/app/src/public/html/communities/list.lisp +++ b/crates/app/src/public/html/communities/list.lisp @@ -39,7 +39,7 @@ (span (text "Make this a forum community"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) (text "{% if list|length >= 4 -%} {{ components::supporter_ad(body=\"Become a supporter to create up to 10 communities!\") }} {%- endif %} {%- endif %}") (div ("class" "card_nest w_full") diff --git a/crates/app/src/public/html/communities/settings.lisp b/crates/app/src/public/html/communities/settings.lisp index 622b569..d55a324 100644 --- a/crates/app/src/public/html/communities/settings.lisp +++ b/crates/app/src/public/html/communities/settings.lisp @@ -1,6 +1,7 @@ (text "{% extends \"root.html\" %} {% block head %}") (title (text "Community settings - {{ config.name }}")) + (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") @@ -302,7 +303,7 @@ ("minlength" "2") ("maxlength" "32"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) (text "{% for channel in channels %}") (div ("class" "card_nest") @@ -446,7 +447,7 @@ (text "{{ text \"communities:label.upload\" }}"))) (form ("class" "card flex flex_col gap_2") - ("onsubmit" "create_emoji_from_form(event)") + ("onsubmit" "upload_emoji(event)") (div ("class" "flex flex_col gap_1") (label @@ -472,10 +473,11 @@ ("accept" "image/png,image/jpeg,image/avif,image/webp") ("class" "w_full"))) (button - (str (text "communities:action.create"))) + (text "{{ text \"communities:action.create\" }}")) (span ("class" "fade") - (text "Emojis can be a maximum of 256 KiB.")))) + (text "Emojis can be a maximum of 256 KiB, or 512x512px (width x + height).")))) (text "{% for emoji in emojis %}") (div ("class" "card secondary flex flex_wrap gap_2 items_center justify_between") @@ -504,7 +506,7 @@ (text "{{ text \"stacks:label.remove\" }}"))))) (text "{% endfor %}")) (script - (text "globalThis.create_emoji_from_form = (e) => { + (text "globalThis.upload_emoji = (e) => { e.preventDefault(); e.target.querySelector(\"button\").style.display = \"none\"; @@ -523,10 +525,6 @@ ]); e.target.querySelector(\"button\").removeAttribute(\"style\"); - - if (res.ok) { - e.target.reset(); - } }); alert(\"Emoji upload in progress. Please wait!\"); @@ -646,7 +644,7 @@ ("min" "0") ("max" "256"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) (text "{% for id, topic in community.topics %}") (div ("class" "card_nest") diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 6b2832a..f88a872 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -87,13 +87,13 @@ (span (text "{{ dislikes }}")) (text "{%- endif %}")) -(text "{%- endif %} {%- endmacro %} {% macro full_username(user, wrap=true, max_width=\"180px\") -%} {% if user and user.username -%}") +(text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%} {% if user and user.username -%}") (div - ("class" "flex {% if wrap -%} flex_wrap {%- endif %} items_center") + ("class" "flex items_center") (a ("href" "/@{{ user.username }}") - ("class" "name flush flex gap_1") - ("style" "font-weight: 600; max-width: {{ max_width }}") + ("class" "flush flex gap_1") + ("style" "font-weight: 600") ("target" "_top") (text "{% if user.settings.private_profile -%}") (span @@ -107,7 +107,7 @@ (text "{% else %}") (text "{{ self::username(user=user) }}") (text "{%- endif %}")) - (text "{{ self::online_indicator(user=user) }} {% if not user.settings.hide_username_badges -%} {% if user.is_verified -%}") + (text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}") (span ("title" "Verified") ("style" "color: var(--color-primary)") @@ -119,19 +119,13 @@ ("style" "color: var(--color-primary);") ("class" "flex items_center") (text "{{ icon \"star\" }}")) - (text "{%- endif %} {% if user.checkouts|length > 0 -%}") - (span - ("title" "Donator") - ("style" "color: var(--color-primary);") - ("class" "flex items_center") - (text "{{ icon \"hand-heart\" }}")) (text "{%- endif %} {% if user.permissions|has_staff_badge -%}") (span ("title" "Staff") ("style" "color: var(--color-primary);") ("class" "flex items_center") (text "{{ icon \"shield-user\" }}")) - (text "{%- endif %} {%- endif %}")) + (text "{%- endif %}")) (text "{%- endif %} {%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}") (div ("style" "display: contents") @@ -384,7 +378,7 @@ (text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}")) (text "{%- endif %}") (span - ; ("class" "name") + ("class" "name") (text "{{ self::full_username(user=owner) }}")) (text "{{ self::post_info(post=post, community=community) }}") (text "{% if post.context.is_pinned or post.context.is_profile_pinned -%} {{ icon \"pin\" }} {%- endif %}")) @@ -465,15 +459,14 @@ (text "{% if community and show_community and community.id != config.town_square or question %}")) (text "{%- endif %} {%- endmacro %}") -(text "{% macro post_media(upload_ids, custom_click=false) -%} {% if upload_ids|length > 0 -%}") +(text "{% macro post_media(upload_ids) -%} {% if upload_ids|length > 0 -%}") (div ("class" "media_gallery gap_2") (text "{% for upload in upload_ids %}") (img ("src" "/api/v1/uploads/{{ upload }}") - ("data-upload-id" "{{ upload }}") ("alt" "Image upload") - ("onclick" "{% if custom_click -%} {{ custom_click }} {%- else -%} trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}']) {%- endif %}")) + ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])")) (text "{% endfor %}")) (text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}") (div @@ -624,8 +617,9 @@ (div ("class" "flex items_center") (b - (text "{{ self::full_username(user=user) }}")) + (text "{{ self::username(user=user) }}")) (text "{{ self::online_indicator(user=user) }}")))) + (text "{%- endmacro %} {% macro pagination(page=0, items=0, key=\"\", value=\"\") -%}") (div ("class" "flex justify_between gap_2 w_full") @@ -894,7 +888,7 @@ (div ("class" "flex gap_2") (button - (str (text "communities:action.create"))) + (text "{{ text \"communities:action.create\" }}")) (text "{% if drawing_enabled -%}") (button @@ -1210,8 +1204,6 @@ (text "{%- endif %}")) (div ("class" "flex gap_2 hidden") - ("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'") - (text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}") (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}"))) (text "{%- endif %}") (div @@ -1234,12 +1226,14 @@ ("ui_ident" "emoji_{{ emoji }}") ("onclick" "trigger('me::message_react', [event.target.parentElement.parentElement, '{{ message.id }}', '{{ emoji }}'])") (span (text "{{ emoji|emojis|safe }} {{ num }}"))) - (text "{%- endfor %}"))) + (text "{%- endfor %}") + + (div + ("class" "hidden") + (text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}")))) (text "{% if grouped -%}") (div - ("class" "hidden flex gap_2 items_center") - ("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'") - (text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}") + ("class" "hidden") (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")) (text "{%- endif %}")))) @@ -1350,15 +1344,15 @@ (text "Listening to ")) (text "{{ other_user.connections.Spotify[1].data.artist }}"))) -(text "{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, show_kick=false, secondary=false, full=false) -%}") +(text "{%- endif %} {%- endmacro %} {% macro user_plate(user, show_menu=false, show_kick=false, secondary=false) -%}") (div - ("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 %}") (a ("href" "/@{{ user.username }}") (text "{{ self::avatar(username=user.username, size=\"42px\", selector_type=\"username\") }}")) (div ("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: 150px{%- endif %}") (text "{{ self::full_username(user=user) }}") (div ("class" "user_status") @@ -2491,13 +2485,6 @@ (text "Create infinite Littleweb sites")) (li (text "Create infinite Littleweb domains")) - (li - (text "Create and sell CSS snippet products")) - - (text "{% if config.enable_user_ads -%}") - (li - (text "No ads")) - (text "{%- endif %}") (text "{% if config.security.enable_invite_codes -%}") (li @@ -2714,33 +2701,3 @@ (icon (text "badge-cent")) (text "{{ product.price }}"))) (text "{%- endmacro %}") - -(text "{% macro ad_listing_card(ad) -%}") -(a - ("class" "card button lowered w_full flex flex_col gap_2") - ("href" "/product/ad/{{ ad.id }}/edit") - (b - ("class" "flex gap_2 items_center") - ("style" "height: 24px; text-decoration: {% if not ad.is_running -%} line-through {%- else -%} none {%- endif %}") - (icon (text "link")) - (text "{{ ad.target }}")) - (b - ("style" "height: 18px") - (text "{% if ad.is_running -%}") - (span - ("class" "green flex gap_2 items_center") - (icon (text "circle-check")) - (text "Running")) - (text "{% else %}") - (span - ("class" "red flex gap_2 items_center") - (icon (text "circle-x")) - (text "Not running")) - (text "{%- endif %}"))) -(text "{%- endmacro %}") - -(text "{% macro advertisement(size=\"Leaderboard\") -%}") -(text "{% if user and not is_supporter and config.enable_user_ads -%}") -(object ("class" "tetratto_ad") ("data-ad-size" "{{ size }}")) -(text "{%- endif %}") -(text "{%- endmacro %}") diff --git a/crates/app/src/public/html/developer/home.lisp b/crates/app/src/public/html/developer/home.lisp index 21dc65e..a5f31e9 100644 --- a/crates/app/src/public/html/developer/home.lisp +++ b/crates/app/src/public/html/developer/home.lisp @@ -54,7 +54,7 @@ ("placeholder" "redirect URL") ("minlength" "2"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) ; app listing (div diff --git a/crates/app/src/public/html/economy/ad.lisp b/crates/app/src/public/html/economy/ad.lisp deleted file mode 100644 index fe4581f..0000000 --- a/crates/app/src/public/html/economy/ad.lisp +++ /dev/null @@ -1,63 +0,0 @@ -(text "") -(html - ("lang" "en") - (head - (meta ("charset" "UTF-8")) - (meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0")) - (meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge"))) - (body - (a - ("href" "{% if not disable_click -%} {{ config.host }}/api/v1/ads/host/{{ host }}/{{ ad.id }}/click {%- endif %}") - ("title" "Advertisement") - ("target" "_blank") - ("class" "ad")) - - (span ("class" "display_tag") (text "Ad")) - - (style - (text "* { - margin: 0; - padding: 0; - box-sizing: border-box; - } - - html, - body { - line-height: 1.5; - letter-spacing: 0.15px; - font-family: - \"Inter\", \"Poppins\", \"Roboto\", ui-sans-serif, system-ui, sans-serif, - \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", - \"Noto Color Emoji\"; - } - - body { - overflow: hidden; - display: grid; - place-items: center; - } - - a.ad { - display: inline; - width: 100dvw; - height: 100dvh; - background-image: url(\"{{ config.host|safe }}/api/v1/uploads/{{ ad.upload_id }}\"); - background-position: center; - background-size: contain; - } - - .display_tag { - position: absolute; - top: 0.5rem; - left: 0.5rem; - padding: 0.15rem 0.5rem; - background: hsla(0, 0%, 0%, 50%); - color: white; - font-weight: 600; - font-size: 10px; - user-select: none; - pointer-events: none; - border-radius: 0.4rem; - box-shadow: 0 0 2px hsla(0, 0%, 0%, 25%); - opacity: 25%; - }")))) diff --git a/crates/app/src/public/html/economy/edit.lisp b/crates/app/src/public/html/economy/edit.lisp index 28e5db5..30accd2 100644 --- a/crates/app/src/public/html/economy/edit.lisp +++ b/crates/app/src/public/html/economy/edit.lisp @@ -4,23 +4,6 @@ (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main ("class" "flex flex_col gap_2") - (div - ("class" "card_nest") - (div - ("class" "card small flex items_center gap_2") - (icon (text "images")) - (b - (str (text "economy:label.thumbnails")))) - (div - ("class" "card flex flex_col gap_2") - (text "{{ components::post_media(upload_ids=product.uploads.thumbnails, custom_click=\"remove_thumbnail(event.target)\") }}") - (text "{% if product.uploads.thumbnails|length < 4 -%}") - (button - ("onclick" "add_thumbnail()") - (icon (text "plus")) - (str (text "communities:label.upload"))) - (text "{%- endif %}"))) - (div ("class" "card_nest") (div @@ -94,22 +77,10 @@ ("oninput" "event.preventDefault(); update_on_sale_from_form(event.target.checked)")) (span (str (text "economy:label.on_sale")))) - (label - ("for" "single_use") - ("class" "flex items_center gap_2") - (input - ("type" "checkbox") - ("id" "single_use") - ("name" "single_use") - ("class" "w_content") - ("checked" "{{ product.single_use }}") - ("oninput" "event.preventDefault(); update_single_use_from_form(event.target.checked)")) - (span - (str (text "economy:label.single_use")))) (div ("class" "flex flex_col gap_1") (label - ("for" "price") + ("for" "title") (str (text "economy:label.price"))) (input ("type" "number") @@ -147,7 +118,7 @@ (div ("class" "flex flex_col gap_1") (label - ("for" "stock") + ("for" "title") (str (text "economy:label.stock"))) (input ("type" "number") @@ -167,158 +138,44 @@ (icon (text "package-check")) (b (str (text "economy:label.fulfillment_style")))) - (div + (form ("class" "card flex flex_col gap_2") - (select - ("id" "fulfillment_style_select") - ("onchange" "mirror_fulfillment_style_select(true)") - (option ("value" "mail") (text "Mail") ("selected" "{{ not product.method == \"ProfileStyle\" }}")) - (option ("value" "snippet") (text "CSS Snippet") ("selected" "{{ product.method == \"ProfileStyle\" }}"))) - (form - ("class" "flex flex_col gap_2 hidden") - ("id" "mail_fulfillment") - ("onsubmit" "update_method_from_form(event)") - (p (text "If you choose to send an automated mail letter upon purchase, users will automatically receive the message you supply below.")) - (p (text "If you disable automail, you'll be required to manually mail users who have purchased your product before the transfer is finalized.")) - (text "{% set is_automail = product.method != \"ManualMail\" and product.method != \"ProfileStyle\" %}") + ("onsubmit" "update_method_from_form(event)") + (p (text "If you choose to send an automated mail letter upon purchase, users will automatically receive the message you supply below.")) + (p (text "If you disable automail, you'll be required to manually mail users who have purchased your product before the transfer is finalized.")) + (label + ("for" "use_automail") + ("class" "flex items_center gap_2") + (input + ("type" "checkbox") + ("id" "use_automail") + ("name" "use_automail") + ("class" "w_content") + ("oninput" "mirror_use_automail()") + ("checked" "{% if product.method != \"ManualMail\" -%} true {%- else -%} false {%- endif %}")) + (span + (str (text "economy:label.use_automail")))) + (div + ("class" "flex flex_col gap_1") (label - ("for" "use_automail") - ("class" "flex items_center gap_2") - (input - ("type" "checkbox") - ("id" "use_automail") - ("name" "use_automail") - ("class" "w_content") - ("oninput" "mirror_use_automail()") - ("checked" "{% if is_automail -%} true {%- else -%} false {%- endif %}")) - (span - (str (text "economy:label.use_automail")))) - (div - ("class" "flex flex_col gap_1") - (label - ("for" "automail_message") - (str (text "economy:label.automail_message"))) - (textarea - ("name" "automail_message") - ("id" "automail_message") - ("placeholder" "automail_message") - (text "{% if is_automail -%} {{ product.method.AutoMail }} {%- endif %}"))) - (button (str (text "general:action.save")))) - (form - ("class" "flex flex_col gap_2 hidden") - ("id" "snippet_fulfillment") - ("onsubmit" "update_data_from_form(event)") - (text "{{ components::supporter_ad(body=\"Become a supporter to create snippets!\") }}") - (div - ("class" "flex flex_col gap_1") - (label - ("for" "data") - (str (text "economy:label.snippet_data"))) - (textarea - ("name" "data") - ("id" "data") - ("placeholder" "data") - (text "{{ product.data }}"))) - (button (str (text "general:action.save")))))) + ("for" "automail_message") + (str (text "economy:label.automail_message"))) + (textarea + ("name" "automail_message") + ("id" "automail_message") + ("placeholder" "automail_message") + (text "{% if product.method != \"ManualMail\" -%} {{ product.method.AutoMail }} {%- endif %}"))) + (button (str (text "general:action.save"))))) - (div - ("class" "flex gap_2") - (a - ("class" "button secondary") - ("href" "/product/{{ product.id }}") - (icon (text "arrow-left")) - (str (text "general:action.back"))) - - (button - ("class" "lowered red") - ("onclick" "delete_product()") - (icon (text "trash")) - (str (text "general:action.delete"))))) + (a + ("class" "button secondary") + ("href" "/product/{{ product.id }}") + (icon (text "arrow-left")) + (str (text "general:action.back")))) (script - (text "async function add_thumbnail() { - await trigger(\"atto::debounce\", [\"products::update\"]); - - const picker = document.createElement(\"input\"); - picker.type = \"file\"; - picker.accept = \"image/*\"; - document.body.appendChild(picker); - picker.click(); - - picker.addEventListener(\"change\", () => { - // create body - const body = new FormData(); - - for (const file of picker.files) { - body.append(file.name, file); - } - - body.append( - \"body\", - JSON.stringify({ - target: \"Thumbnails\" - }), - ); - - // ... - picker.remove(); - fetch(\"/api/v1/products/{{ product.id }}/uploads\", { - method: \"POST\", - body, - }) - .then((res) => res.json()) - .then(async (res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - setTimeout(() => { - window.location.reload(); - }, 100); - } - }); - }); - } - - async function remove_thumbnail(target) { - await trigger(\"atto::debounce\", [\"products::update\"]); - - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(\"/api/v1/products/{{ product.id }}/uploads/thumbnails\", { - method: \"DELETE\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - idx: Array.from(target.parentElement.children).findIndex((x) => x.getAttribute(\"data-upload-id\") === target.getAttribute(\"data-upload-id\")), - }), - }) - .then((res) => res.json()) - .then(async (res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - setTimeout(() => { - window.location.reload(); - }, 100); - } - }); - } - - async function update_title_from_form(e) { + (text "async function update_title_from_form(e) { e.preventDefault(); await trigger(\"atto::debounce\", [\"products::update\"]); @@ -383,27 +240,6 @@ }); } - async function update_single_use_from_form(single_use) { - await trigger(\"atto::debounce\", [\"products::update\"]); - - fetch(\"/api/v1/products/{{ product.id }}/single_use\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - single_use, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - } - async function update_price_from_form(e) { e.preventDefault(); await trigger(\"atto::debounce\", [\"products::update\"]); @@ -470,49 +306,6 @@ }); } - async function update_data_from_form(e) { - e.preventDefault(); - await trigger(\"atto::debounce\", [\"products::update\"]); - - fetch(\"/api/v1/products/{{ product.id }}/data\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - data: e.target.data.value, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - } - - async function delete_product() { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(\"/api/v1/products/{{ product.id }}\", { - method: \"DELETE\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - globalThis.mirror_use_automail = () => { const use_automail = document.getElementById(\"use_automail\").checked; @@ -523,46 +316,7 @@ } } - globalThis.mirror_fulfillment_style_select = (send = false) => { - const selected = document.getElementById(\"fulfillment_style_select\").selectedOptions[0].value; - - if (selected === \"mail\") { - document.getElementById(\"mail_fulfillment\").classList.remove(\"hidden\"); - document.getElementById(\"snippet_fulfillment\").classList.add(\"hidden\"); - - if (send) { - update_method_from_form({ - preventDefault: () => {}, - target: document.getElementById(\"mail_fulfillment\"), - }); - } - } else { - document.getElementById(\"mail_fulfillment\").classList.add(\"hidden\"); - document.getElementById(\"snippet_fulfillment\").classList.remove(\"hidden\"); - - if (send) { - fetch(\"/api/v1/products/{{ product.id }}/method\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - method: \"ProfileStyle\", - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - } - } - } - setTimeout(() => { mirror_use_automail(); - mirror_fulfillment_style_select(); }, 150);")) (text "{% endblock %}") diff --git a/crates/app/src/public/html/economy/edit_ad.lisp b/crates/app/src/public/html/economy/edit_ad.lisp deleted file mode 100644 index 088a91a..0000000 --- a/crates/app/src/public/html/economy/edit_ad.lisp +++ /dev/null @@ -1,97 +0,0 @@ -(text "{% extends \"root.html\" %} {% block head %}") -(title - (text "Manage advertisement - {{ config.name }}")) -(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") -(main - ("class" "flex flex_col gap_2") - (div - ("class" "card_nest") - (div - ("class" "card small flex gap_2 items_center") - (icon (text "link")) - (b - (text "{{ ad.target }}"))) - (form - ("class" "card flex flex_col gap_2") - ("onsubmit" "event.preventDefault(); update_is_running_from_form(event.target.is_running.checked)") - (object ("class" "tetratto_ad") ("data-ad-size" "{{ ad.size }}") ("data-noclick" "true") ("data-ad-id" "{{ ad.id }}")) - (ul - (li - (text "{% if ad.last_charge_time != 0 -%}") - (text "Last charge: ") (span ("class" "date") (text "{{ ad.last_charge_time }}")) - (text "{% else %}") - (text "No previous charges") - (text "{%- endif %}"))) - (div ("class" "squig")) - (p (text "Each day your ad is viewed, you'll be charged 25 coins. This charge only applies to the very first view of the day.")) - (p (text "Additionally, you'll be charged 2 coins per click on your ad. This fee will be paid to the user which hosts the site your ad was shown on.")) - (p (text "Each of these transfers will be shown in your wallet's transfer table as either \"AdClick\" or \"AdCharge\".")) - (label - ("for" "is_running") - ("class" "flex items_center gap_2") - (input - ("type" "checkbox") - ("id" "is_running") - ("name" "is_running") - ("class" "w_content") - ("checked" "{{ ad.is_running }}")) - (span - (str (text "economy:label.running")))) - (button (str (text "general:action.save"))))) - - (div - ("class" "flex gap_2") - (a - ("class" "button secondary") - ("href" "/products") - (icon (text "arrow-left")) - (str (text "general:action.back"))) - - (button - ("class" "lowered red") - ("onclick" "delete_ad()") - (icon (text "trash")) - (str (text "general:action.delete"))))) - -(script - (text "async function update_is_running_from_form(is_running) { - await trigger(\"atto::debounce\", [\"products::update\"]); - fetch(\"/api/v1/ads/{{ ad.id }}/running\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - is_running, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - } - - async function delete_ad() { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(\"/api/v1/ads/{{ ad.id }}\", { - method: \"DELETE\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - };")) -(text "{% endblock %}") diff --git a/crates/app/src/public/html/economy/product.lisp b/crates/app/src/public/html/economy/product.lisp index b59e4fb..bcceb2b 100644 --- a/crates/app/src/public/html/economy/product.lisp +++ b/crates/app/src/public/html/economy/product.lisp @@ -4,7 +4,6 @@ (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}") (main ("class" "flex flex_col gap_2") - (text "{{ components::post_media(upload_ids=product.uploads.thumbnails) }}") (div ("class" "card flex flex_col gap_2") (h3 @@ -20,46 +19,21 @@ ("class" "card lowered w_full no_p_margin") (text "{{ product.description|markdown|safe }}")) - (text "{% if already_purchased -%}") - (span - ("class" "green flex items_center gap_2") - (icon (text "circle-check")) - (str (text "economy:label.already_purchased"))) - (text "{%- endif %}") - (div ("class" "flex gap_2 items_center") - (text "{% if user.id != product.owner -%}") - (text "{% if not already_purchased -%}") - ; price (a ("class" "button camo lowered") ("href" "/wallet") ("target" "_blank") (icon (text "badge-cent")) (text "{{ product.price }}")) - ; buy button + (text "{% if user.id != product.owner -%}") (button ("onclick" "purchase()") ("disabled" "{{ product.stock == 0 }}") (icon (text "piggy-bank")) (str (text "economy:action.buy"))) (text "{% else %}") - ; profile style snippets - (text "{% if product.method == \"ProfileStyle\" -%} {% if not product.id in applied_configurations_mapped -%}") - (button - ("onclick" "apply()") - (icon (text "check")) - (str (text "economy:action.apply"))) - (text "{% else %}") - (button - ("onclick" "remove()") - (icon (text "x")) - (str (text "economy:action.unapply"))) - (text "{%- endif %} {%- endif %}") - ; ... - (text "{%- endif %}") - (text "{% else %}") (a ("class" "button") ("href" "/product/{{ product.id }}/edit") @@ -88,59 +62,6 @@ res.ok ? \"success\" : \"error\", res.message, ]); - - if (res.ok) { - window.location.reload(); - } - }); - } - - async function apply() { - await trigger(\"atto::debounce\", [\"user::update\"]); - fetch(\"/api/v1/auth/user/{{ user.id }}/applied_configuration\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - \"type\": \"StyleSnippet\", - \"id\": \"{{ product.id }}\", - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - window.location.reload(); - } - }); - } - - async function remove() { - await trigger(\"atto::debounce\", [\"user::update\"]); - fetch(\"/api/v1/auth/user/{{ user.id }}/applied_configuration\", { - method: \"DELETE\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - \"id\": \"{{ product.id }}\", - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - window.location.reload(); - } }); }")) (text "{% endblock %}") diff --git a/crates/app/src/public/html/economy/products.lisp b/crates/app/src/public/html/economy/products.lisp index fcd69f0..c6f734a 100644 --- a/crates/app/src/public/html/economy/products.lisp +++ b/crates/app/src/public/html/economy/products.lisp @@ -43,7 +43,7 @@ ("minlength" "2") ("maxlength" "1024"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) ; product listing (div @@ -56,105 +56,7 @@ (div ("class" "card flex flex_col gap_2") (text "{% for item in list %} {{ components::product_listing_card(product=item, edit=true) }} {% endfor %}") - ; selective pagination - (text "{% if page_set_id == 0 -%}") - (text "{{ components::pagination(page=page, items=list|length) }}") - (text "{% else %}") - (text "{{ components::pagination(page=0, items=list|length) }}") - (text "{%- endif %}"))) - - (text "{% if config.enable_user_ads -%}") - (div ("class" "squig") ("style" "--background: var(--color-surface)")) - - ; create new ad - (div - ("class" "card_nest") - (div - ("class" "card small") - (b - (str (text "economy:label.create_new_ad")))) - (form - ("class" "card flex flex_col gap_2") - ("onsubmit" "create_ad_from_form(event)") - (div - ("class" "flex flex_col gap_1") - (label - ("for" "target") - (str (text "economy:label.target"))) - (input - ("type" "url") - ("name" "target") - ("id" "target") - ("placeholder" "target url") - ("required" "") - ("minlength" "2") - ("maxlength" "128"))) - (div - ("class" "flex flex_col gap_1") - (label - ("for" "file") - (str (text "economy:label.image"))) - (input - ("id" "file") - ("name" "file") - ("type" "file") - ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif") - ("required" "") - ("class" "w_content"))) - (div - ("class" "flex flex_col gap_1") - (label - ("for" "size_base") - (str (text "economy:label.size_base"))) - (select - ("id" "size_base") - ("name" "size_base") - (option ("value" "Leaderboard") (text "Leaderboard (720x90)")) - (option ("value" "Billboard") (text "Billboard (970x250)")) - (option ("value" "Skyscraper") (text "Skyscraper (160x600)")) - (option ("value" "MediumRectangle") (text "Medium rectangle (300x250)")) - (option ("value" "MobileLeaderboard") (text "Mobile leaderboard (320x50, mobile only)")))) - (button - (str (text "communities:action.create"))))) - - ; ad listing - (div - ("class" "card_nest") - (div - ("class" "card small flex items_center gap_2") - (icon (text "images")) - (str (text "economy:label.my_ads"))) - - (div - ("class" "card flex flex_col gap_2") - (text "{% for item in ads_list %} {{ components::ad_listing_card(ad=item) }} {% endfor %}") - ; selective pagination - (text "{% if page_set_id == 1 -%}") - (text "{{ components::pagination(page=page, items=ads_list|length, key=\"&page_set_id=1\") }}") - (text "{% else %}") - (text "{{ components::pagination(page=0, items=ads_list|length, key=\"&page_set_id=1\") }}") - (text "{%- endif %}"))) - - (div - ("class" "card_nest") - (div - ("class" "card small flex items_center gap_2") - (icon (text "code")) - (str (text "economy:label.embed_ads_on_my_site"))) - - (div - ("class" "card flex flex_col gap_2") - (p (text "You can embed the advertising network into your site to earn a (coin) commission from clicks.")) - (p (text "Place the following into your site's HTML:")) - (pre (code (text "<script src=\"{{ config.host }}\"/js/ads.js\"></script> -<script>TetrattoAds.init(); TetrattoAds.render_ads(\"{{ user.id }}\", \"{{ config.host }}\")</script>"))) - (p (text "After you've done that, you can place your ads like so:")) - (pre (code (text "<object class=\"tetratto_ad\" data-ad-size=\"$size$\"></object>"))) - (p - (text "In the above example, replace \"$size$\" with a size from ") - (a ("href" "https://tetratto.com/reference/tetratto/model/economy/enum.UserAdSize.html") (text "here")) - (text " keep in mind that the name of a size must be in title case. That means it's \"Leaderboard\", not \"leaderboard\".")))) - (text "{%- endif %}")) + (text "{{ components::pagination(page=page, items=list|length) }}")))) (script (text "async function create_product_from_form(e) { @@ -185,43 +87,5 @@ }, 100); } }); - } - - async function create_ad_from_form(e) { - e.preventDefault(); - await trigger(\"atto::debounce\", [\"products::create\"]); - - // create body - const body = new FormData(); - - for (const file of e.target.file.files) { - body.append(file.name, file); - } - - body.append( - \"body\", - JSON.stringify({ - target: e.target.target.value, - size: e.target.size_base.selectedOptions[0].value, - }), - ); - - // ... - fetch(\"/api/v1/ads\", { - method: \"POST\", - body, - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - e.target.reset(); - window.location.reload(); - } - }); }")) (text "{% endblock %}") diff --git a/crates/app/src/public/html/economy/wallet.lisp b/crates/app/src/public/html/economy/wallet.lisp index 0981f09..0fb5b92 100644 --- a/crates/app/src/public/html/economy/wallet.lisp +++ b/crates/app/src/public/html/economy/wallet.lisp @@ -11,13 +11,7 @@ (span ("class" "flex items_center gap_2") (icon (text "piggy-bank")) - (span (str (text "general:link.wallet")))) - - (button - ("class" "lowered small square tiny big_icon") - ("onclick" "document.getElementById('buy_dialog').showModal()") - ("title" "Buy coins") - (icon (text "plus")))) + (span (str (text "general:link.wallet"))))) (div ("class" "card lowered flex flex_col gap_4") (button @@ -38,7 +32,7 @@ (icon (text "clock")) (span (str (text "economy:label.recent_transfers"))))) (div - ("class" "card flex flex_col gap_4") + ("class" "card lowered flex flex_col gap_4") (div ("class" "w_full") ("style" "overflow: auto") @@ -49,39 +43,26 @@ (th (text "Sender")) (th (text "Receiver")) (th (text "Amount")) - (th (text "Product")) - (th (text "Source")) - (th (text "Actions"))) + (th (text "Product"))) (tbody (text "{% for transfer in list -%}") (tr - (td (span ("class" "date short") (text "{{ transfer[3].created }}"))) - (td ("class" "w_content") (text "{{ components::full_username(user=transfer[0], wrap=false) }}")) - (td ("class" "w_content") (text "{{ components::full_username(user=transfer[1], wrap=false) }}")) + (td (span ("class" "date short") (text "{{ transfer[1] }}"))) + (td ("class" "w_content") (text "{{ components::full_username(user=transfer[3]) }}")) + (td ("class" "w_content") (text "{{ components::full_username(user=transfer[4]) }}")) (td ("class" "flex items_center gap_1") - (text "{{ transfer[3].amount }}") - (text "{% if transfer[3].is_pending -%}") + (text "{{ transfer[2] }}") + (text "{% if transfer[6] -%}") (span ("class" "flex items_center gap_1") ("title" "Pending") (icon (text "clock"))) (text "{%- endif %}")) (td - (text "{% if transfer[2] -%}") + (text "{% if transfer[5] -%}") (a - ("href" "/product/{{ transfer[2].id }}") + ("href" "/product/{{ transfer[5].id }}") (icon (text "external-link"))) - (text "{%- endif %}")) - (td (text "{{ transfer[3].source }}")) - (td - (text "{% if user.id == transfer[1].id and transfer[3].source == '\"Sale\"' -%}") - ; we're the receiver - (button - ("class" "small tiny square raised camo big_icon") - ("onclick" "issue_refund('{{ transfer[3].id }}')") - ("title" "Issue refund") - (icon (text "undo"))) (text "{%- endif %}"))) - (text "{%- endfor %}")))) - (text "{{ components::pagination(page=page, items=list|length) }}")))) + (text "{%- endfor %}"))))))) (dialog ("id" "buy_dialog") @@ -131,23 +112,5 @@ window.location.href = res.payload; } }); - } - - globalThis.issue_refund = async (transfer) => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(`/api/v1/transfers/${transfer}/refund`, { - method: \"POST\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [res.ok ? \"success\" : \"error\", res.message]); - }); }")) (text "{% endblock %}") diff --git a/crates/app/src/public/html/forge/home.lisp b/crates/app/src/public/html/forge/home.lisp index 56f1d01..a25cf5d 100644 --- a/crates/app/src/public/html/forge/home.lisp +++ b/crates/app/src/public/html/forge/home.lisp @@ -30,7 +30,7 @@ ("minlength" "2") ("maxlength" "32"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) (text "{% else %}") (text "{{ components::developer_pass_ad(body=\"Get a developer pass to create forges!\") }}") (text "{%- endif %}") diff --git a/crates/app/src/public/html/littleweb/domains.lisp b/crates/app/src/public/html/littleweb/domains.lisp index c0a3779..b39c716 100644 --- a/crates/app/src/public/html/littleweb/domains.lisp +++ b/crates/app/src/public/html/littleweb/domains.lisp @@ -59,7 +59,7 @@ (option ("value" "{{ tld }}") (text ".{{ tld|lower }}")) (text "{%- endfor %}"))) (button - (str (text "communities:action.create"))) + (text "{{ text \"communities:action.create\" }}")) (details (summary diff --git a/crates/app/src/public/html/littleweb/services.lisp b/crates/app/src/public/html/littleweb/services.lisp index 1325780..1b7eea0 100644 --- a/crates/app/src/public/html/littleweb/services.lisp +++ b/crates/app/src/public/html/littleweb/services.lisp @@ -45,7 +45,7 @@ ("minlength" "2") ("maxlength" "32"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) (text "{%- endif %}") (div ("class" "card_nest w_full") diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp index cadeeba..ab7c3af 100644 --- a/crates/app/src/public/html/macros.lisp +++ b/crates/app/src/public/html/macros.lisp @@ -79,7 +79,6 @@ ("href" "/mail") (icon (text "mail")) (str (text "general:link.mail"))) - (text "{% if config.stripe -%}") (a ("href" "/wallet") (icon (text "piggy-bank")) @@ -88,7 +87,6 @@ ("href" "/products") (icon (text "store")) (str (text "economy:label.my_products"))) - (text "{%- endif %}") (a ("href" "/journals/0/0") (icon (text "notebook")) diff --git a/crates/app/src/public/html/mod/file_report.lisp b/crates/app/src/public/html/mod/file_report.lisp index 154583f..f1748c0 100644 --- a/crates/app/src/public/html/mod/file_report.lisp +++ b/crates/app/src/public/html/mod/file_report.lisp @@ -28,7 +28,7 @@ ("required" "") ("minlength" "16"))) (button - (str (text "communities:action.create")))))) + (text "{{ text \"communities:action.create\" }}"))))) (script (text "function create_report_from_form(e) { diff --git a/crates/app/src/public/html/mod/ip_bans.lisp b/crates/app/src/public/html/mod/ip_bans.lisp index d48b3cf..c612cf9 100644 --- a/crates/app/src/public/html/mod/ip_bans.lisp +++ b/crates/app/src/public/html/mod/ip_bans.lisp @@ -19,7 +19,7 @@ ("class" "lowered small") (text "{{ icon \"plus\" }}") (span - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) (div ("class" "card flex flex_col gap_2") (text "{% for item in items %}") diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp index 7e12f8c..8dcf616 100644 --- a/crates/app/src/public/html/mod/profile.lisp +++ b/crates/app/src/public/html/mod/profile.lisp @@ -84,7 +84,7 @@ const ui = await ns(\"ui\"); const element = document.getElementById(\"mod_options\"); - globalThis.profile_request = async (do_confirm, path, body = null, headers = { \"Content-Type\": \"application/json\" }, method = \"POST\") => { + globalThis.profile_request = async (do_confirm, path, body) => { if (do_confirm) { if ( !(await trigger(\"atto::confirm\", [ @@ -96,9 +96,11 @@ } fetch(`/api/v1/auth/user/{{ profile.id }}/${path}`, { - method, - headers: headers != null ? headers : undefined, - body: body != null ? JSON.stringify(body) : undefined, + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify(body), }) .then((res) => res.json()) .then((res) => { @@ -263,15 +265,9 @@ (span (text "{{ text \"mod_panel:label.associations\" }}")))) (div - ("class" "card flex flex_wrap gap_4 flex_collapse") + ("class" "card lowered flex flex_wrap gap_2") (text "{% for user in associations -%}") - (div - ("class" "flex flex_row gap_2 items_center card small secondary") - (text "{{ components::user_plate(user=user, show_menu=false, secondary=true, full=true) }}") - (button - ("class" "small square red lowered") - ("onclick" "profile_request(true, 'associations/{{ user.id }}', null, null, 'DELETE')") - (icon (text "x")))) + (text "{{ components::user_plate(user=user, show_menu=false) }}") (text "{%- endfor %}"))) (text "{% if invite -%}") (div diff --git a/crates/app/src/public/html/mod/warnings.lisp b/crates/app/src/public/html/mod/warnings.lisp index d84ab53..c5b783a 100644 --- a/crates/app/src/public/html/mod/warnings.lisp +++ b/crates/app/src/public/html/mod/warnings.lisp @@ -37,7 +37,7 @@ ("minlength" "2") ("maxlength" "4096"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) (div ("class" "card_nest") (div diff --git a/crates/app/src/public/html/post/post.lisp b/crates/app/src/public/html/post/post.lisp index e53ca2e..1eae1ab 100644 --- a/crates/app/src/public/html/post/post.lisp +++ b/crates/app/src/public/html/post/post.lisp @@ -88,7 +88,7 @@ ("class" "flex gap_2") (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}") (button - (str (text "communities:action.create")))))) + (text "{{ text \"communities:action.create\" }}"))))) (text "{%- endif %}") (div ("class" "pillmenu") diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp index 1fde607..c1788b3 100644 --- a/crates/app/src/public/html/profile/base.lisp +++ b/crates/app/src/public/html/profile/base.lisp @@ -304,7 +304,7 @@ globalThis.request_transfer = async () => { await trigger(\"atto::debounce\", [\"economy::transfer\"]); - const amount = Number.parseInt((await trigger(\"atto::prompt\", [\"Request amount:\"])) || \"0\"); + const amount = Number.parseInt((await trigger(\"atto::prompt\", [\"Request amount:\"])) || \"\"); if (amount === 0) { return; @@ -468,12 +468,6 @@ ("class" "rhs w_full flex flex_col gap_4") (text "{% block content %}{% endblock %}"))))) -(text "{% if not use_user_theme -%}") -(text "{% for cnf in applied_configurations -%}") -(text "{{ cnf|safe }}") -(text "{%- endfor %}") -(text "{%- endif %}") - (text "{% if not is_self and profile.settings.warning -%}") (script (text "setTimeout(() => { diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index 8bda6c2..0356283 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -1162,26 +1162,6 @@ ("class" "fade") (text "This represents the site theme shown to users viewing your profile."))))) - (text "{% if profile.applied_configurations|length > 0 -%}") - (div - ("class" "card_nest") - ("ui_ident" "applied_configurations") - (div - ("class" "card small flex items_center gap_2") - (icon (text "cog")) - (str (text "setttings:label.applied_configurations"))) - (div - ("class" "card") - (p (text "Products that you have purchased and applied to your profile are displayed below. Snippets are always synced to the product, meaning the owner could update it at any time.")) - (ul - (text "{% for cnf in profile.applied_configurations -%}") - (li - (text "{{ cnf[0] }} ") - (a - ("href" "/product/{{ cnf[1] }}") - (text "{{ cnf[1] }}"))) - (text "{%- endfor %}")))) - (text "{%- endif %}") (button ("onclick" "save_settings()") ("id" "save_button") @@ -1762,7 +1742,6 @@ \"import_export\", \"theme_preference\", \"profile_theme\", - \"applied_configurations\", ]); ui.generate_settings_ui( @@ -1927,14 +1906,6 @@ \"{{ profile.settings.hide_social_follows }}\", \"checkbox\", ], - [ - [ - \"hide_username_badges\", - \"Hide badges from your username (outside of your profile)\", - ], - \"{{ profile.settings.hide_username_badges }}\", - \"checkbox\", - ], [[], \"Questions\", \"title\"], [ [ diff --git a/crates/app/src/public/html/root.lisp b/crates/app/src/public/html/root.lisp index 4fc5b81..9a253fe 100644 --- a/crates/app/src/public/html/root.lisp +++ b/crates/app/src/public/html/root.lisp @@ -151,53 +151,7 @@ } }); }")))))) - (text "{% elif user and renew_policy_consent -%}") - ; renew policy consent - (article - (main - (div - ("class" "card_nest") - (div - ("class" "card small flex items_center gap_2") - (icon (text "scroll-text")) - (text "Our policies have been updated!")) - - (div - ("class" "card flex flex_col gap_2 no_p_margin") - (p (text "Your consent is needed for the updated versions of our Terms of Service and Privacy Policy. Please reread them and click \"Accept\" if you agree to these updated terms.")) - (ul - (li - (a - ("href" "{{ config.policies.terms_of_service }}") - (text "Terms of service"))) - (li - (a - ("href" "{{ config.policies.privacy }}") - (text "Privacy policy")))) - (hr ("class" "margin")) - (button - ("onclick" "update_policy_consent()") - (icon (text "check")) - (str (text "general:action.accept"))))))) - - (script - (text "globalThis.update_policy_consent = async () => { - fetch(\"/api/v1/auth/user/me/policy_consent\", { - method: \"POST\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - window.location.reload(); - } - }); - };")) - (text "{% elif user and user.is_deactivated -%}") + (text "{% elif user.is_deactivated -%}") ; account deactivated message (article (main diff --git a/crates/app/src/public/html/stacks/list.lisp b/crates/app/src/public/html/stacks/list.lisp index 27714ce..dbcf944 100644 --- a/crates/app/src/public/html/stacks/list.lisp +++ b/crates/app/src/public/html/stacks/list.lisp @@ -29,7 +29,7 @@ ("minlength" "2") ("maxlength" "32"))) (button - (str (text "communities:action.create"))))) + (text "{{ text \"communities:action.create\" }}")))) (text "{%- endif %}") (div ("class" "card_nest w_full") diff --git a/crates/app/src/public/html/timelines/all.lisp b/crates/app/src/public/html/timelines/all.lisp index abf3ede..b3235b9 100644 --- a/crates/app/src/public/html/timelines/all.lisp +++ b/crates/app/src/public/html/timelines/all.lisp @@ -4,9 +4,7 @@ (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex_col gap_2") - (text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\", forum_posts=\"/all/forum_posts\") }}") - (text "{{ components::advertisement(size=\"Leaderboard\") }}") - (text "{% if not user -%}") + (text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\", forum_posts=\"/all/forum_posts\") }} {% if not user -%}") (div ("class" "card_nest") (div diff --git a/crates/app/src/public/js/ads.js b/crates/app/src/public/js/ads.js deleted file mode 100644 index 654ae72..0000000 --- a/crates/app/src/public/js/ads.js +++ /dev/null @@ -1,65 +0,0 @@ -globalThis.TetrattoAds = { - AD_SIZES: { - Billboard: [970, 250], - Leaderboard: [720, 90], - Skyscraper: [160, 600], - MediumRectangle: [300, 250], - MobileLeaderboard: [320, 50], - }, - IS_MOBILE: window.innerWidth <= 900 && window.innerHeight <= 900, -}; - -globalThis.TetrattoAds.init = () => { - const styles = document.createElement("style"); - styles.id = "tetratto_ads_css"; - styles.setAttribute("data-turbo-permanent", "true"); - - styles.innerHTML = `.tetratto_ad { - width: 100%; - display: grid; - place-items: center; - } - - .tetratto_ad, - .tetratto_ad iframe { - max-width: 100%; - background: transparent; - }`; - - document.head.appendChild(styles); -}; - -globalThis.TetrattoAds.render_ads = ( - host_id = 0, - tetratto = "https://tetratto.com", -) => { - for (const element of Array.from( - document.querySelectorAll(".tetratto_ad"), - )) { - if (element.children.length > 0) { - continue; - } - - const iframe = document.createElement("iframe"); - let size = element.getAttribute("data-ad-size") || "MediumRectangle"; - - if (size === "Leaderboard" && TetrattoAds.IS_MOBILE) { - size = "MobileLeaderboard"; - } - - const size_px = TetrattoAds.AD_SIZES[size]; - - const noclick = - element.getAttribute("data-noclick") === "true" || false; - const ad_id = element.getAttribute("data-ad-id"); - - iframe.src = `${tetratto}/adn/${ad_id ? ad_id : "random"}?size=${size}&host=${host_id}&noclick=${noclick}`; - iframe.setAttribute("frameborder", "0"); - iframe.loading = "lazy"; - - iframe.style.width = `${size_px[0]}px`; - iframe.style.height = `${size_px[1]}px`; - - element.appendChild(iframe); - } -}; diff --git a/crates/app/src/routes/api/v1/ads.rs b/crates/app/src/routes/api/v1/ads.rs deleted file mode 100644 index 505b49e..0000000 --- a/crates/app/src/routes/api/v1/ads.rs +++ /dev/null @@ -1,195 +0,0 @@ -use crate::{ - cookie::CookieJar, - get_user_from_token, - image::{save_webp_buffer, JsonMultipart}, - State, -}; -use axum::{ - extract::Path, - response::{Html, IntoResponse}, - Extension, Json, - http::StatusCode, -}; -use tetratto_core::model::{ - economy::UserAd, - oauth, - uploads::{MediaType, MediaUpload}, - ApiReturn, Error, -}; -use super::{CreateAd, UpdateAdIsRunning}; - -const MAXIMUM_AD_FILE_SIZE: usize = 2_097_152; - -pub async fn create_request( - jar: CookieJar, - Extension(data): Extension, - JsonMultipart(bytes_parts, req): JsonMultipart, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - // get file - let file = match bytes_parts.get(0) { - Some(x) => x, - None => return Json(Error::Unknown.into()), - }; - - if file.len() > MAXIMUM_AD_FILE_SIZE { - return Json(Error::FileTooLarge.into()); - } - - let upload = match data - .create_upload(MediaUpload::new(MediaType::Webp, user.id)) - .await - { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - match data - .create_ad(UserAd::new(user.id, upload.id, req.target, req.size)) - .await - { - Ok(_) => { - // write image - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None) - { - return Json(Error::MiscError(e.to_string()).into()); - } - - // ... - Json(ApiReturn { - ok: true, - message: "Ad created".to_string(), - payload: (), - }) - } - Err(e) => Json(e.into()), - } -} - -pub async fn delete_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.delete_ad(id, &user).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Ad deleted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn update_is_running_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let ad = match data.get_ad_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if !ad.is_running && user.coins < 50 { - return Json( - Error::MiscError( - "You must have a minimum of 50 coins in your balance to run ads".to_string(), - ) - .into(), - ); - } - - match data - .update_ad_is_running(id, &user, if req.is_running { 1 } else { 0 }) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Ad updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn click_request( - jar: CookieJar, - Extension(data): Extension, - Path((host, id)): Path<(usize, usize)>, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = get_user_from_token!(jar, data); - - let bad_return = ( - StatusCode::FOUND, - [ - ( - "Set-Cookie".to_string(), - format!( - "Atto-Clicked=true; Path=/api/v1/ads/host/{host}/{id}/click; Max-Age=86400" - ), - ), - ("Location".to_string(), data.0.0.host.clone()), - ], - Html(""), - ); - - if jar.get("Atto-Clicked").is_some() { - // we've already clicked this ad, don't charge - match data.get_ad_by_id(id).await { - Ok(x) => { - return ( - StatusCode::FOUND, - [ - ( - "Set-Cookie".to_string(), - format!( - "Atto-Clicked=true; Path=/api/v1/ads/host/{host}/{id}/click; Max-Age=86400" - ), - ), - ("Location".to_string(), x.target), - ], - Html(""), - ); - } - Err(_) => return bad_return, - } - } - - match data.ad_click(host, id, user).await { - Ok(t) => ( - StatusCode::FOUND, - [ - ( - "Set-Cookie".to_string(), - format!( - "Atto-Clicked=true; Path=/api/v1/ads/host/{host}/{id}/click; Max-Age=86400" - ), - ), - ("Location".to_string(), t), - ], - Html(""), - ), - Err(_) => bad_return, - } -} diff --git a/crates/app/src/routes/api/v1/auth/connections/stripe.rs b/crates/app/src/routes/api/v1/auth/connections/stripe.rs index 7721f6c..58aa1e8 100644 --- a/crates/app/src/routes/api/v1/auth/connections/stripe.rs +++ b/crates/app/src/routes/api/v1/auth/connections/stripe.rs @@ -7,7 +7,7 @@ use axum::{ }; use tetratto_core::model::{ auth::{Notification, User}, - economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource}, + economy::{CoinTransfer, CoinTransferMethod}, moderation::AuditLogEntry, permissions::{FinePermission, SecondaryPermission}, ApiReturn, Error, @@ -190,7 +190,7 @@ pub async fn stripe_webhook( return Json(e.into()); } - if user.awaiting_purchase { + if data.0.0.security.enable_invite_codes && user.awaiting_purchase { if let Err(e) = data .update_user_awaiting_purchased_status(user.id, false, user.clone(), false) .await @@ -635,7 +635,6 @@ pub async fn handle_stupid_fucking_checkout_success_session( user.id, 100, CoinTransferMethod::Transfer, - CoinTransferSource::Purchase, ), true, ) @@ -652,7 +651,6 @@ pub async fn handle_stupid_fucking_checkout_success_session( user.id, 400, CoinTransferMethod::Transfer, - CoinTransferSource::Purchase, ), true, ) diff --git a/crates/app/src/routes/api/v1/auth/mod.rs b/crates/app/src/routes/api/v1/auth/mod.rs index 5ab25e4..dff259e 100644 --- a/crates/app/src/routes/api/v1/auth/mod.rs +++ b/crates/app/src/routes/api/v1/auth/mod.rs @@ -84,6 +84,7 @@ pub async fn register_request( // ... let mut user = User::new(props.username.to_lowercase(), props.password); + user.settings.policy_consent = true; // check invite code if data.0.0.security.enable_invite_codes { diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index e378567..ba3c17a 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -3,11 +3,10 @@ use crate::{ get_user_from_token, model::{ApiReturn, Error}, routes::api::v1::{ - AddAppliedConfiguration, AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, - RefreshGrantToken, RemoveAppliedConfiguration, UpdateSecondaryUserRole, - UpdateUserAwaitingPurchase, UpdateUserBanExpire, UpdateUserBanReason, UpdateUserInviteCode, - UpdateUserIsDeactivated, UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, - UpdateUserUsername, + AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken, + UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserBanExpire, + UpdateUserBanReason, UpdateUserInviteCode, UpdateUserIsDeactivated, UpdateUserIsVerified, + UpdateUserPassword, UpdateUserRole, UpdateUserUsername, }, State, }; @@ -25,7 +24,6 @@ use tetratto_core::{ cache::Cache, model::{ auth::{AchievementName, InviteCode, Token, UserSettings, SELF_SERVE_ACHIEVEMENTS}, - economy::CoinTransferMethod, moderation::AuditLogEntry, oauth, permissions::FinePermission, @@ -112,29 +110,6 @@ pub async fn me_request(jar: CookieJar, Extension(data): Extension) -> im }) } -pub async fn policy_consent_request( - jar: CookieJar, - Extension(data): Extension, -) -> 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_user_last_policy_consent(user.id, unix_epoch_timestamp() as i64) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Consent given".to_string(), - payload: Some(user), - }), - Err(e) => Json(e.into()), - } -} - /// Update the settings of the given user. pub async fn update_user_settings_request( jar: CookieJar, @@ -205,108 +180,8 @@ pub async fn update_user_settings_request( } } -/// Add the given applied configuration. -pub async fn add_applied_configuration_request( - jar: CookieJar, - Path(id): Path, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProfile) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if user.id != id && !user.permissions.check(FinePermission::MANAGE_USERS) { - return Json(Error::NotAllowed.into()); - } - - let product_id: usize = match req.id.parse() { - Ok(x) => x, - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - }; - - let product = match data.get_product_by_id(product_id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if data - .get_transfer_by_sender_method(user.id, CoinTransferMethod::Purchase(product.id)) - .await - .is_err() - { - return Json(Error::NotAllowed.into()); - } - - // update - user.applied_configurations.push((req.r#type, product.id)); - - // ... - match data - .update_user_applied_configurations(id, user.applied_configurations) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Applied configurations updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -/// Remove the given applied configuration. -pub async fn remove_applied_configuration_request( - jar: CookieJar, - Path(id): Path, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProfile) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if user.id != id && !user.permissions.check(FinePermission::MANAGE_USERS) { - return Json(Error::NotAllowed.into()); - } - - let product_id: usize = match req.id.parse() { - Ok(x) => x, - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - }; - - // update - user.applied_configurations.remove( - match user - .applied_configurations - .iter() - .position(|x| x.1 == product_id) - { - Some(x) => x, - None => return Json(Error::GeneralNotFound("configuration".to_string()).into()), - }, - ); - - // ... - match data - .update_user_applied_configurations(id, user.applied_configurations) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Applied configurations updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - /// Append associations to the current user. -pub async fn append_association_request( +pub async fn append_associations_request( jar: CookieJar, Extension(data): Extension, Json(req): Json, @@ -354,50 +229,6 @@ pub async fn append_association_request( } } -/// Remove an association from the given user. -pub async fn remove_association_request( - jar: CookieJar, - Extension(data): Extension, - Path((uid, association)): Path<(usize, usize)>, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProfile) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if !user.permissions.check(FinePermission::MANAGE_USERS) { - return Json(Error::NotAllowed.into()); - } - - // get user - let mut other_user = match data.get_user_by_id(uid).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - // find association and remove - other_user.associated.remove( - match other_user.associated.iter().position(|x| x == &association) { - Some(x) => x, - None => return Json(Error::GeneralNotFound("association".to_string()).into()), - }, - ); - - // ... - match data - .update_user_associated(other_user.id, other_user.associated) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Associations updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - /// Update the password of the given user. /// /// Does not support third-party grants. diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 294ebb9..3a3b081 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -1,4 +1,3 @@ -pub mod ads; pub mod app_data; pub mod apps; pub mod auth; @@ -26,13 +25,13 @@ use axum::{ use serde::Deserialize; use tetratto_core::model::{ apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota, DeveloperPassStorageQuota}, - auth::{AchievementName, AppliedConfigType}, + auth::AchievementName, communities::{ CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess, PollOption, PostContext, }, communities_permissions::CommunityPermission, - economy::{ProductFulfillmentMethod, UserAdSize}, + economy::ProductFulfillmentMethod, journals::JournalPrivacyPermission, littleweb::{DomainData, DomainTld, ServiceFsEntry}, oauth::AppScope, @@ -302,10 +301,6 @@ pub fn routes() -> Router { ) // profile .route("/auth/user/me", get(auth::profile::me_request)) - .route( - "/auth/user/me/policy_consent", - 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)) @@ -338,14 +333,6 @@ pub fn routes() -> Router { "/auth/user/{id}/settings", post(auth::profile::update_user_settings_request), ) - .route( - "/auth/user/{id}/applied_configuration", - post(auth::profile::add_applied_configuration_request), - ) - .route( - "/auth/user/{id}/applied_configuration", - delete(auth::profile::remove_applied_configuration_request), - ) .route( "/auth/user/{id}/role", post(auth::profile::update_user_role_request), @@ -402,10 +389,6 @@ pub fn routes() -> Router { "/auth/user/{id}/totp/codes", post(auth::profile::refresh_totp_codes_request), ) - .route( - "/auth/user/{id}/associations/{association}", - delete(auth::profile::remove_association_request), - ) .route( "/auth/user/{username}/totp/check", get(auth::profile::has_totp_enabled_request), @@ -413,7 +396,7 @@ pub fn routes() -> Router { .route("/auth/user/me/seen", post(auth::profile::seen_request)) .route( "/auth/user/me/append_associations", - put(auth::profile::append_association_request), + put(auth::profile::append_associations_request), ) .route("/auth/user/find/{id}", get(auth::profile::redirect_from_id)) .route( @@ -736,10 +719,6 @@ pub fn routes() -> Router { // transfers .route("/transfers", post(transfers::create_request)) .route("/transfers/ask", post(transfers::ask_request)) - .route( - "/transfers/{id}/refund", - post(transfers::create_refund_request), - ) // products .route("/products", post(products::create_request)) .route("/products/{id}", delete(products::delete_request)) @@ -749,34 +728,16 @@ pub fn routes() -> Router { "/products/{id}/description", post(products::update_description_request), ) - .route("/products/{id}/data", post(products::update_data_request)) .route( "/products/{id}/on_sale", post(products::update_on_sale_request), ) - .route( - "/products/{id}/single_use", - post(products::update_single_use_request), - ) .route("/products/{id}/price", post(products::update_price_request)) .route( "/products/{id}/method", post(products::update_method_request), ) .route("/products/{id}/stock", post(products::update_stock_request)) - .route( - "/products/{id}/uploads", - post(products::update_uploads_request), - ) - .route( - "/products/{id}/uploads/thumbnails", - delete(products::remove_thumbnail_request), - ) - // ads - .route("/ads", post(ads::create_request)) - .route("/ads/{id}", delete(ads::delete_request)) - .route("/ads/{id}/running", post(ads::update_is_running_request)) - .route("/ads/host/{host}/{id}/click", get(ads::click_request)) } pub fn lw_routes() -> Router { @@ -1309,21 +1270,11 @@ pub struct UpdateProductDescription { pub description: String, } -#[derive(Deserialize)] -pub struct UpdateProductData { - pub data: String, -} - #[derive(Deserialize)] pub struct UpdateProductOnSale { pub on_sale: bool, } -#[derive(Deserialize)] -pub struct UpdateProductSingleUse { - pub single_use: bool, -} - #[derive(Deserialize)] pub struct UpdateProductPrice { pub price: i32, @@ -1338,41 +1289,3 @@ pub struct UpdateProductMethod { pub struct UpdateProductStock { pub stock: i32, } - -#[derive(Deserialize)] -pub struct AddAppliedConfiguration { - pub r#type: AppliedConfigType, - pub id: String, -} - -#[derive(Deserialize)] -pub struct RemoveAppliedConfiguration { - pub id: String, -} - -#[derive(Deserialize, PartialEq, Eq)] -pub enum ProductUploadTarget { - Thumbnails, - Reward, -} - -#[derive(Deserialize)] -pub struct UpdateProductUploads { - pub target: ProductUploadTarget, -} - -#[derive(Deserialize)] -pub struct RemoveProductThumbnail { - pub idx: usize, -} - -#[derive(Deserialize)] -pub struct CreateAd { - pub target: String, - pub size: UserAdSize, -} - -#[derive(Deserialize)] -pub struct UpdateAdIsRunning { - pub is_running: bool, -} diff --git a/crates/app/src/routes/api/v1/products.rs b/crates/app/src/routes/api/v1/products.rs index 2975bbe..e532d01 100644 --- a/crates/app/src/routes/api/v1/products.rs +++ b/crates/app/src/routes/api/v1/products.rs @@ -1,21 +1,9 @@ -use crate::{ - cookie::CookieJar, - get_user_from_token, - image::{save_webp_buffer, JsonMultipart}, - State, -}; +use crate::{get_user_from_token, State, cookie::CookieJar}; use axum::{extract::Path, response::IntoResponse, Extension, Json}; -use tetratto_core::model::{ - economy::{Product, ProductFulfillmentMethod}, - oauth, - permissions::FinePermission, - uploads::{MediaType, MediaUpload}, - ApiReturn, Error, -}; +use tetratto_core::model::{economy::Product, oauth, ApiReturn, Error}; use super::{ - CreateProduct, ProductUploadTarget, RemoveProductThumbnail, UpdateProductData, - UpdateProductDescription, UpdateProductMethod, UpdateProductOnSale, UpdateProductPrice, - UpdateProductSingleUse, UpdateProductStock, UpdateProductTitle, UpdateProductUploads, + CreateProduct, UpdateProductDescription, UpdateProductMethod, UpdateProductOnSale, + UpdateProductPrice, UpdateProductStock, UpdateProductTitle, }; pub async fn create_request( @@ -48,7 +36,7 @@ pub async fn delete_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateProducts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -124,33 +112,6 @@ pub async fn update_description_request( } } -pub async fn update_data_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, - Json(mut req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - req.data = req.data.trim().to_string(); - if req.data.len() > 16384 { - return Json(Error::DataTooLong("data".to_string()).into()); - } - - match data.update_product_data(id, &user, &req.data).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Product updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - pub async fn update_on_sale_request( jar: CookieJar, Extension(data): Extension, @@ -176,31 +137,6 @@ pub async fn update_on_sale_request( } } -pub async fn update_single_use_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data - .update_product_single_use(id, &user, if req.single_use { 1 } else { 0 }) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Product updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - pub async fn update_price_request( jar: CookieJar, Extension(data): Extension, @@ -213,14 +149,7 @@ pub async fn update_price_request( None => return Json(Error::NotAllowed.into()), }; - let product = match data.get_product_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - let can_be_free = product.method != ProductFulfillmentMethod::ProfileStyle; - - if req.price < 25 && (!can_be_free || req.price != 0) { + if req.price < 25 { return Json( Error::MiscError( "Price is too low, please use a price of 25 coins or more".to_string(), @@ -251,24 +180,6 @@ pub async fn update_method_request( None => return Json(Error::NotAllowed.into()), }; - if req.method == ProductFulfillmentMethod::ProfileStyle - && !user.permissions.check(FinePermission::SUPPORTER) - { - return Json(Error::RequiresSupporter.into()); - } - - let product = match data.get_product_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if req.method == ProductFulfillmentMethod::ProfileStyle && product.price == 0 { - // no free profile styles - if let Err(e) = data.update_product_price(id, &user, 25).await { - return Json(e.into()); - } - } - match data.update_product_method(id, &user, req.method).await { Ok(_) => Json(ApiReturn { ok: true, @@ -321,163 +232,3 @@ pub async fn buy_request( Err(e) => Json(e.into()), } } - -const MAXIMUM_THUMBNAIL_FILE_SIZE: usize = 2_097_152; -const MAXIMUM_REWARD_FILE_SIZE: usize = 4_194_304; - -/// Update the product's uploads. Only reads one multipart file entry. -pub async fn update_uploads_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, - JsonMultipart(bytes_parts, req): JsonMultipart, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let mut product = match data.get_product_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) { - return Json(Error::NotAllowed.into()); - } - - // apply to target - match req.target { - ProductUploadTarget::Thumbnails => { - if product.uploads.thumbnails.len() == 4 { - return Json( - Error::MiscError("Too many thumbnails exist. Please remove one".to_string()) - .into(), - ); - } - - // create upload - let file = match bytes_parts.get(0) { - Some(x) => x, - None => return Json(Error::Unknown.into()), - }; - - if file.len() > MAXIMUM_THUMBNAIL_FILE_SIZE { - return Json(Error::FileTooLarge.into()); - } - - let upload = match data - .create_upload(MediaUpload::new(MediaType::Webp, user.id)) - .await - { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - product.uploads.thumbnails.push(upload.id); - - // write image - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None) - { - return Json(Error::MiscError(e.to_string()).into()); - } - } - ProductUploadTarget::Reward => { - // remove old - if product.uploads.reward != 0 { - if let Err(e) = data.delete_upload(product.uploads.reward).await { - return Json(e.into()); - } - } - - // create upload - let file = match bytes_parts.get(0) { - Some(x) => x, - None => return Json(Error::Unknown.into()), - }; - - if file.len() > MAXIMUM_REWARD_FILE_SIZE { - return Json(Error::FileTooLarge.into()); - } - - let upload = match data - .create_upload(MediaUpload::new(MediaType::Webp, user.id)) - .await - { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - product.uploads.reward = upload.id; - - // write image - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None) - { - return Json(Error::MiscError(e.to_string()).into()); - } - } - } - - // ... - match data - .update_product_uploads(id, &user, product.uploads) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Product updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn remove_thumbnail_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let mut product = match data.get_product_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) { - return Json(Error::NotAllowed.into()); - } - - // remove upload - let thumbnail = match product.uploads.thumbnails.get(req.idx) { - Some(x) => x, - None => return Json(Error::GeneralNotFound("thumbnail".to_string()).into()), - }; - - if let Err(e) = data.delete_upload(*thumbnail).await { - return Json(e.into()); - } - - product.uploads.thumbnails.remove(req.idx); - - // ... - match data - .update_product_uploads(id, &user, product.uploads) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Product updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/transfers.rs b/crates/app/src/routes/api/v1/transfers.rs index d3a1346..be26656 100644 --- a/crates/app/src/routes/api/v1/transfers.rs +++ b/crates/app/src/routes/api/v1/transfers.rs @@ -1,7 +1,7 @@ use crate::{get_user_from_token, State, cookie::CookieJar}; -use axum::{response::IntoResponse, Extension, Json, extract::Path}; +use axum::{response::IntoResponse, Extension, Json}; use tetratto_core::model::{ - economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource}, + economy::{CoinTransfer, CoinTransferMethod}, oauth, requests::{ActionData, ActionRequest, ActionType}, ApiReturn, Error, @@ -29,7 +29,6 @@ pub async fn create_request( }, req.amount, CoinTransferMethod::Transfer, // this endpoint is ONLY for regular transfers; products export a buy endpoint for the other method - CoinTransferSource::General, ), true, ) @@ -75,46 +74,3 @@ pub async fn ask_request( Err(e) => Json(e.into()), } } - -pub async fn create_refund_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserSendCoins) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let other_transfer = match data.get_transfer_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if user.id != other_transfer.receiver || other_transfer.source != CoinTransferSource::Sale { - // only the receiver of the funds can issue a refund (atm) - return Json(Error::NotAllowed.into()); - } - - match data - .create_transfer( - &mut CoinTransfer::new( - other_transfer.receiver, - other_transfer.sender, - other_transfer.amount, - CoinTransferMethod::Transfer, - CoinTransferSource::Refund, - ), - true, - ) - .await - { - Ok(s) => Json(ApiReturn { - ok: true, - message: "Transfer created".to_string(), - payload: s.to_string(), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/uploads.rs b/crates/app/src/routes/api/v1/uploads.rs index 0e1d807..a1d11f8 100644 --- a/crates/app/src/routes/api/v1/uploads.rs +++ b/crates/app/src/routes/api/v1/uploads.rs @@ -20,7 +20,7 @@ pub async fn get_request( Body::from(read_image(PathBufD::current().extend(&[ data.0.0.dirs.media.as_str(), "images", - "default-banner.svg", + "default-avatar.svg", ]))), )); } @@ -34,7 +34,7 @@ pub async fn get_request( Body::from(read_image(PathBufD::current().extend(&[ data.0.0.dirs.media.as_str(), "images", - "default-banner.svg", + "default-avatar.svg", ]))), )); } diff --git a/crates/app/src/routes/assets.rs b/crates/app/src/routes/assets.rs index 7f220eb..f18ede0 100644 --- a/crates/app/src/routes/assets.rs +++ b/crates/app/src/routes/assets.rs @@ -21,4 +21,3 @@ serve_asset!(streams_js_request: STREAMS_JS("text/javascript")); serve_asset!(carp_js_request: CARP_JS("text/javascript")); serve_asset!(proto_links_request: PROTO_LINKS_JS("text/javascript")); serve_asset!(app_sdk_request: APP_SDK_JS("text/javascript")); -serve_asset!(ads_request: ADS_JS("text/javascript")); diff --git a/crates/app/src/routes/mod.rs b/crates/app/src/routes/mod.rs index f9a6df9..cde54f5 100644 --- a/crates/app/src/routes/mod.rs +++ b/crates/app/src/routes/mod.rs @@ -22,7 +22,6 @@ pub fn routes(config: &Config) -> Router { .route("/js/carp.js", get(assets::carp_js_request)) .route("/js/proto_links.js", get(assets::proto_links_request)) .route("/js/app_sdk.js", get(assets::app_sdk_request)) - .route("/js/ads.js", get(assets::ads_request)) .nest_service( "/public", get_service(tower_http::services::ServeDir::new(&config.dirs.assets)), diff --git a/crates/app/src/routes/pages/chats.rs b/crates/app/src/routes/pages/chats.rs index 4134888..65ff437 100644 --- a/crates/app/src/routes/pages/chats.rs +++ b/crates/app/src/routes/pages/chats.rs @@ -348,25 +348,12 @@ pub async fn channels_request( None }; - let membership = match data - .0 - .get_membership_by_owner_community(user.id, community) - .await - { - Ok(m) => m, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }; - - let can_manage_channels = membership.role.check(CommunityPermission::MANAGE_CHANNELS) - | user.permissions.check(FinePermission::MANAGE_CHANNELS); - let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; context.insert("channels", &channels); context.insert("page", &props.page); - context.insert("can_manage_channels", &can_manage_channels); context.insert("members", &members); context.insert("channel", &channel); diff --git a/crates/app/src/routes/pages/economy.rs b/crates/app/src/routes/pages/economy.rs index 6f0c419..36c9cbe 100644 --- a/crates/app/src/routes/pages/economy.rs +++ b/crates/app/src/routes/pages/economy.rs @@ -4,13 +4,9 @@ use axum::{ Extension, }; use crate::cookie::CookieJar; -use tetratto_core::model::{ - economy::{CoinTransferMethod, UserAdSize}, - Error, -}; +use tetratto_core::model::Error; use crate::{assets::initial_context, get_lang, get_user_from_token, State}; use super::{render_error, PaginatedQuery}; -use serde::Deserialize; /// `/wallet` pub async fn wallet_request( @@ -64,36 +60,7 @@ pub async fn products_request( } }; - let list = match data - .0 - .get_products_by_user( - user.id, - 12, - if props.page_set_id == 0 { - props.page - } else { - 0 - }, - ) - .await - { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }; - - let ads_list = match data - .0 - .get_ads_by_user( - user.id, - 12, - if props.page_set_id == 1 { - props.page - } else { - 0 - }, - ) - .await - { + let list = match data.0.get_products_by_user(user.id, 12, props.page).await { Ok(x) => x, Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; @@ -102,9 +69,7 @@ pub async fn products_request( let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; context.insert("list", &list); - context.insert("ads_list", &ads_list); context.insert("page", &props.page); - context.insert("page_set_id", &props.page_set_id); // return Ok(Html( @@ -173,142 +138,14 @@ pub async fn product_request( Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; - let already_purchased = if product.single_use { - data.0 - .get_transfer_by_sender_method(user.id, CoinTransferMethod::Purchase(product.id)) - .await - .is_ok() - } else { - false - }; - - let applied_configurations_mapped: Vec = - user.applied_configurations.iter().map(|x| x.1).collect(); - let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; context.insert("product", &product); context.insert("owner", &owner); - context.insert("already_purchased", &already_purchased); - context.insert( - "applied_configurations_mapped", - &applied_configurations_mapped, - ); // return Ok(Html( data.1.render("economy/product.html", &context).unwrap(), )) } - -/// `/product/ad/{id}/edit` -pub async fn edit_ad_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = data.read().await; - let user = match get_user_from_token!(jar, data.0) { - Some(ua) => ua, - None => { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &None).await, - )); - } - }; - - let ad = match data.0.get_ad_by_id(id).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }; - - if user.id != ad.owner { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &None).await, - )); - } - - let lang = get_lang!(jar, data.0); - let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; - context.insert("ad", &ad); - - // return - Ok(Html( - data.1.render("economy/edit_ad.html", &context).unwrap(), - )) -} - -#[derive(Deserialize)] -pub struct RandomAdQuery { - pub host: usize, - #[serde(default)] - pub size: UserAdSize, - #[serde(default)] - pub noclick: bool, -} - -/// `/adn/random` -pub async fn random_ad_request( - Extension(data): Extension, - Query(props): Query, -) -> impl IntoResponse { - let data = data.read().await; - let headers = [( - "content-security-policy", - "default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: *; frame-ancestors *", - )]; - - let ad = match data.0.random_ad_charged(props.size.clone()).await { - Ok(x) => x, - Err(_) => { - // no ad found, show nothing - return (headers, Html(String::new())); - } - }; - - let mut context = tera::Context::new(); - context.insert("disable_click", &props.noclick); - context.insert("config", &data.0.0.0); - context.insert("host", &props.host); - context.insert("ad", &ad); - - // return - ( - headers, - Html(data.1.render("economy/ad.html", &context).unwrap()), - ) -} - -/// `/adn/{id}` -pub async fn known_ad_request( - Extension(data): Extension, - Query(props): Query, - Path(id): Path, -) -> impl IntoResponse { - let data = data.read().await; - let headers = [ - ( - "content-security-policy", - "default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: *; frame-ancestors *", - ), - ("Cache-Control", "no-cache"), - ]; - - let ad = match data.0.get_ad_by_id(id).await { - Ok(x) => x, - Err(_) => return (headers, Html(String::new())), - }; - - let mut context = tera::Context::new(); - context.insert("disable_click", &props.noclick); - context.insert("config", &data.0.0.0); - context.insert("host", &props.host); - context.insert("ad", &ad); - - // return - ( - headers, - Html(data.1.render("economy/ad.html", &context).unwrap()), - ) -} diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index 16011cc..a4d6ad5 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -1,7 +1,6 @@ use super::{PaginatedQuery, render_error}; use crate::{ - assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token, - InnerState, State, + assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token, State, }; use axum::{ extract::{Path, Query}, @@ -10,14 +9,11 @@ use axum::{ }; use crate::cookie::CookieJar; use serde::Deserialize; -use tetratto_core::{ - database::FullPost, - model::{ - auth::{AchievementName, DefaultTimelineChoice, ACHIEVEMENTS, User}, - permissions::FinePermission, - requests::ActionType, - Error, - }, +use tetratto_core::model::{ + auth::{AchievementName, DefaultTimelineChoice, ACHIEVEMENTS}, + permissions::FinePermission, + requests::ActionType, + Error, }; use std::fs::read_to_string; use pathbufd::PathBufD; @@ -689,12 +685,15 @@ pub struct TimelineQuery { pub responses_only: bool, } -async fn swiss_army_timeline( - data: &InnerState, - user: Option, - req: &TimelineQuery, - jar: &CookieJar, -) -> std::result::Result, Html> { +/// `/_swiss_army_timeline` +pub async fn swiss_army_timeline_request( + jar: CookieJar, + Extension(data): Extension, + Query(req): Query, +) -> impl IntoResponse { + let data = data.read().await; + let user = get_user_from_token!(jar, data.0); + let ignore_users = crate::ignore_users_gen!(user, data); let list = if req.stack_id != 0 { @@ -808,41 +807,12 @@ async fn swiss_army_timeline( None }, ), - Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), + Err(e) => return Ok(Html(render_error(e, &jar, &data, &user).await)), }, - Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), + Err(e) => return Ok(Html(render_error(e, &jar, &data, &user).await)), } }; - Ok(list) -} - -/// `/_swiss_army_timeline` -pub async fn swiss_army_timeline_request( - jar: CookieJar, - Extension(data): Extension, - Query(mut req): Query, -) -> impl IntoResponse { - let data = data.read().await; - let user = get_user_from_token!(jar, data.0); - - let mut empty_retries = 0; // how many times we've retried because of an empty list - let mut list = Vec::new(); - - while empty_retries < 2 { - list = match swiss_army_timeline(&data, user.clone(), &req, &jar).await { - Ok(x) => x, - Err(e) => return Err(e), - }; - - if list.len() == 0 && empty_retries != 2 { - empty_retries += 1; - req.page += 1; - } else { - break; - } - } - let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &user).await; diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index e147c1b..be8c3df 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -166,9 +166,6 @@ pub fn routes() -> Router { .route("/products", get(economy::products_request)) .route("/product/{id}/edit", get(economy::edit_product_request)) .route("/product/{id}", get(economy::product_request)) - .route("/product/ad/{id}/edit", get(economy::edit_ad_request)) - .route("/adn/random", get(economy::random_ad_request)) - .route("/adn/{id}", get(economy::known_ad_request)) } pub fn lw_routes() -> Router { @@ -191,11 +188,6 @@ pub async fn render_error( pub struct PaginatedQuery { #[serde(default)] pub page: usize, - /// The list set on this page to be affected by the page increment. - /// - /// This value depends on the page this query is for. - #[serde(default)] - pub page_set_id: usize, #[serde(default)] pub before: usize, } diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index dc6eca1..3f58805 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -232,7 +232,6 @@ pub fn profile_context( user: &Option, profile: &User, communities: &Vec, - applied_configurations: Vec, is_self: bool, is_following: bool, is_following_you: bool, @@ -245,7 +244,6 @@ pub fn profile_context( context.insert("is_following_you", &is_following_you); context.insert("is_blocking", &is_blocking); context.insert("warning_hash", &hash(profile.settings.warning.clone())); - context.insert("applied_configurations", &applied_configurations); context.insert( "is_supporter", @@ -378,10 +376,6 @@ pub async fn posts_request( &user, &other_user, &communities, - match data.0.get_applied_configurations(&other_user).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), - }, is_self, is_following, is_following_you, @@ -498,10 +492,6 @@ pub async fn replies_request( &user, &other_user, &communities, - match data.0.get_applied_configurations(&other_user).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), - }, is_self, is_following, is_following_you, @@ -614,10 +604,6 @@ pub async fn media_request( &user, &other_user, &communities, - match data.0.get_applied_configurations(&other_user).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), - }, is_self, is_following, is_following_you, @@ -704,13 +690,9 @@ pub async fn shop_request( context.insert("page", &props.page); profile_context( &mut context, - &Some(user.clone()), + &Some(user), &other_user, &communities, - match data.0.get_applied_configurations(&other_user).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }, is_self, is_following, is_following_you, @@ -802,13 +784,9 @@ pub async fn outbox_request( context.insert("page", &props.page); profile_context( &mut context, - &Some(user.clone()), + &Some(user), &other_user, &communities, - match data.0.get_applied_configurations(&other_user).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }, is_self, is_following, is_following_you, @@ -918,10 +896,6 @@ pub async fn following_request( &user, &other_user, &communities, - match data.0.get_applied_configurations(&other_user).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), - }, is_self, is_following, is_following_you, @@ -1031,10 +1005,6 @@ pub async fn followers_request( &user, &other_user, &communities, - match data.0.get_applied_configurations(&other_user).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), - }, is_self, is_following, is_following_you, diff --git a/crates/app/src/sanitize.rs b/crates/app/src/sanitize.rs index 1163c68..fc992b3 100644 --- a/crates/app/src/sanitize.rs +++ b/crates/app/src/sanitize.rs @@ -9,9 +9,9 @@ pub fn color_escape(color: &str) -> String { .replace(">", "%gt;") .replace("}", "") .replace("{", "") - .replace("url(\"", "url(\"/api/v1/util/proxy?url=") - .replace("url('", "url('/api/v1/util/proxy?url=") - .replace("url(https://", "url(/api/v1/util/proxy?url=https://"), + .replace("url(\"", "url(\"/api/v0/util/ext/image?img=") + .replace("url('", "url('/api/v0/util/ext/image?img=") + .replace("url(https://", "url(/api/v0/util/ext/image?img=https://"), ) } diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 3843f0a..30a1928 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -121,15 +121,6 @@ pub struct PoliciesConfig { /// /// Same deal as terms of service page. pub privacy: String, - /// The time (in ms since unix epoch) in which the site's policies last updated. - /// - /// This is required to automatically ask users to re-consent to policies. - /// - /// In user whose consent time in LESS THAN this date will be shown a dialog to re-consent to the policies. - /// - /// You can get this easily by running `echo "console.log(new Date().getTime())" | node`. - #[serde(default)] - pub last_updated: usize, } impl Default for PoliciesConfig { @@ -137,7 +128,6 @@ impl Default for PoliciesConfig { Self { terms_of_service: "/public/tos.html".to_string(), privacy: "/public/privacy.html".to_string(), - last_updated: 0, } } } @@ -354,9 +344,6 @@ pub struct Config { /// A list of banned content in posts. #[serde(default)] pub banned_data: Vec, - /// If user ads are enabled. - #[serde(default)] - pub enable_user_ads: bool, } fn default_name() -> String { @@ -476,7 +463,6 @@ impl Default for Config { stripe: None, manuals: default_manuals(), banned_data: default_banned_data(), - enable_user_ads: false, } } } diff --git a/crates/core/src/database/ads.rs b/crates/core/src/database/ads.rs deleted file mode 100644 index cb6463a..0000000 --- a/crates/core/src/database/ads.rs +++ /dev/null @@ -1,268 +0,0 @@ -use crate::model::{ - auth::{User, UserWarning}, - economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource, UserAd, UserAdSize}, - permissions::FinePermission, - Error, Result, -}; -use crate::{auto_method, DataManager}; -use oiseau::{cache::Cache, execute, get, params, query_row, query_rows, PostgresRow}; -use tetratto_shared::unix_epoch_timestamp; - -impl DataManager { - /// Get a [`UserAd`] from an SQL row. - pub(crate) fn get_ad_from_row(x: &PostgresRow) -> UserAd { - UserAd { - id: get!(x->0(i64)) as usize, - created: get!(x->1(i64)) as usize, - owner: get!(x->2(i64)) as usize, - upload_id: get!(x->3(i64)) as usize, - target: get!(x->4(String)), - last_charge_time: get!(x->5(i64)) as usize, - is_running: get!(x->6(i32)) as i8 == 1, - size: serde_json::from_str(&get!(x->7(String))).unwrap(), - } - } - - auto_method!(get_ad_by_id(usize as i64)@get_ad_from_row -> "SELECT * FROM ads WHERE id = $1" --name="ad" --returns=UserAd --cache-key-tmpl="atto.ad:{}"); - - /// Get all ads by user. - /// - /// # Arguments - /// * `id` - the ID of the user to fetch ads for - /// * `batch` - the limit of items in each page - /// * `page` - the page number - pub async fn get_ads_by_user( - &self, - id: usize, - batch: usize, - page: usize, - ) -> Result> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_rows!( - &conn, - "SELECT * FROM ads WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", - &[&(id as i64), &(batch as i64), &((page * batch) as i64)], - |x| { Self::get_ad_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("ad".to_string())); - } - - Ok(res.unwrap()) - } - - /// Disable all ads by the given user. - /// - /// # Arguments - /// * `id` - the ID of the user to kill ads from - pub async fn stop_all_ads_by_user(&self, id: usize) -> Result> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_rows!( - &conn, - "UPDATE ads SET is_running = 0 WHERE owner = $1", - &[&(id as i64)], - |x| { Self::get_ad_from_row(x) } - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(res.unwrap()) - } - - /// Create a new ad in the database. - /// - /// # Arguments - /// * `data` - a mock [`UserAd`] object to insert - pub async fn create_ad(&self, data: UserAd) -> Result { - // check values - if data.target.len() < 2 { - return Err(Error::DataTooShort("description".to_string())); - } else if data.target.len() > 256 { - return Err(Error::DataTooLong("description".to_string())); - } - - // charge for first day - if data.is_running { - self.create_transfer( - &mut CoinTransfer::new( - data.owner, - self.0.0.system_user, - Self::AD_RUN_CHARGE, - CoinTransferMethod::Transfer, - CoinTransferSource::AdCharge, - ), - true, - ) - .await?; - } - - // ... - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "INSERT INTO ads VALUES (DEFAULT, $1, $2, $3, $4, $5, $6, $7)", - params![ - &(data.created as i64), - &(data.owner as i64), - &(data.upload_id as i64), - &data.target, - &(data.last_charge_time as i64), - &if data.is_running { 1 } else { 0 }, - &serde_json::to_string(&data.size).unwrap() - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(data) - } - - pub async fn delete_ad(&self, id: usize, user: &User) -> Result<()> { - let ad = self.get_ad_by_id(id).await?; - - // check user permission - if user.id != ad.owner && !user.permissions.check(FinePermission::MANAGE_USERS) { - return Err(Error::NotAllowed); - } - - // remove upload - self.delete_upload(ad.upload_id).await?; - - // ... - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!(&conn, "DELETE FROM ads WHERE id = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // ... - self.0.1.remove(format!("atto.ad:{}", id)).await; - Ok(()) - } - - /// Pull a random running ad. - pub async fn random_ad(&self, size: UserAdSize) -> Result { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_row!( - &conn, - "SELECT * FROM ads WHERE is_running = 1 AND size = $1 ORDER BY RANDOM() DESC LIMIT 1", - &[&serde_json::to_string(&size).unwrap()], - |x| { Ok(Self::get_ad_from_row(x)) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("ad".to_string())); - } - - Ok(res.unwrap()) - } - - const MINIMUM_DELTA_FOR_CHARGE: usize = 604_800_000; // 7 days - /// The amount charged to a [`UserAd`] owner each day the ad is running (and is pulled from the pool). - pub const AD_RUN_CHARGE: i32 = 25; - /// The amount charged to a [`UserAd`] owner each time the ad is clicked. - pub const AD_CLICK_CHARGE: i32 = 2; - - /// Get a random ad and check if the ad owner needs to be charged for this period. - pub async fn random_ad_charged(&self, size: UserAdSize) -> Result { - let ad = self.random_ad(size).await?; - - let now = unix_epoch_timestamp(); - let delta = now - ad.last_charge_time; - - if delta >= Self::MINIMUM_DELTA_FOR_CHARGE { - if let Err(e) = self - .create_transfer( - &mut CoinTransfer::new( - ad.owner, - self.0.0.system_user, - Self::AD_RUN_CHARGE, - CoinTransferMethod::Transfer, - CoinTransferSource::AdCharge, - ), - true, - ) - .await - { - // boo user cannot afford to keep running their ads - self.stop_all_ads_by_user(ad.owner).await?; - return Err(e); - }; - - self.update_ad_last_charge_time(ad.id, now as i64).await?; - } - - Ok(ad) - } - - /// Handle a click on an ad from the given host. - /// - /// Hosts are just the ID of the user that is embedding the ad on their page. - pub async fn ad_click(&self, host: usize, ad: usize, user: Option) -> Result { - let ad = self.get_ad_by_id(ad).await?; - - if let Some(ref ua) = user { - if ua.id == host { - self.create_user_warning( - UserWarning::new( - ua.id, - self.0.0.system_user, - "Automated warning: do not click on ads on your own site! This incident has been reported.".to_string() - ) - ).await?; - - return Ok(ad.target); - } - } - - // create click transfer - if let Err(e) = self - .create_transfer( - &mut CoinTransfer::new( - ad.owner, - host, - Self::AD_CLICK_CHARGE, - CoinTransferMethod::Transfer, - CoinTransferSource::AdClick, - ), - true, - ) - .await - { - self.stop_all_ads_by_user(ad.owner).await?; - return Err(e); - } - - // return - Ok(ad.target) - } - - auto_method!(update_ad_is_running(i32)@get_ad_by_id:FinePermission::MANAGE_USERS; -> "UPDATE ads SET is_running = $1 WHERE id = $2" --cache-key-tmpl="atto.ad:{}"); - auto_method!(update_ad_last_charge_time(i64) -> "UPDATE ads SET last_charge_time = $1 WHERE id = $2" --cache-key-tmpl="atto.ad:{}"); -} diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 2245e47..1a37e3a 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -1,15 +1,15 @@ use super::common::NAME_REGEX; use oiseau::cache::Cache; +use crate::model::auth::{ + Achievement, AchievementName, AchievementRarity, Notification, UserConnections, ACHIEVEMENTS, +}; +use crate::model::moderation::AuditLogEntry; +use crate::model::oauth::AuthGrant; +use crate::model::permissions::SecondaryPermission; use crate::model::{ Error, Result, auth::{Token, User, UserSettings}, - permissions::{FinePermission, SecondaryPermission}, - oauth::AuthGrant, - moderation::AuditLogEntry, - auth::{ - Achievement, AchievementName, AchievementRarity, Notification, UserConnections, - ACHIEVEMENTS, AppliedConfigType, - }, + permissions::FinePermission, }; use pathbufd::PathBufD; use std::fs::{exists, remove_file}; @@ -130,8 +130,6 @@ impl DataManager { ban_expire: get!(x->30(i64)) as usize, coins: get!(x->31(i32)), checkouts: serde_json::from_str(&get!(x->32(String)).to_string()).unwrap(), - applied_configurations: serde_json::from_str(&get!(x->33(String)).to_string()).unwrap(), - last_policy_consent: get!(x->34(i64)) as usize, } } @@ -288,7 +286,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33)", params![ &(data.id as i64), &(data.created as i64), @@ -323,8 +321,6 @@ impl DataManager { &(data.ban_expire as i64), &(data.coins as i32), &serde_json::to_string(&data.checkouts).unwrap(), - &serde_json::to_string(&data.applied_configurations).unwrap(), - &(data.last_policy_consent as i64) ] ); @@ -542,68 +538,6 @@ impl DataManager { return Err(Error::DatabaseError(e.to_string())); } - // delete transfers - let res = execute!( - &conn, - "DELETE FROM transfers WHERE sender = $1 OR receiver = $1", - &[&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // delete products - let res = execute!( - &conn, - "DELETE FROM products WHERE owner = $1", - &[&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // delete domains - let res = execute!( - &conn, - "DELETE FROM domains WHERE owner = $1", - &[&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // delete services - let res = execute!( - &conn, - "DELETE FROM services WHERE owner = $1", - &[&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // delete letters - let res = execute!( - &conn, - "DELETE FROM letters WHERE owner = $1", - &[&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // delete ads - let res = execute!(&conn, "DELETE FROM ads WHERE owner = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - // delete user follows... individually since it requires updating user counts for follow in self.get_userfollows_by_receiver_all(id).await? { self.delete_userfollow(follow.id, &user, true).await?; @@ -1102,38 +1036,6 @@ impl DataManager { Ok((totp.get_secret_base32(), qr, recovery)) } - /// Get all applied configurations as a vector of strings from the given user. - pub async fn get_applied_configurations(&self, user: &User) -> Result> { - let mut out = Vec::new(); - - for config in &user.applied_configurations { - let product = self.get_product_by_id(config.1).await?; - let owner = self.get_user_by_id_with_void(product.owner).await?; - - if config.0 == AppliedConfigType::StyleSnippet - && !owner.permissions.check(FinePermission::SUPPORTER) - { - out.push(format!( - "", - owner.username - )); - - continue; - } - - out.push(match config.0 { - AppliedConfigType::StyleSnippet => { - format!( - "", - product.data.replace("<", "<").replace(">", ">") - ) - } - }) - } - - Ok(out) - } - pub async fn cache_clear_user(&self, user: &User) { self.0.1.remove(format!("atto.user:{}", user.id)).await; self.0 @@ -1162,8 +1064,6 @@ impl DataManager { auto_method!(update_user_ban_expire(i64)@get_user_by_id -> "UPDATE users SET ban_expire = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(update_user_coins(i32)@get_user_by_id -> "UPDATE users SET coins = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(update_user_checkouts(Vec)@get_user_by_id -> "UPDATE users SET checkouts = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); - auto_method!(update_user_applied_configurations(Vec<(AppliedConfigType, usize)>)@get_user_by_id -> "UPDATE users SET applied_configurations = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); - auto_method!(update_user_last_policy_consent(i64)@get_user_by_id -> "UPDATE users SET last_policy_consent = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User); auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 5a4cf82..4d3fe55 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -46,7 +46,6 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_LETTERS).unwrap(); execute!(&conn, common::CREATE_TABLE_TRANSFERS).unwrap(); execute!(&conn, common::CREATE_TABLE_PRODUCTS).unwrap(); - execute!(&conn, common::CREATE_TABLE_ADS).unwrap(); for x in common::VERSION_MIGRATIONS.split(";") { execute!(&conn, x).unwrap(); diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index ca8fa79..7c1b2e5 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -34,4 +34,3 @@ pub const CREATE_TABLE_APP_DATA: &str = include_str!("./sql/create_app_data.sql" pub const CREATE_TABLE_LETTERS: &str = include_str!("./sql/create_letters.sql"); pub const CREATE_TABLE_TRANSFERS: &str = include_str!("./sql/create_transfers.sql"); pub const CREATE_TABLE_PRODUCTS: &str = include_str!("./sql/create_products.sql"); -pub const CREATE_TABLE_ADS: &str = include_str!("./sql/create_ads.sql"); diff --git a/crates/core/src/database/drivers/sql/create_ads.sql b/crates/core/src/database/drivers/sql/create_ads.sql deleted file mode 100644 index 1c7fbf6..0000000 --- a/crates/core/src/database/drivers/sql/create_ads.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE IF NOT EXISTS ads ( - id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - created BIGINT NOT NULL, - owner BIGINT NOT NULL, - upload_id BIGINT NOT NULL, - target TEXT NOT NULL, - last_charge_time BIGINT NOT NULL, - is_running INT NOT NULL, - size TEXT NOT NULL -) diff --git a/crates/core/src/database/drivers/sql/create_products.sql b/crates/core/src/database/drivers/sql/create_products.sql index 2f27dcd..be32f3c 100644 --- a/crates/core/src/database/drivers/sql/create_products.sql +++ b/crates/core/src/database/drivers/sql/create_products.sql @@ -7,8 +7,5 @@ CREATE TABLE IF NOT EXISTS products ( method TEXT NOT NULL, on_sale INT NOT NULL, price INT NOT NULL, - stock INT NOT NULL, - single_use INT NOT NULL, - data TEXT NOT NULL, - uploads TEXT NOT NULL + stock INT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/create_transfers.sql b/crates/core/src/database/drivers/sql/create_transfers.sql index ea157e4..d747c78 100644 --- a/crates/core/src/database/drivers/sql/create_transfers.sql +++ b/crates/core/src/database/drivers/sql/create_transfers.sql @@ -5,6 +5,5 @@ CREATE TABLE IF NOT EXISTS transfers ( receiver BIGINT NOT NULL, amount INT NOT NULL, is_pending INT NOT NULL, - method TEXT NOT NULL, - source TEXT NOT NULL + method TEXT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index 1726d10..d86bcbe 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -31,7 +31,5 @@ CREATE TABLE IF NOT EXISTS users ( is_deactivated INT NOT NULL, ban_expire BIGINT NOT NULL, coins INT NOT NULL, - checkouts TEXT NOT NULL, - applied_configurations TEXT NOT NULL, - last_policy_consent BIGINT NOT NULL + checkouts TEXT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/version_migrations.sql b/crates/core/src/database/drivers/sql/version_migrations.sql index 4598560..a935589 100644 --- a/crates/core/src/database/drivers/sql/version_migrations.sql +++ b/crates/core/src/database/drivers/sql/version_migrations.sql @@ -45,27 +45,3 @@ ADD COLUMN IF NOT EXISTS data TEXT DEFAULT '"Null"'; -- users checkouts ALTER TABLE users ADD COLUMN IF NOT EXISTS checkouts TEXT DEFAULT '[]'; - --- products single_use -ALTER TABLE products -ADD COLUMN IF NOT EXISTS single_use INT DEFAULT 1; - --- transfers source -ALTER TABLE transfers -ADD COLUMN IF NOT EXISTS source TEXT DEFAULT '"General"'; - --- products data -ALTER TABLE products -ADD COLUMN IF NOT EXISTS data TEXT DEFAULT ''; - --- users applied_configurations -ALTER TABLE users -ADD COLUMN IF NOT EXISTS applied_configurations TEXT DEFAULT '[]'; - --- products uploads -ALTER TABLE products -ADD COLUMN IF NOT EXISTS uploads TEXT DEFAULT '{}'; - --- users last_policy_consent -ALTER TABLE users -ADD COLUMN IF NOT EXISTS last_policy_consent BIGINT DEFAULT 0; diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 81d0c83..c52a107 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -1,4 +1,3 @@ -mod ads; pub mod app_data; mod apps; mod audit_log; @@ -40,4 +39,3 @@ mod userfollows; pub use drivers::DataManager; pub use common::NAME_REGEX; -pub use posts::FullPost; diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 35448de..7a7a2a7 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -2221,9 +2221,7 @@ impl DataManager { // decr parent comment count if let Some(replying_to) = y.replying_to { - if replying_to != 0 { - self.decr_post_comments(replying_to).await.unwrap(); - } + self.decr_post_comments(replying_to).await.unwrap(); } // decr user post count diff --git a/crates/core/src/database/products.rs b/crates/core/src/database/products.rs index db8fce9..714531d 100644 --- a/crates/core/src/database/products.rs +++ b/crates/core/src/database/products.rs @@ -1,9 +1,6 @@ use crate::model::{ auth::User, - economy::{ - CoinTransfer, CoinTransferMethod, CoinTransferSource, Product, ProductFulfillmentMethod, - ProductUploads, - }, + economy::{CoinTransfer, CoinTransferMethod, Product, ProductFulfillmentMethod}, mail::Letter, permissions::FinePermission, Error, Result, @@ -24,9 +21,6 @@ impl DataManager { on_sale: get!(x->6(i32)) as i8 == 1, price: get!(x->7(i32)), stock: get!(x->8(i32)), - single_use: get!(x->9(i32)) as i8 == 1, - data: get!(x->10(String)), - uploads: serde_json::from_str(&get!(x->11(String))).unwrap(), } } @@ -63,7 +57,7 @@ impl DataManager { Ok(res.unwrap()) } - const MAXIMUM_FREE_PRODUCTS: usize = 10; + const MAXIMUM_FREE_PRODUCTS: usize = 5; /// Create a new product in the database. /// @@ -109,7 +103,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO products VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + "INSERT INTO products VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", params![ &(data.id as i64), &(data.created as i64), @@ -119,10 +113,7 @@ impl DataManager { &serde_json::to_string(&data.method).unwrap(), &{ if data.on_sale { 1 } else { 0 } }, &data.price, - &(data.stock as i32), - &{ if data.single_use { 1 } else { 0 } }, - &data.data, - &serde_json::to_string(&data.uploads).unwrap(), + &(data.stock as i32) ] ); @@ -140,28 +131,11 @@ impl DataManager { customer: &mut User, ) -> Result { let product = self.get_product_by_id(product).await?; - - // handle single_use product - if product.single_use { - if self - .get_transfer_by_sender_method( - customer.id, - CoinTransferMethod::Purchase(product.id), - ) - .await - .is_ok() - { - return Err(Error::MiscError("You already own this product".to_string())); - } - } - - // ... let mut transfer = CoinTransfer::new( customer.id, product.owner, product.price, CoinTransferMethod::Purchase(product.id), - CoinTransferSource::Sale, ); if !product.stock.is_negative() { @@ -224,21 +198,6 @@ If your product is a purchase of goods or services, please be sure to fulfill th // return Ok(transfer) } - ProductFulfillmentMethod::ProfileStyle => { - // pretty much an automail without the message - self.create_transfer(&mut transfer, true).await?; - - self.create_letter(Letter::new( - self.0.0.system_user, - vec![customer.id], - format!("Thank you for purchasing \"{}\"", product.title), - "You've purchased a CSS snippet which can be applied to your profile through the product's page!".to_string(), - 0, - )) - .await?; - - Ok(transfer) - } } } @@ -250,15 +209,6 @@ If your product is a purchase of goods or services, please be sure to fulfill th return Err(Error::NotAllowed); } - // remove uploads - for upload in product.uploads.thumbnails { - self.delete_upload(upload).await?; - } - - if product.uploads.reward != 0 { - self.delete_upload(product.uploads.reward).await?; - } - // ... let conn = match self.0.connect().await { Ok(c) => c, @@ -281,9 +231,6 @@ If your product is a purchase of goods or services, please be sure to fulfill th auto_method!(update_product_price(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET price = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}"); auto_method!(update_product_on_sale(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET on_sale = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}"); auto_method!(update_product_method(ProductFulfillmentMethod)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET method = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}"); - auto_method!(update_product_single_use(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET single_use = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}"); - auto_method!(update_product_data(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET data = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}"); - auto_method!(update_product_uploads(ProductUploads)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET uploads = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}"); auto_method!(update_product_stock(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET stock = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}"); auto_method!(incr_product_stock() -> "UPDATE products SET stock = stock + 1 WHERE id = $1" --cache-key-tmpl="atto.product:{}" --incr); diff --git a/crates/core/src/database/requests.rs b/crates/core/src/database/requests.rs index aad24ab..84356ac 100644 --- a/crates/core/src/database/requests.rs +++ b/crates/core/src/database/requests.rs @@ -29,14 +29,7 @@ impl DataManager { .get(format!("atto.request:{}:{}", id, linked_asset)) .await { - if let Ok(x) = serde_json::from_str(&cached) { - return Ok(x); - } else { - self.0 - .1 - .remove(format!("atto.request:{}:{}", id, linked_asset)) - .await; - } + return Ok(serde_json::from_str(&cached).unwrap()); } let conn = match self.0.connect().await { diff --git a/crates/core/src/database/transfers.rs b/crates/core/src/database/transfers.rs index 4653166..40507b8 100644 --- a/crates/core/src/database/transfers.rs +++ b/crates/core/src/database/transfers.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use crate::model::{ - auth::{Notification, User}, - economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource, Product}, Error, Result, + economy::{CoinTransferMethod, Product, CoinTransfer}, + auth::{Notification, User}, }; use crate::{auto_method, DataManager}; -use oiseau::{cache::Cache, execute, get, params, query_row, query_rows, PostgresRow}; +use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow}; impl DataManager { /// Get a [`CoinTransfer`] from an SQL row. @@ -18,7 +18,6 @@ impl DataManager { amount: get!(x->4(i32)), is_pending: get!(x->5(i32)) as i8 == 1, method: serde_json::from_str(&get!(x->6(String))).unwrap(), - source: serde_json::from_str(&get!(x->7(String))).unwrap(), } } @@ -28,13 +27,16 @@ impl DataManager { pub async fn fill_transfers( &self, list: Vec, - ) -> Result, CoinTransfer)>> { + ) -> Result, bool)>> { let mut out = Vec::new(); let mut seen_users: HashMap = HashMap::new(); let mut seen_products: HashMap = HashMap::new(); for transfer in list { out.push(( + transfer.id, + transfer.created, + transfer.amount, if let Some(user) = seen_users.get(&transfer.sender) { user.to_owned() } else { @@ -52,20 +54,16 @@ impl DataManager { match transfer.method { CoinTransferMethod::Transfer => None, CoinTransferMethod::Purchase(id) => { - if let Some(product) = seen_products.get(&id) { - Some(product.to_owned()) + Some(if let Some(product) = seen_products.get(&id) { + product.to_owned() } else { - match self.get_product_by_id(id).await { - Ok(product) => { - seen_products.insert(product.id, product.clone()); - Some(product) - } - Err(_) => None, - } - } + let product = self.get_product_by_id(id).await?; + seen_products.insert(product.id, product.clone()); + product + }) } }, - transfer, + transfer.is_pending, )); } @@ -103,35 +101,6 @@ impl DataManager { Ok(res.unwrap()) } - /// Get a transfer by user and method. - /// - /// # Arguments - /// * `id` - the ID of the user to fetch transfers for - /// * `method` - the transfer method - pub async fn get_transfer_by_sender_method( - &self, - id: usize, - method: CoinTransferMethod, - ) -> Result { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_row!( - &conn, - "SELECT * FROM transfers WHERE sender = $1 AND method = $2 LIMIT 1", - params![&(id as i64), &serde_json::to_string(&method).unwrap()], - |x| { Ok(Self::get_transfer_from_row(x)) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("transfer".to_string())); - } - - Ok(res.unwrap()) - } - /// Create a new transfer in the database. /// /// # Arguments @@ -160,16 +129,6 @@ impl DataManager { } self.update_user_coins(receiver.id, receiver.coins).await?; - - // handle refund notification - if data.source == CoinTransferSource::Refund { - self.create_notification(Notification::new( - "A coin refund has been issued to your account!".to_string(), - "You've been issued a refund for a prior purchase. The product will remain in your account, but your coins have been returned.".to_string(), - receiver.id, - )) - .await?; - } } else { // we haven't applied the transfer, so this must be pending data.is_pending = true; @@ -183,7 +142,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO transfers VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + "INSERT INTO transfers VALUES ($1, $2, $3, $4, $5, $6, $7)", params![ &(data.id as i64), &(data.created as i64), @@ -192,7 +151,6 @@ impl DataManager { &data.amount, &{ if data.is_pending { 1 } else { 0 } }, &serde_json::to_string(&data.method).unwrap(), - &serde_json::to_string(&data.source).unwrap(), ] ); diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 3da4db8..afd6aa1 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -101,23 +101,11 @@ pub struct User { /// already applied this purchase. #[serde(default)] pub checkouts: Vec, - /// The IDs of products to be applied to the user's profile. - #[serde(default)] - pub applied_configurations: Vec<(AppliedConfigType, usize)>, - /// The time in which the user last consented to the site's policies. - #[serde(default)] - pub last_policy_consent: usize, } pub type UserConnections = HashMap; -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub enum AppliedConfigType { - /// An HTML `