add poll type and table
This commit is contained in:
parent
bacb0a1ee0
commit
6d49a39273
10 changed files with 360 additions and 195 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
DROP TABLE IF EXISTS poll_vote;
|
||||||
DROP MATERIALIZED VIEW IF EXISTS reaction_by_id;
|
DROP MATERIALIZED VIEW IF EXISTS reaction_by_id;
|
||||||
DROP MATERIALIZED VIEW IF EXISTS reaction_by_user_id;
|
DROP MATERIALIZED VIEW IF EXISTS reaction_by_user_id;
|
||||||
DROP INDEX IF EXISTS reaction_by_id;
|
DROP INDEX IF EXISTS reaction_by_id;
|
||||||
|
@ -15,6 +16,7 @@ DROP INDEX IF EXISTS note_by_reply_id;
|
||||||
DROP INDEX IF EXISTS note_by_uri;
|
DROP INDEX IF EXISTS note_by_uri;
|
||||||
DROP INDEX IF EXISTS note_by_url;
|
DROP INDEX IF EXISTS note_by_url;
|
||||||
DROP TABLE IF EXISTS note;
|
DROP TABLE IF EXISTS note;
|
||||||
|
DROP TYPE IF EXISTS poll;
|
||||||
DROP TYPE IF EXISTS emoji;
|
DROP TYPE IF EXISTS emoji;
|
||||||
DROP TYPE IF EXISTS note_edit_history;
|
DROP TYPE IF EXISTS note_edit_history;
|
||||||
DROP TYPE IF EXISTS drive_file;
|
DROP TYPE IF EXISTS drive_file;
|
||||||
|
|
|
@ -29,6 +29,12 @@ CREATE TYPE IF NOT EXISTS emoji (
|
||||||
"height" int,
|
"height" int,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TYPE IF NOT EXISTS poll (
|
||||||
|
"expiresAt" timestamp,
|
||||||
|
"multiple" boolean,
|
||||||
|
"choices" map<int, text>,
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS note ( -- Store all posts
|
CREATE TABLE IF NOT EXISTS note ( -- Store all posts
|
||||||
"createdAtDate" date, -- For partitioning
|
"createdAtDate" date, -- For partitioning
|
||||||
"createdAt" timestamp,
|
"createdAt" timestamp,
|
||||||
|
@ -50,6 +56,7 @@ CREATE TABLE IF NOT EXISTS note ( -- Store all posts
|
||||||
"emojis" set<text>,
|
"emojis" set<text>,
|
||||||
"tags" set<text>,
|
"tags" set<text>,
|
||||||
"hasPoll" boolean,
|
"hasPoll" boolean,
|
||||||
|
"poll" poll,
|
||||||
"threadId" ascii,
|
"threadId" ascii,
|
||||||
"channelId" ascii, -- Channel
|
"channelId" ascii, -- Channel
|
||||||
"userId" ascii, -- User
|
"userId" ascii, -- User
|
||||||
|
@ -172,6 +179,7 @@ CREATE TABLE IF NOT EXISTS home_timeline (
|
||||||
"emojis" set<text>,
|
"emojis" set<text>,
|
||||||
"tags" set<text>,
|
"tags" set<text>,
|
||||||
"hasPoll" boolean,
|
"hasPoll" boolean,
|
||||||
|
"poll" poll,
|
||||||
"threadId" ascii,
|
"threadId" ascii,
|
||||||
"channelId" ascii, -- Channel
|
"channelId" ascii, -- Channel
|
||||||
"userId" ascii, -- User
|
"userId" ascii, -- User
|
||||||
|
@ -220,3 +228,11 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS reaction_by_id AS
|
||||||
AND "reaction" IS NOT NULL
|
AND "reaction" IS NOT NULL
|
||||||
AND "userId" IS NOT NULL
|
AND "userId" IS NOT NULL
|
||||||
PRIMARY KEY ("noteId", "reaction", "userId");
|
PRIMARY KEY ("noteId", "reaction", "userId");
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS poll_vote (
|
||||||
|
"noteId" ascii,
|
||||||
|
"userId" ascii,
|
||||||
|
"choice" set<int>,
|
||||||
|
"createdAt" timestamp,
|
||||||
|
PRIMARY KEY ("noteId", "userId")
|
||||||
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const scyllaQueries = {
|
||||||
"emojis",
|
"emojis",
|
||||||
"tags",
|
"tags",
|
||||||
"hasPoll",
|
"hasPoll",
|
||||||
|
"poll",
|
||||||
"threadId",
|
"threadId",
|
||||||
"channelId",
|
"channelId",
|
||||||
"userId",
|
"userId",
|
||||||
|
@ -40,7 +41,7 @@ export const scyllaQueries = {
|
||||||
"reactions",
|
"reactions",
|
||||||
"noteEdit",
|
"noteEdit",
|
||||||
"updatedAt"
|
"updatedAt"
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
select: {
|
select: {
|
||||||
byDate: `SELECT * FROM note WHERE "createdAtDate" = ?`,
|
byDate: `SELECT * FROM note WHERE "createdAtDate" = ?`,
|
||||||
byUri: `SELECT * FROM note WHERE "uri" = ?`,
|
byUri: `SELECT * FROM note WHERE "uri" = ?`,
|
||||||
|
@ -91,6 +92,7 @@ export const scyllaQueries = {
|
||||||
"emojis",
|
"emojis",
|
||||||
"tags",
|
"tags",
|
||||||
"hasPoll",
|
"hasPoll",
|
||||||
|
"poll",
|
||||||
"threadId",
|
"threadId",
|
||||||
"channelId",
|
"channelId",
|
||||||
"userId",
|
"userId",
|
||||||
|
@ -110,7 +112,7 @@ export const scyllaQueries = {
|
||||||
"reactions",
|
"reactions",
|
||||||
"noteEdit",
|
"noteEdit",
|
||||||
"updatedAt"
|
"updatedAt"
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
select: {
|
select: {
|
||||||
byUserAndDate: `SELECT * FROM home_timeline WHERE "feedUserId" = ? AND "createdAtDate" = ?`,
|
byUserAndDate: `SELECT * FROM home_timeline WHERE "feedUserId" = ? AND "createdAtDate" = ?`,
|
||||||
byId: `SELECT * FROM home_timeline WHERE "id" = ?`,
|
byId: `SELECT * FROM home_timeline WHERE "id" = ?`,
|
||||||
|
@ -153,4 +155,8 @@ export const scyllaQueries = {
|
||||||
},
|
},
|
||||||
delete: `DELETE FROM reaction WHERE "noteId" = ? AND "userId" = ?`,
|
delete: `DELETE FROM reaction WHERE "noteId" = ? AND "userId" = ?`,
|
||||||
},
|
},
|
||||||
|
poll: {
|
||||||
|
select: `SELECT * FROM poll_vote WHERE "noteId" = ?`,
|
||||||
|
insert: `INSERT INTO poll_vote ("noteId", "userId", "choice", "createdAt") VALUES (?, ?, ?, ?)`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -111,6 +111,28 @@ export interface ScyllaNoteEditHistory {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScyllaPoll {
|
||||||
|
expiresAt: Date | null;
|
||||||
|
multiple: boolean;
|
||||||
|
choices: Map<number, string>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScyllaPollVote {
|
||||||
|
noteId: string,
|
||||||
|
userId: string,
|
||||||
|
choice: Set<number>,
|
||||||
|
createdAt: Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseScyllaPollVote(row: types.Row): ScyllaPollVote {
|
||||||
|
return {
|
||||||
|
noteId: row.get("noteId"),
|
||||||
|
userId: row.get("userId"),
|
||||||
|
choice: row.get("choice"),
|
||||||
|
createdAt: row.get("createdAt"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type ScyllaNote = Note & {
|
export type ScyllaNote = Note & {
|
||||||
createdAtDate: Date;
|
createdAtDate: Date;
|
||||||
files: ScyllaDriveFile[];
|
files: ScyllaDriveFile[];
|
||||||
|
@ -121,6 +143,7 @@ export type ScyllaNote = Note & {
|
||||||
renoteText: string | null;
|
renoteText: string | null;
|
||||||
renoteCw: string | null;
|
renoteCw: string | null;
|
||||||
renoteFiles: ScyllaDriveFile[];
|
renoteFiles: ScyllaDriveFile[];
|
||||||
|
poll: ScyllaPoll | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseScyllaNote(row: types.Row): ScyllaNote {
|
export function parseScyllaNote(row: types.Row): ScyllaNote {
|
||||||
|
@ -149,6 +172,7 @@ export function parseScyllaNote(row: types.Row): ScyllaNote {
|
||||||
emojis: row.get("emojis") ?? [],
|
emojis: row.get("emojis") ?? [],
|
||||||
tags: row.get("tags") ?? [],
|
tags: row.get("tags") ?? [],
|
||||||
hasPoll: row.get("hasPoll") ?? false,
|
hasPoll: row.get("hasPoll") ?? false,
|
||||||
|
poll: row.get("poll") ?? null,
|
||||||
threadId: row.get("threadId") ?? null,
|
threadId: row.get("threadId") ?? null,
|
||||||
channelId: row.get("channelId") ?? null,
|
channelId: row.get("channelId") ?? null,
|
||||||
userId: row.get("userId"),
|
userId: row.get("userId"),
|
||||||
|
|
|
@ -53,6 +53,7 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
||||||
import { truncate } from "@/misc/truncate.js";
|
import { truncate } from "@/misc/truncate.js";
|
||||||
import { type Size, getEmojiSize } from "@/misc/emoji-meta.js";
|
import { type Size, getEmojiSize } from "@/misc/emoji-meta.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
|
import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js";
|
||||||
|
|
||||||
const logger = apLogger;
|
const logger = apLogger;
|
||||||
|
|
||||||
|
@ -317,7 +318,37 @@ export async function createNote(
|
||||||
}
|
}
|
||||||
|
|
||||||
// vote
|
// vote
|
||||||
if (reply?.hasPoll) {
|
if (reply?.hasPoll && note.name) {
|
||||||
|
if (scyllaClient) {
|
||||||
|
const result = await scyllaClient.execute(
|
||||||
|
prepared.note.select.byId,
|
||||||
|
[reply.id],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
if (result.rowLength === 0) {
|
||||||
|
throw new Error("reply target note not found");
|
||||||
|
}
|
||||||
|
const scyllaNote = parseScyllaNote(result.first());
|
||||||
|
if (!scyllaNote.hasPoll || !scyllaNote.poll) {
|
||||||
|
throw new Error("reply target does not have poll");
|
||||||
|
}
|
||||||
|
if (scyllaNote.poll.expiresAt && scyllaNote.poll.expiresAt < new Date()) {
|
||||||
|
logger.warn(
|
||||||
|
`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${scyllaNote.id}, choice=${note.name}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = Array.from(scyllaNote.poll.choices.entries()).find(
|
||||||
|
([_, v]) => v === note.name,
|
||||||
|
);
|
||||||
|
if (entry) {
|
||||||
|
await vote(actor, scyllaNote, entry[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const poll = await Polls.findOneByOrFail({ noteId: reply.id });
|
const poll = await Polls.findOneByOrFail({ noteId: reply.id });
|
||||||
|
|
||||||
const tryCreateVote = async (
|
const tryCreateVote = async (
|
||||||
|
@ -337,12 +368,10 @@ export async function createNote(
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (note.name) {
|
return await tryCreateVote(
|
||||||
return await tryCreateVote(
|
note.name,
|
||||||
note.name,
|
poll.choices.findIndex((x) => x === note.name),
|
||||||
poll.choices.findIndex((x) => x === note.name),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojis = await extractEmojis(note.tag || [], actor.host).catch((e) => {
|
const emojis = await extractEmojis(note.tag || [], actor.host).catch((e) => {
|
||||||
|
|
|
@ -73,6 +73,7 @@ import {
|
||||||
parseScyllaNote,
|
parseScyllaNote,
|
||||||
prepared,
|
prepared,
|
||||||
scyllaClient,
|
scyllaClient,
|
||||||
|
ScyllaPoll,
|
||||||
} from "@/db/scylla.js";
|
} from "@/db/scylla.js";
|
||||||
|
|
||||||
export const mutedWordsCache = new Cache<
|
export const mutedWordsCache = new Cache<
|
||||||
|
@ -703,26 +704,28 @@ async function incRenoteCount(renote: Note) {
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
const homeTimelines = await scyllaClient
|
scyllaClient.eachRow(
|
||||||
.execute(prepared.homeTimeline.select.byId, [renote.id], {
|
prepared.homeTimeline.select.byId,
|
||||||
prepare: true,
|
[renote.id],
|
||||||
})
|
{ prepare: true },
|
||||||
.then((result) => result.rows.map(parseHomeTimeline));
|
(_, row) => {
|
||||||
// Do not issue BATCH because different home timelines involve different partitions
|
if (scyllaClient) {
|
||||||
for (const timeline of homeTimelines) {
|
const timeline = parseHomeTimeline(row);
|
||||||
scyllaClient.execute(
|
scyllaClient.execute(
|
||||||
prepared.homeTimeline.update.renoteCount,
|
prepared.homeTimeline.update.renoteCount,
|
||||||
[
|
[
|
||||||
count + 1,
|
count + 1,
|
||||||
score + 1,
|
score + 1,
|
||||||
timeline.feedUserId,
|
timeline.feedUserId,
|
||||||
timeline.createdAtDate,
|
timeline.createdAtDate,
|
||||||
timeline.createdAt,
|
timeline.createdAt,
|
||||||
timeline.userId,
|
timeline.userId,
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Notes.createQueryBuilder()
|
Notes.createQueryBuilder()
|
||||||
.update()
|
.update()
|
||||||
|
@ -806,7 +809,7 @@ async function insertNote(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 投稿を作成
|
// Insert post to DB
|
||||||
try {
|
try {
|
||||||
if (scyllaClient) {
|
if (scyllaClient) {
|
||||||
const fileMapper = (file: DriveFile) => ({
|
const fileMapper = (file: DriveFile) => ({
|
||||||
|
@ -830,6 +833,24 @@ async function insertNote(
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
let poll: ScyllaPoll | null = null;
|
||||||
|
|
||||||
|
if (data.poll) {
|
||||||
|
insert.hasPoll = true;
|
||||||
|
let expiresAt: Date | null;
|
||||||
|
if (!data.poll.expiresAt || isNaN(data.poll.expiresAt.getTime())) {
|
||||||
|
expiresAt = null;
|
||||||
|
} else {
|
||||||
|
expiresAt = data.poll.expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
poll = {
|
||||||
|
expiresAt,
|
||||||
|
choices: new Map(data.poll.choices.map((v, i) => [i, v] as [number, string])),
|
||||||
|
multiple: data.poll.multiple,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const params = [
|
const params = [
|
||||||
insert.createdAt,
|
insert.createdAt,
|
||||||
insert.createdAt,
|
insert.createdAt,
|
||||||
|
@ -851,6 +872,7 @@ async function insertNote(
|
||||||
insert.emojis,
|
insert.emojis,
|
||||||
insert.tags,
|
insert.tags,
|
||||||
insert.hasPoll,
|
insert.hasPoll,
|
||||||
|
poll,
|
||||||
insert.threadId,
|
insert.threadId,
|
||||||
insert.channelId,
|
insert.channelId,
|
||||||
insert.userId,
|
insert.userId,
|
||||||
|
@ -895,38 +917,37 @@ async function insertNote(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
if (insert.hasPoll) {
|
if (insert.hasPoll) {
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await db.transaction(async (transactionalEntityManager) => {
|
await db.transaction(async (transactionalEntityManager) => {
|
||||||
if (!data.poll) throw new Error("Empty poll data");
|
if (!data.poll) throw new Error("Empty poll data");
|
||||||
|
|
||||||
if (!scyllaClient) {
|
|
||||||
await transactionalEntityManager.insert(Note, insert);
|
await transactionalEntityManager.insert(Note, insert);
|
||||||
}
|
|
||||||
|
|
||||||
let expiresAt: Date | null;
|
let expiresAt: Date | null;
|
||||||
if (!data.poll.expiresAt || isNaN(data.poll.expiresAt.getTime())) {
|
if (!data.poll.expiresAt || isNaN(data.poll.expiresAt.getTime())) {
|
||||||
expiresAt = null;
|
expiresAt = null;
|
||||||
} else {
|
} else {
|
||||||
expiresAt = data.poll.expiresAt;
|
expiresAt = data.poll.expiresAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const poll = new Poll({
|
const poll = new Poll({
|
||||||
noteId: insert.id,
|
noteId: insert.id,
|
||||||
choices: data.poll.choices,
|
choices: data.poll.choices,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
multiple: data.poll.multiple,
|
multiple: data.poll.multiple,
|
||||||
votes: new Array(data.poll.choices.length).fill(0),
|
votes: new Array(data.poll.choices.length).fill(0),
|
||||||
noteVisibility: insert.visibility,
|
noteVisibility: insert.visibility,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
userHost: user.host,
|
userHost: user.host,
|
||||||
|
});
|
||||||
|
|
||||||
|
await transactionalEntityManager.insert(Poll, poll);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
await transactionalEntityManager.insert(Poll, poll);
|
await Notes.insert(insert);
|
||||||
});
|
}
|
||||||
} else if (!scyllaClient) {
|
|
||||||
await Notes.insert(insert);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scyllaClient) {
|
if (scyllaClient) {
|
||||||
|
@ -1074,25 +1095,27 @@ async function saveReply(reply: Note) {
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
const homeTimelines = await scyllaClient
|
scyllaClient.eachRow(
|
||||||
.execute(prepared.homeTimeline.select.byId, [reply.id], {
|
prepared.homeTimeline.select.byId,
|
||||||
prepare: true,
|
[reply.id],
|
||||||
})
|
{ prepare: true },
|
||||||
.then((result) => result.rows.map(parseHomeTimeline));
|
(_, row) => {
|
||||||
// Do not issue BATCH because different home timelines involve different partitions
|
if (scyllaClient) {
|
||||||
for (const timeline of homeTimelines) {
|
const timeline = parseHomeTimeline(row);
|
||||||
scyllaClient.execute(
|
scyllaClient.execute(
|
||||||
prepared.homeTimeline.update.repliesCount,
|
prepared.homeTimeline.update.repliesCount,
|
||||||
[
|
[
|
||||||
count + 1,
|
count + 1,
|
||||||
timeline.feedUserId,
|
timeline.feedUserId,
|
||||||
timeline.createdAtDate,
|
timeline.createdAtDate,
|
||||||
timeline.createdAt,
|
timeline.createdAt,
|
||||||
timeline.userId,
|
timeline.userId,
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await Notes.increment({ id: reply.id }, "repliesCount", 1);
|
await Notes.increment({ id: reply.id }, "repliesCount", 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,26 +70,30 @@ export default async function (
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
const homeTimelines = await scyllaClient
|
scyllaClient.eachRow(
|
||||||
.execute(prepared.homeTimeline.select.byId, [renote.id], {
|
prepared.homeTimeline.select.byId,
|
||||||
|
[renote.id],
|
||||||
|
{
|
||||||
prepare: true,
|
prepare: true,
|
||||||
})
|
},
|
||||||
.then((result) => result.rows.map(parseHomeTimeline));
|
(_, row) => {
|
||||||
// Do not issue BATCH because different home timelines involve different partitions
|
if (scyllaClient) {
|
||||||
for (const timeline of homeTimelines) {
|
const timeline = parseHomeTimeline(row);
|
||||||
scyllaClient.execute(
|
scyllaClient.execute(
|
||||||
prepared.homeTimeline.update.renoteCount,
|
prepared.homeTimeline.update.renoteCount,
|
||||||
[
|
[
|
||||||
Math.max(count - 1, 0),
|
Math.max(count - 1, 0),
|
||||||
Math.max(score - 1, 0),
|
Math.max(score - 1, 0),
|
||||||
timeline.feedUserId,
|
timeline.feedUserId,
|
||||||
timeline.createdAtDate,
|
timeline.createdAtDate,
|
||||||
timeline.createdAt,
|
timeline.createdAt,
|
||||||
timeline.userId,
|
timeline.userId,
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Notes.decrement({ id: note.renoteId }, "renoteCount", 1);
|
Notes.decrement({ id: note.renoteId }, "renoteCount", 1);
|
||||||
|
@ -119,25 +123,29 @@ export default async function (
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
const homeTimelines = await scyllaClient
|
scyllaClient.eachRow(
|
||||||
.execute(prepared.homeTimeline.select.byId, [reply.id], {
|
prepared.homeTimeline.select.byId,
|
||||||
|
[reply.id],
|
||||||
|
{
|
||||||
prepare: true,
|
prepare: true,
|
||||||
})
|
},
|
||||||
.then((result) => result.rows.map(parseHomeTimeline));
|
(_, row) => {
|
||||||
// Do not issue BATCH because different home timelines involve different partitions
|
if (scyllaClient) {
|
||||||
for (const timeline of homeTimelines) {
|
const timeline = parseHomeTimeline(row);
|
||||||
scyllaClient.execute(
|
scyllaClient.execute(
|
||||||
prepared.homeTimeline.update.repliesCount,
|
prepared.homeTimeline.update.repliesCount,
|
||||||
[
|
[
|
||||||
Math.max(count - 1, 0),
|
Math.max(count - 1, 0),
|
||||||
timeline.feedUserId,
|
timeline.feedUserId,
|
||||||
timeline.createdAtDate,
|
timeline.createdAtDate,
|
||||||
timeline.createdAt,
|
timeline.createdAt,
|
||||||
timeline.userId,
|
timeline.userId,
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await Notes.decrement({ id: note.replyId }, "repliesCount", 1);
|
await Notes.decrement({ id: note.replyId }, "repliesCount", 1);
|
||||||
|
|
|
@ -1,64 +1,113 @@
|
||||||
import { publishNoteStream } from "@/services/stream.js";
|
import { publishNoteStream } from "@/services/stream.js";
|
||||||
import type { CacheableUser } from "@/models/entities/user.js";
|
import type { CacheableUser } from "@/models/entities/user.js";
|
||||||
import { User } from "@/models/entities/user.js";
|
|
||||||
import type { Note } from "@/models/entities/note.js";
|
import type { Note } from "@/models/entities/note.js";
|
||||||
import { PollVotes, NoteWatchings, Polls, Blockings } from "@/models/index.js";
|
import { PollVotes, NoteWatchings, Polls, Blockings } from "@/models/index.js";
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
import { genId } from "@/misc/gen-id.js";
|
import { genId } from "@/misc/gen-id.js";
|
||||||
import { createNotification } from "../../create-notification.js";
|
import { createNotification } from "../../create-notification.js";
|
||||||
|
import {
|
||||||
|
type ScyllaNote,
|
||||||
|
type ScyllaPollVote,
|
||||||
|
scyllaClient,
|
||||||
|
prepared,
|
||||||
|
parseScyllaPollVote,
|
||||||
|
} from "@/db/scylla.js";
|
||||||
|
import { UserBlockingCache } from "@/misc/cache.js";
|
||||||
|
|
||||||
export default async function (
|
export default async function (
|
||||||
user: CacheableUser,
|
user: CacheableUser,
|
||||||
note: Note,
|
note: Note | ScyllaNote,
|
||||||
choice: number,
|
choice: number,
|
||||||
) {
|
) {
|
||||||
const poll = await Polls.findOneBy({ noteId: note.id });
|
if (scyllaClient) {
|
||||||
|
const scyllaNote = note as ScyllaNote;
|
||||||
if (poll == null) throw new Error("poll not found");
|
if (!scyllaNote.hasPoll || !scyllaNote.poll) {
|
||||||
|
throw new Error("poll not found");
|
||||||
// Check whether is valid choice
|
|
||||||
if (poll.choices[choice] == null) throw new Error("invalid choice param");
|
|
||||||
|
|
||||||
// Check blocking
|
|
||||||
if (note.userId !== user.id) {
|
|
||||||
const block = await Blockings.findOneBy({
|
|
||||||
blockerId: note.userId,
|
|
||||||
blockeeId: user.id,
|
|
||||||
});
|
|
||||||
if (block) {
|
|
||||||
throw new Error("blocked");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if already voted
|
if (!Array.from(scyllaNote.poll.choices.keys()).includes(choice)) {
|
||||||
const exist = await PollVotes.findBy({
|
throw new Error("invalid choice param");
|
||||||
noteId: note.id,
|
}
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (poll.multiple) {
|
if (scyllaNote.userId !== user.id) {
|
||||||
if (exist.some((x) => x.choice === choice)) {
|
const isBlocking = await UserBlockingCache.init(scyllaNote.userId).then(
|
||||||
|
(cache) => cache.has(user.id),
|
||||||
|
);
|
||||||
|
if (isBlocking) {
|
||||||
|
throw new Error("blocked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let newChoice: ScyllaPollVote["choice"] = new Set();
|
||||||
|
const result = await scyllaClient.execute(
|
||||||
|
`${prepared.poll.select} AND "userId" = ?`,
|
||||||
|
[scyllaNote.id, user.id],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
if (result.rowLength > 0) {
|
||||||
|
const vote = parseScyllaPollVote(result.first());
|
||||||
|
if (scyllaNote.poll.multiple && !vote.choice.has(choice)) {
|
||||||
|
newChoice = vote.choice.add(choice);
|
||||||
|
} else {
|
||||||
|
throw new Error("already voted");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newChoice.add(choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
await scyllaClient.execute(
|
||||||
|
prepared.poll.insert,
|
||||||
|
[scyllaNote.id, user.id, newChoice, new Date()],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const poll = await Polls.findOneBy({ noteId: note.id });
|
||||||
|
if (!poll) throw new Error("poll not found");
|
||||||
|
|
||||||
|
// Check whether is valid choice
|
||||||
|
if (poll.choices[choice] == null) throw new Error("invalid choice param");
|
||||||
|
|
||||||
|
// Check blocking
|
||||||
|
if (note.userId !== user.id) {
|
||||||
|
const block = await Blockings.findOneBy({
|
||||||
|
blockerId: note.userId,
|
||||||
|
blockeeId: user.id,
|
||||||
|
});
|
||||||
|
if (block) {
|
||||||
|
throw new Error("blocked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if already voted
|
||||||
|
const exist = await PollVotes.findBy({
|
||||||
|
noteId: note.id,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (poll.multiple) {
|
||||||
|
if (exist.some((x) => x.choice === choice)) {
|
||||||
|
throw new Error("already voted");
|
||||||
|
}
|
||||||
|
} else if (exist.length !== 0) {
|
||||||
throw new Error("already voted");
|
throw new Error("already voted");
|
||||||
}
|
}
|
||||||
} else if (exist.length !== 0) {
|
|
||||||
throw new Error("already voted");
|
// Create vote
|
||||||
|
await PollVotes.insert({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
noteId: note.id,
|
||||||
|
userId: user.id,
|
||||||
|
choice: choice,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Increment votes count
|
||||||
|
const index = choice + 1; // In SQL, array index is 1 based
|
||||||
|
await Polls.query(
|
||||||
|
`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create vote
|
|
||||||
await PollVotes.insert({
|
|
||||||
id: genId(),
|
|
||||||
createdAt: new Date(),
|
|
||||||
noteId: note.id,
|
|
||||||
userId: user.id,
|
|
||||||
choice: choice,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Increment votes count
|
|
||||||
const index = 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", {
|
publishNoteStream(note.id, "pollVoted", {
|
||||||
choice: choice,
|
choice: choice,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
|
@ -123,27 +123,31 @@ export default async (
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
const homeTimelines = await scyllaClient
|
scyllaClient.eachRow(
|
||||||
.execute(prepared.homeTimeline.select.byId, [note.id], {
|
prepared.homeTimeline.select.byId,
|
||||||
|
[note.id],
|
||||||
|
{
|
||||||
prepare: true,
|
prepare: true,
|
||||||
})
|
},
|
||||||
.then((result) => result.rows.map(parseHomeTimeline));
|
(_, row) => {
|
||||||
// Do not issue BATCH because different home timelines involve different partitions
|
if (scyllaClient) {
|
||||||
for (const timeline of homeTimelines) {
|
const timeline = parseHomeTimeline(row);
|
||||||
scyllaClient.execute(
|
scyllaClient.execute(
|
||||||
prepared.homeTimeline.update.reactions,
|
prepared.homeTimeline.update.reactions,
|
||||||
[
|
[
|
||||||
note.emojis.concat(emojiName),
|
note.emojis.concat(emojiName),
|
||||||
note.reactions,
|
note.reactions,
|
||||||
score + 1,
|
score + 1,
|
||||||
timeline.feedUserId,
|
timeline.feedUserId,
|
||||||
timeline.createdAtDate,
|
timeline.createdAtDate,
|
||||||
timeline.createdAt,
|
timeline.createdAt,
|
||||||
timeline.userId,
|
timeline.userId,
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const sql = `jsonb_set("reactions", '{${_reaction}}', (COALESCE("reactions"->>'${_reaction}', '0')::int + 1)::text::jsonb)`;
|
const sql = `jsonb_set("reactions", '{${_reaction}}', (COALESCE("reactions"->>'${_reaction}', '0')::int + 1)::text::jsonb)`;
|
||||||
await Notes.createQueryBuilder()
|
await Notes.createQueryBuilder()
|
||||||
|
|
|
@ -79,27 +79,31 @@ export default async (
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
const homeTimelines = await scyllaClient
|
scyllaClient.eachRow(
|
||||||
.execute(prepared.homeTimeline.select.byId, [note.id], {
|
prepared.homeTimeline.select.byId,
|
||||||
|
[note.id],
|
||||||
|
{
|
||||||
prepare: true,
|
prepare: true,
|
||||||
})
|
},
|
||||||
.then((result) => result.rows.map(parseHomeTimeline));
|
(_, row) => {
|
||||||
// Do not issue BATCH because different home timelines involve different partitions
|
if (scyllaClient) {
|
||||||
for (const timeline of homeTimelines) {
|
const timeline = parseHomeTimeline(row);
|
||||||
scyllaClient.execute(
|
scyllaClient.execute(
|
||||||
prepared.homeTimeline.update.reactions,
|
prepared.homeTimeline.update.reactions,
|
||||||
[
|
[
|
||||||
note.emojis.concat(emojiName),
|
note.emojis.concat(emojiName),
|
||||||
note.reactions,
|
note.reactions,
|
||||||
Math.max(score - 1, 0),
|
Math.max(score - 1, 0),
|
||||||
timeline.feedUserId,
|
timeline.feedUserId,
|
||||||
timeline.createdAtDate,
|
timeline.createdAtDate,
|
||||||
timeline.createdAt,
|
timeline.createdAt,
|
||||||
timeline.userId,
|
timeline.userId,
|
||||||
],
|
],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const sql = `jsonb_set("reactions", '{${reaction.reaction}}', (COALESCE("reactions"->>'${reaction.reaction}', '0')::int - 1)::text::jsonb)`;
|
const sql = `jsonb_set("reactions", '{${reaction.reaction}}', (COALESCE("reactions"->>'${reaction.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||||
await Notes.createQueryBuilder()
|
await Notes.createQueryBuilder()
|
||||||
|
|
Loading…
Reference in a new issue