remove: old chats core

This commit is contained in:
trisua 2025-09-05 22:08:16 -04:00
parent 918d47d873
commit 6d333378a4
33 changed files with 9 additions and 3377 deletions

View file

@ -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);

View file

@ -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();
}

View file

@ -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 %}")

View file

@ -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 %}"))

View file

@ -1 +0,0 @@
(text "{%- import \"components.html\" as components -%} {{ components::message(user=user, message=message, grouped=grouped) }}")

View file

@ -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;
}")))

View file

@ -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,
},

View file

@ -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 }}")

View file

@ -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=")

View file

@ -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,

View file

@ -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\");

View file

@ -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<State>,
Json(req): Json<CreateChannel>,
) -> 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::<usize>() {
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<State>,
Json(req): Json<CreateGroupChannel>,
) -> 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<usize> = Vec::new();
for member in req.members {
members.push(match member.parse::<usize>() {
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<State>,
Path(id): Path<usize>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateChannelTitle>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateChannelPosition>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<KickMember>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<KickMember>,
) -> 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::<usize>() {
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<State>,
) -> 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<State>,
Path(id): Path<usize>,
) -> 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<State>,
Path(id): Path<usize>,
) -> 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<State>,
Path(id): Path<usize>,
) -> 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<State>,
Path(id): Path<usize>,
) -> 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()),
}
}

View file

@ -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<State>,
Path(id): Path<usize>,
) -> 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<State>,
Json(req): Json<CreateMessageReaction>,
) -> 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::<usize>() {
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<State>,
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()),
}
}

View file

@ -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<State>,
Path(id): Path<String>,
) -> 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<SocketHeaders> = 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::<usize>() {
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<usize, bool> = HashMap::new();
if !headers.is_channel {
// check permissions for every channel in community
let community_id = match community_id.parse::<usize>() {
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::<String>().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::<usize, String, ()>(
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<State>,
Json(req): Json<CreateMessage>,
) -> 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::<usize>() {
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<State>,
Path(id): Path<usize>,
) -> 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<State>,
Path(id): Path<usize>,
Query(props): Query<PaginatedQuery>,
) -> 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()),
}
}

View file

@ -1,3 +0,0 @@
pub mod channels;
pub mod message_reactions;
pub mod messages;

View file

@ -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<String>,
}
#[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,

View file

@ -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<State>,
Path((selected_community, selected_channel)): Path<(usize, usize)>,
Query(props): Query<ChatsAppQuery>,
) -> 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!(
"<!doctype html><html><head><meta http-equiv=\"refresh\" content=\"0; url=/chats/{}/{}?nav={}\" /></head></html>",
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<State>,
Path((community, channel)): Path<(usize, usize)>,
Query(props): Query<ChatsAppQuery>,
) -> 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<State>,
Path((community, channel)): Path<(usize, usize)>,
Json(req): Json<RenderMessage>,
) -> 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<State>,
Path((community, channel_id)): Path<(usize, usize)>,
Query(props): Query<PaginatedQuery>,
) -> 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(),
))
}

View file

@ -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);

View file

@ -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)]

View file

@ -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<usize>,
ignore_users: Vec<usize>,
) -> Result<Vec<User>> {
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<Vec<Channel>> {
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<Vec<Channel>> {
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<Channel> {
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<usize>)@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:{}");
}

View file

@ -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();

View file

@ -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(),

View file

@ -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");

View file

@ -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
)

View file

@ -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)
)

View file

@ -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
)

View file

@ -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<Vec<MessageReaction>> {
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<MessageReaction> {
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(())
}
}

View file

@ -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<Message>,
ignore_users: &[usize],
) -> Result<Vec<(Message, User, bool)>> {
let mut out: Vec<(Message, User, bool)> = Vec::new();
let mut users: HashMap<usize, User> = 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<Vec<Message>> {
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<String, User> = 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!(
"<a href=\"/api/v1/auth/user/find/{}\" target=\"_top\">@{username}</a>",
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::<String, String, ()>(
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::<String, String, ()>(
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<String, usize>) -> "UPDATE messages SET reactions = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.message:{}");
}

View file

@ -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;

View file

@ -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<usize>,
/// 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::<usize>().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<CommunityPermission>) -> 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<CommunityPermission>) -> 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<String, usize>,
}
impl Message {
pub fn new(channel: usize, owner: usize, content: String) -> Self {
let now = unix_epoch_timestamp();
Self {
id: Snowflake::new().to_string().parse::<usize>().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::<usize>().unwrap(),
created: unix_epoch_timestamp(),
owner,
message,
emoji,
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;