add channel timeline
This commit is contained in:
parent
72ee60de88
commit
a472d2027f
4 changed files with 88 additions and 15 deletions
|
@ -119,6 +119,17 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_renote_id_and_user_id AS
|
||||||
PRIMARY KEY (("renoteId", "userId"), "createdAt", "createdAtDate", "userHost", "visibility")
|
PRIMARY KEY (("renoteId", "userId"), "createdAt", "createdAtDate", "userHost", "visibility")
|
||||||
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_channel_id AS
|
||||||
|
SELECT * FROM note
|
||||||
|
WHERE "channelId" IS NOT NULL
|
||||||
|
AND "createdAt" IS NOT NULL
|
||||||
|
AND "createdAtDate" IS NOT NULL
|
||||||
|
AND "userId" IS NOT NULL
|
||||||
|
AND "userHost" IS NOT NULL
|
||||||
|
AND "visibility" IS NOT NULL
|
||||||
|
PRIMARY KEY ("channelId", "createdAt", "createdAtDate", "userId", "userHost", "visibility")
|
||||||
|
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS global_timeline AS
|
CREATE MATERIALIZED VIEW IF NOT EXISTS global_timeline AS
|
||||||
SELECT * FROM note
|
SELECT * FROM note
|
||||||
WHERE "createdAtDate" IS NOT NULL
|
WHERE "createdAtDate" IS NOT NULL
|
||||||
|
|
|
@ -50,6 +50,7 @@ export const scyllaQueries = {
|
||||||
byUserId: `SELECT * FROM note_by_user_id WHERE "userId" = ?`,
|
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" = ?`,
|
||||||
|
byChannelId: `SELECT * FROM note_by_channel_id WHERE "channelId" = ?`,
|
||||||
},
|
},
|
||||||
delete: `DELETE FROM note WHERE "createdAtDate" = ? AND "createdAt" = ? AND "userId" = ? AND "userHost" = ? AND "visibility" = ?`,
|
delete: `DELETE FROM note WHERE "createdAtDate" = ? AND "createdAt" = ? AND "userId" = ? AND "userHost" = ? AND "visibility" = ?`,
|
||||||
update: {
|
update: {
|
||||||
|
|
|
@ -200,7 +200,8 @@ export type FeedType =
|
||||||
| "recommended"
|
| "recommended"
|
||||||
| "global"
|
| "global"
|
||||||
| "renotes"
|
| "renotes"
|
||||||
| "user";
|
| "user"
|
||||||
|
| "channel";
|
||||||
|
|
||||||
export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
|
export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
|
||||||
return {
|
return {
|
||||||
|
@ -241,6 +242,9 @@ export function prepareNoteQuery(
|
||||||
case "user":
|
case "user":
|
||||||
queryParts.push(prepared.note.select.byUserId);
|
queryParts.push(prepared.note.select.byUserId);
|
||||||
break;
|
break;
|
||||||
|
case "channel":
|
||||||
|
queryParts.push(prepared.note.select.byChannelId);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
queryParts.push(prepared.note.select.byDate);
|
queryParts.push(prepared.note.select.byDate);
|
||||||
}
|
}
|
||||||
|
@ -285,6 +289,7 @@ export async function execNotePaginationQuery(
|
||||||
sinceId?: string;
|
sinceId?: string;
|
||||||
sinceDate?: number;
|
sinceDate?: number;
|
||||||
noteId?: string;
|
noteId?: string;
|
||||||
|
channelId?: string;
|
||||||
},
|
},
|
||||||
filter?: (_: ScyllaNote[]) => Promise<ScyllaNote[]>,
|
filter?: (_: ScyllaNote[]) => Promise<ScyllaNote[]>,
|
||||||
userId?: User["id"],
|
userId?: User["id"],
|
||||||
|
@ -301,6 +306,10 @@ export async function execNotePaginationQuery(
|
||||||
case "renotes":
|
case "renotes":
|
||||||
if (!ps.noteId) throw new Error("Query of renotes needs noteId");
|
if (!ps.noteId) throw new Error("Query of renotes needs noteId");
|
||||||
break;
|
break;
|
||||||
|
case "channel":
|
||||||
|
if (!ps.channelId)
|
||||||
|
throw new Error("Query of channel timeline needs channelId");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { query, untilDate, sinceDate } = prepareNoteQuery(kind, ps);
|
let { query, untilDate, sinceDate } = prepareNoteQuery(kind, ps);
|
||||||
|
@ -317,6 +326,8 @@ export async function execNotePaginationQuery(
|
||||||
params.push(userId, untilDate);
|
params.push(userId, untilDate);
|
||||||
} else if (kind === "renotes" && ps.noteId) {
|
} else if (kind === "renotes" && ps.noteId) {
|
||||||
params.push(ps.noteId, untilDate);
|
params.push(ps.noteId, untilDate);
|
||||||
|
} else if (kind === "channel" && ps.channelId) {
|
||||||
|
params.push(ps.channelId, untilDate);
|
||||||
} else {
|
} else {
|
||||||
params.push(untilDate, untilDate);
|
params.push(untilDate, untilDate);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,23 @@
|
||||||
import define from "../../define.js";
|
import define from "../../define.js";
|
||||||
import { ApiError } from "../../error.js";
|
import { ApiError } from "../../error.js";
|
||||||
import { Notes, Channels } from "@/models/index.js";
|
import { Notes, Channels, UserProfiles } from "@/models/index.js";
|
||||||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||||
import { activeUsersChart } from "@/services/chart/index.js";
|
import { activeUsersChart } from "@/services/chart/index.js";
|
||||||
|
import {
|
||||||
|
ScyllaNote,
|
||||||
|
execNotePaginationQuery,
|
||||||
|
filterBlockUser,
|
||||||
|
filterMutedNote,
|
||||||
|
filterMutedUser,
|
||||||
|
filterVisibility,
|
||||||
|
scyllaClient,
|
||||||
|
} from "@/db/scylla.js";
|
||||||
|
import {
|
||||||
|
UserBlockedCache,
|
||||||
|
UserBlockingCache,
|
||||||
|
UserMutingsCache,
|
||||||
|
userWordMuteCache,
|
||||||
|
} from "@/misc/cache.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["notes", "channels"],
|
tags: ["notes", "channels"],
|
||||||
|
@ -49,10 +64,57 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel == null) {
|
if (!channel) {
|
||||||
throw new ApiError(meta.errors.noSuchChannel);
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user) activeUsersChart.read(user);
|
||||||
|
|
||||||
|
if (scyllaClient) {
|
||||||
|
let [mutedUserIds, blockerIds, blockingIds]: string[][] = [];
|
||||||
|
let mutedWords: string[][];
|
||||||
|
if (user) {
|
||||||
|
[mutedUserIds, mutedWords, blockerIds, blockingIds] = await Promise.all([
|
||||||
|
UserMutingsCache.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()),
|
||||||
|
UserBlockingCache.init(user.id).then((cache) => cache.getAll()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = async (notes: ScyllaNote[]) => {
|
||||||
|
if (!user) return notes;
|
||||||
|
let filtered = await filterMutedUser(notes, user, mutedUserIds, []);
|
||||||
|
filtered = await filterMutedNote(filtered, user, mutedWords);
|
||||||
|
filtered = await filterBlockUser(filtered, user, [
|
||||||
|
...blockerIds,
|
||||||
|
...blockingIds,
|
||||||
|
]);
|
||||||
|
return filtered;
|
||||||
|
};
|
||||||
|
|
||||||
|
const foundPacked = [];
|
||||||
|
while (foundPacked.length < ps.limit) {
|
||||||
|
const foundNotes = (
|
||||||
|
await execNotePaginationQuery("channel", ps, filter)
|
||||||
|
).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"),
|
||||||
|
@ -63,22 +125,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
)
|
)
|
||||||
.andWhere("note.channelId = :channelId", { channelId: channel.id })
|
.andWhere("note.channelId = :channelId", { channelId: channel.id })
|
||||||
.innerJoinAndSelect("note.user", "user")
|
.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")
|
|
||||||
.leftJoinAndSelect("note.channel", "channel");
|
.leftJoinAndSelect("note.channel", "channel");
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const timeline = await query.take(ps.limit).getMany();
|
const timeline = await query.take(ps.limit).getMany();
|
||||||
|
|
||||||
if (user) activeUsersChart.read(user);
|
|
||||||
|
|
||||||
return await Notes.packMany(timeline, user);
|
return await Notes.packMany(timeline, user);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue