fix reaction and unrenote
This commit is contained in:
parent
965ad49c9b
commit
2e6c04a6cb
6 changed files with 146 additions and 21 deletions
|
@ -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: {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue