refactor: remove scheduled note entity from backend

This commit is contained in:
naskya 2024-05-27 21:44:39 +09:00
parent abbbfa9a0a
commit a0f65cc6bc
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
9 changed files with 75 additions and 151 deletions

View file

@ -74,7 +74,6 @@ import { Webhook } from "@/models/entities/webhook.js";
import { UserIp } from "@/models/entities/user-ip.js";
import { NoteEdit } from "@/models/entities/note-edit.js";
import { NoteFile } from "@/models/entities/note-file.js";
import { ScheduledNote } from "@/models/entities/scheduled-note.js";
import { entities as charts } from "@/services/chart/entities.js";
import { dbLogger } from "./logger.js";
@ -183,7 +182,6 @@ export const entities = [
UserPending,
Webhook,
UserIp,
ScheduledNote,
...charts,
];

View file

@ -59,10 +59,10 @@ export class RefactorScheduledPosts1716804636187 implements MigrationInterface {
await queryRunner.query("DROP EXTENSION pgcrypto");
await queryRunner.query(`DROP FUNCTION "generate_scheduled_note_id"`);
await queryRunner.query(
`CREATE INDEX "IDX_noteId_ScheduledNote" ON "scheduled_note" ("noteId")`
`CREATE INDEX "IDX_noteId_ScheduledNote" ON "scheduled_note" ("noteId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_userId_ScheduledNote" ON "scheduled_note" ("userId")`
`CREATE INDEX "IDX_userId_ScheduledNote" ON "scheduled_note" ("userId")`,
);
await queryRunner.query(`
ALTER TABLE "scheduled_note"

View file

@ -31,6 +31,11 @@ export class Note {
})
public createdAt: Date;
@Column("timestamp with time zone", {
nullable: true,
})
public scheduledAt: Date | null;
@Index()
@Column({
...id(),

View file

@ -1,48 +0,0 @@
import {
Entity,
JoinColumn,
Column,
ManyToOne,
OneToOne,
PrimaryColumn,
Index,
type Relation,
} from "typeorm";
import { Note } from "./note.js";
import { id } from "../id.js";
import { User } from "./user.js";
@Entity()
export class ScheduledNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
comment:
"The ID of the temporarily created note that corresponds to the schedule.",
})
public noteId: Note["id"];
@Index()
@Column(id())
public userId: User["id"];
@Column("timestamp with time zone")
public scheduledAt: Date;
//#region Relations
@OneToOne(() => Note, {
onDelete: "CASCADE",
})
@JoinColumn()
public note: Relation<Note>;
@ManyToOne(() => User, {
onDelete: "CASCADE",
})
@JoinColumn()
public user: Relation<User>;
//#endregion
}

View file

@ -67,7 +67,6 @@ import { Webhook } from "./entities/webhook.js";
import { UserIp } from "./entities/user-ip.js";
import { NoteFileRepository } from "./repositories/note-file.js";
import { NoteEditRepository } from "./repositories/note-edit.js";
import { ScheduledNote } from "./entities/scheduled-note.js";
export const Announcements = db.getRepository(Announcement);
export const AnnouncementReads = db.getRepository(AnnouncementRead);
@ -136,4 +135,3 @@ export const RegistryItems = db.getRepository(RegistryItem);
export const Webhooks = db.getRepository(Webhook);
export const Ads = db.getRepository(Ad);
export const PasswordResetRequests = db.getRepository(PasswordResetRequest);
export const ScheduledNotes = db.getRepository(ScheduledNote);

View file

@ -11,7 +11,6 @@ import {
Polls,
Channels,
Notes,
ScheduledNotes,
} from "../index.js";
import type { Packed } from "@/misc/schema.js";
import { countReactions, decodeReaction, nyaify } from "backend-rs";
@ -199,19 +198,11 @@ export const NoteRepository = db.getRepository(Note).extend({
host,
);
let scheduledAt: string | undefined;
if (note.visibility === "specified" && note.visibleUserIds.length === 0) {
scheduledAt = (
await ScheduledNotes.findOneBy({
noteId: note.id,
})
)?.scheduledAt?.toISOString();
}
const reactionEmoji = await populateEmojis(reactionEmojiNames, host);
const packed: Packed<"Note"> = await awaitAll({
id: note.id,
createdAt: note.createdAt.toISOString(),
scheduledAt: note.scheduledAt,
userId: note.userId,
user: Users.pack(note.user ?? note.userId, me, {
detail: false,
@ -241,7 +232,6 @@ export const NoteRepository = db.getRepository(Note).extend({
},
})
: undefined,
scheduledAt,
reactions: countReactions(note.reactions),
reactionEmojis: reactionEmoji,
emojis: noteEmoji,

View file

@ -1,4 +1,4 @@
import { Users, Notes, ScheduledNotes, DriveFiles } from "@/models/index.js";
import { Users, Notes, DriveFiles } from "@/models/index.js";
import type { DbUserScheduledNoteData } from "@/queue/types.js";
import { queueLogger } from "../../logger.js";
import type Bull from "bull";
@ -20,46 +20,46 @@ export async function scheduledNote(
return;
}
const note = await Notes.findOneBy({ id: job.data.noteId });
if (note == null) {
const draftNote = await Notes.findOneBy({ id: job.data.noteId });
if (draftNote == null) {
logger.warn(`Note ${job.data.noteId} does not exist`);
done();
return;
}
const files = await DriveFiles.findBy({ id: In(note.fileIds) });
const files = await DriveFiles.findBy({ id: In(draftNote.fileIds) });
if (user.isSuspended) {
deleteNote(user, note);
logger.info(`Cancelled due to user ${job.data.user.id} being suspended`);
deleteNote(user, draftNote);
done();
return;
}
await ScheduledNotes.delete({
noteId: note.id,
userId: user.id,
});
const visibleUsers = job.data.option.visibleUserIds
? await Users.findBy({
id: In(job.data.option.visibleUserIds),
})
: [];
// Create scheduled (actual) note
await createNote(user, {
createdAt: new Date(),
scheduledAt: null,
files,
poll: job.data.option.poll,
text: note.text || undefined,
lang: note.lang,
reply: note.reply,
renote: note.renote,
cw: note.cw,
localOnly: note.localOnly,
text: draftNote.text || undefined,
lang: draftNote.lang,
reply: draftNote.reply,
renote: draftNote.renote,
cw: draftNote.cw,
localOnly: draftNote.localOnly,
visibility: job.data.option.visibility,
visibleUsers,
channel: note.channel,
channel: draftNote.channel,
});
await deleteNote(user, note);
// Delete temporal (draft) note
await deleteNote(user, draftNote);
logger.info("Success");

View file

@ -7,7 +7,6 @@ import {
Notes,
Channels,
Blockings,
ScheduledNotes,
} from "@/models/index.js";
import type { DriveFile } from "@/models/entities/drive-file.js";
import type { Note } from "@/models/entities/note.js";
@ -16,7 +15,7 @@ import { config } from "@/config.js";
import { noteVisibilities } from "@/types.js";
import { ApiError } from "@/server/api/error.js";
import define from "@/server/api/define.js";
import { HOUR, genId } from "backend-rs";
import { HOUR } from "backend-rs";
import { getNote } from "@/server/api/common/getters.js";
import { langmap } from "firefish-js";
import { createScheduledNoteJob } from "@/queue/index.js";
@ -303,7 +302,7 @@ export default define(meta, paramDef, async (ps, user) => {
}
let delay: number | null = null;
if (ps.scheduledAt) {
if (ps.scheduledAt != null) {
delay = ps.scheduledAt - Date.now();
if (delay < 0) {
delay = null;
@ -315,12 +314,14 @@ export default define(meta, paramDef, async (ps, user) => {
user,
{
createdAt: new Date(),
scheduledAt: delay != null ? new Date(ps.scheduledAt!) : null,
files: files,
poll: ps.poll
? {
choices: ps.poll.choices,
multiple: ps.poll.multiple,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiresAt:
ps.poll.expiresAt != null ? new Date(ps.poll.expiresAt) : null,
}
: undefined,
text: ps.text || undefined,
@ -346,13 +347,6 @@ export default define(meta, paramDef, async (ps, user) => {
false,
delay
? async (note) => {
await ScheduledNotes.insert({
id: genId(),
noteId: note.id,
userId: user.id,
scheduledAt: new Date(ps.scheduledAt as number),
});
createScheduledNoteJob(
{
user: { id: user.id },

View file

@ -133,15 +133,10 @@ class NotificationManager {
}
}
type MinimumUser = {
id: User["id"];
host: User["host"];
username: User["username"];
uri: User["uri"];
};
type Option = {
type UserLike = Pick<User, "id" | "host" | "username" | "uri">;
type NoteLike = {
createdAt?: Date | null;
scheduledAt?: Date | null;
name?: string | null;
text?: string | null;
lang?: string | null;
@ -152,9 +147,9 @@ type Option = {
localOnly?: boolean | null;
cw?: string | null;
visibility?: string;
visibleUsers?: MinimumUser[] | null;
visibleUsers?: UserLike[] | null;
channel?: Channel | null;
apMentions?: MinimumUser[] | null;
apMentions?: UserLike[] | null;
apHashtags?: string[] | null;
apEmojis?: string[] | null;
uri?: string | null;
@ -163,20 +158,15 @@ type Option = {
};
export default async (
user: {
id: User["id"];
username: User["username"];
host: User["host"];
isSilenced: User["isSilenced"];
createdAt: User["createdAt"];
isBot: User["isBot"];
inbox?: User["inbox"];
},
data: Option,
user: Pick<
User,
"id" | "username" | "host" | "isSilenced" | "createdAt" | "isBot"
> & { inbox?: User["inbox"] },
data: NoteLike,
silent = false,
waitToPublish?: (note: Note) => Promise<void>,
) =>
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
new Promise<Note>(async (res, rej) => {
const dontFederateInitially =
data.visibility?.startsWith("hidden") === true;
@ -208,6 +198,7 @@ export default async (
data.createdAt > now
)
data.createdAt = now;
if (data.visibility == null) data.visibility = "public";
if (data.localOnly == null) data.localOnly = false;
if (data.channel != null) data.visibility = "public";
@ -277,11 +268,7 @@ export default async (
data.localOnly = true;
}
if (data.text) {
data.text = data.text.trim();
} else {
data.text = null;
}
data.text = data.text?.trim() ?? null;
if (data.lang) {
if (!Object.keys(langmap).includes(data.lang.toLowerCase()))
@ -297,10 +284,10 @@ export default async (
// Parse MFM if needed
if (!(tags && emojis && mentionedUsers)) {
const tokens = data.text ? mfm.parse(data.text)! : [];
const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
const tokens = data.text ? mfm.parse(data.text) : [];
const cwTokens = data.cw ? mfm.parse(data.cw) : [];
const choiceTokens = data.poll?.choices
? concat(data.poll.choices.map((choice) => mfm.parse(choice)!))
? concat(data.poll.choices.map((choice) => mfm.parse(choice)))
: [];
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
@ -318,12 +305,12 @@ export default async (
.splice(0, 32);
if (
data.reply &&
data.reply != null &&
user.id !== data.reply.userId &&
!mentionedUsers.some((u) => u.id === data.reply!.userId)
!mentionedUsers.some((u) => u.id === data.reply?.userId)
) {
mentionedUsers.push(
await Users.findOneByOrFail({ id: data.reply!.userId }),
await Users.findOneByOrFail({ id: data.reply.userId }),
);
}
@ -338,10 +325,10 @@ export default async (
if (
data.reply &&
!data.visibleUsers.some((x) => x.id === data.reply!.userId)
!data.visibleUsers.some((x) => x.id === data.reply?.userId)
) {
data.visibleUsers.push(
await Users.findOneByOrFail({ id: data.reply!.userId }),
await Users.findOneByOrFail({ id: data.reply?.userId }),
);
}
}
@ -561,7 +548,7 @@ export default async (
publishMainStream(data.reply.userId, "reply", packedReply);
const webhooks = (await getActiveWebhooks()).filter(
(x) => x.userId === data.reply!.userId && x.on.includes("reply"),
(x) => x.userId === data.reply?.userId && x.on.includes("reply"),
);
for (const webhook of webhooks) {
webhookDeliver(webhook, "reply", {
@ -672,7 +659,7 @@ export default async (
}
});
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
async function renderNoteOrRenoteActivity(data: NoteLike, note: Note) {
if (data.localOnly) return null;
const content =
@ -704,17 +691,17 @@ function incRenoteCount(renote: Note) {
async function insertNote(
user: { id: User["id"]; host: User["host"] },
data: Option,
data: NoteLike,
tags: string[],
emojis: string[],
mentionedUsers: MinimumUser[],
mentionedUsers: UserLike[],
) {
if (data.createdAt === null || data.createdAt === undefined) {
data.createdAt = new Date();
}
const insert = new Note({
data.createdAt ??= new Date();
const note = new Note({
id: genIdAt(data.createdAt),
createdAt: data.createdAt,
scheduledAt: data.scheduledAt ?? null,
fileIds: data.files ? data.files.map((file) => file.id) : [],
replyId: data.reply ? data.reply.id : null,
renoteId: data.renote ? data.renote.id : null,
@ -743,7 +730,7 @@ async function insertNote(
attachedFileTypes: data.files ? data.files.map((file) => file.type) : [],
// 以下非正規化データ
// denormalized fields
replyUserId: data.reply ? data.reply.userId : null,
replyUserHost: data.reply ? data.reply.userHost : null,
renoteUserId: data.renote ? data.renote.userId : null,
@ -751,22 +738,22 @@ async function insertNote(
userHost: user.host,
});
if (data.uri != null) insert.uri = data.uri;
if (data.url != null) insert.url = data.url;
if (data.uri != null) note.uri = data.uri;
if (data.url != null) note.url = data.url;
// Append mentions data
if (mentionedUsers.length > 0) {
insert.mentions = mentionedUsers.map((u) => u.id);
const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) });
insert.mentionedRemoteUsers = JSON.stringify(
note.mentions = mentionedUsers.map((u) => u.id);
const profiles = await UserProfiles.findBy({ userId: In(note.mentions) });
note.mentionedRemoteUsers = JSON.stringify(
mentionedUsers
.filter((u) => Users.isRemoteUser(u))
.map((u) => {
const profile = profiles.find((p) => p.userId === u.id);
const url = profile != null ? profile.url : null;
const url = profile?.url ?? null;
return {
uri: u.uri,
url: url == null ? undefined : url,
url: url ?? undefined,
username: u.username,
host: u.host,
} as IMentionedRemoteUsers[0];
@ -776,12 +763,12 @@ async function insertNote(
// 投稿を作成
try {
if (insert.hasPoll) {
if (note.hasPoll) {
// Start transaction
await db.transaction(async (transactionalEntityManager) => {
if (!data.poll) throw new Error("Empty poll data");
await transactionalEntityManager.insert(Note, insert);
await transactionalEntityManager.insert(Note, note);
let expiresAt: Date | null;
if (
@ -794,12 +781,12 @@ async function insertNote(
}
const poll = new Poll({
noteId: insert.id,
noteId: note.id,
choices: data.poll.choices,
expiresAt,
multiple: data.poll.multiple,
votes: new Array(data.poll.choices.length).fill(0),
noteVisibility: insert.visibility,
noteVisibility: note.visibility,
userId: user.id,
userHost: user.host,
});
@ -807,10 +794,10 @@ async function insertNote(
await transactionalEntityManager.insert(Poll, poll);
});
} else {
await Notes.insert(insert);
await Notes.insert(note);
}
return insert;
return note;
} catch (e) {
// duplicate key error
if (isDuplicateKeyValueError(e)) {
@ -857,7 +844,7 @@ async function notifyToWatchersOfReplyee(
}
async function createMentionedEvents(
mentionedUsers: MinimumUser[],
mentionedUsers: UserLike[],
note: Note,
nm: NotificationManager,
) {