From 6d333378a4680588cad8925ed5f5df8d999f040d Mon Sep 17 00:00:00 2001 From: trisua Date: Fri, 5 Sep 2025 22:08:16 -0400 Subject: [PATCH] remove: old chats core --- crates/app/src/assets.rs | 10 - crates/app/src/public/html/body.lisp | 8 - crates/app/src/public/html/chats/app.lisp | 509 ------------------ .../app/src/public/html/chats/channels.lisp | 80 --- crates/app/src/public/html/chats/message.lisp | 1 - crates/app/src/public/html/chats/stream.lisp | 55 -- .../src/public/html/communities/settings.lisp | 168 +----- crates/app/src/public/html/components.lisp | 114 +--- crates/app/src/public/html/macros.lisp | 4 - crates/app/src/public/html/mod/profile.lisp | 2 +- crates/app/src/public/html/profile/base.lisp | 26 +- .../src/routes/api/v1/channels/channels.rs | 354 ------------ .../api/v1/channels/message_reactions.rs | 103 ---- .../src/routes/api/v1/channels/messages.rs | 352 ------------ crates/app/src/routes/api/v1/channels/mod.rs | 3 - crates/app/src/routes/api/v1/mod.rs | 104 ---- crates/app/src/routes/pages/chats.rs | 380 ------------- crates/app/src/routes/pages/communities.rs | 12 - crates/app/src/routes/pages/mod.rs | 31 +- crates/core/src/database/channels.rs | 325 ----------- crates/core/src/database/common.rs | 3 - crates/core/src/database/communities.rs | 5 - crates/core/src/database/drivers/common.rs | 3 - .../database/drivers/sql/create_channels.sql | 12 - .../drivers/sql/create_message_reactions.sql | 8 - .../database/drivers/sql/create_messages.sql | 10 - crates/core/src/database/message_reactions.rs | 183 ------- crates/core/src/database/messages.rs | 380 ------------- crates/core/src/database/mod.rs | 3 - crates/core/src/model/channels.rs | 133 ----- .../core/src/model/communities_permissions.rs | 2 +- crates/core/src/model/mod.rs | 1 - crates/core/src/model/permissions.rs | 2 +- 33 files changed, 9 insertions(+), 3377 deletions(-) delete mode 100644 crates/app/src/public/html/chats/app.lisp delete mode 100644 crates/app/src/public/html/chats/channels.lisp delete mode 100644 crates/app/src/public/html/chats/message.lisp delete mode 100644 crates/app/src/public/html/chats/stream.lisp delete mode 100644 crates/app/src/routes/api/v1/channels/channels.rs delete mode 100644 crates/app/src/routes/api/v1/channels/message_reactions.rs delete mode 100644 crates/app/src/routes/api/v1/channels/messages.rs delete mode 100644 crates/app/src/routes/api/v1/channels/mod.rs delete mode 100644 crates/app/src/routes/pages/chats.rs delete mode 100644 crates/core/src/database/channels.rs delete mode 100644 crates/core/src/database/drivers/sql/create_channels.sql delete mode 100644 crates/core/src/database/drivers/sql/create_message_reactions.sql delete mode 100644 crates/core/src/database/drivers/sql/create_messages.sql delete mode 100644 crates/core/src/database/message_reactions.rs delete mode 100644 crates/core/src/database/messages.rs delete mode 100644 crates/core/src/model/channels.rs diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 393fe54..449dedd 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -117,11 +117,6 @@ pub const MOD_PROFILE: &str = include_str!("./public/html/mod/profile.lisp"); pub const MOD_WARNINGS: &str = include_str!("./public/html/mod/warnings.lisp"); pub const MOD_STATS: &str = include_str!("./public/html/mod/stats.lisp"); -pub const CHATS_APP: &str = include_str!("./public/html/chats/app.lisp"); -pub const CHATS_STREAM: &str = include_str!("./public/html/chats/stream.lisp"); -pub const CHATS_MESSAGE: &str = include_str!("./public/html/chats/message.lisp"); -pub const CHATS_CHANNELS: &str = include_str!("./public/html/chats/channels.lisp"); - pub const STACKS_LIST: &str = include_str!("./public/html/stacks/list.lisp"); pub const STACKS_FEED: &str = include_str!("./public/html/stacks/feed.lisp"); pub const STACKS_MANAGE: &str = include_str!("./public/html/stacks/manage.lisp"); @@ -357,11 +352,6 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"mod/warnings.html"(crate::assets::MOD_WARNINGS) --config=config --lisp plugins); write_template!(html_path->"mod/stats.html"(crate::assets::MOD_STATS) --config=config --lisp plugins); - write_template!(html_path->"chats/app.html"(crate::assets::CHATS_APP) -d "chats" --config=config --lisp plugins); - write_template!(html_path->"chats/stream.html"(crate::assets::CHATS_STREAM) --config=config --lisp plugins); - write_template!(html_path->"chats/message.html"(crate::assets::CHATS_MESSAGE) --config=config --lisp plugins); - write_template!(html_path->"chats/channels.html"(crate::assets::CHATS_CHANNELS) --config=config --lisp plugins); - write_template!(html_path->"stacks/list.html"(crate::assets::STACKS_LIST) -d "stacks" --config=config --lisp plugins); write_template!(html_path->"stacks/feed.html"(crate::assets::STACKS_FEED) --config=config --lisp plugins); write_template!(html_path->"stacks/manage.html"(crate::assets::STACKS_MANAGE) --config=config --lisp plugins); diff --git a/crates/app/src/public/html/body.lisp b/crates/app/src/public/html/body.lisp index f021e8b..890b60e 100644 --- a/crates/app/src/public/html/body.lisp +++ b/crates/app/src/public/html/body.lisp @@ -129,14 +129,6 @@ trigger(\"me::seen\"); trigger(\"streams::user\", [\"{{ user.id }}\"]); - if (!window.location.pathname.startsWith(\"/chats/\")) { - if (window.socket) { - window.socket.send(\"Close\"); - window.socket = undefined; - console.log(\"socket disconnect\"); - } - } - if (window.location.pathname.startsWith(\"/reference\")) { window.location.reload(); } diff --git a/crates/app/src/public/html/chats/app.lisp b/crates/app/src/public/html/chats/app.lisp deleted file mode 100644 index 11ab239..0000000 --- a/crates/app/src/public/html/chats/app.lisp +++ /dev/null @@ -1,509 +0,0 @@ -(text "{% extends \"root.html\" %} {% block head %}") -(title - (text "Chats — {{ config.name }}")) -(link ("rel" "stylesheet") ("data-turbo-temporary" "true") ("href" "/css/chats.css?v=tetratto-{{ random_cache_breaker }}")) -(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"chats\", hide_user_menu=true) }}") -(nav - ("class" "chats_nav") - (button - ("class" "flex gap_2 items_center active") - ("onclick" "toggle_sidebars(event)") - (text "{{ icon \"panel-left\" }} {% if community -%}") - (b - ("class" "name shorter") - (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) - (text "{% else %}") - (b - (text "{{ text \"chats:label.my_chats\" }}")) - (text "{%- endif %}"))) -(div - ("class" "flex") - (div - ("class" "sidebar flex flex_col items_center gap_2") - ("id" "community_list") - ("style" "width: var(--list-bar-width)") - (a - ("href" "/chats/0/0") - ("class" "button lowered channel_icon {% if selected_community == 0 -%}selected{%- endif %}") - ("data-turbo" "false") - (text "{{ icon \"message-circle\" }}")) - (text "{% for community in communities %} {% if community.id != 0 -%}") - (a - ("href" "/chats/{{ community.id }}/0") - ("class" "button lowered channel_icon {% if selected_community == community.id -%}selected{%- endif %}") - ("data-turbo" "false") - (text "{{ components::community_avatar(id=community.id, community=community, size=\"48px\") }}")) - (text "{%- endif %} {% endfor %}")) - (div - ("class" "sidebar flex flex_col gap_2 justify_between") - ("id" "channels_list") - (div - ("class" "flex flex_col gap_2 w_full") - (div - ("class" "title flex items_center justify_between channel_header") - (text "{% if community -%}") - (b - ("class" "name shorter") - (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) - (text "{% else %}") - (b - (text "{{ text \"chats:label.my_chats\" }}")) - (text "{%- endif %} {% if selected_community != 0 -%}") - (div - ("class" "dropdown") - (button - ("class" "camo small") - ("onclick" "trigger('atto::hooks::dropdown', [event])") - ("exclude" "dropdown") - (text "{{ icon \"ellipsis\" }}")) - (div - ("class" "inner") - (a - ("href" "/community/{{ selected_community }}") - (text "{{ icon \"book-heart\" }}") - (span - (text "{{ text \"communities:label.show_community\" }}"))) - (text "{% if can_manage_channels -%}") - (a - ("href" "/community/{{ selected_community }}/manage") - (text "{{ icon \"settings\" }}") - (span - (text "{{ text \"general:action.manage\" }}"))) - (text "{%- endif %}"))) - (text "{%- endif %}")) - (text "{% if can_manage_channels -%}") - (a - ("class" "button w_full justify_start lowered") - ("href" "/community/{{ selected_community }}/manage#/channels") - (text "{{ icon \"plus\" }}") - (span - (text "{{ text \"communities:action.create_channel\" }}"))) - (text "{%- endif %}") - (turbo-frame - ("id" "channels_list_frame") - ("src" "/chats/{{ selected_community }}/{{ selected_channel }}/_channels") - ("target" "_top"))) - (text "{{ components::user_plate(user=user, show_menu=true) }}")) - (text "{% if channel -%}") - (div - ("class" "w_full flex flex_col gap_2 padded_section") - ("id" "stream") - ("style" "padding: var(--pad-4)") - (turbo-frame - ("id" "stream_body_frame") - ("src" "/chats/{{ selected_community }}/{{ selected_channel }}/_stream?page={{ page }}&message={{ message }}")) - (form - ("class" "card flex flex_row gap_2") - ("onsubmit" "create_message_from_form(event)") - (textarea - ("type" "text") - ("name" "content") - ("id" "content") - ("placeholder" "message {{ channel.title }}") - ("required" "") - ("minlength" "2") - ("maxlength" "2048") - ("style" "min-height: 48px !important; height: 48px")) - (button - ("class" "camo send_button") - ("title" "Send") - (text "{{ icon \"send-horizontal\" }}")))) - (text "{%- endif %}") - - ; emoji picker - (text "{{ components::emoji_picker(element_id=\"\", render_dialog=true, render_button=false) }}") - (input ("id" "react_emoji_picker_field") ("class" "hidden") ("type" "hidden")) - - (script - (text "window.EMOJI_PICKER_MODE = \"replace\"; - document.getElementById(\"react_emoji_picker_field\").addEventListener(\"change\", (e) => { - if (!EMOJI_PICKER_REACTION_MESSAGE_ID) { - return; - } - - const emoji = e.target.value === \"::\" ? \":heart:\" : e.target.value; - trigger(\"me::message_react\", [document.getElementById(`message-${EMOJI_PICKER_REACTION_MESSAGE_ID}`), EMOJI_PICKER_REACTION_MESSAGE_ID, emoji]); - });")) - - ; ... - (script - (text "window.CURRENT_PAGE = Number.parseInt(\"{{ page }}\"); - window.VIEWING_SINGLE = \"{{ message }}\".length > 0; - window.CHAT_PROPS = { - selected_community: \"{{ selected_community }}\", - selected_channel: \"{{ selected_channel }}\", - membership_role: Number.parseInt(\"{{ membership_role }}\"), - }; - - window.SIDEBARS_OPEN = false; - if (new URLSearchParams(window.location.search).get(\"nav\") === \"true\") { - window.SIDEBARS_OPEN = true; - } - - if ( - window.SIDEBARS_OPEN && - !document.body.classList.contains(\"sidebars_shown\") - ) { - toggle_sidebars(); - window.SIDEBARS_OPEN = true; - } - - for (const anchor of document.querySelectorAll(\"[data-turbo=false]\")) { - anchor.href += `?nav=${window.SIDEBARS_OPEN}`; - } - - function mention_user(username) { - document.getElementById(\"content\").value += ` @${username} `; - } - - function toggle_sidebars() { - window.SIDEBARS_OPEN = !window.SIDEBARS_OPEN; - - for (const anchor of document.querySelectorAll( - \"[data-turbo=false]\", - )) { - anchor.href = anchor.href.replace( - `?nav=${!window.SIDEBARS_OPEN}`, - `?nav=${window.SIDEBARS_OPEN}`, - ); - } - - const community_list = document.getElementById(\"community_list\"); - const channels_list = document.getElementById(\"channels_list\"); - - if (document.body.classList.contains(\"sidebars_shown\")) { - // hide - document.body.classList.remove(\"sidebars_shown\"); - community_list.style.left = \"-200%\"; - channels_list.style.left = \"-200%\"; - } else { - // show - document.body.classList.add(\"sidebars_shown\"); - community_list.style.left = \"0\"; - channels_list.style.left = \"var(--list-bar-width)\"; - } - } - - globalThis.add_member = async (id) => { - await trigger(\"atto::debounce\", [\"channels::add_member\"]); - const member = await trigger(\"atto::prompt\", [\"Member username:\"]); - - if (!member) { - return; - } - - fetch(`/api/v1/channels/${id}/add`, { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - member, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - - globalThis.mute_channel = async (id, mute = true) => { - await trigger(\"atto::debounce\", [\"channels::mute\"]); - fetch(`/api/v1/channels/${id}/mute`, { - method: mute ? \"POST\" : \"DELETE\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - if (mute) { - document.querySelector(`[ui_ident=channel\\\\.mute\\\\:${id}]`).classList.add(\"hidden\"); - document.querySelector(`[ui_ident=channel\\\\.unmute\\\\:${id}]`).classList.remove(\"hidden\"); - } else { - document.querySelector(`[ui_ident=channel\\\\.mute\\\\:${id}]`).classList.remove(\"hidden\"); - document.querySelector(`[ui_ident=channel\\\\.unmute\\\\:${id}]`).classList.add(\"hidden\"); - } - } - }); - }; - - globalThis.update_channel_title = async (id) => { - await trigger(\"atto::debounce\", [\"channels::update_title\"]); - const title = await trigger(\"atto::prompt\", [\"New channel title:\"]); - - if (!title) { - return; - } - - fetch(`/api/v1/channels/${id}/title`, { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - title, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - - globalThis.kick_member = async (cid, uid) => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(`/api/v1/channels/${cid}/kick`, { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - member: uid, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - - globalThis.delete_channel = async (id) => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(`/api/v1/channels/${id}`, { - method: \"DELETE\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - };")) - (script - ("id" "socket_init") - ("data-turbo-permanent" "true") - (text "globalThis.socket_init = () => { - if (window.socket) { - window.socket.send(\"Close\"); - window.socket.close(); - window.socket = undefined; - console.log(\"closed lingering\"); - } - - if (window.CHAT_PROPS.selected_community !== \"0\") { - const endpoint = `${window.location.origin.replace(\"http\", \"ws\")}/api/v1/_connect/${window.CHAT_PROPS.selected_community}`; - const socket = new WebSocket(endpoint); - window.socket = socket; - window.socket_id = window.CHAT_PROPS.selected_community; - } else { - const endpoint = `${window.location.origin.replace(\"http\", \"ws\")}/api/v1/_connect/${window.CHAT_PROPS.selected_channel}`; - const socket = new WebSocket(endpoint); - window.socket = socket; - window.socket_id = window.CHAT_PROPS.selected_channel; - } - - if (window.CHANNEL_NOTIFS_INTERVAL) { - window.clearInterval(window.CHANNEL_NOTIFS_INTERVAL); - } - - window.CHANNEL_NOTIFS_INTERVAL = setInterval(() => { - if (!window.CHAT_PROPS.selected_channel) { - return; - } - - if (!window.location.href.includes(\"{{ selected_channel }}\")) { - window.clearInterval(window.CHANNEL_NOTIFS_INTERVAL); - return; - } - - fetch( - `/api/v1/notifications/tag/chats/${window.CHAT_PROPS.selected_channel}`, - { method: \"DELETE\" }, - ); - }, 10000); - - window.socket.addEventListener(\"open\", () => { - // auth - window.socket.send( - JSON.stringify({ - method: \"Headers\", - data: JSON.stringify({ - // SocketHeaders - is_channel: window.SUBSCRIBE_CHANNEL, - }), - }), - ); - }); - - setTimeout(() => { - window.LAST_MESSAGE_AUTHOR_ID = null; - window.socket.addEventListener(\"message\", async (event) => { - if (event.data === \"Ping\") { - return socket.send(\"Pong\"); - } - - const msg = JSON.parse(event.data); - - if ( - msg.method === \"Message\" && - window.CURRENT_PAGE === 0 && - window.VIEWING_SINGLE - ) { - const [channel_id, data] = JSON.parse(msg.data); - if (channel_id !== window.CHAT_PROPS.selected_channel) { - // message not for us... maybe send notification later - // something like /api/v1/messages/{id}/mark_unread - return; - } - - if (document.getElementById(\"stream_body\")) { - const element = document.createElement(\"div\"); - element.style.display = \"contents\"; - - const message_owner = JSON.parse(msg.data)[1].owner; - element.innerHTML = await ( - await fetch( - `/chats/${window.CHAT_PROPS.selected_community}/${window.CHAT_PROPS.selected_channel}/_render`, - { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - data: msg.data, - grouped: - message_owner === - window.LAST_MESSAGE_AUTHOR_ID, - }), - }, - ) - ).text(); - - document - .getElementById(\"stream_body\") - .prepend(element); - clean_text(); - - window.LAST_MESSAGE_AUTHOR_ID = message_owner; - } else { - console.log(\"abandoned remote\"); - socket.close(); - } - } else if (msg.method === \"Delete\") { - const data = JSON.parse(msg.data); - if (document.getElementById(`message-${data.id}`)) { - document - .getElementById(`message-${data.id}`) - .remove(); - } - } - }); - - globalThis.create_message_from_form = async (e) => { - e.preventDefault(); - await trigger(\"atto::debounce\", [\"messages::create\"]); - - fetch(\"/api/v1/messages\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - content: e.target.content.value.trim(), - channel: window.CHAT_PROPS.selected_channel, - }), - }) - .then((res) => res.json()) - .then((res) => { - if (!res.ok) { - trigger(\"atto::toast\", [\"error\", res.message]); - } - - e.target.reset(); - }); - }; - - globalThis.delete_message = async (id) => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(`/api/v1/messages/${id}`, { - method: \"DELETE\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - - const clean_text = () => { - trigger(\"atto::clean_date_codes\"); - trigger(\"atto::hooks::online_indicator\"); - trigger(\"atto::hooks::check_message_reactions\"); - }; - - document.addEventListener( - \"turbo:before-frame-render\", - (event) => { - setTimeout(clean_text, 50); - }, - ); - - setTimeout(clean_text, 150); - }, 250); - };")) - (text "{% if selected_channel -%}") - (script - (text "window.SUBSCRIBE_CHANNEL = \"{{ selected_community }}\" === \"0\"; - - setTimeout(() => { - if (!window.SUBSCRIBE_CHANNEL) { - // sub community - if (window.socket_id !== \"{{ selected_community }}\") { - socket_init(); - } - } else { - // sub channel - if (window.socket_id !== \"{{ selected_channel }}\") { - socket_init(); - } - } - }, 100);")) - (text "{%- endif %}")) -(text "{% endblock %}") diff --git a/crates/app/src/public/html/chats/channels.lisp b/crates/app/src/public/html/chats/channels.lisp deleted file mode 100644 index ced2f93..0000000 --- a/crates/app/src/public/html/chats/channels.lisp +++ /dev/null @@ -1,80 +0,0 @@ -(text "{%- import \"components.html\" as components -%}") -(turbo-frame - ("id" "channels_list_frame") - (div - ("class" "channels_list_half flex flex_col gap_2 {% if selected_community != 0 or selected_channel == 0%}no_members{%- endif -%}") - (text "{% for channel in channels %}") - (div - ("class" "flex flex_row gap_1") - (a - ("class" "w_full justify_start button {% if selected_channel == channel.id -%}lowered{% else %}camo{%- endif %}") - ("href" "/chats/{{ selected_community }}/{{ channel.id }}") - ("data-turbo" "{{ selected_community == '0' }}") - (text "{{ icon \"rss\" }}") - (b - ("class" "name shortest") - (text "{{ channel.title }}"))) - (div - ("class" "dropdown") - (button - ("class" "big_icon {% if selected_channel == channel.id -%}lowered{% else %}camo{%- endif %}") - ("onclick" "trigger('atto::hooks::dropdown', [event])") - ("exclude" "dropdown") - ("style" "width: 32px") - (text "{{ icon \"ellipsis\" }}")) - (div - ("class" "inner") - (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 - ("onclick" "add_member('{{ channel.id }}')") - (text "{{ icon \"user-plus\" }}") - (span - (text "{{ text \"chats:action.add_someone\" }}"))) - (button - ("onclick" "update_channel_title('{{ channel.id }}')") - (text "{{ icon \"pencil\" }}") - (span - (text "{{ text \"chats:action.rename\" }}"))) - (button - ("onclick" "delete_channel('{{ channel.id }}')") - ("class" "red") - (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 - (button - ("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')") - ("class" "red") - (text "{{ icon \"door-open\" }}") - (span - (text "{{ text \"chats:action.leave\" }}"))) - (text "{%- endif %} {%- endif %}")))) - (text "{% endfor %}")) - (text "{% if selected_community == 0 and selected_channel -%}") - (div - ("class" "members_list_half flex flex_col gap_2") - (text "{% for member in members %} {{ components::user_plate(user=member, show_kick=user.id == channel.owner) }} {% endfor %}")) - (text "{%- endif %}")) diff --git a/crates/app/src/public/html/chats/message.lisp b/crates/app/src/public/html/chats/message.lisp deleted file mode 100644 index 4d5946c..0000000 --- a/crates/app/src/public/html/chats/message.lisp +++ /dev/null @@ -1 +0,0 @@ -(text "{%- import \"components.html\" as components -%} {{ components::message(user=user, message=message, grouped=grouped) }}") diff --git a/crates/app/src/public/html/chats/stream.lisp b/crates/app/src/public/html/chats/stream.lisp deleted file mode 100644 index af891e5..0000000 --- a/crates/app/src/public/html/chats/stream.lisp +++ /dev/null @@ -1,55 +0,0 @@ -(text "{%- import \"components.html\" as components -%}") -(turbo-frame - ("id" "stream_body_frame") - (div - ("class" "gap_2") - ("id" "stream_body") - (text "{% if page != 0 -%}") - (div - ("class" "card flex gap_2 small lowered flex_wrap") - (b - (text "{{ text \"chats:label.viewing_old_messages\" }}")) - (a - ("href" "/chats/{{ community }}/{{ channel.id }}/_stream?page={{ page - 1}}") - ("class" "button small") - ("onclick" "window.CURRENT_PAGE -= 1") - (text "{{ text \"chats:label.go_back\" }}"))) - (text "{%- endif %} {% if message -%}") - (div - ("class" "card flex gap_2 small lowered flex_wrap") - (b - (text "{{ text \"chats:label.viewing_single_message\" }}")) - (a - ("href" "/chats/{{ community }}/{{ channel.id }}?page={{ page }}") - ("class" "button small") - ("onclick" "window.VIEWING_SINGLE = false") - ("target" "_top") - (text "{{ text \"chats:label.go_back\" }}"))) - (text "{{ components::message(user=message_owner, message=message, grouped=false) }} {% else %} {% for message in messages %} {{ components::message(user=message[1], message=message[0], grouped=message[2]) }} {% endfor %} {%- endif %} {% if messages|length > 0 -%}") - (div - ("class" "flex gap_2 w_full justify_center") - (a - ("class" "button") - ("href" "/chats/{{ community }}/{{ channel.id }}/_stream?page={{ page + 1 }}") - ("onclick" "window.CURRENT_PAGE += 1") - (text "{{ icon \"clock\" }}") - (span - (text "{{ text \"chats:label.view_older\" }}"))) - (text "{% if page != 0 -%}") - (a - ("class" "button lowered") - ("href" "/chats/{{ community }}/{{ channel }}/_stream?page={{ page - 1 }}") - ("onclick" "window.CURRENT_PAGE -= 1") - (text "{{ icon \"rewind\" }}") - (span - (text "{{ text \"chats:label.view_more_recent\" }}"))) - (text "{%- endif %}")) - (text "{%- endif %}")) - (style - (text "#stream_body { - height: 100%; - display: flex; - justify-content: flex-start; - flex-direction: column-reverse; - overflow: auto; - }"))) diff --git a/crates/app/src/public/html/communities/settings.lisp b/crates/app/src/public/html/communities/settings.lisp index a51c69f..a32a573 100644 --- a/crates/app/src/public/html/communities/settings.lisp +++ b/crates/app/src/public/html/communities/settings.lisp @@ -29,14 +29,6 @@ (text "{{ text \"communities:tab.members\" }}")))) (div ("class" "row") - (text "{% if can_manage_channels -%}") - (a - ("href" "#/channels") - ("data-tab-button" "channels") - (text "{{ icon \"rss\" }}") - (span - (text "{{ text \"communities:tab.channels\" }}"))) - (text "{%- endif %}") (a ("href" "#/topics") ("data-tab-button" "topics") @@ -277,163 +269,7 @@ (div ("class" "card flex flex_col gap_2") ("id" "permission_builder")))) - (text "{% if can_manage_channels -%}") - (div - ("class" "card lowered w_full hidden flex flex_col gap_2") - ("data-tab" "channels") - (div - ("class" "card_nest") - (div - ("class" "card small") - (b - (text "{{ text \"communities:action.create_channel\" }}"))) - (form - ("class" "card flex flex_col gap_2") - ("onsubmit" "create_channel_from_form(event)") - (div - ("class" "flex flex_col gap_1") - (label - ("for" "title") - (str (text "communities:label.name"))) - (input - ("type" "text") - ("name" "title") - ("id" "title") - ("placeholder" "name") - ("required" "") - ("minlength" "2") - ("maxlength" "32"))) - (button - (str (text "communities:action.create"))))) - (text "{% for channel in channels %}") - (div - ("class" "card_nest") - (div - ("class" "card small") - (b - (text "{{ channel.position }} ")) - (text "{{ channel.title }}")) - (div - ("class" "card flex gap_2") - (button - ("class" "red lowered small") - ("onclick" "delete_channel('{{ channel.id }}')") - (text "{{ text \"general:action.delete\" }}")) - (button - ("class" "lowered small") - ("onclick" "update_channel_position('{{ channel.id }}')") - (text "{{ text \"chats:action.move\" }}")) - (button - ("class" "lowered small") - ("onclick" "update_channel_title('{{ channel.id }}')") - (text "{{ text \"chats:action.rename\" }}")))) - (text "{% endfor %}")) - (script - (text "globalThis.delete_channel = async (id) => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(`/api/v1/channels/${id}`, { - method: \"DELETE\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - - globalThis.update_channel_position = async (id) => { - await trigger(\"atto::debounce\", [\"channels::move\"]); - - const position = Number.parseInt( - await trigger(\"atto::prompt\", [ - \"New channel position (number):\", - ]), - ); - - if (!position && position !== 0) { - return alert(\"Must be a number!\"); - } - - fetch(`/api/v1/channels/${id}/move`, { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - position, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - - globalThis.update_channel_title = async (id) => { - await trigger(\"atto::debounce\", [\"channels::update_title\"]); - const title = await trigger(\"atto::prompt\", [\"New channel title:\"]); - - if (!title) { - return; - } - - fetch(`/api/v1/channels/${id}/title`, { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - title, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - - async function create_channel_from_form(e) { - e.preventDefault(); - await trigger(\"atto::debounce\", [\"channels::create\"]); - - fetch(\"/api/v1/channels\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - title: e.target.title.value, - community: \"{{ community.id }}\", - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - window.location.reload(); - } - }); - }")) - (text "{%- endif %} {% if can_manage_emojis -%}") + (text "{% if can_manage_emojis -%}") (div ("class" "card lowered w_full hidden flex flex_col gap_2") ("data-tab" "emojis") @@ -987,7 +823,7 @@ MANAGE_PINS: 1 << 7, MANAGE_COMMUNITY: 1 << 8, MANAGE_QUESTIONS: 1 << 9, - MANAGE_CHANNELS: 1 << 10, + UNUSED_0: 1 << 10, MANAGE_MESSAGES: 1 << 11, MANAGE_EMOJIS: 1 << 12, }, diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 41cd037..28ddeb7 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -1145,106 +1145,8 @@ (div ("style" "display: contents;") (text "{% if key == \"Spotify\" -%} {{ icon \"spotify\" }} {% elif key == \"LastFm\" %} {{ icon \"last_fm\" }} {%- endif %}")) -(text "{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == \"LastFm\" %} https://last.fm/user/{{ value[0].data.name }} {%- endif %} {%- endmacro %} {% macro message_actions(can_manage_message, owner, message, owner) -%}") -(div - ("class" "dropdown") - (button - ("class" "camo small") - ("onclick" "trigger('atto::hooks::dropdown', [event])") - ("exclude" "dropdown") - ("title" "More options") - (text "{{ icon \"ellipsis\" }}")) - (div - ("class" "inner") - (text "{% if can_manage_message or (user and user.id == message.owner) -%}") - (button - ("class" "red") - ("onclick" "delete_message('{{ message.id }}')") - (text "{{ icon \"trash\" }}") - (span - (text "{{ text \"general:action.delete\" }}"))) - (text "{%- endif %}") - (button - ("onclick" "window.location.href = `${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`") - (text "{{ icon \"external-link\" }}") - (span - (text "{{ text \"general:action.open\" }}"))) - (button - ("onclick" "trigger('atto::copy_text', [`${window.location.origin}/chats/{{ community }}/{{ channel }}?message={{ message.id }}`])") - (text "{{ icon \"copy\" }}") - (span - (text "{{ text \"general:action.copy_link\" }}"))) - (button - ("onclick" "mention_user('{{ owner.username }}')") - (text "{{ icon \"at-sign\" }}") - (span - (text "{{ text \"chats:action.mention_user\" }}"))))) - -(text "{%- endmacro %} {% macro message(user, message, can_manage_message=false, grouped=false) -%}") -(div - ("class" "card secondary message flex gap_2 {% if grouped -%}grouped{%- endif %}") - ("id" "message-{{ message.id }}") - (text "{% if not grouped -%}") - (a - ("href" "/@{{ user.username }}") - ("target" "_top") - (text "{{ self::avatar(id=user.id, size=\"42px\") }}")) - (text "{%- endif %}") - (div - ("class" "flex flex_col gap_1 w_full") - (text "{% if not grouped -%}") - (div - ("class" "flex gap_2 w_full justify_between flex_wrap") - (div - ("class" "flex gap_2") - (text "{{ self::full_username(user=user) }} {% if message.edited != message.created %}") - (span - ("class" "date") - (text "{{ message.edited }}") - (sup - ("title" "Edited") - (text "*"))) - (text "{% else %}") - (span - ("class" "date") - (text "{{ message.created }}")) - (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 - ("class" "flex w_full gap_2 justify_between") - (div - ("class" "flex flex_col gap_2") - (span - ("class" "no_p_margin") - (text "{{ message.content|markdown|safe }}")) - - (div - ("class" "flex w_full gap_1 flex_wrap") - ("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'") - ("hook" "check_message_reactions") - ("hook-arg:id" "{{ message.id }}") - - (text "{% for emoji,num in message.reactions -%}") - (button - ("class" "small lowered") - ("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 "{% 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) }}") - (text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")) - (text "{%- endif %}")))) - -(text "{%- endmacro %} {% macro user_menu() -%}") +(text "{%- endmacro %} {% macro connection_url(key, value) -%} {% if value[0].data.url %} {{ value[0].data.url }} {% elif key == \"LastFm\" %} https://last.fm/user/{{ value[0].data.name }} {%- endif %} {%- endmacro %}") +(text "{% macro user_menu() -%}") (div ("class" "inner") (b @@ -2050,12 +1952,6 @@ (text "{{ icon \"circle-minus\" }}") (span (text "{{ text \"communities:action.leave\" }}"))) - (a - ("href" "/chats/{{ community.id }}/0") - ("class" "button lowered") - (text "{{ icon \"message-circle\" }}") - (span - (text "{{ text \"communities:label.chats\" }}"))) (text "{% if user and can_post -%}") (a ("href" "/communities/intents/post?community={{ community.id }}") @@ -2094,12 +1990,6 @@ }); };")) (text "{%- endif %} {% else %}") - (a - ("href" "/chats/{{ community.id }}/0") - ("class" "button lowered") - (text "{{ icon \"message-circle\" }}") - (span - (text "{{ text \"communities:label.chats\" }}"))) (text "{% if not community.is_forum -%}") (a ("href" "/communities/intents/post?community={{ community.id }}") diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp index 040ffc9..4903146 100644 --- a/crates/app/src/public/html/macros.lisp +++ b/crates/app/src/public/html/macros.lisp @@ -68,10 +68,6 @@ (div ("class" "inner") - (a - ("href" "/chats/0/0") - (icon (text "message-circle")) - (str (text "communities:label.chats"))) (text "{% if config.service_hosts.tawny -%}") (a ("href" "{{ config.service_hosts.tawny }}/api/v1/auth/set_token?token=") diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp index af048e5..efac7d7 100644 --- a/crates/app/src/public/html/mod/profile.lisp +++ b/crates/app/src/public/html/mod/profile.lisp @@ -409,7 +409,7 @@ SUPPORTER: 1 << 19, MANAGE_REQUESTS: 1 << 20, MANAGE_QUESTIONS: 1 << 21, - MANAGE_CHANNELS: 1 << 22, + UNUSED_0: 1 << 22, MANAGE_MESSAGES: 1 << 23, MANAGE_UPLOADS: 1 << 24, MANAGE_EMOJIS: 1 << 25, diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp index 399ce39..31e6404 100644 --- a/crates/app/src/public/html/profile/base.lisp +++ b/crates/app/src/public/html/profile/base.lisp @@ -313,31 +313,7 @@ (text "{{ text \"general:action.manage\" }}"))) (text "{%- endif %}") (script - (text "globalThis.create_group_chat = async () => { - fetch(\"/api/v1/channels/group\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - title: \"{{ user.username }} & {{ profile.username }}\", - members: [\"{{ profile.id }}\"], - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - window.location.href = `/chats/0/${res.payload}`; - } - }); - }; - - globalThis.request_transfer = async () => { + (text "globalThis.request_transfer = async () => { await trigger(\"atto::debounce\", [\"economy::transfer\"]); const amount = Number.parseInt((await trigger(\"atto::prompt\", [\"Request amount:\"])) || \"0\"); diff --git a/crates/app/src/routes/api/v1/channels/channels.rs b/crates/app/src/routes/api/v1/channels/channels.rs deleted file mode 100644 index 2059a0f..0000000 --- a/crates/app/src/routes/api/v1/channels/channels.rs +++ /dev/null @@ -1,354 +0,0 @@ -use axum::{Extension, Json, extract::Path, response::IntoResponse}; -use crate::cookie::CookieJar; -use tetratto_core::model::{oauth, channels::Channel, ApiReturn, Error}; -use crate::{ - get_user_from_token, - routes::api::v1::{ - CreateChannel, CreateGroupChannel, KickMember, UpdateChannelPosition, UpdateChannelTitle, - }, - State, -}; - -pub async fn create_request( - jar: CookieJar, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityCreateChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data - .create_channel(Channel::new( - match req.community.parse::() { - Ok(c) => c, - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - }, - user.id, - 0, - req.title, - )) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel created".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn create_group_request( - jar: CookieJar, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let mut members: Vec = Vec::new(); - - for member in req.members { - members.push(match member.parse::() { - Ok(c) => c, - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - }) - } - - // check for existing - if members.len() == 1 { - let other_user = members.first().unwrap().to_owned(); - if let Ok(channel) = data.get_channel_by_owner_member(user.id, other_user).await { - return Json(ApiReturn { - ok: true, - message: "Channel exists".to_string(), - payload: Some(channel.id.to_string()), - }); - } - } - - // check member permissions - for member in &members { - let other_user = match data.get_user_by_id(member.to_owned()).await { - Ok(ua) => ua, - Err(e) => return Json(e.into()), - }; - - if other_user.settings.private_chats - && data - .get_userfollow_by_initiator_receiver(other_user.id, user.id) - .await - .is_err() - { - return Json(Error::NotAllowed.into()); - } - } - - // ... - let mut props = Channel::new(0, user.id, 0, req.title); - props.members = members; - let id = props.id; - - match data.create_channel(props).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel created".to_string(), - payload: Some(id.to_string()), - }), - 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::CommunityManageChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.delete_channel(id, &user).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel deleted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn update_title_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::CommunityManageChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.update_channel_title(id, &user, &req.title).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn update_position_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::CommunityManageChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.update_channel_position(id, &user, req.position).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn add_member_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::CommunityManageChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.add_channel_member(id, user, req.member).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Member added".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn kick_member_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::CommunityManageChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data - .remove_channel_member( - id, - user, - match req.member.parse::() { - Ok(c) => c, - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - }, - ) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Member removed".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn get_dm_channels_request( - jar: CookieJar, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.get_channels_by_user(user.id).await { - Ok(c) => Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(c), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn get_community_channels_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::CommunityManageChannels) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if data - .get_membership_by_owner_community_no_void(user.id, id) - .await - .is_err() - { - // must be a member of the community to request channels - return Json(Error::NotAllowed.into()); - } - - match data.get_channels_by_community(id).await { - Ok(c) => Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(c), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn get_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - if get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels).is_none() { - return Json(Error::NotAllowed.into()); - } - - match data.get_channel_by_id(id).await { - Ok(c) => Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(c), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn mute_channel_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageChannelMutes) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if user.channel_mutes.contains(&id) { - return Json(Error::MiscError("Channel already muted".to_string()).into()); - } - - user.channel_mutes.push(id); - match data - .update_user_channel_mutes(user.id, user.channel_mutes) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel muted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn unmute_channel_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageChannelMutes) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let pos = match user.channel_mutes.iter().position(|x| *x == id) { - Some(x) => x, - None => return Json(Error::MiscError("Channel not muted".to_string()).into()), - }; - - user.channel_mutes.remove(pos); - match data - .update_user_channel_mutes(user.id, user.channel_mutes) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel muted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/channels/message_reactions.rs b/crates/app/src/routes/api/v1/channels/message_reactions.rs deleted file mode 100644 index 5f5c79c..0000000 --- a/crates/app/src/routes/api/v1/channels/message_reactions.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::{get_user_from_token, routes::api::v1::CreateMessageReaction, State}; -use axum::{Extension, Json, extract::Path, response::IntoResponse}; -use crate::cookie::CookieJar; -use tetratto_core::model::{channels::MessageReaction, oauth, ApiReturn, Error}; - -pub async fn get_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::UserReact) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data - .get_message_reactions_by_owner_message(user.id, id) - .await - { - Ok(r) => Json(ApiReturn { - ok: true, - message: "Reactions exists".to_string(), - payload: Some(r), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn create_request( - jar: CookieJar, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReact) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let message_id = match req.message.parse::() { - Ok(n) => n, - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - }; - - // check for existing reaction - if let Ok(r) = data - .get_message_reaction_by_owner_message_emoji(user.id, message_id, &req.emoji) - .await - { - if let Err(e) = data.delete_message_reaction(r.id, &user).await { - return Json(e.into()); - } else { - return Json(ApiReturn { - ok: true, - message: "Reaction removed".to_string(), - payload: (), - }); - } - } - - // create reaction - match data - .create_message_reaction(MessageReaction::new(user.id, message_id, req.emoji), &user) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Reaction created".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn delete_request( - jar: CookieJar, - Extension(data): Extension, - Path((id, emoji)): Path<(usize, String)>, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReact) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let reaction = match data - .get_message_reaction_by_owner_message_emoji(user.id, id, &emoji) - .await - { - Ok(r) => r, - Err(e) => return Json(e.into()), - }; - - match data.delete_message_reaction(reaction.id, &user).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Reaction deleted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/channels/messages.rs b/crates/app/src/routes/api/v1/channels/messages.rs deleted file mode 100644 index 92a5c48..0000000 --- a/crates/app/src/routes/api/v1/channels/messages.rs +++ /dev/null @@ -1,352 +0,0 @@ -use std::{collections::HashMap, time::Duration}; -use axum::{ - extract::{ - ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}, - Path, Query, - }, - response::IntoResponse, - Extension, Json, -}; -use crate::cookie::CookieJar; -use tetratto_core::{ - cache::{Cache, redis::Commands}, - model::{ - oauth, - auth::User, - channels::Message, - socket::{PacketType, SocketMessage, SocketMethod}, - ApiReturn, Error, - }, - DataManager, -}; -use crate::{ - get_user_from_token, - routes::{api::v1::CreateMessage, pages::PaginatedQuery}, - State, -}; -use serde::Deserialize; -use futures_util::{sink::SinkExt, stream::StreamExt}; - -#[derive(Clone, Deserialize)] -pub struct SocketHeaders { - pub is_channel: bool, -} - -/// Handle a subscription to the websocket. -pub async fn subscription_handler( - jar: CookieJar, - ws: WebSocketUpgrade, - 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::UserReadSockets) { - Some(ua) => ua, - None => return Err(Error::NotAllowed.to_string()), - }; - - let data = data.clone(); - Ok(ws.on_upgrade(|socket| async move { - tokio::spawn(async move { - handle_socket(socket, data, id, user).await; - }); - })) -} - -pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: String, user: User) { - let (mut sink, mut stream) = socket.split(); - let mut headers: Option = None; - - let channel_id = format!("chats/{community_id}"); - - // handle incoming messages on socket - let dbc = db.clone(); - if let Some(Ok(WsMessage::Text(text))) = stream.next().await { - let data: SocketMessage = match serde_json::from_str(&text.to_string()) { - Ok(t) => t, - Err(_) => { - let _ = sink.close().await; - return; - } - }; - - if data.method != SocketMethod::Headers && headers.is_none() { - // we've sent something else before authenticating... that's not right - let _ = sink.close().await; - return; - } - - match data.method { - SocketMethod::Headers => { - let data: SocketHeaders = data.data(); - - headers = Some(data.clone()); - - if data.is_channel { - // verify permissions for single channel - let channel = match dbc - .get_channel_by_id(match community_id.parse::() { - Ok(c) => c, - Err(_) => { - let _ = sink.close().await; - return; - } - }) - .await - { - Ok(c) => c, - Err(_) => { - let _ = sink.close().await; - return; - } - }; - - let membership = match dbc - .get_membership_by_owner_community(user.id, channel.id) - .await - { - Ok(ua) => ua, - Err(_) => { - let _ = sink.close().await; - return; - } - }; - - if !channel.check_read(user.id, Some(membership.role)) { - let _ = sink.close().await; - return; - } - } - } - _ => { - let _ = sink.close().await; - return; - } - } - } else { - sink.close().await.unwrap(); - return; - } - - // get channel permissions - let headers = headers.unwrap(); - - let mut channel_read_statuses: HashMap = HashMap::new(); - if !headers.is_channel { - // check permissions for every channel in community - let community_id = match community_id.parse::() { - Ok(c) => c, - Err(_) => return, - }; - - let membership = match dbc - .get_membership_by_owner_community(user.id, community_id) - .await - { - Ok(ua) => ua, - Err(_) => { - return; - } - }; - - for channel in dbc.get_channels_by_community(community_id).await.unwrap() { - channel_read_statuses.insert( - channel.id, - channel.check_read(user.id, Some(membership.role)), - ); - } - } - - // ... - let mut recv_task = tokio::spawn(async move { - while let Some(Ok(WsMessage::Text(text))) = stream.next().await { - if text != "Close" { - continue; - } - - // yes, this is an "unclean" disconnection from the socket... - // i don't care, it works - drop(stream); - break; - } - }); - - let dbc = db.clone(); - let channel_id_c = channel_id.clone(); - let mut redis_task = tokio::spawn(async move { - // forward messages from redis to the socket - let mut pubsub = dbc.0.1.client.get_async_pubsub().await.unwrap(); - - pubsub.subscribe(user.id).await.unwrap(); - pubsub.subscribe(channel_id_c).await.unwrap(); - - // listen for pubsub messages - let mut pubsub = pubsub.into_on_message(); - while let Some(msg) = pubsub.next().await { - // payload is a stringified SocketMessage - let smsg = msg.get_payload::().unwrap(); - let packet: SocketMessage = serde_json::from_str(&smsg).unwrap(); - - if packet.method == SocketMethod::Forward(PacketType::Ping) { - // forward with custom message - if sink.send(WsMessage::Text("Ping".into())).await.is_err() { - drop(sink); - break; - } - } else if packet.method == SocketMethod::Message { - // check perms and then forward - let d: (String, Message) = packet.data(); - - if let Some(cs) = channel_read_statuses.get(&d.1.channel) { - if !cs { - continue; - } - } else if !headers.is_channel { - // since we didn't select by just a channel, there HAS to be - // an entry for the channel for us to check this message - continue; - // we don't need to check messages when we're subscribed to - // a channel, since that is checked on headers submission when - // we subscribe to a channel - } - - if sink.send(WsMessage::Text(smsg.into())).await.is_err() { - drop(sink); - break; - } - } else { - // forward to client - if sink.send(WsMessage::Text(smsg.into())).await.is_err() { - drop(sink); - break; - } - } - } - }); - - let db2c = db.0.1.clone(); - let heartbeat_task = tokio::spawn(async move { - let mut con = db2c.get_con().await; - let mut heartbeat = tokio::time::interval(Duration::from_secs(10)); - - loop { - con.publish::( - user.id, - serde_json::to_string(&SocketMessage { - method: SocketMethod::Forward(PacketType::Ping), - data: "Ping".to_string(), - }) - .unwrap(), - ) - .unwrap(); - - heartbeat.tick().await; - } - }); - - db.0.1 - .incr("atto.active_connections:chats".to_string()) - .await; - - tokio::select! { - _ = (&mut recv_task) => redis_task.abort(), - _ = (&mut redis_task) => recv_task.abort() - } - - heartbeat_task.abort(); // kill - db.0.1 - .decr("atto.active_connections:chats".to_string()) - .await; - tracing::info!("socket terminate"); -} - -pub async fn create_request( - jar: CookieJar, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateMessages) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data - .create_message(Message::new( - match req.channel.parse::() { - Ok(c) => c, - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - }, - user.id, - req.content, - )) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Message 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::UserDeleteMessages) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.delete_message(id, user).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Message deleted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn from_channel_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, - Query(props): Query, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateMessages) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let channel = match data.get_channel_by_id(id).await { - Ok(c) => c, - Err(e) => return Json(e.into()), - }; - - let membership = match data - .get_membership_by_owner_community(user.id, channel.community) - .await - { - Ok(m) => m, - Err(e) => return Json(e.into()), - }; - - if !channel.check_read(user.id, Some(membership.role)) { - return Json(Error::NotAllowed.into()); - } - - match data.get_messages_by_channel(id, 24, props.page).await { - Ok(m) => Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(m), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/channels/mod.rs b/crates/app/src/routes/api/v1/channels/mod.rs deleted file mode 100644 index 33792c3..0000000 --- a/crates/app/src/routes/api/v1/channels/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod channels; -pub mod message_reactions; -pub mod messages; diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 03593c7..b4e0e4c 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -2,7 +2,6 @@ pub mod ads; pub mod app_data; pub mod apps; pub mod auth; -pub mod channels; pub mod communities; pub mod domains; pub mod journals; @@ -55,19 +54,6 @@ pub fn routes() -> Router { .route("/reactions", post(reactions::create_request)) .route("/reactions/{id}", get(reactions::get_request)) .route("/reactions/{id}", delete(reactions::delete_request)) - // message reactions - .route( - "/message_reactions", - post(channels::message_reactions::create_request), - ) - .route( - "/message_reactions/{id}", - get(channels::message_reactions::get_request), - ) - .route( - "/message_reactions/{id}/{emoji}", - delete(channels::message_reactions::delete_request), - ) // communities .route( "/communities/find/{id}", @@ -586,57 +572,6 @@ pub fn routes() -> Router { "/service_hooks/stripe/checkout/success", get(auth::connections::stripe::handle_stupid_fucking_checkout_success_session), ) - // channels - .route("/channels", post(channels::channels::create_request)) - .route( - "/channels/group", - post(channels::channels::create_group_request), - ) - .route( - "/channels/{id}/title", - post(channels::channels::update_title_request), - ) - .route( - "/channels/{id}/move", - post(channels::channels::update_position_request), - ) - .route("/channels/{id}", delete(channels::channels::delete_request)) - .route( - "/channels/{id}/add", - post(channels::channels::add_member_request), - ) - .route( - "/channels/{id}/kick", - post(channels::channels::kick_member_request), - ) - .route( - "/channels/{id}/mute", - post(channels::channels::mute_channel_request), - ) - .route( - "/channels/{id}/mute", - delete(channels::channels::unmute_channel_request), - ) - .route("/channels/{id}", get(channels::channels::get_request)) - .route( - "/channels/community/{id}", - get(channels::channels::get_community_channels_request), - ) - .route( - "/channels/dms", - get(channels::channels::get_dm_channels_request), - ) - // messages - .route( - "/_connect/{id}", - any(channels::messages::subscription_handler), - ) - .route("/messages", post(channels::messages::create_request)) - .route("/messages/{id}", delete(channels::messages::delete_request)) - .route( - "/messages/from_channel/{id}", - get(channels::messages::from_channel_request), - ) // emojis .route( "/lookup_emoji", @@ -1012,39 +947,6 @@ pub struct CreateQuestion { pub asking_about: String, } -#[derive(Deserialize)] -pub struct CreateChannel { - pub title: String, - pub community: String, -} - -#[derive(Deserialize)] -pub struct CreateGroupChannel { - pub title: String, - pub members: Vec, -} - -#[derive(Deserialize)] -pub struct UpdateChannelTitle { - pub title: String, -} - -#[derive(Deserialize)] -pub struct UpdateChannelPosition { - pub position: i32, -} - -#[derive(Deserialize)] -pub struct CreateMessage { - pub content: String, - pub channel: String, -} - -#[derive(Deserialize)] -pub struct KickMember { - pub member: String, -} - #[derive(Deserialize)] pub struct CreateStack { pub name: String, @@ -1185,12 +1087,6 @@ pub struct RenderMarkdown { pub content: String, } -#[derive(Deserialize)] -pub struct CreateMessageReaction { - pub message: String, - pub emoji: String, -} - #[derive(Deserialize)] pub struct UpdateNoteDir { pub dir: String, diff --git a/crates/app/src/routes/pages/chats.rs b/crates/app/src/routes/pages/chats.rs deleted file mode 100644 index 4134888..0000000 --- a/crates/app/src/routes/pages/chats.rs +++ /dev/null @@ -1,380 +0,0 @@ -use super::{render_error, ChatsAppQuery, PaginatedQuery}; -use crate::{State, assets::initial_context, get_lang, get_user_from_token}; -use axum::{ - extract::{Path, Query}, - response::{Html, IntoResponse, Redirect}, - Extension, Json, -}; -use crate::cookie::CookieJar; -use tetratto_core::model::{ - channels::Message, communities_permissions::CommunityPermission, permissions::FinePermission, - Error, -}; -use serde::Deserialize; - -#[derive(Deserialize)] -pub struct RenderMessage { - pub data: String, - pub grouped: bool, -} - -pub async fn redirect_request() -> impl IntoResponse { - Redirect::to("/chats/0/0") -} - -/// `/chats/{community}/{channel}` -/// -/// `/chats/0` is for channels the user is part of (not in a community) -pub async fn app_request( - jar: CookieJar, - Extension(data): Extension, - Path((selected_community, selected_channel)): Path<(usize, usize)>, - Query(props): Query, -) -> 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 membership = match data - .0 - .get_membership_by_owner_community(user.id, selected_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 communities = match data.0.get_memberships_by_owner(user.id).await { - Ok(p) => match data.0.fill_communities(p).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }; - - if selected_community != 0 && selected_channel == 0 { - let channels = match data.0.get_channels_by_community(selected_community).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }; - - if let Some(channel) = channels.first() { - return Ok(Html(format!( - "", - selected_community, channel.id, props.nav - ))); - } - } - - let community = if selected_community != 0 { - match data.0.get_community_by_id(selected_community).await { - Ok(p) => Some(p), - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - } - } else { - None - }; - - let channel = if selected_channel != 0 { - match data.0.get_channel_by_id(selected_channel).await { - Ok(p) => { - if !p.check_read(user.id, Some(membership.role)) { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &Some(user)).await, - )); - } - - Some(p) - } - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - } - } else { - None - }; - - let lang = get_lang!(jar, data.0); - let mut context = initial_context(&data.0.0.0, lang, &Some(user.clone())).await; - - context.insert("selected_community", &selected_community); - context.insert("selected_channel", &selected_channel); - context.insert("membership_role", &membership.role.bits()); - context.insert("page", &props.page); - context.insert("message", &props.message); - - context.insert( - "can_manage_channels", - &if selected_community == 0 { - false - } else { - can_manage_channels - }, - ); - - context.insert( - "can_manage_channel", - &if selected_community == 0 { - if let Some(ref channel) = channel { - channel.members.contains(&user.id) | (channel.owner == user.id) - } else { - false - } - } else { - can_manage_channels - }, - ); - - context.insert("community", &community); - context.insert("channel", &channel); - context.insert("communities", &communities); - - // return - Ok(Html(data.1.render("chats/app.html", &context).unwrap())) -} - -/// `/chats/{community}/{channel}/_stream` -pub async fn stream_request( - jar: CookieJar, - Extension(data): Extension, - Path((community, channel)): Path<(usize, usize)>, - Query(props): Query, -) -> 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 ignore_users = crate::ignore_users_gen!(user!, data); - - let channel = match data.0.get_channel_by_id(channel).await { - Ok(c) => c, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }; - - 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)), - }; - - if !channel.check_read(user.id, Some(membership.role)) { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &Some(user)).await, - )); - } - - let can_manage_messages = membership.role.check(CommunityPermission::MANAGE_MESSAGES) - | user.permissions.check(FinePermission::MANAGE_MESSAGES); - - let messages = if props.message == 0 { - match data - .0 - .get_messages_by_channel(channel.id, 24, props.page) - .await - { - Ok(p) => match data.0.fill_messages(p, &ignore_users).await { - Ok(p) => p, - Err(e) => { - return Err(Html(render_error(e, &jar, &data, &Some(user)).await)); - } - }, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - } - } else { - Vec::new() - }; - - let message = if props.message == 0 { - None - } else { - Some(match data.0.get_message_by_id(props.message).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }) - }; - - let message_owner = if let Some(ref message) = message { - Some(match data.0.get_user_by_id(message.owner).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }) - } else { - None - }; - - let lang = get_lang!(jar, data.0); - let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; - - context.insert("messages", &messages); - context.insert("message", &message); - context.insert("message_owner", &message_owner); - context.insert("can_manage_messages", &can_manage_messages); - - context.insert("page", &props.page); - context.insert("community", &community); - context.insert("channel", &channel); - - // return - Ok(Html(data.1.render("chats/stream.html", &context).unwrap())) -} - -/// `/chats/{community}/{channel}/_render` -pub async fn message_request( - jar: CookieJar, - Extension(data): Extension, - Path((community, channel)): Path<(usize, usize)>, - Json(req): Json, -) -> 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 message: (String, Message) = match serde_json::from_str(&req.data) { - Ok(m) => m, - Err(e) => { - return Err(Html( - render_error(Error::MiscError(e.to_string()), &jar, &data, &Some(user)).await, - )); - } - }; - - let message = message.1; - - 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_messages = membership.role.check(CommunityPermission::MANAGE_MESSAGES) - | user.permissions.check(FinePermission::MANAGE_MESSAGES); - - let owner = match data.0.get_user_by_id(message.owner).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }; - - let lang = get_lang!(jar, data.0); - let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; - - context.insert("can_manage_messages", &can_manage_messages); - context.insert("message", &message); - context.insert("user", &owner); - - context.insert("channel", &channel); - context.insert("community", &community); - context.insert("grouped", &req.grouped); - - // return - Ok(Html(data.1.render("chats/message.html", &context).unwrap())) -} - -/// `/chats/{community}/{channel/_channels` -pub async fn channels_request( - jar: CookieJar, - Extension(data): Extension, - Path((community, channel_id)): Path<(usize, usize)>, - Query(props): Query, -) -> 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 channels = if community == 0 { - match data.0.get_channels_by_user(user.id).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - } - } else { - match data.0.get_channels_by_community(community).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - } - }; - - let channel = if channel_id != 0 { - Some(match data.0.get_channel_by_id(channel_id).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }) - } else { - None - }; - - let members = if community == 0 && channel.is_some() { - let ignore_users = crate::ignore_users_gen!(user!, data); - - let mut channel = channel.as_ref().unwrap().clone(); - channel.members.insert(0, channel.owner); // include the owner in the members list (at the start) - - Some( - match data.0.fill_members(&channel.members, ignore_users).await { - Ok(p) => p, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), - }, - ) - } else { - 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); - - context.insert("selected_community", &community); - context.insert("selected_channel", &channel_id); - - // return - Ok(Html( - data.1.render("chats/channels.html", &context).unwrap(), - )) -} diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index 1e2ce88..8e309d2 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -793,14 +793,6 @@ pub async fn settings_request( )); } - let channels = match data.0.get_channels_by_community(community.id).await { - Ok(p) => p, - 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 emojis = match data.0.get_emojis_by_community(community.id).await { Ok(p) => p, Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), @@ -814,10 +806,6 @@ pub async fn settings_request( let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; context.insert("community", &community); - - context.insert("can_manage_channels", &can_manage_channels); - context.insert("channels", &channels); - context.insert("can_manage_emojis", &can_manage_emojis); context.insert("emojis", &emojis); diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index 8ab6da5..80e4bb7 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -1,5 +1,4 @@ pub mod auth; -pub mod chats; pub mod communities; pub mod developer; pub mod economy; @@ -12,10 +11,7 @@ pub mod mod_panel; pub mod profile; pub mod stacks; -use axum::{ - routing::{get, post}, - Router, -}; +use axum::{routing::get, Router}; use crate::{cookie::CookieJar, routes::pages::misc::TimelineOrderMode}; use serde::Deserialize; use tetratto_core::model::{Error, auth::User}; @@ -115,21 +111,6 @@ pub fn routes() -> Router { .route("/post/{id}/reposts", get(communities::reposts_request)) .route("/post/{id}/likes", get(communities::likes_request)) .route("/question/{id}", get(communities::question_request)) - // chats - .route("/chats", get(chats::redirect_request)) - .route("/chats/{community}/{channel}", get(chats::app_request)) - .route( - "/chats/{community}/{channel}/_stream", - get(chats::stream_request), - ) - .route( - "/chats/{community}/{channel}/_render", - post(chats::message_request), - ) - .route( - "/chats/{community}/{channel}/_channels", - get(chats::channels_request), - ) // forge .route("/forges", get(forge::home_request)) .route("/forge/{title}", get(forge::info_request)) @@ -200,16 +181,6 @@ pub struct PaginatedQuery { pub before: usize, } -#[derive(Deserialize)] -pub struct ChatsAppQuery { - #[serde(default)] - pub page: usize, - #[serde(default)] - pub nav: bool, - #[serde(default)] - pub message: usize, -} - #[derive(Deserialize)] pub struct ProfileQuery { #[serde(default)] diff --git a/crates/core/src/database/channels.rs b/crates/core/src/database/channels.rs deleted file mode 100644 index c1e9938..0000000 --- a/crates/core/src/database/channels.rs +++ /dev/null @@ -1,325 +0,0 @@ -use oiseau::cache::Cache; -use crate::model::moderation::AuditLogEntry; -use crate::model::{ - Error, Result, auth::User, permissions::FinePermission, - communities_permissions::CommunityPermission, channels::Channel, -}; -use crate::{auto_method, DataManager}; -use oiseau::{PostgresRow, execute, get, query_row, query_rows, params}; - -impl DataManager { - /// Get a [`Channel`] from an SQL row. - pub(crate) fn get_channel_from_row(x: &PostgresRow) -> Channel { - Channel { - id: get!(x->0(i64)) as usize, - community: get!(x->1(i64)) as usize, - owner: get!(x->2(i64)) as usize, - created: get!(x->3(i64)) as usize, - minimum_role_read: get!(x->4(i32)) as u32, - minimum_role_write: get!(x->5(i32)) as u32, - position: get!(x->6(i32)) as usize, - members: serde_json::from_str(&get!(x->7(String))).unwrap(), - title: get!(x->8(String)), - last_message: get!(x->9(i64)) as usize, - } - } - - auto_method!(get_channel_by_id(usize as i64)@get_channel_from_row -> "SELECT * FROM channels WHERE id = $1" --name="channel" --returns=Channel --cache-key-tmpl="atto.channel:{}"); - - /// Get all member profiles from a channel members list. - pub async fn fill_members( - &self, - members: &Vec, - ignore_users: Vec, - ) -> Result> { - let mut out = Vec::new(); - - for member in members { - if ignore_users.contains(member) { - continue; - } - - out.push(self.get_user_by_id(member.to_owned()).await?); - } - - Ok(out) - } - - /// Get all channels by community. - /// - /// # Arguments - /// * `community` - the ID of the community to fetch channels for - pub async fn get_channels_by_community(&self, community: 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 channels WHERE community = $1 ORDER BY position ASC", - &[&(community as i64)], - |x| { Self::get_channel_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("channel".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get all channels by user. - /// - /// # Arguments - /// * `user` - the ID of the user to fetch channels for - pub async fn get_channels_by_user(&self, user: 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 channels WHERE (owner = $1 OR members LIKE $2) AND community = 0 ORDER BY last_message DESC", - params![&(user as i64), &format!("%{user}%")], - |x| { Self::get_channel_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("channel".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get a channel given its `owner` and a member. - /// - /// # Arguments - /// * `owner` - the ID of the owner - /// * `member` - the ID of the member - pub async fn get_channel_by_owner_member( - &self, - owner: usize, - member: usize, - ) -> 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 channels WHERE owner = $1 AND members = $2 AND community = 0 ORDER BY created DESC", - params![&(owner as i64), &format!("[{member}]")], - |x| { Ok(Self::get_channel_from_row(x)) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("channel".to_string())); - } - - Ok(res.unwrap()) - } - - /// Create a new channel in the database. - /// - /// # Arguments - /// * `data` - a mock [`Channel`] object to insert - pub async fn create_channel(&self, data: Channel) -> Result<()> { - let user = self.get_user_by_id(data.owner).await?; - - // check user permission in community - if data.community != 0 { - let membership = self - .get_membership_by_owner_community(user.id, data.community) - .await?; - - if !membership.role.check(CommunityPermission::MANAGE_CHANNELS) - && !user.permissions.check(FinePermission::MANAGE_CHANNELS) - { - return Err(Error::NotAllowed); - } - } - // check members - else { - for member in &data.members { - if self - .get_userblock_by_initiator_receiver(member.to_owned(), data.owner) - .await - .is_ok() - { - return Err(Error::NotAllowed); - } - } - } - - // ... - 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 channels VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", - params![ - &(data.id as i64), - &(data.community as i64), - &(data.owner as i64), - &(data.created as i64), - &(data.minimum_role_read as i32), - &(data.minimum_role_write as i32), - &(data.position as i32), - &serde_json::to_string(&data.members).unwrap(), - &data.title, - &(data.last_message as i64) - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(()) - } - - pub async fn delete_channel(&self, id: usize, user: &User) -> Result<()> { - let channel = self.get_channel_by_id(id).await?; - - // check user permission in community - if user.id != channel.owner { - let membership = self - .get_membership_by_owner_community(user.id, channel.community) - .await?; - - if !membership.role.check(CommunityPermission::MANAGE_CHANNELS) { - return Err(Error::NotAllowed); - } - } - - // ... - 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 channels WHERE id = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // delete messages - let res = execute!( - &conn, - "DELETE FROM messages WHERE channel = $1", - &[&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // ... - self.0.1.remove(format!("atto.channel:{}", id)).await; - Ok(()) - } - - pub async fn add_channel_member(&self, id: usize, user: User, member: String) -> Result<()> { - let mut y = self.get_channel_by_id(id).await?; - - if user.id != y.owner && member != user.username { - if !user.permissions.check(FinePermission::MANAGE_CHANNELS) { - return Err(Error::NotAllowed); - } else { - self.create_audit_log_entry(AuditLogEntry::new( - user.id, - format!("invoked `add_channel_member` with x value `{member}`"), - )) - .await? - } - } - - // check permissions - let member = self.get_user_by_username(&member).await?; - - if self - .get_userblock_by_initiator_receiver(member.id, user.id) - .await - .is_ok() - { - return Err(Error::NotAllowed); - } - - // ... - y.members.push(member.id); - - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "UPDATE channels SET members = $1 WHERE id = $2", - params![&serde_json::to_string(&y.members).unwrap(), &(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.0.1.remove(format!("atto.channel:{}", id)).await; - - Ok(()) - } - - pub async fn remove_channel_member(&self, id: usize, user: User, member: usize) -> Result<()> { - let mut y = self.get_channel_by_id(id).await?; - - if user.id != y.owner && member != user.id { - if !user.permissions.check(FinePermission::MANAGE_CHANNELS) { - return Err(Error::NotAllowed); - } else { - self.create_audit_log_entry(AuditLogEntry::new( - user.id, - format!("invoked `remove_channel_member` with x value `{member}`"), - )) - .await? - } - } - - y.members - .remove(match y.members.iter().position(|x| *x == member) { - Some(i) => i, - None => return Err(Error::GeneralNotFound("member".to_string())), - }); - - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "UPDATE channels SET members = $1 WHERE id = $2", - params![&serde_json::to_string(&y.members).unwrap(), &(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.0.1.remove(format!("atto.channel:{}", id)).await; - - Ok(()) - } - - auto_method!(update_channel_title(&str)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}"); - auto_method!(update_channel_position(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET position = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}"); - auto_method!(update_channel_minimum_role_read(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET minimum_role_read = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}"); - auto_method!(update_channel_minimum_role_write(i32)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET minimum_role_write = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}"); - auto_method!(update_channel_members(Vec)@get_channel_by_id:FinePermission::MANAGE_CHANNELS; -> "UPDATE channels SET members = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.channel:{}"); - auto_method!(update_channel_last_message(i64) -> "UPDATE channels SET last_message = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}"); -} diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 8df91ae..b0882dc 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -26,8 +26,6 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_REQUESTS).unwrap(); execute!(&conn, common::CREATE_TABLE_QUESTIONS).unwrap(); execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap(); - execute!(&conn, common::CREATE_TABLE_CHANNELS).unwrap(); - execute!(&conn, common::CREATE_TABLE_MESSAGES).unwrap(); execute!(&conn, common::CREATE_TABLE_EMOJIS).unwrap(); execute!(&conn, common::CREATE_TABLE_STACKS).unwrap(); execute!(&conn, common::CREATE_TABLE_DRAFTS).unwrap(); @@ -37,7 +35,6 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_STACKBLOCKS).unwrap(); execute!(&conn, common::CREATE_TABLE_JOURNALS).unwrap(); execute!(&conn, common::CREATE_TABLE_NOTES).unwrap(); - execute!(&conn, common::CREATE_TABLE_MESSAGE_REACTIONS).unwrap(); execute!(&conn, common::CREATE_TABLE_INVITE_CODES).unwrap(); execute!(&conn, common::CREATE_TABLE_DOMAINS).unwrap(); execute!(&conn, common::CREATE_TABLE_SERVICES).unwrap(); diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs index 1290bdd..cd7a6ab 100644 --- a/crates/core/src/database/communities.rs +++ b/crates/core/src/database/communities.rs @@ -375,11 +375,6 @@ impl DataManager { return Err(Error::DatabaseError(e.to_string())); } - // remove channels - for channel in self.get_channels_by_community(id).await? { - self.delete_channel(channel.id, &user).await?; - } - // remove images let avatar = PathBufD::current().extend(&[ self.0.0.dirs.media.as_str(), diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index 6a16c2d..6cff2ef 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -14,8 +14,6 @@ pub const CREATE_TABLE_USER_WARNINGS: &str = include_str!("./sql/create_user_war pub const CREATE_TABLE_REQUESTS: &str = include_str!("./sql/create_requests.sql"); pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.sql"); pub const CREATE_TABLE_IPBLOCKS: &str = include_str!("./sql/create_ipblocks.sql"); -pub const CREATE_TABLE_CHANNELS: &str = include_str!("./sql/create_channels.sql"); -pub const CREATE_TABLE_MESSAGES: &str = include_str!("./sql/create_messages.sql"); pub const CREATE_TABLE_EMOJIS: &str = include_str!("./sql/create_emojis.sql"); pub const CREATE_TABLE_STACKS: &str = include_str!("./sql/create_stacks.sql"); pub const CREATE_TABLE_DRAFTS: &str = include_str!("./sql/create_drafts.sql"); @@ -25,7 +23,6 @@ pub const CREATE_TABLE_APPS: &str = include_str!("./sql/create_apps.sql"); pub const CREATE_TABLE_STACKBLOCKS: &str = include_str!("./sql/create_stackblocks.sql"); pub const CREATE_TABLE_JOURNALS: &str = include_str!("./sql/create_journals.sql"); pub const CREATE_TABLE_NOTES: &str = include_str!("./sql/create_notes.sql"); -pub const CREATE_TABLE_MESSAGE_REACTIONS: &str = include_str!("./sql/create_message_reactions.sql"); pub const CREATE_TABLE_INVITE_CODES: &str = include_str!("./sql/create_invite_codes.sql"); pub const CREATE_TABLE_DOMAINS: &str = include_str!("./sql/create_domains.sql"); pub const CREATE_TABLE_SERVICES: &str = include_str!("./sql/create_services.sql"); diff --git a/crates/core/src/database/drivers/sql/create_channels.sql b/crates/core/src/database/drivers/sql/create_channels.sql deleted file mode 100644 index 83f7ff6..0000000 --- a/crates/core/src/database/drivers/sql/create_channels.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE IF NOT EXISTS channels ( - id BIGINT NOT NULL PRIMARY KEY, - community BIGINT NOT NULL, - owner BIGINT NOT NULL, - created BIGINT NOT NULL, - minimum_role_read INT NOT NULL, - minimum_role_write INT NOT NULL, - position INT NOT NULL, - members TEXT NOT NULL, - title TEXT NOT NULL, - last_message BIGINT NOT NULL -) diff --git a/crates/core/src/database/drivers/sql/create_message_reactions.sql b/crates/core/src/database/drivers/sql/create_message_reactions.sql deleted file mode 100644 index f13a033..0000000 --- a/crates/core/src/database/drivers/sql/create_message_reactions.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS message_reactions ( - id BIGINT NOT NULL PRIMARY KEY, - created BIGINT NOT NULL, - owner BIGINT NOT NULL, - message BIGINT NOT NULL, - emoji TEXT NOT NULL, - UNIQUE (owner, message, emoji) -) diff --git a/crates/core/src/database/drivers/sql/create_messages.sql b/crates/core/src/database/drivers/sql/create_messages.sql deleted file mode 100644 index 235d8dc..0000000 --- a/crates/core/src/database/drivers/sql/create_messages.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE IF NOT EXISTS messages ( - id BIGINT NOT NULL PRIMARY KEY, - channel BIGINT NOT NULL, - owner BIGINT NOT NULL, - created BIGINT NOT NULL, - edited BIGINT NOT NULL, - content TEXT NOT NULL, - context TEXT NOT NULL, - reactions TEXT NOT NULL -) diff --git a/crates/core/src/database/message_reactions.rs b/crates/core/src/database/message_reactions.rs deleted file mode 100644 index 4134049..0000000 --- a/crates/core/src/database/message_reactions.rs +++ /dev/null @@ -1,183 +0,0 @@ -use oiseau::{cache::Cache, query_rows}; -use crate::model::{ - Error, Result, - auth::{Notification, User}, - permissions::FinePermission, - channels::MessageReaction, -}; -use crate::{auto_method, DataManager}; - -use oiseau::{PostgresRow, execute, get, query_row, params}; - -impl DataManager { - /// Get a [`MessageReaction`] from an SQL row. - pub(crate) fn get_message_reaction_from_row(x: &PostgresRow) -> MessageReaction { - MessageReaction { - id: get!(x->0(i64)) as usize, - created: get!(x->1(i64)) as usize, - owner: get!(x->2(i64)) as usize, - message: get!(x->3(i64)) as usize, - emoji: get!(x->4(String)), - } - } - - auto_method!(get_message_reaction_by_id()@get_message_reaction_from_row -> "SELECT * FROM message_reactions WHERE id = $1" --name="message_reaction" --returns=MessageReaction --cache-key-tmpl="atto.message_reaction:{}"); - - /// Get message_reactions by `owner` and `message`. - pub async fn get_message_reactions_by_owner_message( - &self, - owner: usize, - message: 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 message_reactions WHERE owner = $1 AND message = $2", - &[&(owner as i64), &(message as i64)], - |x| { Self::get_message_reaction_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("message_reaction".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get a message_reaction by `owner`, `message`, and `emoji`. - pub async fn get_message_reaction_by_owner_message_emoji( - &self, - owner: usize, - message: usize, - emoji: &str, - ) -> 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 message_reactions WHERE owner = $1 AND message = $2 AND emoji = $3", - params![&(owner as i64), &(message as i64), &emoji], - |x| { Ok(Self::get_message_reaction_from_row(x)) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("message_reaction".to_string())); - } - - Ok(res.unwrap()) - } - - /// Create a new message_reaction in the database. - /// - /// # Arguments - /// * `data` - a mock [`MessageReaction`] object to insert - pub async fn create_message_reaction(&self, data: MessageReaction, user: &User) -> Result<()> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let mut message = self.get_message_by_id(data.message).await?; - let channel = self.get_channel_by_id(message.channel).await?; - - // ... - let res = execute!( - &conn, - "INSERT INTO message_reactions VALUES ($1, $2, $3, $4, $5)", - params![ - &(data.id as i64), - &(data.created as i64), - &(data.owner as i64), - &(data.message as i64), - &data.emoji - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // incr corresponding - if let Some(x) = message.reactions.get(&data.emoji) { - message.reactions.insert(data.emoji.clone(), x + 1); - } else { - message.reactions.insert(data.emoji.clone(), 1); - } - - self.update_message_reactions(message.id, message.reactions) - .await?; - - // send notif - if message.owner != user.id { - self - .create_notification(Notification::new( - "Your message has received a reaction!".to_string(), - format!( - "[@{}](/api/v1/auth/user/find/{}) has reacted \"{}\" to your [message](/chats/{}/{}?message={})!", - user.username, user.id, data.emoji, channel.community, channel.id, message.id - ), - message.owner, - )) - .await?; - } - - // return - Ok(()) - } - - pub async fn delete_message_reaction(&self, id: usize, user: &User) -> Result<()> { - let message_reaction = self.get_message_reaction_by_id(id).await?; - - if user.id != message_reaction.owner - && !user.permissions.check(FinePermission::MANAGE_REACTIONS) - { - return Err(Error::NotAllowed); - } - - let mut message = self.get_message_by_id(message_reaction.message).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 message_reactions WHERE id = $1", - &[&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.0 - .1 - .remove(format!("atto.message_reaction:{}", id)) - .await; - - // decr message reaction count - if let Some(x) = message.reactions.get(&message_reaction.emoji) { - if *x == 1 { - // there are no 0 of this reaction - message.reactions.remove(&message_reaction.emoji); - } else { - // decr 1 - message.reactions.insert(message_reaction.emoji, x - 1); - } - } - - self.update_message_reactions(message.id, message.reactions) - .await?; - - // return - Ok(()) - } -} diff --git a/crates/core/src/database/messages.rs b/crates/core/src/database/messages.rs deleted file mode 100644 index 6b7c037..0000000 --- a/crates/core/src/database/messages.rs +++ /dev/null @@ -1,380 +0,0 @@ -use std::collections::HashMap; -use oiseau::cache::Cache; -use crate::model::auth::Notification; -use crate::model::moderation::AuditLogEntry; -use crate::model::socket::{SocketMessage, SocketMethod}; -use crate::model::{ - Error, Result, auth::User, permissions::FinePermission, - communities_permissions::CommunityPermission, channels::Message, -}; -use serde::Serialize; -use tetratto_shared::unix_epoch_timestamp; -use crate::{auto_method, DataManager}; - -use oiseau::{PostgresRow, cache::redis::Commands}; - -use oiseau::{execute, get, query_rows, params}; - -#[derive(Serialize)] -struct DeleteMessageEvent { - pub id: String, -} - -impl DataManager { - /// Get a [`Message`] from an SQL row. - pub(crate) fn get_message_from_row(x: &PostgresRow) -> Message { - Message { - id: get!(x->0(i64)) as usize, - channel: get!(x->1(i64)) as usize, - owner: get!(x->2(i64)) as usize, - created: get!(x->3(i64)) as usize, - edited: get!(x->4(i64)) as usize, - content: get!(x->5(String)), - context: serde_json::from_str(&get!(x->6(String))).unwrap(), - reactions: serde_json::from_str(&get!(x->7(String))).unwrap(), - } - } - - auto_method!(get_message_by_id(usize as i64)@get_message_from_row -> "SELECT * FROM messages WHERE id = $1" --name="message" --returns=Message --cache-key-tmpl="atto.message:{}"); - - /// Complete a vector of just messages with their owner as well. - /// - /// # Returns - /// `(message, owner, group with previous messages in ui)` - pub async fn fill_messages( - &self, - messages: Vec, - ignore_users: &[usize], - ) -> Result> { - let mut out: Vec<(Message, User, bool)> = Vec::new(); - - let mut users: HashMap = HashMap::new(); - for (i, message) in messages.iter().enumerate() { - let next_owner: usize = match messages.get(i + 1) { - Some(m) => m.owner, - None => 0, - }; - - let owner = message.owner; - - if ignore_users.contains(&owner) { - continue; - } - - if let Some(user) = users.get(&owner) { - out.push((message.to_owned(), user.clone(), next_owner == owner)); - } else { - let user = self.get_user_by_id_with_void(owner).await?; - users.insert(owner, user.clone()); - out.push((message.to_owned(), user, next_owner == owner)); - } - } - - Ok(out) - } - - /// Get all messages by channel (paginated). - /// - /// # Arguments - /// * `channel` - the ID of the community to fetch channels for - /// * `batch` - the limit of items in each page - /// * `page` - the page number - pub async fn get_messages_by_channel( - &self, - channel: 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 messages WHERE channel = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", - &[&(channel as i64), &(batch as i64), &((page * batch) as i64)], - |x| { Self::get_message_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("message".to_string())); - } - - Ok(res.unwrap()) - } - - /// Create a new message in the database. - /// - /// # Arguments - /// * `data` - a mock [`Message`] object to insert - pub async fn create_message(&self, mut data: Message) -> Result<()> { - if data.content.len() < 2 { - return Err(Error::DataTooLong("content".to_string())); - } - - if data.content.len() > 2048 { - return Err(Error::DataTooLong("content".to_string())); - } - - let owner = self.get_user_by_id(data.owner).await?; - let channel = self.get_channel_by_id(data.channel).await?; - - // check user permission in community - let membership = self - .get_membership_by_owner_community(owner.id, channel.community) - .await?; - - // check user permission to post in channel - if !channel.check_post(owner.id, Some(membership.role)) { - return Err(Error::NotAllowed); - } - - // send mention notifications - let mut already_notified: HashMap = HashMap::new(); - for username in User::parse_mentions(&data.content) { - let user = { - if let Some(ua) = already_notified.get(&username) { - ua.to_owned() - } else { - let user = self.get_user_by_username(&username).await?; - - // check blocked status - if self - .get_userblock_by_initiator_receiver(user.id, data.owner) - .await - .is_ok() - { - return Err(Error::NotAllowed); - } - - // check private status - if user.settings.private_profile { - if self - .get_userfollow_by_initiator_receiver(user.id, data.owner) - .await - .is_err() - { - return Err(Error::NotAllowed); - } - } - - // check if the user can read the channel - let membership = self - .get_membership_by_owner_community(user.id, channel.community) - .await?; - - if !channel.check_read(user.id, Some(membership.role)) { - continue; - } - - // create notif - self.create_notification(Notification::new( - "You've been mentioned in a message!".to_string(), - format!( - "[@{}](/api/v1/auth/user/find/{}) has mentioned you in their [message](/chats/{}/{}?message={}).", - owner.username, owner.id, channel.community, data.channel, data.id - ), - user.id, - )) - .await?; - - // ... - already_notified.insert(username.to_owned(), user.clone()); - user - } - }; - - data.content = data.content.replace( - &format!("@{username}"), - &format!( - "@{username}", - user.id - ), - ); - } - - // send notifs to members (if this message isn't associated with a channel) - if channel.community == 0 { - for member in [channel.members, vec![channel.owner]].concat() { - if member == owner.id { - continue; - } - - let user = self.get_user_by_id(member).await?; - if user.channel_mutes.contains(&channel.id) { - continue; - } - - let mut notif = Notification::new( - "You've received a new message!".to_string(), - format!( - "[@{}](/api/v1/auth/user/find/{}) has sent a [message](/chats/{}/{}?message={}) in [{}](/chats/{}/{}).", - owner.username, - owner.id, - channel.community, - data.channel, - data.id, - channel.title, - channel.community, - data.channel - ), - member, - ); - - notif.tag = format!("chats/{}", channel.id); - self.create_notification(notif).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 messages VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", - params![ - &(data.id as i64), - &(data.channel as i64), - &(data.owner as i64), - &(data.created as i64), - &(data.edited as i64), - &data.content, - &serde_json::to_string(&data.context).unwrap(), - &serde_json::to_string(&data.reactions).unwrap(), - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // post event - let mut con = self.0.1.get_con().await; - - if let Err(e) = con.publish::( - if channel.community != 0 { - // broadcast to community ws - format!("chats/{}", channel.community) - } else { - // broadcast to channel ws - format!("chats/{}", channel.id) - }, - serde_json::to_string(&SocketMessage { - method: SocketMethod::Message, - data: serde_json::to_string(&(data.channel.to_string(), data)).unwrap(), - }) - .unwrap(), - ) { - return Err(Error::MiscError(e.to_string())); - } - - // update channel position - self.update_channel_last_message(channel.id, unix_epoch_timestamp() as i64) - .await?; - - // ... - Ok(()) - } - - pub async fn delete_message(&self, id: usize, user: User) -> Result<()> { - let message = self.get_message_by_id(id).await?; - let channel = self.get_channel_by_id(message.channel).await?; - - // check user permission in community - if user.id != message.owner { - let membership = self - .get_membership_by_owner_community(user.id, channel.community) - .await?; - - if !membership.role.check(CommunityPermission::MANAGE_MESSAGES) - && !user.permissions.check(FinePermission::MANAGE_MESSAGES) - { - return Err(Error::NotAllowed); - } else if user.permissions.check(FinePermission::MANAGE_MESSAGES) { - self.create_audit_log_entry(AuditLogEntry::new( - user.id, - format!("invoked `delete_message` with x value `{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 messages WHERE id = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.0.1.remove(format!("atto.message:{}", id)).await; - - // post event - let mut con = self.0.1.get_con().await; - - if let Err(e) = con.publish::( - if channel.community != 0 { - // broadcast to community ws - format!("chats/{}", channel.community) - } else { - // broadcast to channel ws - format!("chats/{}", channel.id) - }, - serde_json::to_string(&SocketMessage { - method: SocketMethod::Delete, - data: serde_json::to_string(&DeleteMessageEvent { id: id.to_string() }).unwrap(), - }) - .unwrap(), - ) { - return Err(Error::MiscError(e.to_string())); - } - - // ... - Ok(()) - } - - pub async fn update_message_content(&self, id: usize, user: User, x: String) -> Result<()> { - let y = self.get_message_by_id(id).await?; - - if user.id != y.owner { - if !user.permissions.check(FinePermission::MANAGE_MESSAGES) { - return Err(Error::NotAllowed); - } else { - self.create_audit_log_entry(AuditLogEntry::new( - user.id, - format!("invoked `update_message_content` with x value `{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, - "UPDATE messages SET content = $1, edited = $2 WHERE id = $2", - params![&x, &(unix_epoch_timestamp() as i64), &(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // return - Ok(()) - } - - auto_method!(update_message_reactions(HashMap) -> "UPDATE messages SET reactions = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.message:{}"); -} diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 1adbf88..b027ea9 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -3,7 +3,6 @@ pub mod app_data; mod apps; mod audit_log; mod auth; -mod channels; mod common; mod communities; pub mod connections; @@ -17,8 +16,6 @@ mod ipblocks; mod journals; mod letters; mod memberships; -mod message_reactions; -mod messages; mod notes; mod notifications; mod polls; diff --git a/crates/core/src/model/channels.rs b/crates/core/src/model/channels.rs deleted file mode 100644 index 84180c4..0000000 --- a/crates/core/src/model/channels.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::collections::HashMap; - -use serde::{Serialize, Deserialize}; -use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; - -use super::communities_permissions::CommunityPermission; - -/// A channel is a more "chat-like" feed in communities. -#[derive(Clone, Serialize, Deserialize)] -pub struct Channel { - pub id: usize, - pub community: usize, - pub owner: usize, - pub created: usize, - /// The minimum role (as bits) that can read this channel. - pub minimum_role_read: u32, - /// The minimum role (as bits) that can write to this channel. - pub minimum_role_write: u32, - /// The position of this channel in the UI. - /// - /// Top (0) to bottom. - pub position: usize, - /// The members of the chat (ids). Should be empty if `community > 0`. - /// - /// The owner should not be a member of the channel since any member can update members. - pub members: Vec, - /// The title of the channel. - pub title: String, - /// The timestamp of the last message in the channel. - pub last_message: usize, -} - -impl Channel { - /// Create a new [`Channel`]. - pub fn new(community: usize, owner: usize, position: usize, title: String) -> Self { - let created = unix_epoch_timestamp(); - - Self { - id: Snowflake::new().to_string().parse::().unwrap(), - community, - owner, - created, - minimum_role_read: (CommunityPermission::DEFAULT | CommunityPermission::MEMBER).bits(), - minimum_role_write: (CommunityPermission::DEFAULT | CommunityPermission::MEMBER).bits(), - position, - members: Vec::new(), - title, - last_message: created, - } - } - - /// Check if the given `uid` can post in the channel. - pub fn check_post(&self, uid: usize, membership: Option) -> bool { - let mut is_member = false; - - if let Some(membership) = membership { - is_member = membership.bits() >= self.minimum_role_write - } - - (uid == self.owner) | is_member | self.members.contains(&uid) - } - - /// Check if the given `uid` can post in the channel. - pub fn check_read(&self, uid: usize, membership: Option) -> bool { - let mut is_member = false; - - if let Some(membership) = membership { - is_member = membership.bits() >= self.minimum_role_read - } - - (uid == self.owner) | is_member | self.members.contains(&uid) - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct Message { - pub id: usize, - pub channel: usize, - pub owner: usize, - pub created: usize, - pub edited: usize, - pub content: String, - pub context: MessageContext, - pub reactions: HashMap, -} - -impl Message { - pub fn new(channel: usize, owner: usize, content: String) -> Self { - let now = unix_epoch_timestamp(); - - Self { - id: Snowflake::new().to_string().parse::().unwrap(), - channel, - owner, - created: now, - edited: now, - content, - context: MessageContext, - reactions: HashMap::new(), - } - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct MessageContext; - -impl Default for MessageContext { - fn default() -> Self { - Self - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct MessageReaction { - pub id: usize, - pub created: usize, - pub owner: usize, - pub message: usize, - pub emoji: String, -} - -impl MessageReaction { - /// Create a new [`MessageReaction`]. - pub fn new(owner: usize, message: usize, emoji: String) -> Self { - Self { - id: Snowflake::new().to_string().parse::().unwrap(), - created: unix_epoch_timestamp(), - owner, - message, - emoji, - } - } -} diff --git a/crates/core/src/model/communities_permissions.rs b/crates/core/src/model/communities_permissions.rs index 1d8f0da..b61c1d2 100644 --- a/crates/core/src/model/communities_permissions.rs +++ b/crates/core/src/model/communities_permissions.rs @@ -18,7 +18,7 @@ bitflags! { const MANAGE_PINS = 1 << 7; const MANAGE_COMMUNITY = 1 << 8; const MANAGE_QUESTIONS = 1 << 9; - const MANAGE_CHANNELS = 1 << 10; + const UNUSED_0 = 1 << 10; const MANAGE_MESSAGES = 1 << 11; const MANAGE_EMOJIS = 1 << 12; diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index 75a133f..532408c 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -2,7 +2,6 @@ pub mod addr; pub mod apps; pub mod auth; pub mod carp; -pub mod channels; pub mod communities; pub mod communities_permissions; pub mod economy; diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index 61ebb61..01e1193 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -30,7 +30,7 @@ bitflags! { const SUPPORTER = 1 << 19; const MANAGE_REQUESTS = 1 << 20; const MANAGE_QUESTIONS = 1 << 21; - const MANAGE_CHANNELS = 1 << 22; + const UNUSED_0 = 1 << 22; const MANAGE_MESSAGES = 1 << 23; const MANAGE_UPLOADS = 1 << 24; const MANAGE_EMOJIS = 1 << 25;