Add replies and emoji reactions to chat messages

Every message has a row under it to reply or react. Replying quotes the
message above the composer, and the sent message stores
reply_to_message_id (a plain id, not a foreign key, so deleting the
quoted message only drops the quote). send-message accepts a quote from
any session of the same conversation, so a rollover or a thread that
moved on still sends; the conversation drops a picked quote when its
session changes. The bot reads the quote as "[Replying to ...]" before
the user's turn, file-only replies included, and relayed messages carry
it too.

Reactions are a fixed set (BotMessageReactions) in
bot_message_reactions, one per message, emoji and user or bot. Users
toggle their own through api/admin/bots/react-message, and every bot
gets react_to_message for the session it answers (the user's latest
message by default). Both push the fresh list as server:bot-reaction,
with mine worked out for the session's user.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 21:56:31 +01:00
co-authored by Claude Opus 5.5
parent bc499a7466
commit d1b5b6d68a
44 changed files with 1289 additions and 58 deletions
+23 -1
View File
@@ -2,6 +2,7 @@ import userAuth from "@/src/functions/backend/auth/user-auth";
import grabUserConversation from "@/src/functions/backend/db/bot-sessions/grab-user-conversation";
import ensureBotSession from "@/src/functions/backend/db/bot-sessions/ensure-bot-session";
import insertUserMessage from "@/src/functions/backend/db/bots/insert-user-message";
import grabUserBotMessage from "@/src/functions/backend/db/bots/grab-user-bot-message";
import grabAttachableMedia from "@/src/functions/backend/db/bots/grab-attachable-media";
import enqueueBotReply from "@/src/functions/backend/db/bots-queue/enqueue-bot-reply";
import pushBotNotice from "@/src/bots-queue/push-bot-notice";
@@ -47,7 +48,7 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
throw new AppError(`This user can only read records.`);
}
const { bot_id, workspace_id, text, media_ids } = parseWithSchema({
const { bot_id, workspace_id, text, media_ids, reply_to_message_id } = parseWithSchema({
schema: SendBotMessageRequestSchema,
data: body,
});
@@ -63,11 +64,32 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
rollover: true,
});
/**
* The quoted message only has to be in this conversation, not in
* the session the message lands in: sending can roll over to a new
* one, or the thread may have moved on.
*/
const reply_to = reply_to_message_id
? await grabUserBotMessage({ user_id: user.id, message_id: reply_to_message_id }).catch(
() => null,
)
: null;
if (
reply_to_message_id &&
(!reply_to ||
reply_to.bot_id != bot.id ||
(reply_to.workspace_id || null) != (conversation.workspace_id || null))
) {
throw new AppError(`That message is no longer in this conversation.`);
}
const message = await insertUserMessage({
bot_id: bot.id,
session_id: Number(session.id),
workspace_id: conversation.workspace_id,
text,
reply_to,
media,
});