From 302b112f05427bdae13fb9fe818bd80ad8eb7d0c Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Mon, 20 Nov 2023 22:03:10 +0100 Subject: [PATCH] [backend] Include avatar & banner url and blurhash in the user table This drastically improves timeline performance due to the many (2-6 per query) database joins that are now no longer required --- .../migration/1700517975122-drive-file-url.ts | 32 +++++++++++++++ packages/backend/src/models/entities/user.ts | 28 +++++++++++++ .../src/models/repositories/drive-file.ts | 41 +++++++++++-------- .../backend/src/models/repositories/user.ts | 21 ++++------ .../src/remote/activitypub/models/person.ts | 20 ++++++++- .../server/api/endpoints/antennas/notes.ts | 6 --- .../server/api/endpoints/channels/timeline.ts | 6 --- .../src/server/api/endpoints/clips/notes.ts | 6 --- .../server/api/endpoints/i/notifications.ts | 8 +--- .../src/server/api/endpoints/i/update.ts | 13 ++++-- .../backend/src/server/api/endpoints/notes.ts | 8 +--- .../server/api/endpoints/notes/children.ts | 4 +- .../server/api/endpoints/notes/featured.ts | 8 +--- .../api/endpoints/notes/global-timeline.ts | 8 +--- .../api/endpoints/notes/hybrid-timeline.ts | 6 --- .../api/endpoints/notes/local-timeline.ts | 8 +--- .../server/api/endpoints/notes/mentions.ts | 8 +--- .../endpoints/notes/recommended-timeline.ts | 8 +--- .../src/server/api/endpoints/notes/renotes.ts | 8 +--- .../src/server/api/endpoints/notes/replies.ts | 8 +--- .../api/endpoints/notes/search-by-tag.ts | 8 +--- .../src/server/api/endpoints/notes/search.ts | 8 +--- .../server/api/endpoints/notes/timeline.ts | 8 +--- .../api/endpoints/notes/user-list-timeline.ts | 6 --- .../src/server/api/endpoints/users/notes.ts | 8 +--- .../server/api/mastodon/converters/user.ts | 4 +- .../src/server/api/mastodon/helpers/user.ts | 6 ++- .../backend/src/services/drive/delete-file.ts | 4 +- 28 files changed, 142 insertions(+), 165 deletions(-) create mode 100644 packages/backend/src/migration/1700517975122-drive-file-url.ts diff --git a/packages/backend/src/migration/1700517975122-drive-file-url.ts b/packages/backend/src/migration/1700517975122-drive-file-url.ts new file mode 100644 index 000000000..296136d11 --- /dev/null +++ b/packages/backend/src/migration/1700517975122-drive-file-url.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UserAvatarBannerRefactor1700517975122 implements MigrationInterface { + name = 'UserAvatarBannerRefactor1700517975122' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" ADD "avatarUrl" character varying(512)`); + await queryRunner.query(`COMMENT ON COLUMN "user"."avatarUrl" IS 'The URL of the avatar DriveFile'`); + await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`); + await queryRunner.query(`COMMENT ON COLUMN "user"."avatarBlurhash" IS 'The blurhash of the avatar DriveFile'`); + await queryRunner.query(`ALTER TABLE "user" ADD "bannerUrl" character varying(512)`); + await queryRunner.query(`COMMENT ON COLUMN "user"."bannerUrl" IS 'The URL of the banner DriveFile'`); + await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`); + await queryRunner.query(`COMMENT ON COLUMN "user"."bannerBlurhash" IS 'The blurhash of the banner DriveFile'`); + + await queryRunner.query(`UPDATE "user" SET "avatarUrl" = (SELECT COALESCE("thumbnailUrl", "webpublicUrl", "url") FROM "drive_file" WHERE "id" = "user"."avatarId") WHERE "avatarId" IS NOT NULL`); + await queryRunner.query(`UPDATE "user" SET "avatarBlurhash" = (SELECT "blurhash" FROM "drive_file" WHERE "id" = "user"."avatarId") WHERE "avatarId" IS NOT NULL`); + await queryRunner.query(`UPDATE "user" SET "bannerUrl" = (SELECT COALESCE("webpublicUrl", "url") FROM "drive_file" WHERE "id" = "user"."bannerId") WHERE "bannerId" IS NOT NULL`); + await queryRunner.query(`UPDATE "user" SET "bannerBlurhash" = (SELECT "blurhash" FROM "drive_file" WHERE "id" = "user"."bannerId") WHERE "bannerId" IS NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`COMMENT ON COLUMN "user"."bannerBlurhash" IS 'The blurhash of the banner DriveFile'`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`); + await queryRunner.query(`COMMENT ON COLUMN "user"."bannerUrl" IS 'The URL of the banner DriveFile'`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerUrl"`); + await queryRunner.query(`COMMENT ON COLUMN "user"."avatarBlurhash" IS 'The blurhash of the avatar DriveFile'`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`); + await queryRunner.query(`COMMENT ON COLUMN "user"."avatarUrl" IS 'The URL of the avatar DriveFile'`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarUrl"`); + } +} diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index ddad9f3b2..4b3bf553f 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -103,6 +103,20 @@ export class User { }) public avatarId: DriveFile["id"] | null; + @Column("varchar", { + length: 512, + nullable: true, + comment: "The URL of the avatar DriveFile", + }) + public avatarUrl: string | null; + + @Column("varchar", { + length: 128, + nullable: true, + comment: "The blurhash of the avatar DriveFile", + }) + public avatarBlurhash: string | null; + @OneToOne((type) => DriveFile, { onDelete: "SET NULL", }) @@ -116,6 +130,20 @@ export class User { }) public bannerId: DriveFile["id"] | null; + @Column("varchar", { + length: 512, + nullable: true, + comment: "The URL of the banner DriveFile", + }) + public bannerUrl: string | null; + + @Column("varchar", { + length: 128, + nullable: true, + comment: "The blurhash of the banner DriveFile", + }) + public bannerBlurhash: string | null; + @OneToOne((type) => DriveFile, { onDelete: "SET NULL", }) diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 3918f7947..68f5f1081 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -2,13 +2,11 @@ import { db } from "@/db/postgre.js"; import { DriveFile } from "@/models/entities/drive-file.js"; import type { User } from "@/models/entities/user.js"; import { toPuny } from "@/misc/convert-host.js"; -import { awaitAll, Promiseable } from "@/prelude/await-all.js"; +import { awaitAll } from "@/prelude/await-all.js"; import type { Packed } from "@/misc/schema.js"; import config from "@/config/index.js"; -import { query, appendQuery } from "@/prelude/url.js"; -import { Meta } from "@/models/entities/meta.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Users, DriveFolders } from "../index.js"; +import { appendQuery, query } from "@/prelude/url.js"; +import { DriveFolders, Users } from "../index.js"; import { deepClone } from "@/misc/clone.js"; type PackOptions = { @@ -44,6 +42,19 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ return file.properties; }, + isImage(file: DriveFile): boolean { + return !!file.type && + [ + "image/png", + "image/apng", + "image/gif", + "image/jpeg", + "image/webp", + "image/svg+xml", + "image/avif", + ].includes(file.type); + }, + getPublicUrl(file: DriveFile, thumbnail = false): string | null { // リモートかつメディアプロキシ if ( @@ -70,23 +81,17 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ } } - const isImage = - file.type && - [ - "image/png", - "image/apng", - "image/gif", - "image/jpeg", - "image/webp", - "image/svg+xml", - "image/avif", - ].includes(file.type); - return thumbnail - ? file.thumbnailUrl || (isImage ? file.webpublicUrl || file.url : null) + ? file.thumbnailUrl || (this.isImage(file) ? file.webpublicUrl || file.url : null) : file.webpublicUrl || file.url; }, + getDatabasePrefetchUrl(file: DriveFile, thumbnail = false): string | null { + return thumbnail + ? file.thumbnailUrl ?? file.webpublicUrl ?? file.url + : file.webpublicUrl ?? file.url; + }, + async calcDriveUsageOf( user: User["id"] | { id: User["id"] }, ): Promise { diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 6deb3bad9..43523c95b 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -339,6 +339,7 @@ export const UserRepository = db.getRepository(User).extend({ this.getIdenticonUrl(user.id) ); } else if (user.avatarId) { + if (user.avatarUrl) return user.avatarUrl; const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId }); return ( DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id) @@ -349,7 +350,9 @@ export const UserRepository = db.getRepository(User).extend({ }, getAvatarUrlSync(user: User): string { - if (user.avatar) { + if (user.avatarId && user.avatarUrl) { + return user.avatarUrl; + } else if (user.avatar) { return ( DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id) @@ -388,17 +391,9 @@ export const UserRepository = db.getRepository(User).extend({ if (typeof src === "object") { user = src; - if (src.avatar === undefined && src.avatarId) - src.avatar = (await DriveFiles.findOneBy({ id: src.avatarId })) ?? null; - if (src.banner === undefined && src.bannerId) - src.banner = (await DriveFiles.findOneBy({ id: src.bannerId })) ?? null; } else { user = await this.findOneOrFail({ where: { id: src }, - relations: { - avatar: true, - banner: true, - }, }); } @@ -474,7 +469,7 @@ export const UserRepository = db.getRepository(User).extend({ username: user.username, host: user.host, avatarUrl: this.getAvatarUrlSync(user), - avatarBlurhash: user.avatar?.blurhash || null, + avatarBlurhash: user.avatarId ? (user.avatarBlurhash ?? user.avatar?.blurhash ?? null) : null, avatarColor: null, // 後方互換性のため isAdmin: user.isAdmin || falsy, isModerator: user.isModerator || falsy, @@ -519,10 +514,10 @@ export const UserRepository = db.getRepository(User).extend({ lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, - bannerUrl: user.banner + bannerUrl: user.bannerId ? (user.bannerUrl ?? (user.banner ? DriveFiles.getPublicUrl(user.banner, false) - : null, - bannerBlurhash: user.banner?.blurhash || null, + : null)) : null, + bannerBlurhash: user.bannerId ? (user.bannerBlurhash ?? user.banner?.blurhash ?? null) : null, bannerColor: null, // 後方互換性のため isSilenced: user.isSilenced || falsy, isSuspended: user.isSuspended || falsy, diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 59b0dad69..3870a8f08 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -409,16 +409,28 @@ export async function createPerson( ), ); - const avatarId = avatar ? avatar.id : null; - const bannerId = banner ? banner.id : null; + const avatarId = avatar?.id ?? null; + const avatarBlurhash = avatar?.blurhash ?? null; + const avatarUrl = avatar ? DriveFiles.getDatabasePrefetchUrl(avatar, true) : null; + const bannerId = banner?.id ?? null; + const bannerBlurhash = banner?.blurhash ?? null; + const bannerUrl = banner ? DriveFiles.getDatabasePrefetchUrl(banner, false) : null; await Users.update(user!.id, { avatarId, + avatarBlurhash, + avatarUrl, bannerId, + bannerBlurhash, + bannerUrl, }); user!.avatarId = avatarId; + user!.avatarBlurhash = avatarBlurhash; + user!.avatarUrl = avatarUrl; user!.bannerId = bannerId; + user!.bannerBlurhash = bannerBlurhash; + user!.bannerUrl = bannerUrl; //#endregion //#region Get custom emoji @@ -576,10 +588,14 @@ export async function updatePerson( if (avatar) { updates.avatarId = avatar.id; + updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(avatar, true); + updates.avatarBlurhash = avatar.blurhash; } if (banner) { updates.bannerId = banner.id; + updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(banner, false); + updates.bannerBlurhash = banner.blurhash; } if (host) { diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index a96c16e9a..12f6b7b97 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -97,16 +97,10 @@ export default define(meta, paramDef, async (ps, user) => { const query = makePaginationQuery(Notes.createQueryBuilder("note")) .where("note.id IN (:...noteIds)", { noteIds: noteIds }) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") .andWhere("note.visibility != 'home'"); generateVisibilityQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 7e1924870..9f9efa10c 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -62,16 +62,10 @@ export default define(meta, paramDef, async (ps, user) => { ) .andWhere("note.channelId = :channelId", { channelId: channel.id }) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") .leftJoinAndSelect("note.channel", "channel"); //#endregion diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index c641d9ba9..a414b4689 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -70,16 +70,10 @@ export default define(meta, paramDef, async (ps, user) => { "clipNote.noteId = note.id", ) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") .andWhere("clipNote.clipId = :clipId", { clipId: clip.id }); if (user) { diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 6e1aabef7..1dbcac8f9 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -100,16 +100,10 @@ export default define(meta, paramDef, async (ps, user) => { .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( diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 84e1f0bb2..31af07cfb 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -215,22 +215,27 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; - if (ps.avatarId) { - const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); + const avatar = ps.avatarId ? await DriveFiles.findOneBy({ id: ps.avatarId }) : null; + const banner = ps.bannerId ? await DriveFiles.findOneBy({ id: ps.bannerId }) : null; + if (ps.avatarId) { if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); if (!avatar.type.startsWith("image/")) throw new ApiError(meta.errors.avatarNotAnImage); + + updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(avatar, true); + updates.avatarBlurhash = avatar.blurhash; } if (ps.bannerId) { - const banner = await DriveFiles.findOneBy({ id: ps.bannerId }); - if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); if (!banner.type.startsWith("image/")) throw new ApiError(meta.errors.bannerNotAnImage); + + updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(banner, false); + updates.bannerBlurhash = banner.blurhash; } if (ps.pinnedPageId) { diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 9787740ab..1f9185847 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -43,16 +43,10 @@ export default define(meta, paramDef, async (ps) => { .andWhere("note.visibility = 'public'") .andWhere("note.localOnly = FALSE") .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); if (ps.local) { query.andWhere("note.userHost IS NULL"); diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index a35b17a02..f0f02566a 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -47,9 +47,7 @@ export default define(meta, paramDef, async (ps, user) => { "note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))", { noteId: ps.noteId, depth: ps.depth, limit: ps.limit }, ) - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner"); + .innerJoinAndSelect("note.user", "user"); generateVisibilityQuery(query, user); if (user) { diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 47c1e1381..04a36f974 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -47,16 +47,10 @@ export default define(meta, paramDef, async (ps, user) => { .andWhere("note.createdAt > :date", { date: new Date(Date.now() - day) }) .andWhere("note.visibility = 'public'") .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); switch (ps.origin) { case "local": diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 0a365a6df..54d0a5425 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -81,16 +81,10 @@ export default define(meta, paramDef, async (ps, user) => { .andWhere("note.visibility = 'public'") .andWhere("note.channelId IS NULL") .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); generateRepliesQuery(query, ps.withReplies, user); if (user) { diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 909b11154..5b1095669 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -97,16 +97,10 @@ export default define(meta, paramDef, async (ps, user) => { }), ) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") .setParameters(followingQuery.getParameters()); generateListQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 0045004c5..e40d5d86b 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -91,16 +91,10 @@ export default define(meta, paramDef, async (ps, user) => { .andWhere("note.visibility = 'public'") .andWhere("note.userHost IS NULL") .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); generateChannelQuery(query, user); generateRepliesQuery(query, ps.withReplies, user); diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 68688b504..a4714b1fc 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -56,16 +56,10 @@ export default define(meta, paramDef, async (ps, user) => { }), ) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts index 992efa4b9..1bdd13e71 100644 --- a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts @@ -91,16 +91,10 @@ export default define(meta, paramDef, async (ps, user) => { .andWhere(`note.userHost IN (:...instances)`, { instances: m.recommendedInstances }) .andWhere("note.visibility = 'public'") .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); generateChannelQuery(query, user); generateRepliesQuery(query, ps.withReplies, user); diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index df801c7fc..7bb74d41d 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -66,16 +66,10 @@ export default define(meta, paramDef, async (ps, user) => { } query - .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"); generateVisibilityQuery(query, user); if (user) generateMutedUserQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 5ea4d479c..98c751963 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -43,16 +43,10 @@ export default define(meta, paramDef, async (ps, user) => { ) .andWhere("note.replyId = :replyId", { replyId: ps.noteId }) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); generateVisibilityQuery(query, user); if (user) generateMutedUserQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index f988acaa5..ae65ee2ac 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -76,16 +76,10 @@ export default define(meta, paramDef, async (ps, me) => { ps.untilId, ) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); generateVisibilityQuery(query, me); if (me) generateMutedUserQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index fd211c254..7c2614586 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -75,16 +75,10 @@ export default define(meta, paramDef, async (ps, me) => { query .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); generateFtsQuery(query, ps.query); generateVisibilityQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 731768ead..99f38245d 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -75,16 +75,10 @@ export default define(meta, paramDef, async (ps, user) => { ps.untilDate, ) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); await generateFollowingQuery(query, user); generateListQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 5c3fc55be..be745fb14 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -80,16 +80,10 @@ export default define(meta, paramDef, async (ps, user) => { "userListJoining.userId = note.userId", ) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") .andWhere("userListJoining.userListId = :userListId", { userListId: list.id, }); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 724cfc9af..157ab3a6d 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -76,16 +76,10 @@ export default define(meta, paramDef, async (ps, me) => { ) .andWhere("note.userId = :userId", { userId: user.id }) .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") .leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + .leftJoinAndSelect("renote.user", "renoteUser"); generateVisibilityQuery(query, me); if (me) { diff --git a/packages/backend/src/server/api/mastodon/converters/user.ts b/packages/backend/src/server/api/mastodon/converters/user.ts index 7ba7385d7..07b9add43 100644 --- a/packages/backend/src/server/api/mastodon/converters/user.ts +++ b/packages/backend/src/server/api/mastodon/converters/user.ts @@ -35,11 +35,11 @@ export class UserConverter { const profile = UserProfiles.findOneBy({ userId: u.id }); const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, u.host).then(p => p ?? escapeMFM(profile?.description ?? ""))); const avatar = u.avatarId - ? (DriveFiles.findOneBy({ id: u.avatarId })) + ? u.avatarUrl ?? (DriveFiles.findOneBy({ id: u.avatarId })) .then(p => p?.url ?? Users.getIdenticonUrl(u.id)) : Users.getIdenticonUrl(u.id); const banner = u.bannerId - ? (DriveFiles.findOneBy({ id: u.bannerId })) + ? u.bannerUrl ?? (DriveFiles.findOneBy({ id: u.bannerId })) .then(p => p?.url ?? `${config.url}/static-assets/transparent.png`) : `${config.url}/static-assets/transparent.png`; diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts index 901334349..77dd6cce8 100644 --- a/packages/backend/src/server/api/mastodon/helpers/user.ts +++ b/packages/backend/src/server/api/mastodon/helpers/user.ts @@ -1,7 +1,7 @@ import { Note } from "@/models/entities/note.js"; import { ILocalUser, IRemoteUser, User } from "@/models/entities/user.js"; import { - Blockings, + Blockings, DriveFiles, Followings, FollowRequests, Mutings, @@ -173,11 +173,15 @@ export class UserHelpers { if (avatar) { const file = await MediaHelpers.uploadMediaBasic(avatar, ctx); updates.avatarId = file.id; + updates.avatarBlurhash = file.blurhash; + updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(file, true); } if (header) { const file = await MediaHelpers.uploadMediaBasic(header, ctx); updates.bannerId = file.id; + updates.bannerBlurhash = file.blurhash; + updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(file, false); } if (formData.fields_attributes) { diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts index 215270df6..37b30be93 100644 --- a/packages/backend/src/services/drive/delete-file.ts +++ b/packages/backend/src/services/drive/delete-file.ts @@ -1,6 +1,6 @@ import type { DriveFile } from "@/models/entities/drive-file.js"; import { InternalStorage } from "./internal-storage.js"; -import { DriveFiles, Instances } from "@/models/index.js"; +import { DriveFiles, Instances, Users } from "@/models/index.js"; import { driveChart, perUserDriveChart, @@ -81,6 +81,8 @@ async function postProcess(file: DriveFile, isExpired = false) { thumbnailAccessKey: `thumbnail-${uuid()}`, webpublicAccessKey: `webpublic-${uuid()}`, }); + Users.update({ avatarId: file.id }, { avatarUrl: file.uri }); + Users.update({ bannerId: file.id }, { bannerUrl: file.uri }); } else { DriveFiles.delete(file.id); }