Files
2026-07-26 18:25:37 +09:00

163 lines
5.1 KiB
JavaScript

import { Not } from "typeorm";
import { publishNoteStream } from "../../../../../services/stream.js";
import { createNotification } from "../../../../../services/create-notification.js";
import { deliver } from "../../../../../queue/index.js";
import { renderActivity } from "../../../../../remote/activitypub/renderer/index.js";
import renderVote from "../../../../../remote/activitypub/renderer/vote.js";
import { PollVotes, NoteWatchings, Users, Polls, Blockings } from "../../../../../models/index.js";
import { genId } from "../../../../../misc/gen-id.js";
import { getNote } from "../../../common/getters.js";
import { ApiError } from "../../../error.js";
import define from "../../../define.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
kind: "write:votes",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "ecafbd2e-c283-4d6d-aecb-1a0a33b75396"
},
noPoll: {
message: "The note does not attach a poll.",
code: "NO_POLL",
id: "5f979967-52d9-4314-a911-1c673727f92f"
},
invalidChoice: {
message: "Choice ID is invalid.",
code: "INVALID_CHOICE",
id: "e0cc9a04-f2e8-41e4-a5f1-4127293260cc"
},
alreadyVoted: {
message: "You have already voted.",
code: "ALREADY_VOTED",
id: "0963fc77-efac-419b-9424-b391608dc6d8"
},
alreadyExpired: {
message: "The poll is already expired.",
code: "ALREADY_EXPIRED",
id: "1022a357-b085-4054-9083-8f8de358337e"
},
youHaveBeenBlocked: {
message: "You cannot vote this poll because you have been blocked by this user.",
code: "YOU_HAVE_BEEN_BLOCKED",
id: "85a5377e-b1e9-4617-b0b9-5bea73331e49"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
choice: {
type: "integer"
}
},
required: [
"noteId",
"choice"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const createdAt = new Date();
// Get votee
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (!note.hasPoll) {
throw new ApiError(meta.errors.noPoll);
}
// 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 ApiError(meta.errors.youHaveBeenBlocked);
}
}
const poll = await Polls.findOneByOrFail({
noteId: note.id
});
if (poll.expiresAt && poll.expiresAt < createdAt) {
throw new ApiError(meta.errors.alreadyExpired);
}
if (poll.choices[ps.choice] == null) {
throw new ApiError(meta.errors.invalidChoice);
}
// if already voted
const exist = await PollVotes.findBy({
noteId: note.id,
userId: user.id
});
if (exist.length) {
if (poll.multiple) {
if (exist.some((x)=>x.choice === ps.choice)) {
throw new ApiError(meta.errors.alreadyVoted);
}
} else {
throw new ApiError(meta.errors.alreadyVoted);
}
}
// Create vote
const vote = await PollVotes.insert({
id: genId(),
createdAt,
noteId: note.id,
userId: user.id,
choice: ps.choice
}).then((x)=>PollVotes.findOneByOrFail(x.identifiers[0]));
// Increment votes count
const index = ps.choice + 1; // In SQL, array index is 1 based
await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
publishNoteStream(note.id, "pollVoted", {
choice: ps.choice,
userId: user.id
});
// Notify
createNotification(note.userId, "pollVote", {
notifierId: user.id,
noteId: note.id,
choice: ps.choice
});
// Fetch watchers
NoteWatchings.findBy({
noteId: note.id,
userId: Not(user.id)
}).then((watchers)=>{
for (const watcher of watchers){
createNotification(watcher.userId, "pollVote", {
notifierId: user.id,
noteId: note.id,
choice: ps.choice
});
}
});
// リモート投票の場合リプライ送信
if (note.userHost != null) {
const pollOwner = await Users.findOneByOrFail({
id: note.userId
});
deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
}
});