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 });