remove queries to scheduled_note table

This commit is contained in:
naskya 2024-06-07 08:39:57 +09:00
parent 16f26bc6d7
commit 2c54bc3364
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
4 changed files with 88 additions and 64 deletions

View file

@ -3,7 +3,10 @@ use crate::{
database::db_conn, database::db_conn,
misc::get_note_summary::{get_note_summary, PartialNoteToSummarize}, misc::get_note_summary::{get_note_summary, PartialNoteToSummarize},
model::entity::{access_token, app, sw_subscription}, model::entity::{access_token, app, sw_subscription},
util::{http_client, id::{get_timestamp, InvalidIdError}}, util::{
http_client,
id::{get_timestamp, InvalidIdError},
},
}; };
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use sea_orm::prelude::*; use sea_orm::prelude::*;
@ -255,8 +258,8 @@ pub async fn send_push_notification(
// TODO: refactoring // TODO: refactoring
let mut payload = if use_mastodon_api { let mut payload = if use_mastodon_api {
// Content generated per subscription // Content generated per subscription
"".to_string() "".to_string()
} else { } else {
// Format the `content` passed from the TypeScript backend // Format the `content` passed from the TypeScript backend
// for Firefish push notifications // for Firefish push notifications

View file

@ -40,7 +40,6 @@ import type { NoteReaction } from "@/models/entities/note-reaction.js";
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
import { isFiltered } from "@/misc/is-filtered.js"; import { isFiltered } from "@/misc/is-filtered.js";
import { unfurl } from "unfurl.js"; import { unfurl } from "unfurl.js";
import type { ScheduledNote } from "@/models/entities/scheduled-note.js";
export class NoteConverter { export class NoteConverter {
private static noteContentHtmlCache = new Cache<string | null>( private static noteContentHtmlCache = new Cache<string | null>(
@ -162,9 +161,7 @@ export class NoteConverter {
return renote.url ?? renote.uri ?? `${config.url}/notes/${renote.id}`; return renote.url ?? renote.uri ?? `${config.url}/notes/${renote.id}`;
}); });
const identifier = `${note.id}:${( const identifier = `${note.id}:${(note.updatedAt ?? note.createdAt).getTime()}`;
note.updatedAt ?? note.createdAt
).getTime()}`;
const text = quoteUri.then((quoteUri) => const text = quoteUri.then((quoteUri) =>
note.text !== null note.text !== null
@ -579,13 +576,12 @@ export class NoteConverter {
/** Encode a schduled note. */ /** Encode a schduled note. */
public static async encodeScheduledNote( public static async encodeScheduledNote(
scheduledNote: ScheduledNote, note: Note,
ctx: MastoContext, _: MastoContext,
): Promise<MastodonEntity.ScheduledStatus> { ): Promise<MastodonEntity.ScheduledStatus> {
const { note, user } = scheduledNote;
const renote = const renote =
note.renote ?? (note.renoteId ? getNote(note.renoteId, user) : null); note.renote ??
(note.renoteId ? getNote(note.renoteId, { id: note.userId }) : null);
const quoteUri = Promise.resolve(renote).then((renote) => { const quoteUri = Promise.resolve(renote).then((renote) => {
if (!renote || !isQuote(note)) return null; if (!renote || !isQuote(note)) return null;
return renote.url ?? renote.uri ?? `${config.url}/notes/${renote.id}`; return renote.url ?? renote.uri ?? `${config.url}/notes/${renote.id}`;
@ -604,12 +600,12 @@ export class NoteConverter {
const files = DriveFiles.packMany(note.fileIds); const files = DriveFiles.packMany(note.fileIds);
const a = await awaitAll({ const a = await awaitAll({
id: scheduledNote.noteId, id: note.id,
scheduled_at: scheduledNote.scheduledAt.toISOString(), scheduled_at: note.scheduledAt!.toISOString(),
params: { params: {
text, text,
poll: note.hasPoll poll: note.hasPoll
? populatePoll(note, user?.id ?? null).then((p) => ? populatePoll(note, note.userId ?? null).then((p) =>
PollConverter.encodeScheduledPoll(p), PollConverter.encodeScheduledPoll(p),
) )
: null, : null,
@ -622,7 +618,7 @@ export class NoteConverter {
in_reply_to_id: note.replyId, in_reply_to_id: note.replyId,
language: note.lang, language: note.lang,
application_id: 0, application_id: 0,
idempotency: scheduledNote.id, idempotency: note.id,
with_rate_limit: false, with_rate_limit: false,
}, },
media_attachments: files.then((files) => media_attachments: files.then((files) =>
@ -634,7 +630,7 @@ export class NoteConverter {
/** Encode an array of schduled notes. */ /** Encode an array of schduled notes. */
public static async encodeManyScheduledNotes( public static async encodeManyScheduledNotes(
scheduledNotes: ScheduledNote[], scheduledNotes: Note[],
ctx: MastoContext, ctx: MastoContext,
): Promise<MastodonEntity.ScheduledStatus[]> { ): Promise<MastodonEntity.ScheduledStatus[]> {
const encoded = scheduledNotes.map((n) => this.encodeScheduledNote(n, ctx)); const encoded = scheduledNotes.map((n) => this.encodeScheduledNote(n, ctx));

View file

@ -6,7 +6,6 @@ import {
NoteFavorites, NoteFavorites,
NoteReactions, NoteReactions,
Notes, Notes,
ScheduledNotes,
UserNotePinings, UserNotePinings,
} from "@/models/index.js"; } from "@/models/index.js";
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js"; import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
@ -20,7 +19,7 @@ import deleteReaction from "@/services/note/reaction/delete.js";
import createNote, { extractMentionedUsers } from "@/services/note/create.js"; import createNote, { extractMentionedUsers } from "@/services/note/create.js";
import editNote from "@/services/note/edit.js"; import editNote from "@/services/note/edit.js";
import deleteNote from "@/services/note/delete.js"; import deleteNote from "@/services/note/delete.js";
import { genId } from "backend-rs"; import { fetchMeta, genIdAt } from "backend-rs";
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
import { UserConverter } from "@/server/api/mastodon/converters/user.js"; import { UserConverter } from "@/server/api/mastodon/converters/user.js";
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
@ -36,15 +35,13 @@ import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
import AsyncLock from "async-lock"; import AsyncLock from "async-lock";
import { IdentifiableError } from "@/misc/identifiable-error.js"; import { IdentifiableError } from "@/misc/identifiable-error.js";
import { IsNull } from "typeorm"; import { In, IsNull } from "typeorm";
import { import {
getStubMastoContext, getStubMastoContext,
type MastoContext, type MastoContext,
} from "@/server/api/mastodon/index.js"; } from "@/server/api/mastodon/index.js";
import { fetchMeta } from "backend-rs";
import { translate } from "@/misc/translate.js"; import { translate } from "@/misc/translate.js";
import { createScheduledNoteJob } from "@/queue/index.js"; import { createScheduledNoteJob } from "@/queue/index.js";
import type { ScheduledNote } from "@/models/entities/scheduled-note.js";
export class NoteHelpers { export class NoteHelpers {
public static postIdempotencyCache = new Cache<{ public static postIdempotencyCache = new Cache<{
@ -134,9 +131,11 @@ export class NoteHelpers {
}); });
if (!bookmarked) { if (!bookmarked) {
const now = new Date();
await NoteFavorites.insert({ await NoteFavorites.insert({
id: genId(), id: genIdAt(now),
createdAt: new Date(), createdAt: now,
noteId: note.id, noteId: note.id,
userId: user.id, userId: user.id,
}); });
@ -385,7 +384,7 @@ export class NoteHelpers {
const user = ctx.user as ILocalUser; const user = ctx.user as ILocalUser;
const files = const files =
request.media_ids && request.media_ids.length > 0 request.media_ids && request.media_ids.length > 0
? DriveFiles.findByIds(request.media_ids) ? DriveFiles.findBy({ id: In(request.media_ids) })
: []; : [];
const reply = request.in_reply_to_id const reply = request.in_reply_to_id
@ -403,8 +402,10 @@ export class NoteHelpers {
const visibility = const visibility =
request.visibility ?? UserHelpers.getDefaultNoteVisibility(ctx); request.visibility ?? UserHelpers.getDefaultNoteVisibility(ctx);
const data = { const now = new Date();
createdAt: new Date(),
const data = await awaitAll({
createdAt: now,
files: files, files: files,
poll: request.poll poll: request.poll
? { ? {
@ -432,39 +433,71 @@ export class NoteHelpers {
? this.extractMentions(request.text ?? "", ctx) ? this.extractMentions(request.text ?? "", ctx)
: undefined, : undefined,
), ),
}; });
return createNote( return createNote(
user, user,
await awaitAll(data), {
createdAt: now,
scheduledAt: delay != null ? new Date(data.scheduledAt!) : null,
files: data.files,
poll:
data.poll != null
? {
choices: data.poll.choices,
multiple: data.poll.multiple,
expiresAt:
data.poll.expiresAt != null
? new Date(data.poll.expiresAt)
: null,
}
: undefined,
text: data.text || undefined,
lang: data.lang,
reply,
renote,
cw: data.cw,
...(delay != null
? {
visibility: "specified",
visibleUsers: [],
}
: {
visibility: data.visibility,
visibleUsers: data.visibleUsers,
}),
},
false, false,
!delay delay
? undefined ? async (note) => {
: async (note) => {
await ScheduledNotes.insert({
id: genId(),
noteId: note.id,
userId: user.id,
scheduledAt: request.scheduled_at,
});
createScheduledNoteJob( createScheduledNoteJob(
{ {
user: { id: user.id }, user: { id: user.id },
noteId: note.id, noteId: note.id,
option: { option: {
poll: data.poll, poll: data.poll
visibility: await visibility, ? {
choices: data.poll.choices,
multiple: data.poll.multiple,
expiresAt: data.poll.expiresAt
? new Date(data.poll.expiresAt)
: null,
}
: undefined,
visibility: data.visibility,
visibleUserIds: await Promise.resolve(visibility) visibleUserIds: await Promise.resolve(visibility)
.then((v) => .then((v) =>
v === "specified" ? data.visibleUsers : undefined, v === "specified" ? data.visibleUsers : undefined,
) )
.then((users) => users?.map((u) => u.id)), .then((users) => users?.map((u) => u.id)),
replyId: data.reply?.id ?? undefined,
renoteId: data.renote?.id ?? undefined,
}, },
}, },
delay, delay,
); );
}, }
: undefined,
); );
} }
@ -476,7 +509,7 @@ export class NoteHelpers {
const user = ctx.user as ILocalUser; const user = ctx.user as ILocalUser;
const files = const files =
request.media_ids && request.media_ids.length > 0 request.media_ids && request.media_ids.length > 0
? await DriveFiles.findByIds(request.media_ids) ? await DriveFiles.findBy({ id: In(request.media_ids) })
: []; : [];
if (request.media_attributes && request.media_attributes.length > 0) { if (request.media_attributes && request.media_attributes.length > 0) {
@ -750,17 +783,17 @@ export class NoteHelpers {
minId: string | undefined, minId: string | undefined,
limit = 20, limit = 20,
ctx: MastoContext, ctx: MastoContext,
): Promise<ScheduledNote[]> { ): Promise<Note[]> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
ScheduledNotes.createQueryBuilder("scheduledNote"), Notes.createQueryBuilder("note"),
sinceId, sinceId,
maxId, maxId,
minId, minId,
) )
.leftJoinAndSelect("scheduledNote.note", "note") .andWhere("note.scheduledAt IS NOT NULL")
.leftJoinAndSelect("scheduledNote.user", "user"); .leftJoinAndSelect("note.user", "user");
return PaginationHelpers.execQueryLinkPagination( return PaginationHelpers.execQueryLinkPagination(
query, query,
@ -773,13 +806,13 @@ export class NoteHelpers {
public static async getScheduledNoteOr404( public static async getScheduledNoteOr404(
id: string, id: string,
ctx: MastoContext, ctx: MastoContext,
): Promise<ScheduledNote> { ): Promise<Note> {
const user = ctx.user as ILocalUser | null; const user = ctx.user as ILocalUser | null;
const query = ScheduledNotes.createQueryBuilder("scheduledNote") const query = Notes.createQueryBuilder("note")
.where("scheduledNote.noteId = :id", { id }) .where("note.id = :id", { id })
.andWhere("scheduledNote.userId = :userId", { userId: user?.id }) .andWhere("note.userId = :userId", { userId: user?.id })
.leftJoinAndSelect("scheduledNote.note", "note") .andWhere("note.scheduledAt IS NOT NULL")
.leftJoinAndSelect("scheduledNote.user", "user"); .leftJoinAndSelect("note.user", "user");
return query.getOneOrFail().catch((_) => { return query.getOneOrFail().catch((_) => {
throw new MastoApiError(404); throw new MastoApiError(404);
}); });

View file

@ -1,7 +1,6 @@
import type { Note } from "@/models/entities/note.js"; import type { Note } from "@/models/entities/note.js";
import type { ILocalUser, User } from "@/models/entities/user.js"; import type { ILocalUser, User } from "@/models/entities/user.js";
import { import {
Followings,
Notes, Notes,
Notifications, Notifications,
RegistryItems, RegistryItems,
@ -14,7 +13,7 @@ import { generateVisibilityQuery } from "@/server/api/common/generate-visibility
import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js"; import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/generated-muted-renote-query.js";
import { fetchMeta } from "backend-rs"; import { fetchMeta, genId } from "backend-rs";
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
import type { UserList } from "@/models/entities/user-list.js"; import type { UserList } from "@/models/entities/user-list.js";
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
@ -27,7 +26,6 @@ import { generatePaginationData } from "@/server/api/mastodon/middleware/paginat
import type { MastoContext } from "@/server/api/mastodon/index.js"; import type { MastoContext } from "@/server/api/mastodon/index.js";
import { generateListQuery } from "@/server/api/common/generate-list-query.js"; import { generateListQuery } from "@/server/api/common/generate-list-query.js";
import { generateFollowingQuery } from "@/server/api/common/generate-following-query.js"; import { generateFollowingQuery } from "@/server/api/common/generate-following-query.js";
import { genId } from "backend-rs";
export class TimelineHelpers { export class TimelineHelpers {
public static async getHomeTimeline( public static async getHomeTimeline(
@ -379,14 +377,8 @@ export class TimelineHelpers {
return result; return result;
} }
/** Exclude scheduled notes from Mastodon timeline (visibility === "specified" && visibleUserIds.length === 0) */ /** Exclude scheduled notes from Mastodon timeline */
public static generateNoScheduleNotesQuery(q: SelectQueryBuilder<Note>) { public static generateNoScheduleNotesQuery(q: SelectQueryBuilder<Note>) {
q.andWhere( q.andWhere("note.scheduledAt IS NULL");
new Brackets((qb) => {
qb.where("note.visibility != 'specified'").orWhere(
"array_length(note.visibleUserIds, 1) != 0",
);
}),
);
} }
} }