remove: old chats core
This commit is contained in:
parent
918d47d873
commit
6d333378a4
33 changed files with 9 additions and 3377 deletions
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 %}")
|
|
@ -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 %}"))
|
|
@ -1 +0,0 @@
|
|||
(text "{%- import \"components.html\" as components -%} {{ components::message(user=user, message=message, grouped=grouped) }}")
|
|
@ -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;
|
||||
}")))
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 }}")
|
||||
|
|
|
@ -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=")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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\");
|
||||
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
pub mod channels;
|
||||
pub mod message_reactions;
|
||||
pub mod messages;
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
))
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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:{}");
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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:{}");
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue