Files
FrozenFriendsYume/packages/backend/built/services/note/reaction/create.js
T
2026-07-26 18:25:37 +09:00

164 lines
6.0 KiB
JavaScript

import { publishNoteStream } from "../../stream.js";
import { renderLike } from "../../../remote/activitypub/renderer/like.js";
import DeliverManager from "../../../remote/activitypub/deliver-manager.js";
import { renderActivity } from "../../../remote/activitypub/renderer/index.js";
import { toDbReaction, decodeReaction } from "../../../misc/reaction-lib.js";
import { NoteReactions, Users, NoteWatchings, Notes, Emojis, Blockings } from "../../../models/index.js";
import { IsNull, Not } from "typeorm";
import { perUserReactionsChart } from "../../chart/index.js";
import { genId } from "../../../misc/gen-id.js";
import { createNotification } from "../../create-notification.js";
import deleteReaction from "./delete.js";
import { isDuplicateKeyValueError } from "../../../misc/is-duplicate-key-value-error.js";
import { IdentifiableError } from "../../../misc/identifiable-error.js";
import { populateEmojiOrUserEmoji } from "../../../misc/populate-emojis.js";
export default (async (user, note, reaction)=>{
// Check blocking
if (note.userId !== user.id) {
const blocked = await Blockings.exist({
where: [
{
blockerId: note.userId,
blockeeId: user.id,
groupId: null
},
...note.groupId ? [
{
groupId: note.groupId,
blockeeId: user.id
}
] : []
]
});
if (blocked) {
throw new IdentifiableError("e70412a4-7197-4726-8e74-f3e0deb92aa7");
}
}
// check visibility
if (!await Notes.isVisibleForMe(note, user.id)) {
throw new IdentifiableError("68e9d2d1-48bf-42c2-b90a-b20e09fd3d48", "Note not accessible for you.");
}
// TODO: cache
reaction = await toDbReaction(reaction, user.host);
const record = {
id: genId(),
createdAt: new Date(),
noteId: note.id,
userId: user.id,
groupId: user.groupId ?? null,
reaction
};
// Create reaction
try {
await NoteReactions.insert(record);
} catch (e) {
if (isDuplicateKeyValueError(e)) {
const exists = await NoteReactions.findOneByOrFail({
noteId: note.id,
...user.groupId ? {
groupId: user.groupId
} : {
userId: user.id,
groupId: null
}
});
if (exists.reaction !== reaction) {
// 別のリアクションがすでにされていたら置き換える
await deleteReaction(user, note);
await NoteReactions.insert(record);
} else {
// 同じリアクションがすでにされていたらエラー
throw new IdentifiableError("51c42bb4-931a-456b-bff7-e5a8a70dd298", "Reaction already exists");
}
} else {
throw e;
}
}
// Increment reactions count
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
await Notes.createQueryBuilder().update().set({
reactions: ()=>sql,
score: ()=>'"score" + 1'
}).where("id = :id", {
id: note.id
}).execute();
perUserReactionsChart.update(user, note);
// カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = decodeReaction(reaction);
const emoji = await Emojis.findOne({
where: {
name: decodedReaction.name,
host: decodedReaction.host ?? IsNull()
},
select: [
"name",
"host",
"originalUrl",
"publicUrl"
]
});
const populatedEmoji = emoji == null ? await populateEmojiOrUserEmoji(reaction.slice(1, -1), note.userHost) : null;
publishNoteStream(note.id, "reacted", {
reaction: decodedReaction.reaction,
emoji: emoji != null ? {
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
url: emoji.publicUrl || emoji.originalUrl
} : populatedEmoji != null ? {
name: populatedEmoji.name,
url: populatedEmoji.url
} : null,
userId: user.id,
groupId: user.groupId ?? null
});
// Create notification if the reaction target is a local user.
if (note.userHost === null) {
createNotification(note.userId, "reaction", {
notifierId: user.id,
note: note,
noteId: note.id,
reaction: reaction
});
}
// Fetch watchers
NoteWatchings.findBy({
noteId: note.id,
userId: Not(user.id)
}).then((watchers)=>{
for (const watcher of watchers){
createNotification(watcher.userId, "reaction", {
notifierId: user.id,
note: note,
noteId: note.id,
reaction: reaction
});
}
});
//#region deliver
if (Users.isLocalUser(user) && !note.localOnly && note.visibility !== "hidden") {
const content = renderActivity(await renderLike(record, note));
const dm = new DeliverManager(user, content);
if (note.userHost !== null) {
const reactee = await Users.findOneBy({
id: note.userId
});
dm.addDirectRecipe(reactee);
}
if ([
"public",
"home",
"followers"
].includes(note.visibility)) {
dm.addFollowersRecipe();
} else if (note.visibility === "specified") {
const visibleUsers = await Promise.all(note.visibleUserIds.map((id)=>Users.findOneBy({
id
})));
for (const u of visibleUsers.filter((u)=>u && Users.isRemoteUser(u))){
dm.addDirectRecipe(u);
}
}
dm.execute();
}
//#endregion
});