add: chat message reactions
This commit is contained in:
parent
a4298f95f6
commit
a37312fecf
20 changed files with 557 additions and 25 deletions
|
@ -38,6 +38,10 @@ fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Va
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_emojis(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
|
Ok(CustomEmoji::replace(value.as_str().unwrap()).into())
|
||||||
|
}
|
||||||
|
|
||||||
fn color_escape(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
fn color_escape(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
Ok(sanitize::color_escape(value.as_str().unwrap()).into())
|
Ok(sanitize::color_escape(value.as_str().unwrap()).into())
|
||||||
}
|
}
|
||||||
|
@ -102,6 +106,7 @@ async fn main() {
|
||||||
tera.register_filter("has_staff_badge", check_staff_badge);
|
tera.register_filter("has_staff_badge", check_staff_badge);
|
||||||
tera.register_filter("has_banned", check_banned);
|
tera.register_filter("has_banned", check_banned);
|
||||||
tera.register_filter("remove_script_tags", remove_script_tags);
|
tera.register_filter("remove_script_tags", remove_script_tags);
|
||||||
|
tera.register_filter("emojis", render_emojis);
|
||||||
|
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,23 @@
|
||||||
("title" "Send")
|
("title" "Send")
|
||||||
(text "{{ icon \"send-horizontal\" }}"))))
|
(text "{{ icon \"send-horizontal\" }}"))))
|
||||||
(text "{%- endif %}")
|
(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
|
(script
|
||||||
(text "window.CURRENT_PAGE = Number.parseInt(\"{{ page }}\");
|
(text "window.CURRENT_PAGE = Number.parseInt(\"{{ page }}\");
|
||||||
window.VIEWING_SINGLE = \"{{ message }}\".length > 0;
|
window.VIEWING_SINGLE = \"{{ message }}\".length > 0;
|
||||||
|
@ -434,6 +451,7 @@
|
||||||
const clean_text = () => {
|
const clean_text = () => {
|
||||||
trigger(\"atto::clean_date_codes\");
|
trigger(\"atto::clean_date_codes\");
|
||||||
trigger(\"atto::hooks::online_indicator\");
|
trigger(\"atto::hooks::online_indicator\");
|
||||||
|
trigger(\"atto::hooks::check_message_reactions\");
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
|
|
|
@ -1026,9 +1026,29 @@
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(div
|
(div
|
||||||
("class" "flex w-full gap-2 justify-between")
|
("class" "flex w-full gap-2 justify-between")
|
||||||
|
(div
|
||||||
|
("class" "flex flex-col gap-2")
|
||||||
(span
|
(span
|
||||||
("class" "no_p_margin")
|
("class" "no_p_margin")
|
||||||
(text "{{ message.content|markdown|safe }}"))
|
(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 %}")
|
||||||
|
|
||||||
|
(div
|
||||||
|
("class" "hidden")
|
||||||
|
(text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}"))))
|
||||||
(text "{% if grouped -%}")
|
(text "{% if grouped -%}")
|
||||||
(div
|
(div
|
||||||
("class" "hidden")
|
("class" "hidden")
|
||||||
|
@ -1185,13 +1205,15 @@
|
||||||
(text "{{ text \"chats:action.kick_member\" }}")))))
|
(text "{{ text \"chats:action.kick_member\" }}")))))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
|
|
||||||
(text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}")
|
(text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false, render_button=true, small=false) -%}")
|
||||||
|
(text "{% if render_button -%}")
|
||||||
(button
|
(button
|
||||||
("class" "button small square lowered")
|
("class" "button small {% if not small -%} square {%- endif %} lowered")
|
||||||
("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()")
|
("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()")
|
||||||
("title" "Emojis")
|
("title" "Emojis")
|
||||||
("type" "button")
|
("type" "button")
|
||||||
(text "{{ icon \"smile-plus\" }}"))
|
(text "{{ icon \"smile-plus\" }}"))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
(text "{% if render_dialog -%}")
|
(text "{% if render_dialog -%}")
|
||||||
(dialog
|
(dialog
|
||||||
|
@ -1237,19 +1259,40 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.detail.unicode) {
|
if (event.detail.unicode) {
|
||||||
|
if (window.EMOJI_PICKER_MODE === \"replace\") {
|
||||||
document.getElementById(
|
document.getElementById(
|
||||||
window.EMOJI_PICKER_TEXT_ID,
|
window.EMOJI_PICKER_TEXT_ID,
|
||||||
).value += ` :${await (
|
).value = `:${await (
|
||||||
await fetch(\"/api/v1/lookup_emoji\", {
|
await fetch(\"/api/v1/lookup_emoji\", {
|
||||||
method: \"POST\",
|
method: \"POST\",
|
||||||
body: event.detail.unicode,
|
body: event.detail.unicode,
|
||||||
})
|
})
|
||||||
).text()}:`;
|
).text()}:`;
|
||||||
|
} else {
|
||||||
|
document.getElementById(
|
||||||
|
window.EMOJI_PICKER_TEXT_ID,
|
||||||
|
).value += ` :${await (
|
||||||
|
await fetch(\"/api/v1/lookup_emoji\", {
|
||||||
|
method: \"POST\",
|
||||||
|
body: event.detail.unicode,
|
||||||
|
})
|
||||||
|
).text()}:`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (window.EMOJI_PICKER_MODE === \"replace\") {
|
||||||
|
document.getElementById(
|
||||||
|
window.EMOJI_PICKER_TEXT_ID,
|
||||||
|
).value = `:${event.detail.emoji.shortcodes[0]}:`;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(
|
document.getElementById(
|
||||||
window.EMOJI_PICKER_TEXT_ID,
|
window.EMOJI_PICKER_TEXT_ID,
|
||||||
).value += ` :${event.detail.emoji.shortcodes[0]}:`;
|
).value += ` :${event.detail.emoji.shortcodes[0]}:`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById(
|
||||||
|
window.EMOJI_PICKER_TEXT_ID,
|
||||||
|
).dispatchEvent(new Event(\"change\"));
|
||||||
|
|
||||||
document.getElementById(\"emoji_dialog\").close();
|
document.getElementById(\"emoji_dialog\").close();
|
||||||
});"))
|
});"))
|
||||||
|
|
|
@ -688,6 +688,36 @@ media_theme_pref();
|
||||||
$.OBSERVERS.push(observer);
|
$.OBSERVERS.push(observer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.define("hooks::check_message_reactions", async ({ $ }) => {
|
||||||
|
const observer = $.offload_work_to_client_when_in_view(
|
||||||
|
async (element) => {
|
||||||
|
const reactions = await (
|
||||||
|
await fetch(
|
||||||
|
`/api/v1/message_reactions/${element.getAttribute("hook-arg:id")}`,
|
||||||
|
)
|
||||||
|
).json();
|
||||||
|
|
||||||
|
if (reactions.ok) {
|
||||||
|
for (const reaction of reactions.payload) {
|
||||||
|
element
|
||||||
|
.querySelector(
|
||||||
|
`[ui_ident=emoji_${reaction.emoji.replaceAll(":", "\\:")}]`,
|
||||||
|
)
|
||||||
|
.classList.remove("lowered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const element of Array.from(
|
||||||
|
document.querySelectorAll("[hook=check_message_reactions]") || [],
|
||||||
|
)) {
|
||||||
|
observer.observe(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.OBSERVERS.push(observer);
|
||||||
|
});
|
||||||
|
|
||||||
self.define("hooks::tabs:switch", (_, tab) => {
|
self.define("hooks::tabs:switch", (_, tab) => {
|
||||||
tab = tab.split("?")[0];
|
tab = tab.split("?")[0];
|
||||||
|
|
||||||
|
|
|
@ -204,6 +204,47 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.define("message_react", async (_, element, message, emoji) => {
|
||||||
|
await trigger("atto::debounce", ["reactions::toggle"]);
|
||||||
|
fetch("/api/v1/message_reactions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message,
|
||||||
|
emoji,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
trigger("atto::toast", [
|
||||||
|
res.ok ? "success" : "error",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
if (res.message.includes("created")) {
|
||||||
|
const x = element.querySelector(
|
||||||
|
`[ui_ident=emoji_${emoji.replaceAll(":", "\\:")}]`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (x) {
|
||||||
|
x.classList.remove("lowered");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const x = element.querySelector(
|
||||||
|
`[ui_ident=emoji_${emoji.replaceAll(":", "\\:")}]`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (x) {
|
||||||
|
x.classList.add("lowered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
self.define("remove_notification", (_, id) => {
|
self.define("remove_notification", (_, id) => {
|
||||||
fetch(`/api/v1/notifications/${id}`, {
|
fetch(`/api/v1/notifications/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|
103
crates/app/src/routes/api/v1/channels/message_reactions.rs
Normal file
103
crates/app/src/routes/api/v1/channels/message_reactions.rs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
use crate::{get_user_from_token, routes::api::v1::CreateMessageReaction, State};
|
||||||
|
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||||
|
use axum_extra::extract::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,2 +1,3 @@
|
||||||
pub mod channels;
|
pub mod channels;
|
||||||
|
pub mod message_reactions;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
|
|
@ -16,12 +16,16 @@ use tetratto_core::model::{
|
||||||
|
|
||||||
/// Expand a unicode emoji into its Gemoji shortcode.
|
/// Expand a unicode emoji into its Gemoji shortcode.
|
||||||
pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse {
|
pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse {
|
||||||
match emojis::get(&emoji) {
|
match emoji.as_str() {
|
||||||
|
"👍" => "thumbs_up".to_string(),
|
||||||
|
"👎" => "thumbs_down".to_string(),
|
||||||
|
_ => match emojis::get(&emoji) {
|
||||||
Some(e) => match e.shortcode() {
|
Some(e) => match e.shortcode() {
|
||||||
Some(s) => s.to_string(),
|
Some(s) => s.to_string(),
|
||||||
None => e.name().replace(" ", "-"),
|
None => e.name().replace(" ", "-"),
|
||||||
},
|
},
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,19 @@ pub fn routes() -> Router {
|
||||||
.route("/reactions", post(reactions::create_request))
|
.route("/reactions", post(reactions::create_request))
|
||||||
.route("/reactions/{id}", get(reactions::get_request))
|
.route("/reactions/{id}", get(reactions::get_request))
|
||||||
.route("/reactions/{id}", delete(reactions::delete_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
|
// communities
|
||||||
.route(
|
.route(
|
||||||
"/communities/find/{id}",
|
"/communities/find/{id}",
|
||||||
|
@ -907,3 +920,9 @@ pub struct UpdateNoteContent {
|
||||||
pub struct RenderMarkdown {
|
pub struct RenderMarkdown {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateMessageReaction {
|
||||||
|
pub message: String,
|
||||||
|
pub emoji: String,
|
||||||
|
}
|
||||||
|
|
|
@ -389,6 +389,46 @@ impl DataManager {
|
||||||
return Err(Error::DatabaseError(e.to_string()));
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete stackblocks
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"DELETE FROM stackblocks WHERE owner = $1",
|
||||||
|
&[&(id as i64)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete journals
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"DELETE FROM journals WHERE owner = $1",
|
||||||
|
&[&(id as i64)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete notes
|
||||||
|
let res = execute!(&conn, "DELETE FROM notes WHERE owner = $1", &[&(id as i64)]);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete message reactions
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"DELETE FROM message_reactions WHERE owner = $1",
|
||||||
|
&[&(id as i64)]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
// delete user follows... individually since it requires updating user counts
|
// delete user follows... individually since it requires updating user counts
|
||||||
for follow in self.get_userfollows_by_receiver_all(id).await? {
|
for follow in self.get_userfollows_by_receiver_all(id).await? {
|
||||||
self.delete_userfollow(follow.id, &user, true).await?;
|
self.delete_userfollow(follow.id, &user, true).await?;
|
||||||
|
|
|
@ -38,6 +38,7 @@ impl DataManager {
|
||||||
execute!(&conn, common::CREATE_TABLE_STACKBLOCKS).unwrap();
|
execute!(&conn, common::CREATE_TABLE_STACKBLOCKS).unwrap();
|
||||||
execute!(&conn, common::CREATE_TABLE_JOURNALS).unwrap();
|
execute!(&conn, common::CREATE_TABLE_JOURNALS).unwrap();
|
||||||
execute!(&conn, common::CREATE_TABLE_NOTES).unwrap();
|
execute!(&conn, common::CREATE_TABLE_NOTES).unwrap();
|
||||||
|
execute!(&conn, common::CREATE_TABLE_MESSAGE_REACTIONS).unwrap();
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.1
|
.1
|
||||||
|
|
|
@ -25,3 +25,4 @@ 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_STACKBLOCKS: &str = include_str!("./sql/create_stackblocks.sql");
|
||||||
pub const CREATE_TABLE_JOURNALS: &str = include_str!("./sql/create_journals.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_NOTES: &str = include_str!("./sql/create_notes.sql");
|
||||||
|
pub const CREATE_TABLE_MESSAGE_REACTIONS: &str = include_str!("./sql/create_message_reactions.sql");
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
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)
|
||||||
|
)
|
|
@ -5,5 +5,6 @@ CREATE TABLE IF NOT EXISTS messages (
|
||||||
created BIGINT NOT NULL,
|
created BIGINT NOT NULL,
|
||||||
edited BIGINT NOT NULL,
|
edited BIGINT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
context TEXT NOT NULL
|
context TEXT NOT NULL,
|
||||||
|
reactions TEXT NOT NULL
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,5 +12,6 @@ CREATE TABLE IF NOT EXISTS questions (
|
||||||
dislikes INT NOT NULL,
|
dislikes INT NOT NULL,
|
||||||
-- ...
|
-- ...
|
||||||
context TEXT NOT NULL,
|
context TEXT NOT NULL,
|
||||||
ip TEXT NOT NULL
|
ip TEXT NOT NULL,
|
||||||
|
drawings TEXT NOT NULL
|
||||||
)
|
)
|
||||||
|
|
183
crates/core/src/database/message_reactions.rs
Normal file
183
crates/core/src/database/message_reactions.rs
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ impl DataManager {
|
||||||
edited: get!(x->4(i64)) as usize,
|
edited: get!(x->4(i64)) as usize,
|
||||||
content: get!(x->5(String)),
|
content: get!(x->5(String)),
|
||||||
context: serde_json::from_str(&get!(x->6(String))).unwrap(),
|
context: serde_json::from_str(&get!(x->6(String))).unwrap(),
|
||||||
|
reactions: serde_json::from_str(&get!(x->7(String))).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +219,7 @@ impl DataManager {
|
||||||
|
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&conn,
|
||||||
"INSERT INTO messages VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
"INSERT INTO messages VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||||
params![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.channel as i64),
|
&(data.channel as i64),
|
||||||
|
@ -226,7 +227,8 @@ impl DataManager {
|
||||||
&(data.created as i64),
|
&(data.created as i64),
|
||||||
&(data.edited as i64),
|
&(data.edited as i64),
|
||||||
&data.content,
|
&data.content,
|
||||||
&serde_json::to_string(&data.context).unwrap()
|
&serde_json::to_string(&data.context).unwrap(),
|
||||||
|
&serde_json::to_string(&data.reactions).unwrap(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -357,4 +359,6 @@ impl DataManager {
|
||||||
// return
|
// return
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto_method!(update_message_reactions(HashMap<String, usize>) -> "UPDATE messages SET reactions = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.message:{}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ mod ipbans;
|
||||||
mod ipblocks;
|
mod ipblocks;
|
||||||
mod journals;
|
mod journals;
|
||||||
mod memberships;
|
mod memberships;
|
||||||
|
mod message_reactions;
|
||||||
mod messages;
|
mod messages;
|
||||||
mod notes;
|
mod notes;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||||
|
|
||||||
|
@ -79,6 +81,7 @@ pub struct Message {
|
||||||
pub edited: usize,
|
pub edited: usize,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub context: MessageContext,
|
pub context: MessageContext,
|
||||||
|
pub reactions: HashMap<String, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
|
@ -93,6 +96,7 @@ impl Message {
|
||||||
edited: now,
|
edited: now,
|
||||||
content,
|
content,
|
||||||
context: MessageContext,
|
context: MessageContext,
|
||||||
|
reactions: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,3 +109,25 @@ impl Default for MessageContext {
|
||||||
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
2
sql_changes/messages_reactions.sql
Normal file
2
sql_changes/messages_reactions.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE messages
|
||||||
|
ADD COLUMN reactions TEXT NOT NULL DEFAULT '{}';
|
Loading…
Add table
Add a link
Reference in a new issue