fix reaction and unrenote

This commit is contained in:
Namekuji 2023-08-12 07:00:40 -04:00
parent 965ad49c9b
commit 2e6c04a6cb
No known key found for this signature in database
GPG key ID: 1D62332C07FBA532
6 changed files with 146 additions and 21 deletions

View file

@ -113,6 +113,20 @@ export const scyllaQueries = {
byId: `SELECT * FROM home_timeline WHERE "id" = ?`, byId: `SELECT * FROM home_timeline WHERE "id" = ?`,
}, },
delete: `DELETE FROM home_timeline WHERE "feedUserId" = ? AND "createdAtDate" = ? AND "createdAt" = ? AND "userId" = ?`, delete: `DELETE FROM home_timeline WHERE "feedUserId" = ? AND "createdAtDate" = ? AND "createdAt" = ? AND "userId" = ?`,
update: {
renoteCount: `UPDATE home_timeline SET
"renoteCount" = ?,
"score" = ?
WHERE "feedUserId" = ? AND "createdAtDate" = ? AND "createdAt" = ? AND "userId" = ? IF EXISTS`,
repliesCount: `UPDATE home_timeline SET
"repliesCount" = ?
WHERE "feedUserId" = ? AND "createdAtDate" = ? AND "createdAt" = ? AND "userId" = ? IF EXISTS`,
reactions: `UPDATE home_timeline SET
"emojis" = ?,
"reactions" = ?,
"score" = ?
WHERE "feedUserId" = ? AND "createdAtDate" = ? AND "createdAt" = ? AND "userId" = ? IF EXISTS`,
}
}, },
localTimeline: { localTimeline: {
select: { select: {

View file

@ -125,17 +125,17 @@ export default define(meta, paramDef, async (ps, user) => {
UserBlockedCache.init(user.id).then((cache) => cache.getAll()), UserBlockedCache.init(user.id).then((cache) => cache.getAll()),
RenoteMutingsCache.init(user.id).then((cache) => cache.getAll()), RenoteMutingsCache.init(user.id).then((cache) => cache.getAll()),
]); ]);
const validUserIds = [user.id, ...followingUserIds]; const homeUserIds = [user.id, ...followingUserIds];
const optFilter = (n: ScyllaNote) => const optFilter = (n: ScyllaNote) =>
!n.renoteId || !!n.text || n.files.length > 0 || n.hasPoll; !n.renoteId || !!n.text || n.files.length > 0 || n.hasPoll;
const filter = async (notes: ScyllaNote[]) => { const homeFilter = (notes: ScyllaNote[]) =>
let filtered = notes.filter( notes.filter((n) => homeUserIds.includes(n.userId));
(n) => const localFilter = (notes: ScyllaNote[]) =>
validUserIds.includes(n.userId) || notes.filter((n) => !homeUserIds.includes(n.userId));
(n.visibility === "public" && !n.userHost),
); const commonFilter = async (notes: ScyllaNote[]) => {
filtered = await filterChannel(filtered, user, followingChannelIds); let filtered = await filterChannel(notes, user, followingChannelIds);
filtered = await filterReply(filtered, ps.withReplies, user); filtered = await filterReply(filtered, ps.withReplies, user);
filtered = await filterVisibility(filtered, user, followingUserIds); filtered = await filterVisibility(filtered, user, followingUserIds);
filtered = await filterMutedUser( filtered = await filterMutedUser(
@ -168,8 +168,15 @@ export default define(meta, paramDef, async (ps, user) => {
const foundPacked = []; const foundPacked = [];
while (foundPacked.length < ps.limit) { while (foundPacked.length < ps.limit) {
const [homeFoundNotes, localFoundNotes] = await Promise.all([ const [homeFoundNotes, localFoundNotes] = await Promise.all([
execNotePaginationQuery("home", ps, filter, user.id), execNotePaginationQuery(
execNotePaginationQuery("local", ps, filter), "home",
ps,
(notes) => commonFilter(homeFilter(notes)),
user.id,
),
execNotePaginationQuery("local", ps, (notes) =>
commonFilter(localFilter(notes)),
),
]); ]);
const foundNotes = [...homeFoundNotes, ...localFoundNotes] const foundNotes = [...homeFoundNotes, ...localFoundNotes]
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) // Descendent .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) // Descendent

View file

@ -4,6 +4,8 @@ import define from "../../define.js";
import { getNote } from "../../common/getters.js"; import { getNote } from "../../common/getters.js";
import { ApiError } from "../../error.js"; import { ApiError } from "../../error.js";
import { SECOND, HOUR } from "@/const.js"; import { SECOND, HOUR } from "@/const.js";
import type { Note } from "@/models/entities/note.js";
import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js";
export const meta = { export const meta = {
tags: ["notes"], tags: ["notes"],
@ -42,10 +44,19 @@ export default define(meta, paramDef, async (ps, user) => {
throw err; throw err;
}); });
const renotes = await Notes.findBy({ let renotes: Note[] = [];
userId: user.id,
renoteId: note.id, if (scyllaClient) {
}); const notes = await scyllaClient
.execute(prepared.note.select.byRenoteId, [note.id], { prepare: true })
.then((result) => result.rows.map(parseScyllaNote));
renotes = notes.filter((n) => n.userId === user.id);
} else {
renotes = await Notes.findBy({
userId: user.id,
renoteId: note.id,
});
}
for (const note of renotes) { for (const note of renotes) {
deleteNote(await Users.findOneByOrFail({ id: user.id }), note); deleteNote(await Users.findOneByOrFail({ id: user.id }), note);

View file

@ -68,7 +68,12 @@ import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
import meilisearch from "../../db/meilisearch.js"; import meilisearch from "../../db/meilisearch.js";
import { redisClient } from "@/db/redis.js"; import { redisClient } from "@/db/redis.js";
import { Mutex } from "redis-semaphore"; import { Mutex } from "redis-semaphore";
import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js"; import {
parseHomeTimeline,
parseScyllaNote,
prepared,
scyllaClient,
} from "@/db/scylla.js";
export const mutedWordsCache = new Cache< export const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@ -696,6 +701,26 @@ async function incRenoteCount(renote: Note) {
], ],
{ prepare: true }, { prepare: true },
); );
const homeTimelines = await scyllaClient
.execute(prepared.homeTimeline.select.byId, [renote.id], {
prepare: true,
})
.then((result) => result.rows.map(parseHomeTimeline));
// Do not issue BATCH because different home timelines involve different partitions
for (const timeline of homeTimelines) {
scyllaClient.execute(
prepared.homeTimeline.update.renoteCount,
[
count + 1,
score + 1,
timeline.feedUserId,
timeline.createdAtDate,
timeline.createdAt,
timeline.userId,
],
{ prepare: true },
);
}
} else { } else {
Notes.createQueryBuilder() Notes.createQueryBuilder()
.update() .update()
@ -857,7 +882,7 @@ async function insertNote(
// Include the local user itself // Include the local user itself
localFollowers.push(user.id); localFollowers.push(user.id);
} }
// Do not issue BATCH because different queries of inserting post to home timelines involve different partitions // Do not issue BATCH because different home timelines involve different partitions
for (const follower of localFollowers) { for (const follower of localFollowers) {
// no need to wait // no need to wait
scyllaClient.execute( scyllaClient.execute(
@ -1047,6 +1072,25 @@ async function saveReply(reply: Note) {
], ],
{ prepare: true }, { prepare: true },
); );
const homeTimelines = await scyllaClient
.execute(prepared.homeTimeline.select.byId, [reply.id], {
prepare: true,
})
.then((result) => result.rows.map(parseHomeTimeline));
// Do not issue BATCH because different home timelines involve different partitions
for (const timeline of homeTimelines) {
scyllaClient.execute(
prepared.homeTimeline.update.repliesCount,
[
count + 1,
timeline.feedUserId,
timeline.createdAtDate,
timeline.createdAt,
timeline.userId,
],
{ prepare: true },
);
}
} else { } else {
await Notes.increment({ id: reply.id }, "repliesCount", 1); await Notes.increment({ id: reply.id }, "repliesCount", 1);
} }

View file

@ -21,7 +21,7 @@ import deleteReaction from "./delete.js";
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
import type { NoteReaction } from "@/models/entities/note-reaction.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js";
import { IdentifiableError } from "@/misc/identifiable-error.js"; import { IdentifiableError } from "@/misc/identifiable-error.js";
import { prepared, scyllaClient } from "@/db/scylla.js"; import { parseHomeTimeline, prepared, scyllaClient } from "@/db/scylla.js";
import { EmojiCache } from "@/misc/populate-emojis.js"; import { EmojiCache } from "@/misc/populate-emojis.js";
export default async ( export default async (
@ -108,12 +108,13 @@ export default async (
note.reactions[_reaction] = current + 1; note.reactions[_reaction] = current + 1;
const emojiName = decodeReaction(_reaction).reaction.replaceAll(":", ""); const emojiName = decodeReaction(_reaction).reaction.replaceAll(":", "");
const date = new Date(note.createdAt.getTime()); const date = new Date(note.createdAt.getTime());
const score = isNaN(note.score) ? 0 : note.score;
await scyllaClient.execute( await scyllaClient.execute(
prepared.note.update.reactions, prepared.note.update.reactions,
[ [
note.emojis.concat(emojiName), note.emojis.concat(emojiName),
note.reactions, note.reactions,
(note.score ?? 0) + 1, score + 1,
date, date,
date, date,
note.userId, note.userId,
@ -122,6 +123,27 @@ export default async (
], ],
{ prepare: true }, { prepare: true },
); );
const homeTimelines = await scyllaClient
.execute(prepared.homeTimeline.select.byId, [note.id], {
prepare: true,
})
.then((result) => result.rows.map(parseHomeTimeline));
// Do not issue BATCH because different home timelines involve different partitions
for (const timeline of homeTimelines) {
scyllaClient.execute(
prepared.homeTimeline.update.reactions,
[
note.emojis.concat(emojiName),
note.reactions,
score + 1,
timeline.feedUserId,
timeline.createdAtDate,
timeline.createdAt,
timeline.userId,
],
{ 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()

View file

@ -8,7 +8,12 @@ import type { User, IRemoteUser } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js"; import type { Note } from "@/models/entities/note.js";
import { NoteReactions, Users, Notes } from "@/models/index.js"; import { NoteReactions, Users, Notes } from "@/models/index.js";
import { decodeReaction } from "@/misc/reaction-lib.js"; import { decodeReaction } from "@/misc/reaction-lib.js";
import { parseScyllaReaction, prepared, scyllaClient } from "@/db/scylla.js"; import {
parseHomeTimeline,
parseScyllaReaction,
prepared,
scyllaClient,
} from "@/db/scylla.js";
import type { NoteReaction } from "@/models/entities/note-reaction.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js";
export default async ( export default async (
@ -19,7 +24,7 @@ export default async (
if (scyllaClient) { if (scyllaClient) {
const result = await scyllaClient.execute( const result = await scyllaClient.execute(
prepared.reaction.select.byNoteAndUser, prepared.reaction.select.byNoteAndUser,
[note.id, user.id], [[note.id], [user.id]],
{ prepare: true }, { prepare: true },
); );
reaction = reaction =
@ -58,13 +63,14 @@ export default async (
const date = new Date(note.createdAt.getTime()); const date = new Date(note.createdAt.getTime());
const emojiName = reaction.reaction.replaceAll(":", ""); const emojiName = reaction.reaction.replaceAll(":", "");
const emojiIndex = note.emojis.indexOf(emojiName); const emojiIndex = note.emojis.indexOf(emojiName);
const score = isNaN(note.score) ? 0 : note.score;
if (emojiIndex >= 0 && count === 0) note.emojis.splice(emojiIndex, 1); if (emojiIndex >= 0 && count === 0) note.emojis.splice(emojiIndex, 1);
await scyllaClient.execute( await scyllaClient.execute(
prepared.note.update.reactions, prepared.note.update.reactions,
[ [
note.emojis, note.emojis,
note.reactions, note.reactions,
Math.max((note.score ?? 0) - 1, 0), Math.max(score - 1, 0),
date, date,
date, date,
note.userId, note.userId,
@ -73,6 +79,27 @@ export default async (
], ],
{ prepare: true }, { prepare: true },
); );
const homeTimelines = await scyllaClient
.execute(prepared.homeTimeline.select.byId, [note.id], {
prepare: true,
})
.then((result) => result.rows.map(parseHomeTimeline));
// Do not issue BATCH because different home timelines involve different partitions
for (const timeline of homeTimelines) {
scyllaClient.execute(
prepared.homeTimeline.update.reactions,
[
note.emojis.concat(emojiName),
note.reactions,
Math.max(score - 1, 0),
timeline.feedUserId,
timeline.createdAtDate,
timeline.createdAt,
timeline.userId,
],
{ 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()