[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
This commit is contained in:
Laura Hausmann 2023-11-20 22:03:10 +01:00
parent 6e82e18eea
commit 302b112f05
Signed by: zotan
GPG key ID: D044E84C5BE01605
28 changed files with 142 additions and 165 deletions

View file

@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UserAvatarBannerRefactor1700517975122 implements MigrationInterface {
name = 'UserAvatarBannerRefactor1700517975122'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View file

@ -103,6 +103,20 @@ export class User {
}) })
public avatarId: DriveFile["id"] | null; 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, { @OneToOne((type) => DriveFile, {
onDelete: "SET NULL", onDelete: "SET NULL",
}) })
@ -116,6 +130,20 @@ export class User {
}) })
public bannerId: DriveFile["id"] | null; 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, { @OneToOne((type) => DriveFile, {
onDelete: "SET NULL", onDelete: "SET NULL",
}) })

View file

@ -2,13 +2,11 @@ import { db } from "@/db/postgre.js";
import { DriveFile } from "@/models/entities/drive-file.js"; import { DriveFile } from "@/models/entities/drive-file.js";
import type { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import { toPuny } from "@/misc/convert-host.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 type { Packed } from "@/misc/schema.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import { query, appendQuery } from "@/prelude/url.js"; import { appendQuery, query } from "@/prelude/url.js";
import { Meta } from "@/models/entities/meta.js"; import { DriveFolders, Users } from "../index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Users, DriveFolders } from "../index.js";
import { deepClone } from "@/misc/clone.js"; import { deepClone } from "@/misc/clone.js";
type PackOptions = { type PackOptions = {
@ -44,6 +42,19 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
return file.properties; 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 { getPublicUrl(file: DriveFile, thumbnail = false): string | null {
// リモートかつメディアプロキシ // リモートかつメディアプロキシ
if ( 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 return thumbnail
? file.thumbnailUrl || (isImage ? file.webpublicUrl || file.url : null) ? file.thumbnailUrl || (this.isImage(file) ? file.webpublicUrl || file.url : null)
: file.webpublicUrl || file.url; : 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( async calcDriveUsageOf(
user: User["id"] | { id: User["id"] }, user: User["id"] | { id: User["id"] },
): Promise<number> { ): Promise<number> {

View file

@ -339,6 +339,7 @@ export const UserRepository = db.getRepository(User).extend({
this.getIdenticonUrl(user.id) this.getIdenticonUrl(user.id)
); );
} else if (user.avatarId) { } else if (user.avatarId) {
if (user.avatarUrl) return user.avatarUrl;
const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId }); const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId });
return ( return (
DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id) DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id)
@ -349,7 +350,9 @@ export const UserRepository = db.getRepository(User).extend({
}, },
getAvatarUrlSync(user: User): string { getAvatarUrlSync(user: User): string {
if (user.avatar) { if (user.avatarId && user.avatarUrl) {
return user.avatarUrl;
} else if (user.avatar) {
return ( return (
DriveFiles.getPublicUrl(user.avatar, true) || DriveFiles.getPublicUrl(user.avatar, true) ||
this.getIdenticonUrl(user.id) this.getIdenticonUrl(user.id)
@ -388,17 +391,9 @@ export const UserRepository = db.getRepository(User).extend({
if (typeof src === "object") { if (typeof src === "object") {
user = src; 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 { } else {
user = await this.findOneOrFail({ user = await this.findOneOrFail({
where: { id: src }, where: { id: src },
relations: {
avatar: true,
banner: true,
},
}); });
} }
@ -474,7 +469,7 @@ export const UserRepository = db.getRepository(User).extend({
username: user.username, username: user.username,
host: user.host, host: user.host,
avatarUrl: this.getAvatarUrlSync(user), avatarUrl: this.getAvatarUrlSync(user),
avatarBlurhash: user.avatar?.blurhash || null, avatarBlurhash: user.avatarId ? (user.avatarBlurhash ?? user.avatar?.blurhash ?? null) : null,
avatarColor: null, // 後方互換性のため avatarColor: null, // 後方互換性のため
isAdmin: user.isAdmin || falsy, isAdmin: user.isAdmin || falsy,
isModerator: user.isModerator || falsy, isModerator: user.isModerator || falsy,
@ -519,10 +514,10 @@ export const UserRepository = db.getRepository(User).extend({
lastFetchedAt: user.lastFetchedAt lastFetchedAt: user.lastFetchedAt
? user.lastFetchedAt.toISOString() ? user.lastFetchedAt.toISOString()
: null, : null,
bannerUrl: user.banner bannerUrl: user.bannerId ? (user.bannerUrl ?? (user.banner
? DriveFiles.getPublicUrl(user.banner, false) ? DriveFiles.getPublicUrl(user.banner, false)
: null, : null)) : null,
bannerBlurhash: user.banner?.blurhash || null, bannerBlurhash: user.bannerId ? (user.bannerBlurhash ?? user.banner?.blurhash ?? null) : null,
bannerColor: null, // 後方互換性のため bannerColor: null, // 後方互換性のため
isSilenced: user.isSilenced || falsy, isSilenced: user.isSilenced || falsy,
isSuspended: user.isSuspended || falsy, isSuspended: user.isSuspended || falsy,

View file

@ -409,16 +409,28 @@ export async function createPerson(
), ),
); );
const avatarId = avatar ? avatar.id : null; const avatarId = avatar?.id ?? null;
const bannerId = banner ? banner.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, { await Users.update(user!.id, {
avatarId, avatarId,
avatarBlurhash,
avatarUrl,
bannerId, bannerId,
bannerBlurhash,
bannerUrl,
}); });
user!.avatarId = avatarId; user!.avatarId = avatarId;
user!.avatarBlurhash = avatarBlurhash;
user!.avatarUrl = avatarUrl;
user!.bannerId = bannerId; user!.bannerId = bannerId;
user!.bannerBlurhash = bannerBlurhash;
user!.bannerUrl = bannerUrl;
//#endregion //#endregion
//#region Get custom emoji //#region Get custom emoji
@ -576,10 +588,14 @@ export async function updatePerson(
if (avatar) { if (avatar) {
updates.avatarId = avatar.id; updates.avatarId = avatar.id;
updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(avatar, true);
updates.avatarBlurhash = avatar.blurhash;
} }
if (banner) { if (banner) {
updates.bannerId = banner.id; updates.bannerId = banner.id;
updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(banner, false);
updates.bannerBlurhash = banner.blurhash;
} }
if (host) { if (host) {

View file

@ -97,16 +97,10 @@ export default define(meta, paramDef, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder("note")) const query = makePaginationQuery(Notes.createQueryBuilder("note"))
.where("note.id IN (:...noteIds)", { noteIds: noteIds }) .where("note.id IN (:...noteIds)", { noteIds: noteIds })
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser") .leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
.andWhere("note.visibility != 'home'"); .andWhere("note.visibility != 'home'");
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);

View file

@ -62,16 +62,10 @@ export default define(meta, paramDef, async (ps, user) => {
) )
.andWhere("note.channelId = :channelId", { channelId: channel.id }) .andWhere("note.channelId = :channelId", { channelId: channel.id })
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser") .leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
.leftJoinAndSelect("note.channel", "channel"); .leftJoinAndSelect("note.channel", "channel");
//#endregion //#endregion

View file

@ -70,16 +70,10 @@ export default define(meta, paramDef, async (ps, user) => {
"clipNote.noteId = note.id", "clipNote.noteId = note.id",
) )
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser") .leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
.andWhere("clipNote.clipId = :clipId", { clipId: clip.id }); .andWhere("clipNote.clipId = :clipId", { clipId: clip.id });
if (user) { if (user) {

View file

@ -100,16 +100,10 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect("notifier.avatar", "notifierAvatar") .leftJoinAndSelect("notifier.avatar", "notifierAvatar")
.leftJoinAndSelect("notifier.banner", "notifierBanner") .leftJoinAndSelect("notifier.banner", "notifierBanner")
.leftJoinAndSelect("note.user", "user") .leftJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
// muted users // muted users
query.andWhere( query.andWhere(

View file

@ -215,22 +215,27 @@ export default define(meta, paramDef, async (ps, _user, token) => {
if (ps.emailNotificationTypes !== undefined) if (ps.emailNotificationTypes !== undefined)
profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
if (ps.avatarId) { const avatar = ps.avatarId ? await DriveFiles.findOneBy({ id: ps.avatarId }) : null;
const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); const banner = ps.bannerId ? await DriveFiles.findOneBy({ id: ps.bannerId }) : null;
if (ps.avatarId) {
if (avatar == null || avatar.userId !== user.id) if (avatar == null || avatar.userId !== user.id)
throw new ApiError(meta.errors.noSuchAvatar); throw new ApiError(meta.errors.noSuchAvatar);
if (!avatar.type.startsWith("image/")) if (!avatar.type.startsWith("image/"))
throw new ApiError(meta.errors.avatarNotAnImage); throw new ApiError(meta.errors.avatarNotAnImage);
updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(avatar, true);
updates.avatarBlurhash = avatar.blurhash;
} }
if (ps.bannerId) { if (ps.bannerId) {
const banner = await DriveFiles.findOneBy({ id: ps.bannerId });
if (banner == null || banner.userId !== user.id) if (banner == null || banner.userId !== user.id)
throw new ApiError(meta.errors.noSuchBanner); throw new ApiError(meta.errors.noSuchBanner);
if (!banner.type.startsWith("image/")) if (!banner.type.startsWith("image/"))
throw new ApiError(meta.errors.bannerNotAnImage); throw new ApiError(meta.errors.bannerNotAnImage);
updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(banner, false);
updates.bannerBlurhash = banner.blurhash;
} }
if (ps.pinnedPageId) { if (ps.pinnedPageId) {

View file

@ -43,16 +43,10 @@ export default define(meta, paramDef, async (ps) => {
.andWhere("note.visibility = 'public'") .andWhere("note.visibility = 'public'")
.andWhere("note.localOnly = FALSE") .andWhere("note.localOnly = FALSE")
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
if (ps.local) { if (ps.local) {
query.andWhere("note.userHost IS NULL"); query.andWhere("note.userHost IS NULL");

View file

@ -47,9 +47,7 @@ export default define(meta, paramDef, async (ps, user) => {
"note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))", "note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))",
{ noteId: ps.noteId, depth: ps.depth, limit: ps.limit }, { noteId: ps.noteId, depth: ps.depth, limit: ps.limit },
) )
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user");
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner");
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) { if (user) {

View file

@ -47,16 +47,10 @@ export default define(meta, paramDef, async (ps, user) => {
.andWhere("note.createdAt > :date", { date: new Date(Date.now() - day) }) .andWhere("note.createdAt > :date", { date: new Date(Date.now() - day) })
.andWhere("note.visibility = 'public'") .andWhere("note.visibility = 'public'")
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
switch (ps.origin) { switch (ps.origin) {
case "local": case "local":

View file

@ -81,16 +81,10 @@ export default define(meta, paramDef, async (ps, user) => {
.andWhere("note.visibility = 'public'") .andWhere("note.visibility = 'public'")
.andWhere("note.channelId IS NULL") .andWhere("note.channelId IS NULL")
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateRepliesQuery(query, ps.withReplies, user); generateRepliesQuery(query, ps.withReplies, user);
if (user) { if (user) {

View file

@ -97,16 +97,10 @@ export default define(meta, paramDef, async (ps, user) => {
}), }),
) )
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser") .leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
.setParameters(followingQuery.getParameters()); .setParameters(followingQuery.getParameters());
generateListQuery(query, user); generateListQuery(query, user);

View file

@ -91,16 +91,10 @@ export default define(meta, paramDef, async (ps, user) => {
.andWhere("note.visibility = 'public'") .andWhere("note.visibility = 'public'")
.andWhere("note.userHost IS NULL") .andWhere("note.userHost IS NULL")
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateChannelQuery(query, user); generateChannelQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user); generateRepliesQuery(query, ps.withReplies, user);

View file

@ -56,16 +56,10 @@ export default define(meta, paramDef, async (ps, user) => {
}), }),
) )
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user); generateMutedUserQuery(query, user);

View file

@ -91,16 +91,10 @@ export default define(meta, paramDef, async (ps, user) => {
.andWhere(`note.userHost IN (:...instances)`, { instances: m.recommendedInstances }) .andWhere(`note.userHost IN (:...instances)`, { instances: m.recommendedInstances })
.andWhere("note.visibility = 'public'") .andWhere("note.visibility = 'public'")
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateChannelQuery(query, user); generateChannelQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user); generateRepliesQuery(query, ps.withReplies, user);

View file

@ -66,16 +66,10 @@ export default define(meta, paramDef, async (ps, user) => {
} }
query query
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user); if (user) generateMutedUserQuery(query, user);

View file

@ -43,16 +43,10 @@ export default define(meta, paramDef, async (ps, user) => {
) )
.andWhere("note.replyId = :replyId", { replyId: ps.noteId }) .andWhere("note.replyId = :replyId", { replyId: ps.noteId })
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user); if (user) generateMutedUserQuery(query, user);

View file

@ -76,16 +76,10 @@ export default define(meta, paramDef, async (ps, me) => {
ps.untilId, ps.untilId,
) )
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me); if (me) generateMutedUserQuery(query, me);

View file

@ -75,16 +75,10 @@ export default define(meta, paramDef, async (ps, me) => {
query query
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateFtsQuery(query, ps.query); generateFtsQuery(query, ps.query);
generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);

View file

@ -75,16 +75,10 @@ export default define(meta, paramDef, async (ps, user) => {
ps.untilDate, ps.untilDate,
) )
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
await generateFollowingQuery(query, user); await generateFollowingQuery(query, user);
generateListQuery(query, user); generateListQuery(query, user);

View file

@ -80,16 +80,10 @@ export default define(meta, paramDef, async (ps, user) => {
"userListJoining.userId = note.userId", "userListJoining.userId = note.userId",
) )
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser") .leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
.andWhere("userListJoining.userListId = :userListId", { .andWhere("userListJoining.userListId = :userListId", {
userListId: list.id, userListId: list.id,
}); });

View file

@ -76,16 +76,10 @@ export default define(meta, paramDef, async (ps, me) => {
) )
.andWhere("note.userId = :userId", { userId: user.id }) .andWhere("note.userId = :userId", { userId: user.id })
.innerJoinAndSelect("note.user", "user") .innerJoinAndSelect("note.user", "user")
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply") .leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser") .leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") .leftJoinAndSelect("renote.user", "renoteUser");
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);
if (me) { if (me) {

View file

@ -35,11 +35,11 @@ export class UserConverter {
const profile = UserProfiles.findOneBy({ userId: u.id }); 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 bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, u.host).then(p => p ?? escapeMFM(profile?.description ?? "")));
const avatar = u.avatarId const avatar = u.avatarId
? (DriveFiles.findOneBy({ id: u.avatarId })) ? u.avatarUrl ?? (DriveFiles.findOneBy({ id: u.avatarId }))
.then(p => p?.url ?? Users.getIdenticonUrl(u.id)) .then(p => p?.url ?? Users.getIdenticonUrl(u.id))
: Users.getIdenticonUrl(u.id); : Users.getIdenticonUrl(u.id);
const banner = u.bannerId 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`) .then(p => p?.url ?? `${config.url}/static-assets/transparent.png`)
: `${config.url}/static-assets/transparent.png`; : `${config.url}/static-assets/transparent.png`;

View file

@ -1,7 +1,7 @@
import { Note } from "@/models/entities/note.js"; import { Note } from "@/models/entities/note.js";
import { ILocalUser, IRemoteUser, User } from "@/models/entities/user.js"; import { ILocalUser, IRemoteUser, User } from "@/models/entities/user.js";
import { import {
Blockings, Blockings, DriveFiles,
Followings, Followings,
FollowRequests, FollowRequests,
Mutings, Mutings,
@ -173,11 +173,15 @@ export class UserHelpers {
if (avatar) { if (avatar) {
const file = await MediaHelpers.uploadMediaBasic(avatar, ctx); const file = await MediaHelpers.uploadMediaBasic(avatar, ctx);
updates.avatarId = file.id; updates.avatarId = file.id;
updates.avatarBlurhash = file.blurhash;
updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(file, true);
} }
if (header) { if (header) {
const file = await MediaHelpers.uploadMediaBasic(header, ctx); const file = await MediaHelpers.uploadMediaBasic(header, ctx);
updates.bannerId = file.id; updates.bannerId = file.id;
updates.bannerBlurhash = file.blurhash;
updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(file, false);
} }
if (formData.fields_attributes) { if (formData.fields_attributes) {

View file

@ -1,6 +1,6 @@
import type { DriveFile } from "@/models/entities/drive-file.js"; import type { DriveFile } from "@/models/entities/drive-file.js";
import { InternalStorage } from "./internal-storage.js"; import { InternalStorage } from "./internal-storage.js";
import { DriveFiles, Instances } from "@/models/index.js"; import { DriveFiles, Instances, Users } from "@/models/index.js";
import { import {
driveChart, driveChart,
perUserDriveChart, perUserDriveChart,
@ -81,6 +81,8 @@ async function postProcess(file: DriveFile, isExpired = false) {
thumbnailAccessKey: `thumbnail-${uuid()}`, thumbnailAccessKey: `thumbnail-${uuid()}`,
webpublicAccessKey: `webpublic-${uuid()}`, webpublicAccessKey: `webpublic-${uuid()}`,
}); });
Users.update({ avatarId: file.id }, { avatarUrl: file.uri });
Users.update({ bannerId: file.id }, { bannerUrl: file.uri });
} else { } else {
DriveFiles.delete(file.id); DriveFiles.delete(file.id);
} }