add: profile backgrounds

This commit is contained in:
Mar0xy 2023-10-06 02:32:09 +02:00
parent 6dd0b88050
commit 4e64397635
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
14 changed files with 205 additions and 4 deletions

View file

@ -0,0 +1,19 @@
export class Background1696548899000 {
name = 'Background1696548899000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "REL_fwvhvbijn8nocsdpqhn012pfo5" UNIQUE ("backgroundId")`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_q5lm0tbgejtfskzg0rc4wd7t1n" FOREIGN KEY ("backgroundId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "user" ADD "backgroundBlurhash" character varying(128)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "REL_fwvhvbijn8nocsdpqhn012pfo5"`);
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_q5lm0tbgejtfskzg0rc4wd7t1n"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundId"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundUrl"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "backgroundBlurhash"`);
}
}

View file

@ -423,6 +423,10 @@ export class DriveService {
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
}
if (user.backgroundId) {
q.andWhere('file.id != :backgroundId', { backgroundId: user.backgroundId });
}
//This selete is hard coded, be careful if change database schema
q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
q.orderBy('file.id', 'ASC');

View file

@ -454,9 +454,10 @@ export class ApRendererService {
const id = this.userEntityService.genLocalUserUri(user.id);
const isSystem = user.username.includes('.');
const [avatar, banner, profile] = await Promise.all([
const [avatar, banner, background, profile] = await Promise.all([
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
user.backgroundId ? this.driveFilesRepository.findOneBy({ id: user.backgroundId }) : undefined,
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
]);
@ -496,6 +497,7 @@ export class ApRendererService {
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null,
backgroundUrl: background ? this.renderImage(background) : null,
tag,
manuallyApprovesFollowers: user.isLocked,
discoverable: user.isExplorable,
@ -650,6 +652,9 @@ export class ApRendererService {
// Firefish
firefish: "https://joinfirefish.org/ns#",
speakAsCat: "firefish:speakAsCat",
// Sharkey
sharkey: "https://joinsharkey.org/ns#",
backgroundUrl: "sharkey:backgroundUrl",
// vcard
vcard: 'http://www.w3.org/2006/vcard/ns#',
},

View file

@ -225,8 +225,8 @@ export class ApPersonService implements OnModuleInit {
return null;
}
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
const [avatar, banner] = await Promise.all([icon, image].map(img => {
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>> {
const [avatar, banner, background] = await Promise.all([icon, image].map(img => {
if (img == null) return null;
if (user == null) throw new Error('failed to create user: user is null');
return this.apImageService.resolveImage(user, img).catch(() => null);
@ -235,10 +235,13 @@ export class ApPersonService implements OnModuleInit {
return {
avatarId: avatar?.id ?? null,
bannerId: banner?.id ?? null,
backgroundId: background?.id ?? null,
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
backgroundUrl: background ? this.driveFileEntityService.getPublicUrl(background) : null,
avatarBlurhash: avatar?.blurhash ?? null,
bannerBlurhash: banner?.blurhash ?? null,
backgroundBlurhash: background?.blurhash ?? null
};
}

View file

@ -308,6 +308,14 @@ export class UserEntityService implements OnModuleInit {
bannerBlurhash: banner.blurhash,
});
}
if (user.backgroundId != null && user.backgroundUrl === null) {
const background = await this.driveFilesRepository.findOneByOrFail({ id: user.backgroundId });
user.backgroundUrl = this.driveFileEntityService.getPublicUrl(background);
this.usersRepository.update(user.id, {
backgroundUrl: user.backgroundUrl,
backgroundBlurhash: background.blurhash,
});
}
const meId = me ? me.id : null;
const isMe = meId === user.id;
@ -385,6 +393,8 @@ export class UserEntityService implements OnModuleInit {
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
backgroundUrl: user.backgroundUrl,
backgroundBlurhash: user.backgroundBlurhash,
isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
isSuspended: user.isSuspended ?? falsy,
@ -429,6 +439,7 @@ export class UserEntityService implements OnModuleInit {
...(opts.detail && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
backgroundId: user.backgroundId,
isModerator: isModerator,
isAdmin: isAdmin,
injectFeaturedNote: profile!.injectFeaturedNote,

View file

@ -124,6 +124,19 @@ export class MiUser {
@JoinColumn()
public banner: MiDriveFile | null;
@Column({
...id(),
nullable: true,
comment: 'The ID of background DriveFile.',
})
public backgroundId: MiDriveFile['id'] | null;
@OneToOne(type => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()
public background: MiDriveFile | null;
@Column('varchar', {
length: 512, nullable: true,
})
@ -134,6 +147,11 @@ export class MiUser {
})
public bannerUrl: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public backgroundUrl: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
@ -144,6 +162,11 @@ export class MiUser {
})
public bannerBlurhash: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public backgroundBlurhash: string | null;
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}',

View file

@ -122,6 +122,15 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string',
nullable: true, optional: false,
},
backgroundUrl: {
type: 'string',
format: 'url',
nullable: true, optional: false,
},
backgroundBlurhash: {
type: 'string',
nullable: true, optional: false,
},
isLocked: {
type: 'boolean',
nullable: false, optional: false,
@ -304,6 +313,11 @@ export const packedMeDetailedOnlySchema = {
nullable: true, optional: false,
format: 'id',
},
backgroundId: {
type: 'string',
nullable: true, optional: false,
format: 'id',
},
injectFeaturedNote: {
type: 'boolean',
nullable: true, optional: false,

View file

@ -60,6 +60,12 @@ export const meta = {
id: '0d8f5629-f210-41c2-9433-735831a58595',
},
noSuchBackground: {
message: 'No such background file.',
code: 'NO_SUCH_BACKGROUND',
id: '0d8f5629-f210-41c2-9433-735831a58582',
},
avatarNotAnImage: {
message: 'The file specified as an avatar is not an image.',
code: 'AVATAR_NOT_AN_IMAGE',
@ -72,6 +78,12 @@ export const meta = {
id: '75aedb19-2afd-4e6d-87fc-67941256fa60',
},
backgroundNotAnImage: {
message: 'The file specified as a background is not an image.',
code: 'BACKGROUND_NOT_AN_IMAGE',
id: '75aedb19-2afd-4e6d-87fc-67941256fa40',
},
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
@ -133,6 +145,7 @@ export const paramDef = {
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
avatarId: { type: 'string', format: 'misskey:id', nullable: true },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
backgroundId: { type: 'string', format: 'misskey:id', nullable: true },
fields: {
type: 'array',
minItems: 0,
@ -300,6 +313,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.bannerBlurhash = null;
}
if (ps.backgroundId) {
const background = await this.driveFilesRepository.findOneBy({ id: ps.backgroundId });
if (background == null || background.userId !== user.id) throw new ApiError(meta.errors.noSuchBackground);
if (!background.type.startsWith('image/')) throw new ApiError(meta.errors.backgroundNotAnImage);
updates.backgroundId = background.id;
updates.backgroundUrl = this.driveFileEntityService.getPublicUrl(background);
updates.backgroundBlurhash = background.blurhash;
} else if (ps.backgroundId === null) {
updates.backgroundId = null;
updates.backgroundUrl = null;
updates.backgroundBlurhash = null;
}
if (ps.pinnedPageId) {
const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });

View file

@ -95,6 +95,8 @@ describe('ユーザー', () => {
lastFetchedAt: user.lastFetchedAt,
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
backgroundUrl: user.backgroundUrl,
backgroundBlurhash: user.backgroundBlurhash,
isLocked: user.isLocked,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
@ -366,6 +368,8 @@ describe('ユーザー', () => {
assert.strictEqual(response.lastFetchedAt, null);
assert.strictEqual(response.bannerUrl, null);
assert.strictEqual(response.bannerBlurhash, null);
assert.strictEqual(response.backgroundUrl, null);
assert.strictEqual(response.backgroundBlurhash, null);
assert.strictEqual(response.isLocked, false);
assert.strictEqual(response.isSilenced, false);
assert.strictEqual(response.isSuspended, false);
@ -561,6 +565,31 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response2, expected2, inspect(parameters));
});
test('を書き換えることができる(Background)', async () => {
const aliceFile = (await uploadFile(alice)).body;
const parameters = { bannerId: aliceFile.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.backgroundUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.backgroundBlurhash ?? '.', /[ -~]{54}/);
const expected = {
...meDetailed(alice, true),
backgroundId: aliceFile.id,
backgroundBlurhash: response.baackgroundBlurhash,
backgroundUrl: response.backgroundUrl,
};
assert.deepStrictEqual(response, expected, inspect(parameters));
const parameters2 = { backgroundId: null };
const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice });
const expected2 = {
...meDetailed(alice, true),
backgroundId: null,
backgroundBlurhash: null,
backgroundUrl: null,
};
assert.deepStrictEqual(response2, expected2, inspect(parameters));
});
//#endregion
//#region 自分の情報の更新(i/pin, i/unpin)