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:
@@ -0,0 +1,60 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabUserBotMessage from "@/src/functions/backend/db/bots/grab-user-bot-message";
|
||||
import toggleMessageReaction from "@/src/functions/backend/db/bots/toggle-message-reaction";
|
||||
import pushToSessionUser from "@/src/bots-queue/push-to-session-user";
|
||||
import { ReactToBotMessageRequestSchema } from "@/src/schemas/bot-schema";
|
||||
import type { APIResponseObject, BunextAPIRouteHandler } from "@moduletrace/bunext/types";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
|
||||
/**
|
||||
* Adds or takes back one of the fixed emoji reactions on a message in the
|
||||
* session user's own conversation, and pushes the message's fresh
|
||||
* reactions to their other open tabs.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({ req, body }) => {
|
||||
try {
|
||||
if (req.method != "POST") {
|
||||
throw new AppError(`Method not allowed`);
|
||||
}
|
||||
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status || !user.id) {
|
||||
return { success: false, msg: `Unauthorized`, logoutUser: true };
|
||||
}
|
||||
|
||||
if (isReadOnlyUser({ user_types: user_types || [] })) {
|
||||
throw new AppError(`This user can only read records.`);
|
||||
}
|
||||
|
||||
const { message_id, emoji } = parseWithSchema({
|
||||
schema: ReactToBotMessageRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const message = await grabUserBotMessage({ user_id: user.id, message_id });
|
||||
const reactions = await toggleMessageReaction({
|
||||
message_id: Number(message.id),
|
||||
user_id: user.id,
|
||||
emoji,
|
||||
});
|
||||
|
||||
await pushToSessionUser({
|
||||
session_id: message.session_id,
|
||||
data: {
|
||||
event: "server:bot-reaction",
|
||||
data: { message_id: Number(message.id), reactions },
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
singleRes: { message_id: Number(message.id), reactions },
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { success: false, msg: safeErrorMessage({ error }) };
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const handler: BunextAPIRouteHandler<
|
||||
payload: await grabBotMessages({
|
||||
session_id: session.id,
|
||||
limit: AppData.BotReplyCatchUpMessages,
|
||||
user_id: user.id,
|
||||
}),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user