fix: include quotes
This commit is contained in:
parent
aecc243033
commit
37215ae0fa
5 changed files with 167 additions and 66 deletions
|
@ -46,7 +46,7 @@ export const scyllaQueries = {
|
||||||
byUri: `SELECT * FROM note WHERE "uri" = ?`,
|
byUri: `SELECT * FROM note WHERE "uri" = ?`,
|
||||||
byUrl: `SELECT * FROM note WHERE "url" = ?`,
|
byUrl: `SELECT * FROM note WHERE "url" = ?`,
|
||||||
byId: `SELECT * FROM note WHERE "id" = ?`,
|
byId: `SELECT * FROM note WHERE "id" = ?`,
|
||||||
byUserId: `SELECT * FROM note_by_user_id WHERE "userId" IN ?`,
|
byUserId: `SELECT * FROM note_by_user_id WHERE "userId" = ?`,
|
||||||
byRenoteId: `SELECT * FROM note_by_renote_id WHERE "renoteId" = ?`,
|
byRenoteId: `SELECT * FROM note_by_renote_id WHERE "renoteId" = ?`,
|
||||||
byReplyId: `SELECT * FROM note WHERE "replyId" = ?`
|
byReplyId: `SELECT * FROM note WHERE "replyId" = ?`
|
||||||
},
|
},
|
||||||
|
|
|
@ -82,7 +82,10 @@ export interface ScyllaDriveFile {
|
||||||
height: number | null;
|
height: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScyllaDrivePublicUrl(file: ScyllaDriveFile, thumbnail = false): string | null {
|
export function getScyllaDrivePublicUrl(
|
||||||
|
file: ScyllaDriveFile,
|
||||||
|
thumbnail = false,
|
||||||
|
): string | null {
|
||||||
const isImage =
|
const isImage =
|
||||||
file.type &&
|
file.type &&
|
||||||
[
|
[
|
||||||
|
@ -190,12 +193,13 @@ export interface ScyllaNoteReaction extends NoteReaction {
|
||||||
|
|
||||||
const QUERY_LIMIT = 1000; // TODO: should this be configurable?
|
const QUERY_LIMIT = 1000; // TODO: should this be configurable?
|
||||||
|
|
||||||
export type TimelineKind =
|
export type FeedType =
|
||||||
| "home"
|
| "home"
|
||||||
| "local"
|
| "local"
|
||||||
| "recommended"
|
| "recommended"
|
||||||
| "global"
|
| "global"
|
||||||
| "renotes";
|
| "renotes"
|
||||||
|
| "user";
|
||||||
|
|
||||||
export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
|
export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
|
||||||
return {
|
return {
|
||||||
|
@ -209,7 +213,7 @@ export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareNoteQuery(
|
export function prepareNoteQuery(
|
||||||
kind: TimelineKind,
|
kind: FeedType,
|
||||||
ps: {
|
ps: {
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
untilDate?: number;
|
untilDate?: number;
|
||||||
|
@ -233,6 +237,9 @@ export function prepareNoteQuery(
|
||||||
case "renotes":
|
case "renotes":
|
||||||
queryParts.push(prepared.note.select.byRenoteId);
|
queryParts.push(prepared.note.select.byRenoteId);
|
||||||
break;
|
break;
|
||||||
|
case "user":
|
||||||
|
queryParts.push(prepared.note.select.byUserId);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
queryParts.push(prepared.note.select.byDate);
|
queryParts.push(prepared.note.select.byDate);
|
||||||
}
|
}
|
||||||
|
@ -269,7 +276,7 @@ export function prepareNoteQuery(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function execNotePaginationQuery(
|
export async function execNotePaginationQuery(
|
||||||
kind: TimelineKind,
|
kind: FeedType,
|
||||||
ps: {
|
ps: {
|
||||||
limit: number;
|
limit: number;
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
|
@ -284,11 +291,15 @@ export async function execNotePaginationQuery(
|
||||||
): Promise<ScyllaNote[]> {
|
): Promise<ScyllaNote[]> {
|
||||||
if (!scyllaClient) return [];
|
if (!scyllaClient) return [];
|
||||||
|
|
||||||
if (kind === "home" && !userId) {
|
switch (kind) {
|
||||||
throw new Error("Query of home timeline needs userId");
|
case "home":
|
||||||
}
|
case "user":
|
||||||
if (kind === "renotes" && !ps.noteId) {
|
if (!userId)
|
||||||
throw new Error("Query of renotes needs noteId");
|
throw new Error("Query of home and user timelines needs userId");
|
||||||
|
break;
|
||||||
|
case "renotes":
|
||||||
|
if (!ps.noteId) throw new Error("Query of renotes needs noteId");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { query, untilDate, sinceDate } = prepareNoteQuery(kind, ps);
|
let { query, untilDate, sinceDate } = prepareNoteQuery(kind, ps);
|
||||||
|
@ -300,14 +311,15 @@ export async function execNotePaginationQuery(
|
||||||
while (foundNotes.length < ps.limit && scannedPartitions < maxPartitions) {
|
while (foundNotes.length < ps.limit && scannedPartitions < maxPartitions) {
|
||||||
const params: (Date | string | string[] | number)[] = [];
|
const params: (Date | string | string[] | number)[] = [];
|
||||||
if (kind === "home" && userId) {
|
if (kind === "home" && userId) {
|
||||||
params.push(userId);
|
params.push(userId, untilDate, untilDate);
|
||||||
}
|
} else if (kind === "user" && userId) {
|
||||||
|
params.push(userId, untilDate);
|
||||||
if (kind === "renotes" && ps.noteId) {
|
} else if (kind === "renotes" && ps.noteId) {
|
||||||
params.push(ps.noteId, untilDate);
|
params.push(ps.noteId, untilDate);
|
||||||
} else {
|
} else {
|
||||||
params.push(untilDate, untilDate);
|
params.push(untilDate, untilDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sinceDate) {
|
if (sinceDate) {
|
||||||
params.push(sinceDate);
|
params.push(sinceDate);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,18 +41,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
ps.untilId,
|
ps.untilId,
|
||||||
)
|
)
|
||||||
.andWhere("note.visibility = 'public'")
|
.andWhere("note.visibility = 'public'")
|
||||||
.andWhere("note.localOnly = FALSE")
|
.andWhere("note.localOnly = FALSE");
|
||||||
.innerJoinAndSelect("note.user", "user")
|
|
||||||
.leftJoinAndSelect("user.avatar", "avatar")
|
|
||||||
.leftJoinAndSelect("user.banner", "banner")
|
|
||||||
.leftJoinAndSelect("note.reply", "reply")
|
|
||||||
.leftJoinAndSelect("note.renote", "renote")
|
|
||||||
.leftJoinAndSelect("reply.user", "replyUser")
|
|
||||||
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
|
|
||||||
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
|
|
||||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
|
||||||
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
|
||||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
|
|
||||||
|
|
||||||
if (ps.local) {
|
if (ps.local) {
|
||||||
query.andWhere("note.userHost IS NULL");
|
query.andWhere("note.userHost IS NULL");
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { generateVisibilityQuery } from "../../common/generate-visibility-query.
|
||||||
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
|
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
|
||||||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||||
import {
|
import {
|
||||||
|
ScyllaNote,
|
||||||
filterBlockedUser,
|
filterBlockedUser,
|
||||||
filterMutedNote,
|
filterMutedNote,
|
||||||
filterMutedUser,
|
filterMutedUser,
|
||||||
|
@ -89,44 +90,54 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
return await Notes.packMany([]);
|
return await Notes.packMany([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find replies in BFS manner
|
const filter = async (notes: ScyllaNote[]) => {
|
||||||
const queue = [root];
|
let filtered = await filterVisibility(notes, user, followingUserIds);
|
||||||
const foundReplies: Note[] = [];
|
if (user) {
|
||||||
let depth = 0;
|
filtered = await filterMutedUser(
|
||||||
|
filtered,
|
||||||
|
user,
|
||||||
|
mutedUserIds,
|
||||||
|
mutedInstances,
|
||||||
|
);
|
||||||
|
filtered = await filterMutedNote(filtered, user, mutedWords);
|
||||||
|
filtered = await filterBlockedUser(filtered, user, blockerIds);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
};
|
||||||
|
|
||||||
while (
|
// Find quotes first
|
||||||
queue.length > 0 &&
|
const renoteResult = await scyllaClient.execute(
|
||||||
foundReplies.length < ps.limit &&
|
prepared.note.select.byRenoteId,
|
||||||
depth < ps.depth
|
[root.id],
|
||||||
) {
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
const foundNotes = await filter(
|
||||||
|
renoteResult.rows.map(parseScyllaNote).filter((note) => !!note.text),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then find replies in BFS manner
|
||||||
|
const queue = [root];
|
||||||
|
let depth = 0;
|
||||||
|
const limit = ps.limit + foundNotes.length;
|
||||||
|
|
||||||
|
while (queue.length > 0 && foundNotes.length < limit && depth < ps.depth) {
|
||||||
const note = queue.shift();
|
const note = queue.shift();
|
||||||
if (note) {
|
if (note) {
|
||||||
const result = await scyllaClient.execute(
|
const replyResult = await scyllaClient.execute(
|
||||||
prepared.note.select.byReplyId,
|
prepared.note.select.byReplyId,
|
||||||
[note.id],
|
[note.id],
|
||||||
{ prepare: true },
|
{ prepare: true },
|
||||||
);
|
);
|
||||||
if (result.rowLength > 0) {
|
const replies = await filter(replyResult.rows.map(parseScyllaNote));
|
||||||
let replies = result.rows.map(parseScyllaNote);
|
if (replies.length > 0) {
|
||||||
replies = await filterVisibility(replies, user, followingUserIds);
|
foundNotes.push(...replies);
|
||||||
if (user) {
|
|
||||||
replies = await filterMutedUser(
|
|
||||||
replies,
|
|
||||||
user,
|
|
||||||
mutedUserIds,
|
|
||||||
mutedInstances,
|
|
||||||
);
|
|
||||||
replies = await filterMutedNote(replies, user, mutedWords);
|
|
||||||
replies = await filterBlockedUser(replies, user, blockerIds);
|
|
||||||
}
|
|
||||||
foundReplies.push(...replies);
|
|
||||||
queue.push(...replies);
|
queue.push(...replies);
|
||||||
depth++;
|
depth++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Notes.packMany(foundReplies, user, {
|
return await Notes.packMany(foundNotes, user, {
|
||||||
detail: false,
|
detail: false,
|
||||||
scyllaNote: true,
|
scyllaNote: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Brackets } from "typeorm";
|
import { Brackets } from "typeorm";
|
||||||
import { Notes } from "@/models/index.js";
|
import { Notes, UserProfiles } from "@/models/index.js";
|
||||||
import define from "../../define.js";
|
import define from "../../define.js";
|
||||||
import { ApiError } from "../../error.js";
|
import { ApiError } from "../../error.js";
|
||||||
import { getUser } from "../../common/getters.js";
|
import { getUser } from "../../common/getters.js";
|
||||||
|
@ -7,6 +7,22 @@ import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||||
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
|
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
|
||||||
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
|
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
|
||||||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||||
|
import {
|
||||||
|
ScyllaNote,
|
||||||
|
execNotePaginationQuery,
|
||||||
|
filterBlockedUser,
|
||||||
|
filterMutedNote,
|
||||||
|
filterMutedUser,
|
||||||
|
filterVisibility,
|
||||||
|
scyllaClient,
|
||||||
|
} from "@/db/scylla.js";
|
||||||
|
import {
|
||||||
|
InstanceMutingsCache,
|
||||||
|
LocalFollowingsCache,
|
||||||
|
UserBlockedCache,
|
||||||
|
UserMutingsCache,
|
||||||
|
userWordMuteCache,
|
||||||
|
} from "@/misc/cache.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["users", "notes"],
|
tags: ["users", "notes"],
|
||||||
|
@ -66,6 +82,91 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (scyllaClient) {
|
||||||
|
const [
|
||||||
|
followingUserIds,
|
||||||
|
mutedUserIds,
|
||||||
|
mutedInstances,
|
||||||
|
mutedWords,
|
||||||
|
blockerIds,
|
||||||
|
] = await Promise.all([
|
||||||
|
LocalFollowingsCache.init(user.id).then((cache) => cache.getAll()),
|
||||||
|
UserMutingsCache.init(user.id).then((cache) => cache.getAll()),
|
||||||
|
InstanceMutingsCache.init(user.id).then((cache) => cache.getAll()),
|
||||||
|
userWordMuteCache
|
||||||
|
.fetchMaybe(user.id, () =>
|
||||||
|
UserProfiles.findOne({
|
||||||
|
select: ["mutedWords"],
|
||||||
|
where: { userId: user.id },
|
||||||
|
}).then((profile) => profile?.mutedWords),
|
||||||
|
)
|
||||||
|
.then((words) => words ?? []),
|
||||||
|
UserBlockedCache.init(user.id).then((cache) => cache.getAll()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
mutedUserIds.includes(user.id) ||
|
||||||
|
blockerIds.includes(user.id) ||
|
||||||
|
(user.host && mutedInstances.includes(user.host))
|
||||||
|
) {
|
||||||
|
return Notes.packMany([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = async (notes: ScyllaNote[]) => {
|
||||||
|
let filtered = notes.filter((n) => n.userId === ps.userId);
|
||||||
|
filtered = await filterVisibility(filtered, user, followingUserIds);
|
||||||
|
filtered = await filterMutedUser(
|
||||||
|
filtered,
|
||||||
|
user,
|
||||||
|
mutedUserIds,
|
||||||
|
mutedInstances,
|
||||||
|
);
|
||||||
|
filtered = await filterMutedNote(filtered, user, mutedWords);
|
||||||
|
filtered = await filterBlockedUser(filtered, user, blockerIds);
|
||||||
|
if (ps.withFiles) {
|
||||||
|
filtered = filtered.filter((n) => n.files.length > 0);
|
||||||
|
}
|
||||||
|
if (ps.fileType) {
|
||||||
|
filtered = filtered.filter((n) =>
|
||||||
|
n.files.some((f) => ps.fileType?.includes(f.type)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (ps.excludeNsfw) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(n) => !n.cw && n.files.every((f) => !f.isSensitive),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!ps.includeMyRenotes) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(n) =>
|
||||||
|
n.userId !== user.id ||
|
||||||
|
!n.renoteId ||
|
||||||
|
!!n.text ||
|
||||||
|
n.files.length > 0 ||
|
||||||
|
n.hasPoll,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!ps.includeReplies) {
|
||||||
|
filtered = filtered.filter((n) => !n.replyId);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
};
|
||||||
|
|
||||||
|
const foundPacked = [];
|
||||||
|
while (foundPacked.length < ps.limit) {
|
||||||
|
const foundNotes = (
|
||||||
|
await execNotePaginationQuery("user", ps, filter, user.id)
|
||||||
|
).slice(0, ps.limit * 1.5); // Some may filtered out by Notes.packMany, thus we take more than ps.limit.
|
||||||
|
foundPacked.push(
|
||||||
|
...(await Notes.packMany(foundNotes, user, { scyllaNote: true })),
|
||||||
|
);
|
||||||
|
if (foundNotes.length < ps.limit) break;
|
||||||
|
ps.untilDate = foundNotes[foundNotes.length - 1].createdAt.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundPacked.slice(0, ps.limit);
|
||||||
|
}
|
||||||
|
|
||||||
//#region Construct query
|
//#region Construct query
|
||||||
const query = makePaginationQuery(
|
const query = makePaginationQuery(
|
||||||
Notes.createQueryBuilder("note"),
|
Notes.createQueryBuilder("note"),
|
||||||
|
@ -73,19 +174,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
ps.untilId,
|
ps.untilId,
|
||||||
ps.sinceDate,
|
ps.sinceDate,
|
||||||
ps.untilDate,
|
ps.untilDate,
|
||||||
)
|
).andWhere("note.userId = :userId", { userId: user.id });
|
||||||
.andWhere("note.userId = :userId", { userId: user.id })
|
|
||||||
.innerJoinAndSelect("note.user", "user")
|
|
||||||
.leftJoinAndSelect("user.avatar", "avatar")
|
|
||||||
.leftJoinAndSelect("user.banner", "banner")
|
|
||||||
.leftJoinAndSelect("note.reply", "reply")
|
|
||||||
.leftJoinAndSelect("note.renote", "renote")
|
|
||||||
.leftJoinAndSelect("reply.user", "replyUser")
|
|
||||||
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
|
|
||||||
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
|
|
||||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
|
||||||
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
|
||||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
|
|
||||||
|
|
||||||
generateVisibilityQuery(query, me);
|
generateVisibilityQuery(query, me);
|
||||||
if (me) {
|
if (me) {
|
||||||
|
|
Loading…
Reference in a new issue