[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
No known key found for this signature in database
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;
@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",
})

View file

@ -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<number> {

View file

@ -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,

View file

@ -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) {

View file

@ -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);

View file

@ -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

View file

@ -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) {

View file

@ -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(

View file

@ -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) {

View file

@ -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");

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))",
{ 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) {

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.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":

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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,
});

View file

@ -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) {

View file

@ -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`;

View file

@ -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) {

View file

@ -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);
}