refactor: remove scheduled note entity from backend
This commit is contained in:
parent
abbbfa9a0a
commit
a0f65cc6bc
9 changed files with 75 additions and 151 deletions
|
@ -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,
|
||||
];
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -31,6 +31,11 @@ export class Note {
|
|||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column("timestamp with time zone", {
|
||||
nullable: true,
|
||||
})
|
||||
public scheduledAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
|
|
Loading…
Reference in a new issue