164 lines
6.0 KiB
JavaScript
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
|
|
});
|