store notifications to scylla
This commit is contained in:
parent
6b156d792a
commit
d315c80fd1
28 changed files with 530 additions and 213 deletions
|
@ -76,8 +76,12 @@ db:
|
|||
# # 15 days after signing up. In this case, the user's first post does not
|
||||
# # appear in their home timeline.
|
||||
# #
|
||||
# # (default: 14)
|
||||
# # (default 14)
|
||||
# sparseTimelineDays: 14
|
||||
#
|
||||
# # <queryLimit> determines the number of maximum rows read within one pagination.
|
||||
# # (default 1000, max 5000, min 1)
|
||||
# queryLimit: 1000
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
DROP TABLE IF EXISTS poll_vote;
|
||||
DROP MATERIALIZED VIEW IF EXISTS reaction_by_id;
|
||||
DROP MATERIALIZED VIEW IF EXISTS reaction_by_user_id;
|
||||
DROP INDEX IF EXISTS reaction_by_id;
|
||||
DROP TABLE IF EXISTS reaction;
|
||||
DROP INDEX IF EXISTS home_by_id;
|
||||
DROP TABLE IF EXISTS home_timeline;
|
||||
DROP MATERIALIZED VIEW IF EXISTS local_timeline;
|
||||
DROP MATERIALIZED VIEW IF EXISTS global_timeline;
|
||||
DROP MATERIALIZED VIEW IF EXISTS note_by_channel_id;
|
||||
DROP MATERIALIZED VIEW IF EXISTS note_by_renote_id_and_user_id;
|
||||
DROP MATERIALIZED VIEW IF EXISTS note_by_renote_id;
|
||||
DROP MATERIALIZED VIEW IF EXISTS note_by_user_id;
|
||||
DROP MATERIALIZED VIEW IF EXISTS note_by_id;
|
||||
DROP INDEX IF EXISTS note_by_reply_id;
|
||||
DROP INDEX IF EXISTS note_by_uri;
|
||||
DROP INDEX IF EXISTS note_by_url;
|
||||
DROP TABLE IF EXISTS note;
|
||||
DROP TYPE IF EXISTS poll;
|
||||
DROP TYPE IF EXISTS emoji;
|
||||
DROP TYPE IF EXISTS note_edit_history;
|
||||
DROP TYPE IF EXISTS drive_file;
|
||||
DROP TABLE poll_vote;
|
||||
DROP MATERIALIZED VIEW reaction_by_id;
|
||||
DROP MATERIALIZED VIEW reaction_by_user_id;
|
||||
DROP INDEX reaction_by_id;
|
||||
DROP TABLE reaction;
|
||||
DROP INDEX home_by_id;
|
||||
DROP TABLE home_timeline;
|
||||
DROP MATERIALIZED VIEW local_timeline;
|
||||
DROP MATERIALIZED VIEW global_timeline;
|
||||
DROP MATERIALIZED VIEW note_by_channel_id;
|
||||
DROP MATERIALIZED VIEW note_by_renote_id_and_user_id;
|
||||
DROP MATERIALIZED VIEW note_by_renote_id;
|
||||
DROP MATERIALIZED VIEW note_by_user_id;
|
||||
DROP MATERIALIZED VIEW note_by_id;
|
||||
DROP INDEX note_by_reply_id;
|
||||
DROP INDEX note_by_uri;
|
||||
DROP INDEX note_by_url;
|
||||
DROP TABLE note;
|
||||
DROP TYPE poll;
|
||||
DROP TYPE emoji;
|
||||
DROP TYPE note_edit_history;
|
||||
DROP TYPE drive_file;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
CREATE TYPE IF NOT EXISTS drive_file (
|
||||
CREATE TYPE drive_file (
|
||||
"id" ascii,
|
||||
"type" ascii,
|
||||
"createdAt" timestamp,
|
||||
|
@ -15,27 +15,27 @@ CREATE TYPE IF NOT EXISTS drive_file (
|
|||
"height" int,
|
||||
);
|
||||
|
||||
CREATE TYPE IF NOT EXISTS note_edit_history (
|
||||
CREATE TYPE note_edit_history (
|
||||
"content" text,
|
||||
"cw" text,
|
||||
"files" set<frozen<drive_file>>,
|
||||
"updatedAt" timestamp,
|
||||
);
|
||||
|
||||
CREATE TYPE IF NOT EXISTS emoji (
|
||||
CREATE TYPE emoji (
|
||||
"name" text,
|
||||
"url" text,
|
||||
"width" int,
|
||||
"height" int,
|
||||
);
|
||||
|
||||
CREATE TYPE IF NOT EXISTS poll (
|
||||
CREATE TYPE poll (
|
||||
"expiresAt" timestamp,
|
||||
"multiple" boolean,
|
||||
"choices" map<int, text>,
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS note ( -- Store all posts
|
||||
CREATE TABLE note ( -- Store all posts
|
||||
"createdAtDate" date, -- For partitioning
|
||||
"createdAt" timestamp,
|
||||
"id" ascii, -- Post
|
||||
|
@ -79,11 +79,11 @@ CREATE TABLE IF NOT EXISTS note ( -- Store all posts
|
|||
PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility")
|
||||
) WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS note_by_uri ON note ("uri");
|
||||
CREATE INDEX IF NOT EXISTS note_by_url ON note ("url");
|
||||
CREATE INDEX IF NOT EXISTS note_by_reply_id ON note ("replyId");
|
||||
CREATE INDEX note_by_uri ON note ("uri");
|
||||
CREATE INDEX note_by_url ON note ("url");
|
||||
CREATE INDEX note_by_reply_id ON note ("replyId");
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_id AS
|
||||
CREATE MATERIALIZED VIEW note_by_id AS
|
||||
SELECT * FROM note
|
||||
WHERE "id" IS NOT NULL
|
||||
AND "createdAt" IS NOT NULL
|
||||
|
@ -94,7 +94,7 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_id AS
|
|||
PRIMARY KEY ("id", "createdAt", "createdAtDate", "userId", "userHost", "visibility")
|
||||
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_user_id AS
|
||||
CREATE MATERIALIZED VIEW note_by_user_id AS
|
||||
SELECT * FROM note
|
||||
WHERE "userId" IS NOT NULL
|
||||
AND "createdAt" IS NOT NULL
|
||||
|
@ -104,7 +104,7 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_user_id AS
|
|||
PRIMARY KEY ("userId", "createdAt", "createdAtDate", "userHost", "visibility")
|
||||
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_renote_id AS
|
||||
CREATE MATERIALIZED VIEW note_by_renote_id AS
|
||||
SELECT * FROM note
|
||||
WHERE "renoteId" IS NOT NULL
|
||||
AND "createdAt" IS NOT NULL
|
||||
|
@ -115,7 +115,7 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_renote_id AS
|
|||
PRIMARY KEY ("renoteId", "createdAt", "createdAtDate", "userId", "userHost", "visibility")
|
||||
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_renote_id_and_user_id AS
|
||||
CREATE MATERIALIZED VIEW note_by_renote_id_and_user_id AS
|
||||
SELECT "renoteId", "userId", "createdAt", "createdAtDate", "userHost", "visibility", "id" FROM note
|
||||
WHERE "renoteId" IS NOT NULL
|
||||
AND "createdAt" IS NOT NULL
|
||||
|
@ -126,7 +126,7 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_renote_id_and_user_id AS
|
|||
PRIMARY KEY (("renoteId", "userId"), "createdAt", "createdAtDate", "userHost", "visibility")
|
||||
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_channel_id AS
|
||||
CREATE MATERIALIZED VIEW note_by_channel_id AS
|
||||
SELECT * FROM note
|
||||
WHERE "channelId" IS NOT NULL
|
||||
AND "createdAt" IS NOT NULL
|
||||
|
@ -137,7 +137,7 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_channel_id AS
|
|||
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 global_timeline AS
|
||||
SELECT * FROM note
|
||||
WHERE "createdAtDate" IS NOT NULL
|
||||
AND "createdAt" IS NOT NULL
|
||||
|
@ -147,7 +147,7 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS global_timeline AS
|
|||
PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility")
|
||||
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS local_timeline AS
|
||||
CREATE MATERIALIZED VIEW local_timeline AS
|
||||
SELECT * FROM note
|
||||
WHERE "createdAtDate" IS NOT NULL
|
||||
AND "createdAt" IS NOT NULL
|
||||
|
@ -157,7 +157,7 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS local_timeline AS
|
|||
PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility")
|
||||
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS home_timeline (
|
||||
CREATE TABLE home_timeline (
|
||||
"feedUserId" ascii, -- For partitioning
|
||||
"createdAtDate" date, -- For partitioning
|
||||
"createdAt" timestamp,
|
||||
|
@ -202,9 +202,9 @@ CREATE TABLE IF NOT EXISTS home_timeline (
|
|||
PRIMARY KEY (("feedUserId", "createdAtDate"), "createdAt", "userId")
|
||||
) WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS home_by_id ON home_timeline ("id");
|
||||
CREATE INDEX home_by_id ON home_timeline ("id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reaction (
|
||||
CREATE TABLE reaction (
|
||||
"id" text,
|
||||
"noteId" ascii,
|
||||
"userId" ascii,
|
||||
|
@ -214,7 +214,7 @@ CREATE TABLE IF NOT EXISTS reaction (
|
|||
PRIMARY KEY ("noteId", "userId") -- this key constraints one reaction per user for the same post
|
||||
);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS reaction_by_user_id AS
|
||||
CREATE MATERIALIZED VIEW reaction_by_user_id AS
|
||||
SELECT * FROM reaction
|
||||
WHERE "userId" IS NOT NULL
|
||||
AND "createdAt" IS NOT NULL
|
||||
|
@ -222,14 +222,14 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS reaction_by_user_id AS
|
|||
PRIMARY KEY ("userId", "createdAt", "noteId")
|
||||
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS reaction_by_id AS
|
||||
CREATE MATERIALIZED VIEW reaction_by_id AS
|
||||
SELECT * FROM reaction
|
||||
WHERE "noteId" IS NOT NULL
|
||||
AND "reaction" IS NOT NULL
|
||||
AND "userId" IS NOT NULL
|
||||
PRIMARY KEY ("noteId", "reaction", "userId");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS poll_vote (
|
||||
CREATE TABLE poll_vote (
|
||||
"noteId" ascii,
|
||||
"userId" ascii,
|
||||
"choice" set<int>,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
DROP INDEX notification_by_id;
|
||||
DROP TABLE notification;
|
|
@ -0,0 +1,18 @@
|
|||
CREATE TABLE notification (
|
||||
"targetId" ascii,
|
||||
"createdAtDate" date,
|
||||
"createdAt" timestamp,
|
||||
"id" ascii,
|
||||
"notifierId" ascii,
|
||||
"notifierHost" text,
|
||||
"type" ascii,
|
||||
"entityId" ascii,
|
||||
"reaction" text,
|
||||
"choice" int,
|
||||
"customBody" text,
|
||||
"customHeader" text,
|
||||
"customIcon" text,
|
||||
PRIMARY KEY (("targetId", "createdAtDate"), "createdAt")
|
||||
) WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||
|
||||
CREATE INDEX notification_by_id ON notification ("id");
|
|
@ -38,28 +38,32 @@ impl Initializer {
|
|||
}
|
||||
|
||||
pub(crate) async fn setup(&self) -> Result<(), Error> {
|
||||
let pairs = vec![
|
||||
let mut conn = PgConnection::connect(&self.postgres_url).await?;
|
||||
|
||||
let fk_pairs = vec![
|
||||
("channel_note_pining", "FK_10b19ef67d297ea9de325cd4502"),
|
||||
("clip_note", "FK_a012eaf5c87c65da1deb5fdbfa3"),
|
||||
("muted_note", "FK_70ab9786313d78e4201d81cdb89"),
|
||||
("note_edit", "FK_702ad5ae993a672e4fbffbcd38c"),
|
||||
("note_favorite", "FK_0e00498f180193423c992bc4370"),
|
||||
("note_unread", "FK_e637cba4dc4410218c4251260e4"),
|
||||
("note_watching", "FK_03e7028ab8388a3f5e3ce2a8619"),
|
||||
("promo_note", "FK_e263909ca4fe5d57f8d4230dd5c"),
|
||||
("promo_read", "FK_a46a1a603ecee695d7db26da5f4"),
|
||||
("user_note_pining", "FK_68881008f7c3588ad7ecae471cf"),
|
||||
("notification", "FK_769cb6b73a1efe22ddf733ac453"),
|
||||
];
|
||||
|
||||
let mut conn = PgConnection::connect(&self.postgres_url).await?;
|
||||
|
||||
for (table, fk) in pairs {
|
||||
for (table, fk) in fk_pairs {
|
||||
sqlx::query(&format!("ALTER TABLE {} DROP CONSTRAINT \"{}\"", table, fk))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let tables = vec!["note", "note_edit", "poll", "poll_vote", "notification"];
|
||||
for table in tables {
|
||||
sqlx::query(&format!("DROP TABLE {}", table))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export type Source = {
|
|||
replicationFactor: number;
|
||||
localDataCentre: string;
|
||||
sparseTimelineDays?: number;
|
||||
queryLimit?: number;
|
||||
};
|
||||
redis: {
|
||||
host: string;
|
||||
|
|
|
@ -159,4 +159,13 @@ export const scyllaQueries = {
|
|||
select: `SELECT * FROM poll_vote WHERE "noteId" = ?`,
|
||||
insert: `INSERT INTO poll_vote ("noteId", "userId", "choice", "createdAt") VALUES (?, ?, ?, ?)`,
|
||||
},
|
||||
notification: {
|
||||
insert: `INSERT INTO notification
|
||||
("targetId", "createdAtDate", "createdAt", "id", "notifierId", "notifierHost", "type", "entityId", "reaction", "choice", "customBody", "customHeader", "customIcon")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
select: {
|
||||
byTargetId: `SELECT * FROM notification WHERE "targetId" = ? AND "createdAtDate" = ?`,
|
||||
byId: `SELECT * FROM notification WHERE "id" = ?`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -40,7 +40,7 @@ import { Signin } from "@/models/entities/signin.js";
|
|||
import { AuthSession } from "@/models/entities/auth-session.js";
|
||||
import { FollowRequest } from "@/models/entities/follow-request.js";
|
||||
import { Emoji } from "@/models/entities/emoji.js";
|
||||
import { UserNotePining, UserNotePiningScylla } from "@/models/entities/user-note-pining.js";
|
||||
import { UserNotePining } from "@/models/entities/user-note-pining.js";
|
||||
import { Poll } from "@/models/entities/poll.js";
|
||||
import { UserKeypair } from "@/models/entities/user-keypair.js";
|
||||
import { UserPublickey } from "@/models/entities/user-publickey.js";
|
||||
|
@ -130,7 +130,7 @@ export const entities = [
|
|||
UserGroup,
|
||||
UserGroupJoining,
|
||||
UserGroupInvitation,
|
||||
config.scylla ? UserNotePiningScylla : UserNotePining,
|
||||
UserNotePining,
|
||||
UserSecurityKey,
|
||||
UsedUsername,
|
||||
AttestationChallenge,
|
||||
|
|
|
@ -21,6 +21,7 @@ import { UserProfiles } from "@/models/index.js";
|
|||
import { getWordHardMute } from "@/misc/check-word-mute.js";
|
||||
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||
import { scyllaQueries } from "@/db/cql.js";
|
||||
import { notificationTypes } from "@/types.js";
|
||||
|
||||
function newClient(): Client | null {
|
||||
if (!config.scylla) {
|
||||
|
@ -66,6 +67,40 @@ export const scyllaClient = newClient();
|
|||
|
||||
export const prepared = scyllaQueries;
|
||||
|
||||
export interface ScyllaNotification {
|
||||
targetId: string;
|
||||
createdAtDate: Date;
|
||||
createdAt: Date;
|
||||
id: string;
|
||||
notifierId: string | null;
|
||||
notifierHost: string | null;
|
||||
type: typeof notificationTypes[number];
|
||||
entityId: string | null;
|
||||
reaction: string | null;
|
||||
choice: number | null;
|
||||
customBody: string | null;
|
||||
customHeader: string | null;
|
||||
customIcon: string | null;
|
||||
}
|
||||
|
||||
export function parseScyllaNotification(row: types.Row): ScyllaNotification {
|
||||
return {
|
||||
targetId: row.get("targetId"),
|
||||
createdAtDate: row.get("createdAt"),
|
||||
createdAt: row.get("createdAt"),
|
||||
id: row.get("id"),
|
||||
type: row.get("type"),
|
||||
notifierId: row.get("notifierId") ?? null,
|
||||
notifierHost: row.get("notifierHost") ?? null,
|
||||
entityId: row.get("entityId") ?? null,
|
||||
reaction: row.get("reaction") ?? null,
|
||||
choice: row.get("choice") ?? null,
|
||||
customBody: row.get("customBody") ?? null,
|
||||
customHeader: row.get("customHeader") ?? null,
|
||||
customIcon: row.get("customIcon") ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScyllaDriveFile {
|
||||
id: string;
|
||||
type: string;
|
||||
|
@ -114,14 +149,14 @@ export interface ScyllaNoteEditHistory {
|
|||
export interface ScyllaPoll {
|
||||
expiresAt: Date | null;
|
||||
multiple: boolean;
|
||||
choices: Record<number, string>,
|
||||
choices: Record<number, string>;
|
||||
}
|
||||
|
||||
export interface ScyllaPollVote {
|
||||
noteId: string,
|
||||
userId: string,
|
||||
choice: Set<number>,
|
||||
createdAt: Date,
|
||||
noteId: string;
|
||||
userId: string;
|
||||
choice: Set<number>;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export function parseScyllaPollVote(row: types.Row): ScyllaPollVote {
|
||||
|
@ -130,7 +165,7 @@ export function parseScyllaPollVote(row: types.Row): ScyllaPollVote {
|
|||
userId: row.get("userId"),
|
||||
choice: new Set(row.get("choice") ?? []),
|
||||
createdAt: row.get("createdAt"),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type ScyllaNote = Note & {
|
||||
|
@ -151,7 +186,7 @@ export function parseScyllaNote(row: types.Row): ScyllaNote {
|
|||
const userHost = row.get("userHost");
|
||||
|
||||
return {
|
||||
createdAtDate: row.get("createdAtDate"),
|
||||
createdAtDate: row.get("createdAt"),
|
||||
createdAt: row.get("createdAt"),
|
||||
id: row.get("id"),
|
||||
visibility: row.get("visibility"),
|
||||
|
@ -216,8 +251,6 @@ export interface ScyllaNoteReaction extends NoteReaction {
|
|||
emoji: PopulatedEmoji;
|
||||
}
|
||||
|
||||
const QUERY_LIMIT = 1000; // TODO: should this be configurable?
|
||||
|
||||
export type FeedType =
|
||||
| "home"
|
||||
| "local"
|
||||
|
@ -225,7 +258,8 @@ export type FeedType =
|
|||
| "global"
|
||||
| "renotes"
|
||||
| "user"
|
||||
| "channel";
|
||||
| "channel"
|
||||
| "notification";
|
||||
|
||||
export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
|
||||
return {
|
||||
|
@ -238,7 +272,7 @@ export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction {
|
|||
};
|
||||
}
|
||||
|
||||
export function prepareNoteQuery(
|
||||
export function preparePaginationQuery(
|
||||
kind: FeedType,
|
||||
ps: {
|
||||
untilId?: string;
|
||||
|
@ -269,6 +303,9 @@ export function prepareNoteQuery(
|
|||
case "channel":
|
||||
queryParts.push(prepared.note.select.byChannelId);
|
||||
break;
|
||||
case "notification":
|
||||
queryParts.push(prepared.notification.select.byTargetId);
|
||||
break;
|
||||
default:
|
||||
queryParts.push(prepared.note.select.byDate);
|
||||
}
|
||||
|
@ -293,7 +330,8 @@ export function prepareNoteQuery(
|
|||
queryParts.push(`AND "createdAt" > ?`);
|
||||
}
|
||||
|
||||
queryParts.push(`LIMIT ${QUERY_LIMIT}`);
|
||||
const queryLimit = config.scylla?.queryLimit ?? 1000;
|
||||
queryParts.push(`LIMIT ${queryLimit}`);
|
||||
|
||||
const query = queryParts.join(" ");
|
||||
|
||||
|
@ -304,7 +342,7 @@ export function prepareNoteQuery(
|
|||
};
|
||||
}
|
||||
|
||||
export async function execNotePaginationQuery(
|
||||
export async function execPaginationQuery(
|
||||
kind: FeedType,
|
||||
ps: {
|
||||
limit: number;
|
||||
|
@ -315,34 +353,37 @@ export async function execNotePaginationQuery(
|
|||
noteId?: string;
|
||||
channelId?: string;
|
||||
},
|
||||
filter?: (_: ScyllaNote[]) => Promise<ScyllaNote[]>,
|
||||
filter?: {
|
||||
note?: (_: ScyllaNote[]) => Promise<ScyllaNote[]>;
|
||||
notification?: (_: ScyllaNotification[]) => ScyllaNotification[];
|
||||
},
|
||||
userId?: User["id"],
|
||||
maxPartitions = config.scylla?.sparseTimelineDays ?? 14,
|
||||
): Promise<ScyllaNote[]> {
|
||||
): Promise<ScyllaNote[] | ScyllaNotification[]> {
|
||||
if (!scyllaClient) return [];
|
||||
|
||||
switch (kind) {
|
||||
case "home":
|
||||
case "user":
|
||||
if (!userId)
|
||||
throw new Error("Query of home and user timelines needs userId");
|
||||
case "notification":
|
||||
if (!userId) throw new Error(`Feed ${kind} needs userId`);
|
||||
break;
|
||||
case "renotes":
|
||||
if (!ps.noteId) throw new Error("Query of renotes needs noteId");
|
||||
if (!ps.noteId) throw new Error(`Feed ${kind} needs noteId`);
|
||||
break;
|
||||
case "channel":
|
||||
if (!ps.channelId)
|
||||
throw new Error("Query of channel timeline needs channelId");
|
||||
if (!ps.channelId) throw new Error(`Feed ${kind} needs channelId`);
|
||||
break;
|
||||
}
|
||||
|
||||
let { query, untilDate, sinceDate } = prepareNoteQuery(kind, ps);
|
||||
let { query, untilDate, sinceDate } = preparePaginationQuery(kind, ps);
|
||||
|
||||
let scannedPartitions = 0;
|
||||
const foundNotes: ScyllaNote[] = [];
|
||||
const found: (ScyllaNote | ScyllaNotification)[] = [];
|
||||
const queryLimit = config.scylla?.queryLimit ?? 1000;
|
||||
|
||||
// Try to get posts of at most <maxPartitions> in the single request
|
||||
while (foundNotes.length < ps.limit && scannedPartitions < maxPartitions) {
|
||||
while (found.length < ps.limit && scannedPartitions < maxPartitions) {
|
||||
const params: (Date | string | string[] | number)[] = [];
|
||||
if (kind === "home" && userId) {
|
||||
params.push(userId, untilDate, untilDate);
|
||||
|
@ -352,6 +393,8 @@ export async function execNotePaginationQuery(
|
|||
params.push(ps.noteId, untilDate);
|
||||
} else if (kind === "channel" && ps.channelId) {
|
||||
params.push(ps.channelId, untilDate);
|
||||
} else if (kind === "notification" && userId) {
|
||||
params.push(userId, untilDate, untilDate);
|
||||
} else {
|
||||
params.push(untilDate, untilDate);
|
||||
}
|
||||
|
@ -365,12 +408,22 @@ export async function execNotePaginationQuery(
|
|||
});
|
||||
|
||||
if (result.rowLength > 0) {
|
||||
const notes = result.rows.map(parseScyllaNote);
|
||||
foundNotes.push(...(filter ? await filter(notes) : notes));
|
||||
untilDate = notes[notes.length - 1].createdAt;
|
||||
if (kind === "notification") {
|
||||
const notifications = result.rows.map(parseScyllaNotification);
|
||||
found.push(
|
||||
...(filter?.notification
|
||||
? filter.notification(notifications)
|
||||
: notifications),
|
||||
);
|
||||
untilDate = notifications[notifications.length - 1].createdAt;
|
||||
} else {
|
||||
const notes = result.rows.map(parseScyllaNote);
|
||||
found.push(...(filter?.note ? await filter.note(notes) : notes));
|
||||
untilDate = notes[notes.length - 1].createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.rowLength < QUERY_LIMIT) {
|
||||
if (result.rowLength < queryLimit) {
|
||||
// Reached the end of partition. Queries posts created one day before.
|
||||
scannedPartitions++;
|
||||
const yesterday = new Date(untilDate.getTime() - 86400000);
|
||||
|
@ -380,7 +433,11 @@ export async function execNotePaginationQuery(
|
|||
}
|
||||
}
|
||||
|
||||
return foundNotes;
|
||||
if (kind === "notification") {
|
||||
return found as ScyllaNotification[];
|
||||
}
|
||||
|
||||
return found as ScyllaNote[];
|
||||
}
|
||||
|
||||
export async function filterVisibility(
|
||||
|
|
|
@ -65,8 +65,8 @@ export class Notification {
|
|||
* reply - A post that a user made (or was watching) has been replied to.
|
||||
* renote - A post that a user made (or was watching) has been renoted.
|
||||
* quote - A post that a user made (or was watching) has been quoted and renoted.
|
||||
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
|
||||
* pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された
|
||||
* reaction - Someone reacted my post or one I'm wathing
|
||||
* pollVote - Someone voted to my poll or one I'm wathing
|
||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||
* receiveFollowRequest - フォローリクエストされた
|
||||
* followRequestAccepted - A follow request has been accepted.
|
||||
|
|
|
@ -10,8 +10,9 @@ import { Note } from "./note.js";
|
|||
import { User } from "./user.js";
|
||||
import { id } from "../id.js";
|
||||
|
||||
@Entity()
|
||||
@Index(["userId", "noteId"], { unique: true })
|
||||
class UserNotePiningBase {
|
||||
export class UserNotePining {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
|
@ -32,16 +33,10 @@ class UserNotePiningBase {
|
|||
|
||||
@Column(id())
|
||||
public noteId: Note["id"];
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class UserNotePining extends UserNotePiningBase {
|
||||
@ManyToOne((type) => Note, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn()
|
||||
public note: Note | null;
|
||||
}
|
||||
|
||||
@Entity({ name: "user_note_pining" })
|
||||
export class UserNotePiningScylla extends UserNotePiningBase {}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { NoteRepository } from "./repositories/note.js";
|
|||
import { DriveFileRepository } from "./repositories/drive-file.js";
|
||||
import { DriveFolderRepository } from "./repositories/drive-folder.js";
|
||||
import { AccessToken } from "./entities/access-token.js";
|
||||
import { UserNotePining, UserNotePiningScylla } from "./entities/user-note-pining.js";
|
||||
import { UserNotePining } from "./entities/user-note-pining.js";
|
||||
import { SigninRepository } from "./repositories/signin.js";
|
||||
import { MessagingMessageRepository } from "./repositories/messaging-message.js";
|
||||
import { UserListRepository } from "./repositories/user-list.js";
|
||||
|
@ -93,7 +93,7 @@ export const UserListJoinings = db.getRepository(UserListJoining);
|
|||
export const UserGroups = UserGroupRepository;
|
||||
export const UserGroupJoinings = db.getRepository(UserGroupJoining);
|
||||
export const UserGroupInvitations = UserGroupInvitationRepository;
|
||||
export const UserNotePinings = db.getRepository(config.scylla ? UserNotePiningScylla : UserNotePining);
|
||||
export const UserNotePinings = db.getRepository(UserNotePining);
|
||||
export const UserIps = db.getRepository(UserIp);
|
||||
export const UsedUsernames = db.getRepository(UsedUsername);
|
||||
export const Followings = FollowingRepository;
|
||||
|
|
|
@ -231,7 +231,6 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
me?: { id: User["id"] } | null | undefined,
|
||||
options?: {
|
||||
detail?: boolean;
|
||||
scyllaNote?: boolean;
|
||||
_hint_?: {
|
||||
myReactions: Map<Note["id"], NoteReaction | null>;
|
||||
};
|
||||
|
@ -246,24 +245,21 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
|
||||
const meId = me ? me.id : null;
|
||||
let note: Note | null = null;
|
||||
const isSrcNote = typeof src === "object";
|
||||
|
||||
// Always lookup from ScyllaDB if enabled
|
||||
if (isSrcNote && (!scyllaClient || options?.scyllaNote)) {
|
||||
if (typeof src === "object") {
|
||||
note = src;
|
||||
} else {
|
||||
const noteId = isSrcNote ? src.id : src;
|
||||
if (scyllaClient) {
|
||||
const result = await scyllaClient.execute(
|
||||
prepared.note.select.byId,
|
||||
[noteId],
|
||||
[src],
|
||||
{ prepare: true },
|
||||
);
|
||||
if (result.rowLength > 0) {
|
||||
note = parseScyllaNote(result.first());
|
||||
}
|
||||
} else {
|
||||
note = await this.findOneBy({ id: noteId });
|
||||
note = await this.findOneBy({ id: src });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,7 +401,6 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
me?: { id: User["id"] } | null | undefined,
|
||||
options?: {
|
||||
detail?: boolean;
|
||||
scyllaNote?: boolean;
|
||||
},
|
||||
) {
|
||||
if (notes.length === 0) return [];
|
||||
|
|
|
@ -15,18 +15,99 @@ import {
|
|||
AccessTokens,
|
||||
NoteReactions,
|
||||
} from "../index.js";
|
||||
import {
|
||||
parseScyllaNote,
|
||||
parseScyllaNotification,
|
||||
parseScyllaReaction,
|
||||
prepared,
|
||||
scyllaClient,
|
||||
type ScyllaNotification,
|
||||
} from "@/db/scylla.js";
|
||||
|
||||
export const NotificationRepository = db.getRepository(Notification).extend({
|
||||
async pack(
|
||||
src: Notification["id"] | Notification,
|
||||
src: Notification["id"] | Notification | ScyllaNotification,
|
||||
options: {
|
||||
_hintForEachNotes_?: {
|
||||
myReactions: Map<Note["id"], NoteReaction | null>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<"Notification">> {
|
||||
if (scyllaClient) {
|
||||
let notification: ScyllaNotification;
|
||||
if (typeof src === "object") {
|
||||
notification = src as ScyllaNotification;
|
||||
} else {
|
||||
const result = await scyllaClient.execute(
|
||||
prepared.notification.select.byId,
|
||||
[src],
|
||||
{ prepare: true },
|
||||
);
|
||||
if (result.rowLength === 0) {
|
||||
throw new Error("notification not found");
|
||||
}
|
||||
notification = parseScyllaNotification(result.first());
|
||||
}
|
||||
const token =
|
||||
notification.type === "app" && notification.entityId
|
||||
? await AccessTokens.findOneByOrFail({
|
||||
id: notification.entityId,
|
||||
})
|
||||
: null;
|
||||
|
||||
let data = null;
|
||||
|
||||
if (notification.entityId) {
|
||||
switch (notification.type) {
|
||||
case "mention":
|
||||
case "reply":
|
||||
case "renote":
|
||||
case "quote":
|
||||
case "reaction":
|
||||
case "pollVote":
|
||||
case "pollEnded":
|
||||
data = {
|
||||
note: Notes.pack(
|
||||
notification.entityId,
|
||||
{ id: notification.targetId },
|
||||
{ detail: true, _hint_: options._hintForEachNotes_ },
|
||||
),
|
||||
reaction: notification.reaction,
|
||||
choice: notification.choice,
|
||||
};
|
||||
break;
|
||||
case "groupInvited":
|
||||
data = {
|
||||
invitation: UserGroupInvitations.pack(notification.entityId),
|
||||
};
|
||||
break;
|
||||
case "app":
|
||||
data = {
|
||||
body: notification.customBody,
|
||||
header: notification.customHeader || token?.name,
|
||||
icon: notification.customIcon || token?.iconUrl,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return await awaitAll({
|
||||
id: notification.id,
|
||||
createdAt: notification.createdAt.toISOString(),
|
||||
type: notification.type,
|
||||
isRead: true, // FIXME: Implement read checker on DragonflyDB
|
||||
userId: notification.notifierId,
|
||||
user: notification.notifierId
|
||||
? Users.pack(notification.notifierId)
|
||||
: null,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
const notification =
|
||||
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
|
||||
typeof src === "object"
|
||||
? (src as Notification)
|
||||
: await this.findOneByOrFail({ id: src });
|
||||
const token = notification.appAccessTokenId
|
||||
? await AccessTokens.findOneByOrFail({
|
||||
id: notification.appAccessTokenId,
|
||||
|
@ -145,22 +226,52 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
|||
});
|
||||
},
|
||||
|
||||
async packMany(notifications: Notification[], meId: User["id"]) {
|
||||
async packMany(
|
||||
notifications: Notification[] | ScyllaNotification[],
|
||||
meId: User["id"],
|
||||
) {
|
||||
if (notifications.length === 0) return [];
|
||||
|
||||
const notes = notifications
|
||||
.filter((x) => x.note != null)
|
||||
.map((x) => x.note!);
|
||||
const noteIds = notes.map((n) => n.id);
|
||||
let notes: Note[] = [];
|
||||
let noteIds: Note["id"][] = [];
|
||||
let renoteIds: Note["id"][] = [];
|
||||
const myReactionsMap = new Map<Note["id"], NoteReaction | null>();
|
||||
const renoteIds = notes
|
||||
.filter((n) => n.renoteId != null)
|
||||
.map((n) => n.renoteId!);
|
||||
|
||||
if (scyllaClient) {
|
||||
noteIds = (notifications as ScyllaNotification[])
|
||||
.filter((n) => !["groupInvited", "app"].includes(n.type) && n.entityId)
|
||||
.map(({ entityId }) => entityId as string);
|
||||
notes = await scyllaClient
|
||||
.execute(prepared.note.select.byIds, [noteIds], { prepare: true })
|
||||
.then((result) => result.rows.map(parseScyllaNote));
|
||||
renoteIds = notes
|
||||
.filter((note) => !!note.renoteId)
|
||||
.map(({ renoteId }) => renoteId as string);
|
||||
} else {
|
||||
const notes = (notifications as Notification[])
|
||||
.filter((x) => !!x.note)
|
||||
.map((x) => x.note as Note);
|
||||
noteIds = notes.map((n) => n.id);
|
||||
renoteIds = notes
|
||||
.filter((n) => !!n.renoteId)
|
||||
.map((n) => n.renoteId as string);
|
||||
}
|
||||
|
||||
const targets = [...noteIds, ...renoteIds];
|
||||
const myReactions = await NoteReactions.findBy({
|
||||
userId: meId,
|
||||
noteId: In(targets),
|
||||
});
|
||||
let myReactions: NoteReaction[] = [];
|
||||
if (scyllaClient) {
|
||||
const result = await scyllaClient.execute(
|
||||
prepared.reaction.select.byNoteAndUser,
|
||||
[targets, [meId]],
|
||||
{ prepare: true },
|
||||
);
|
||||
myReactions = result.rows.map(parseScyllaReaction);
|
||||
} else {
|
||||
myReactions = await NoteReactions.findBy({
|
||||
userId: meId,
|
||||
noteId: In(targets),
|
||||
});
|
||||
}
|
||||
|
||||
for (const target of targets) {
|
||||
myReactionsMap.set(
|
||||
|
@ -177,7 +288,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
|||
_hintForEachNotes_: {
|
||||
myReactions: myReactionsMap,
|
||||
},
|
||||
}).catch((e) => null),
|
||||
}).catch((_) => null),
|
||||
),
|
||||
);
|
||||
return results.filter((x) => x != null);
|
||||
|
|
|
@ -528,7 +528,6 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
pinnedNoteIds,
|
||||
pinnedNotes: Notes.packMany(pinnedNotes, me, {
|
||||
detail: true,
|
||||
scyllaNote: !!scyllaClient,
|
||||
}),
|
||||
pinnedPageId: profile!.pinnedPageId,
|
||||
pinnedPage: profile!.pinnedPageId
|
||||
|
|
|
@ -4,12 +4,11 @@ import { Notes, Channels, UserProfiles } from "@/models/index.js";
|
|||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||
import { activeUsersChart } from "@/services/chart/index.js";
|
||||
import {
|
||||
ScyllaNote,
|
||||
execNotePaginationQuery,
|
||||
type ScyllaNote,
|
||||
execPaginationQuery,
|
||||
filterBlockUser,
|
||||
filterMutedNote,
|
||||
filterMutedUser,
|
||||
filterVisibility,
|
||||
scyllaClient,
|
||||
} from "@/db/scylla.js";
|
||||
import {
|
||||
|
@ -103,11 +102,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const foundPacked = [];
|
||||
while (foundPacked.length < ps.limit) {
|
||||
const foundNotes = (
|
||||
await execNotePaginationQuery("channel", ps, filter)
|
||||
(await execPaginationQuery("channel", ps, {
|
||||
note: filter,
|
||||
})) as ScyllaNote[]
|
||||
).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 })),
|
||||
);
|
||||
foundPacked.push(...(await Notes.packMany(foundNotes, user)));
|
||||
if (foundNotes.length < ps.limit) break;
|
||||
ps.untilDate = foundNotes[foundNotes.length - 1].createdAt.getTime();
|
||||
}
|
||||
|
|
|
@ -11,6 +11,20 @@ import read from "@/services/note/read.js";
|
|||
import { readNotification } from "../../common/read-notification.js";
|
||||
import define from "../../define.js";
|
||||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||
import {
|
||||
ScyllaNotification,
|
||||
execPaginationQuery,
|
||||
filterMutedUser,
|
||||
scyllaClient,
|
||||
} from "@/db/scylla.js";
|
||||
import {
|
||||
InstanceMutingsCache,
|
||||
LocalFollowingsCache,
|
||||
UserBlockedCache,
|
||||
UserBlockingCache,
|
||||
UserMutingsCache,
|
||||
userWordMuteCache,
|
||||
} from "@/misc/cache.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["account", "notifications"],
|
||||
|
@ -66,13 +80,75 @@ export const paramDef = {
|
|||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
// includeTypes が空の場合はクエリしない
|
||||
if (ps.includeTypes && ps.includeTypes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
// excludeTypes に全指定されている場合はクエリしない
|
||||
if (notificationTypes.every((type) => ps.excludeTypes?.includes(type))) {
|
||||
return [];
|
||||
if (
|
||||
(ps.includeTypes && ps.includeTypes.length === 0) ||
|
||||
notificationTypes.every((type) => ps.excludeTypes?.includes(type))
|
||||
) {
|
||||
return await Notifications.packMany([], user.id);
|
||||
}
|
||||
|
||||
if (scyllaClient) {
|
||||
const [
|
||||
followingUserIds,
|
||||
mutedUserIds,
|
||||
mutedInstances,
|
||||
blockerIds,
|
||||
blockingIds,
|
||||
] = 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()),
|
||||
UserBlockedCache.init(user.id).then((cache) => cache.getAll()),
|
||||
UserBlockingCache.init(user.id).then((cache) => cache.getAll()),
|
||||
]);
|
||||
const validUserIds = [user.id, ...followingUserIds];
|
||||
|
||||
const filter = (notifications: ScyllaNotification[]) => {
|
||||
let filtered = notifications;
|
||||
if (ps.unreadOnly) {
|
||||
// FIXME: isRead is always true at the moment
|
||||
filtered = [];
|
||||
}
|
||||
if (ps.following) {
|
||||
filtered = filtered.filter(
|
||||
(n) => n.notifierId && validUserIds.includes(n.notifierId),
|
||||
);
|
||||
}
|
||||
if (ps.includeTypes && ps.includeTypes.length > 0) {
|
||||
filtered = filtered.filter((n) => ps.includeTypes?.includes(n.type));
|
||||
} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
|
||||
filtered = filtered.filter(
|
||||
(n) => ps.excludeTypes && !ps.excludeTypes.includes(n.type),
|
||||
);
|
||||
}
|
||||
filtered = filtered.filter(
|
||||
(n) => !(n.notifierHost && mutedInstances.includes(n.notifierHost)),
|
||||
);
|
||||
filtered = filtered.filter(
|
||||
(n) =>
|
||||
!(
|
||||
n.notifierId &&
|
||||
(mutedUserIds.includes(n.notifierId) ||
|
||||
blockingIds.includes(n.notifierId) ||
|
||||
blockerIds.includes(n.notifierId))
|
||||
),
|
||||
);
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const foundNotifications = (
|
||||
(await execPaginationQuery(
|
||||
"notification",
|
||||
ps,
|
||||
{ notification: filter },
|
||||
user.id,
|
||||
30,
|
||||
)) as ScyllaNotification[]
|
||||
).slice(0, ps.limit);
|
||||
return await Notifications.packMany(foundNotifications, user.id);
|
||||
}
|
||||
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", { followerId: user.id });
|
||||
|
@ -97,19 +173,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.andWhere("notification.notifieeId = :meId", { meId: user.id })
|
||||
.leftJoinAndSelect("notification.notifier", "notifier")
|
||||
.leftJoinAndSelect("notification.note", "note")
|
||||
.leftJoinAndSelect("notifier.avatar", "notifierAvatar")
|
||||
.leftJoinAndSelect("notifier.banner", "notifierBanner")
|
||||
.leftJoinAndSelect("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("renote.user", "renoteUser");
|
||||
|
||||
// muted users
|
||||
query.andWhere(
|
||||
|
|
|
@ -151,7 +151,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
return await Notes.packMany(foundNotes, user, {
|
||||
detail: false,
|
||||
scyllaNote: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.j
|
|||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||
import {
|
||||
ScyllaNote,
|
||||
execNotePaginationQuery,
|
||||
type ScyllaNote,
|
||||
execPaginationQuery,
|
||||
filterBlockUser,
|
||||
filterMutedNote,
|
||||
filterMutedRenotes,
|
||||
|
@ -157,11 +157,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const foundPacked = [];
|
||||
while (foundPacked.length < ps.limit) {
|
||||
const foundNotes = (
|
||||
await execNotePaginationQuery("global", ps, filter)
|
||||
(await execPaginationQuery("global", ps, {
|
||||
note: filter,
|
||||
})) as ScyllaNote[]
|
||||
).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 })),
|
||||
);
|
||||
foundPacked.push(...(await Notes.packMany(foundNotes, user)));
|
||||
if (foundNotes.length < ps.limit) break;
|
||||
ps.untilDate = foundNotes[foundNotes.length - 1].createdAt.getTime();
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ import { generateChannelQuery } from "../../common/generate-channel-query.js";
|
|||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||
import {
|
||||
ScyllaNote,
|
||||
execNotePaginationQuery,
|
||||
type ScyllaNote,
|
||||
execPaginationQuery,
|
||||
filterBlockUser,
|
||||
filterChannel,
|
||||
filterMutedNote,
|
||||
|
@ -174,22 +174,23 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const foundPacked = [];
|
||||
while (foundPacked.length < ps.limit) {
|
||||
const [homeFoundNotes, localFoundNotes] = await Promise.all([
|
||||
execNotePaginationQuery(
|
||||
execPaginationQuery(
|
||||
"home",
|
||||
ps,
|
||||
(notes) => commonFilter(homeFilter(notes)),
|
||||
{ note: (notes) => commonFilter(homeFilter(notes)) },
|
||||
user.id,
|
||||
),
|
||||
execNotePaginationQuery("local", ps, (notes) =>
|
||||
commonFilter(localFilter(notes)),
|
||||
),
|
||||
execPaginationQuery("local", ps, {
|
||||
note: (notes) => commonFilter(localFilter(notes)),
|
||||
}),
|
||||
]);
|
||||
const foundNotes = [...homeFoundNotes, ...localFoundNotes]
|
||||
const foundNotes = [
|
||||
...(homeFoundNotes as ScyllaNote[]),
|
||||
...(localFoundNotes as ScyllaNote[]),
|
||||
]
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) // Descendent
|
||||
.slice(0, ps.limit * 1.5); // Some may be filtered out by Notes.packMany, thus we take more than ps.limit.
|
||||
foundPacked.push(
|
||||
...(await Notes.packMany(foundNotes, user, { scyllaNote: true })),
|
||||
);
|
||||
foundPacked.push(...(await Notes.packMany(foundNotes, user)));
|
||||
if (foundNotes.length < ps.limit) break;
|
||||
ps.untilDate = foundNotes[foundNotes.length - 1].createdAt.getTime();
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ import { generateChannelQuery } from "../../common/generate-channel-query.js";
|
|||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||
import {
|
||||
ScyllaNote,
|
||||
execNotePaginationQuery,
|
||||
type ScyllaNote,
|
||||
execPaginationQuery,
|
||||
filterBlockUser,
|
||||
filterChannel,
|
||||
filterMutedNote,
|
||||
|
@ -187,11 +187,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const foundPacked = [];
|
||||
while (foundPacked.length < ps.limit) {
|
||||
const foundNotes = (
|
||||
await execNotePaginationQuery("local", ps, filter)
|
||||
(await execPaginationQuery("local", ps, {
|
||||
note: filter,
|
||||
})) as ScyllaNote[]
|
||||
).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 })),
|
||||
);
|
||||
foundPacked.push(...(await Notes.packMany(foundNotes, user)));
|
||||
if (foundNotes.length < ps.limit) break;
|
||||
ps.untilDate = foundNotes[foundNotes.length - 1].createdAt.getTime();
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ import { generateChannelQuery } from "../../common/generate-channel-query.js";
|
|||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
|
||||
import {
|
||||
ScyllaNote,
|
||||
execNotePaginationQuery,
|
||||
type ScyllaNote,
|
||||
execPaginationQuery,
|
||||
filterBlockUser,
|
||||
filterMutedNote,
|
||||
filterMutedRenotes,
|
||||
|
@ -184,10 +184,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
return filtered;
|
||||
};
|
||||
|
||||
const foundNotes = await execNotePaginationQuery("recommended", ps, filter);
|
||||
return await Notes.packMany(foundNotes.slice(0, ps.limit), user, {
|
||||
scyllaNote: true,
|
||||
});
|
||||
const foundNotes = (await execPaginationQuery("recommended", ps, {
|
||||
note: filter,
|
||||
})) as ScyllaNote[];
|
||||
return await Notes.packMany(foundNotes.slice(0, ps.limit), user);
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
|
|
|
@ -7,8 +7,8 @@ import { generateMutedUserQuery } from "../../common/generate-muted-user-query.j
|
|||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||
import {
|
||||
ScyllaNote,
|
||||
execNotePaginationQuery,
|
||||
type ScyllaNote,
|
||||
execPaginationQuery,
|
||||
filterBlockUser,
|
||||
filterMutedUser,
|
||||
filterVisibility,
|
||||
|
@ -113,17 +113,15 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
let untilDate: number | undefined;
|
||||
while (foundPacked.length < ps.limit) {
|
||||
const foundNotes = (
|
||||
await execNotePaginationQuery(
|
||||
(await execPaginationQuery(
|
||||
"renotes",
|
||||
{ ...ps, untilDate },
|
||||
filter,
|
||||
{ note: filter },
|
||||
user?.id,
|
||||
1,
|
||||
)
|
||||
)) as ScyllaNote[]
|
||||
).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 })),
|
||||
);
|
||||
foundPacked.push(...(await Notes.packMany(foundNotes, user)));
|
||||
if (foundNotes.length < ps.limit) break;
|
||||
untilDate = foundNotes[foundNotes.length - 1].createdAt.getTime();
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
return await Notes.pack(note, user, {
|
||||
// FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774)
|
||||
detail: true,
|
||||
scyllaNote: !!scyllaClient
|
||||
}).catch((err) => {
|
||||
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24")
|
||||
throw new ApiError(meta.errors.noSuchNote);
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
filterChannel,
|
||||
filterReply,
|
||||
filterVisibility,
|
||||
execNotePaginationQuery,
|
||||
execPaginationQuery,
|
||||
filterMutedUser,
|
||||
filterMutedNote,
|
||||
filterBlockUser,
|
||||
|
@ -161,11 +161,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const foundPacked = [];
|
||||
while (foundPacked.length < ps.limit) {
|
||||
const foundNotes = (
|
||||
await execNotePaginationQuery("home", ps, filter, user.id)
|
||||
(await execPaginationQuery(
|
||||
"home",
|
||||
ps,
|
||||
{ note: filter },
|
||||
user.id,
|
||||
)) as ScyllaNote[]
|
||||
).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 })),
|
||||
);
|
||||
foundPacked.push(...(await Notes.packMany(foundNotes, user)));
|
||||
if (foundNotes.length < ps.limit) break;
|
||||
ps.untilDate = foundNotes[foundNotes.length - 1].createdAt.getTime();
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import { generateVisibilityQuery } from "../../common/generate-visibility-query.
|
|||
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
|
||||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||
import {
|
||||
ScyllaNote,
|
||||
execNotePaginationQuery,
|
||||
type ScyllaNote,
|
||||
execPaginationQuery,
|
||||
filterBlockUser,
|
||||
filterMutedNote,
|
||||
filterMutedUser,
|
||||
|
@ -158,19 +158,15 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
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);
|
||||
const foundNotes = (
|
||||
(await execPaginationQuery(
|
||||
"user",
|
||||
ps,
|
||||
{ note: filter },
|
||||
user.id,
|
||||
)) as ScyllaNote[]
|
||||
).slice(0, ps.limit);
|
||||
return await Notes.packMany(foundNotes, user);
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
|
|
|
@ -12,8 +12,13 @@ import type { User } from "@/models/entities/user.js";
|
|||
import type { Notification } from "@/models/entities/notification.js";
|
||||
import { sendEmailNotification } from "./send-email-notification.js";
|
||||
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
|
||||
import { UserMutingsCache } from "@/misc/cache.js";
|
||||
import { LocalFollowingsCache, UserMutingsCache } from "@/misc/cache.js";
|
||||
import { userByIdCache } from "./user-cache.js";
|
||||
import {
|
||||
type ScyllaNotification,
|
||||
prepared,
|
||||
scyllaClient,
|
||||
} from "@/db/scylla.js";
|
||||
|
||||
export async function createNotification(
|
||||
notifieeId: User["id"],
|
||||
|
@ -24,22 +29,31 @@ export async function createNotification(
|
|||
return null;
|
||||
}
|
||||
|
||||
let notifierHost: string | null = null;
|
||||
|
||||
if (
|
||||
data.notifierId &&
|
||||
["mention", "reply", "renote", "quote", "reaction"].includes(type)
|
||||
) {
|
||||
const notifier = await Users.findOneBy({ id: data.notifierId });
|
||||
const notifier = await userByIdCache.fetchMaybe(data.notifierId, () =>
|
||||
Users.findOneBy({ id: data.notifierId ?? "" }).then(
|
||||
(user) => user ?? undefined,
|
||||
),
|
||||
);
|
||||
// suppress if the notifier does not exist or is silenced.
|
||||
if (!notifier) return null;
|
||||
|
||||
// suppress if the notifier is silenced or in a silenced instance, and not followed by the notifiee.
|
||||
notifierHost = notifier.host;
|
||||
|
||||
// suppress if the notifier is silenced, suspended, or in a silenced instance, and not followed by the notifiee.
|
||||
if (
|
||||
(notifier.isSilenced ||
|
||||
notifier.isSuspended ||
|
||||
(Users.isRemoteUser(notifier) &&
|
||||
(await shouldSilenceInstance(notifier.host)))) &&
|
||||
!(await Followings.exist({
|
||||
where: { followerId: notifieeId, followeeId: data.notifierId },
|
||||
}))
|
||||
!(await LocalFollowingsCache.init(notifieeId).then((cache) =>
|
||||
cache.has(notifier.id),
|
||||
))
|
||||
)
|
||||
return null;
|
||||
}
|
||||
|
@ -62,17 +76,62 @@ export async function createNotification(
|
|||
}
|
||||
|
||||
// Create notification
|
||||
const notification = await Notifications.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
notifieeId: notifieeId,
|
||||
type: type,
|
||||
// Make this notification read if muted
|
||||
isRead: isMuted,
|
||||
...data,
|
||||
} as Partial<Notification>).then((x) =>
|
||||
Notifications.findOneByOrFail(x.identifiers[0]),
|
||||
);
|
||||
let notification: Notification | ScyllaNotification;
|
||||
if (scyllaClient) {
|
||||
const entityId =
|
||||
data.noteId ||
|
||||
data.followRequestId ||
|
||||
data.userGroupInvitationId ||
|
||||
data.appAccessTokenId ||
|
||||
null;
|
||||
const now = new Date();
|
||||
notification = {
|
||||
id: genId(),
|
||||
createdAtDate: now,
|
||||
createdAt: now,
|
||||
targetId: notifieeId,
|
||||
notifierId: data.notifierId ?? null,
|
||||
notifierHost,
|
||||
entityId,
|
||||
type,
|
||||
choice: data.choice ?? null,
|
||||
customBody: data.customBody ?? null,
|
||||
customHeader: data.customHeader ?? null,
|
||||
customIcon: data.customIcon ?? null,
|
||||
reaction: data.reaction ?? null,
|
||||
};
|
||||
await scyllaClient.execute(
|
||||
prepared.notification.insert,
|
||||
[
|
||||
notification.targetId,
|
||||
notification.createdAtDate,
|
||||
notification.createdAt,
|
||||
notification.id,
|
||||
notification.notifierId,
|
||||
notification.notifierHost,
|
||||
notification.type,
|
||||
notification.entityId,
|
||||
notification.reaction,
|
||||
notification.choice,
|
||||
notification.customBody,
|
||||
notification.customHeader,
|
||||
notification.customIcon,
|
||||
],
|
||||
{ prepare: true },
|
||||
);
|
||||
} else {
|
||||
notification = await Notifications.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
notifieeId: notifieeId,
|
||||
type: type,
|
||||
// Make this notification read if muted
|
||||
isRead: isMuted,
|
||||
...data,
|
||||
} as Partial<Notification>).then((x) =>
|
||||
Notifications.findOneByOrFail(x.identifiers[0]),
|
||||
);
|
||||
}
|
||||
|
||||
const packed = await Notifications.pack(notification, {});
|
||||
|
||||
|
|
Loading…
Reference in a new issue