feat(profile): 相互リンク機能の追加 (MisskeyIO#675)

This commit is contained in:
まっちゃてぃー。 2024-08-10 01:05:19 +09:00 committed by GitHub
parent b059162324
commit b6a5a36eaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1130 additions and 20 deletions

40
locales/index.d.ts vendored
View File

@ -2400,6 +2400,14 @@ export interface Locale extends ILocale {
*
*/
"unsetUserBannerConfirm": string;
/**
*
*/
"unsetUserMutualBanner": string;
/**
*
*/
"unsetUserMutualBannerConfirm": string;
/**
*
*/
@ -5083,6 +5091,18 @@ export interface Locale extends ILocale {
*
*/
"here": string;
/**
*
*/
"mutualBanner": string;
/**
*
*/
"mutualBannerThisUser": string;
/**
*
*/
"maximum": string;
"_bubbleGame": {
/**
*
@ -8212,6 +8232,10 @@ export interface Locale extends ILocale {
*
*/
"write:admin:unset-user-banner": string;
/**
*
*/
"write:admin:unset-user-mutual-banner": string;
/**
*
*/
@ -8798,6 +8822,22 @@ export interface Locale extends ILocale {
* {max}
*/
"avatarDecorationMax": ParameterizedString<"max">;
/**
*
*/
"myMutualBanner": string;
/**
*
*/
"myMutualBannerDescription": string;
/**
*
*/
"mutualBanner": string;
/**
*
*/
"mutualBannerDescriptionEdit": string;
};
"_exportOrImport": {
/**

View File

@ -596,6 +596,8 @@ unsetUserAvatar: "アイコンを解除"
unsetUserAvatarConfirm: "アイコンを解除しますか?"
unsetUserBanner: "バナーを解除"
unsetUserBannerConfirm: "バナーを解除しますか?"
unsetUserMutualBanner: "相互バナーを解除"
unsetUserMutualBannerConfirm: "相互バナーを解除しますか?"
deleteAllFiles: "すべてのファイルを削除"
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
removeAllFollowing: "フォローを全解除"
@ -1266,6 +1268,9 @@ reportComplete: "通報完了"
blockThisUser: "このユーザーをブロックする"
muteThisUser: "このユーザーをミュートする"
here: "こちら"
mutualBanner: "相互バナー"
mutualBannerThisUser: "このユーザーのバナー"
maximum: "最大"
_bubbleGame:
howToPlay: "遊び方"
@ -2152,6 +2157,7 @@ _permissions:
"write:admin:suspend-user": "ユーザーを凍結する"
"write:admin:unset-user-avatar": "ユーザーのアバターを削除する"
"write:admin:unset-user-banner": "ユーザーのバーナーを削除する"
"write:admin:unset-user-mutual-banner": "ユーザーの相互バナーを削除する"
"write:admin:unsuspend-user": "ユーザーの凍結を解除する"
"write:admin:meta": "インスタンスのメタデータを操作する"
"write:admin:user-note": "モデレーションノートを操作する"
@ -2313,6 +2319,10 @@ _profile:
changeBanner: "バナー画像を変更"
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
myMutualBanner: "自身の相互リンクのバナーを設定"
myMutualBannerDescription: "あなた自身が相互リンクのバナーとして設定してほしい画像を設定することができます。"
mutualBanner: "相互リンクのバナー"
mutualBannerDescriptionEdit: "説明"
_exportOrImport:
allNotes: "全てのノート"

View File

@ -0,0 +1,27 @@
export class MutualBanner1723213482131 {
name = 'MutualBanner1723213482131'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "user_banner" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "description" character varying(1024), "url" character varying(1024), "fileId" character varying(32) NOT NULL, CONSTRAINT "PK_0d9a418f048e308dbfb6562149d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_fa06ea2e2375449537ced781f1" ON "user_banner" ("userId") `);
await queryRunner.query(`CREATE TABLE "user_banner_pining" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "pinnedBannerId" character varying(32) NOT NULL, CONSTRAINT "PK_970d24f72e8d2b20f8c21ec5d11" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_3b74dc21b68da606011c81609c" ON "user_banner_pining" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_7d51b5a8ae859e0023a98837a1" ON "user_banner_pining" ("userId", "pinnedBannerId") `);
await queryRunner.query(`ALTER TABLE "user_banner" ADD CONSTRAINT "FK_fa06ea2e2375449537ced781f15" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_banner" ADD CONSTRAINT "FK_3de9f17cce2c10f6938fb261c0b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_banner_pining" ADD CONSTRAINT "FK_3b74dc21b68da606011c81609c9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_banner_pining" ADD CONSTRAINT "FK_d13be8242980f7018d664f780f6" FOREIGN KEY ("pinnedBannerId") REFERENCES "user_banner"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_banner_pining" DROP CONSTRAINT "FK_d13be8242980f7018d664f780f6"`);
await queryRunner.query(`ALTER TABLE "user_banner_pining" DROP CONSTRAINT "FK_3b74dc21b68da606011c81609c9"`);
await queryRunner.query(`ALTER TABLE "user_banner" DROP CONSTRAINT "FK_3de9f17cce2c10f6938fb261c0b"`);
await queryRunner.query(`ALTER TABLE "user_banner" DROP CONSTRAINT "FK_fa06ea2e2375449537ced781f15"`);
await queryRunner.query(`DROP INDEX "public"."IDX_7d51b5a8ae859e0023a98837a1"`);
await queryRunner.query(`DROP INDEX "public"."IDX_3b74dc21b68da606011c81609c"`);
await queryRunner.query(`DROP TABLE "user_banner_pining"`);
await queryRunner.query(`DROP INDEX "public"."IDX_fa06ea2e2375449537ced781f1"`);
await queryRunner.query(`DROP TABLE "user_banner"`);
}
}

View File

@ -5,6 +5,8 @@
import { Module } from '@nestjs/common';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@ -35,6 +37,8 @@ import { ModerationLogService } from './ModerationLogService.js';
import { NoteCreateService } from './NoteCreateService.js';
import { NoteDeleteService } from './NoteDeleteService.js';
import { NotePiningService } from './NotePiningService.js';
import { UserBannerPiningService } from './UserBannerPiningService.js';
import { UserBannerService } from './UserBannerService.js';
import { NoteReadService } from './NoteReadService.js';
import { NotificationService } from './NotificationService.js';
import { PollService } from './PollService.js';
@ -173,6 +177,8 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
const $UserBannerPiningService: Provider = { provide: 'UserBannerPiningService', useExisting: UserBannerPiningService };
const $UserBannerService: Provider = { provide: 'UserBannerService', useExisting: UserBannerService };
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
@ -253,6 +259,8 @@ const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', use
const $SigninEntityService: Provider = { provide: 'SigninEntityService', useExisting: SigninEntityService };
const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting: UserEntityService };
const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService };
const $UserBannerEntityService: Provider = { provide: 'UserBannerEntityService', useExisting: UserBannerEntityService };
const $UserBannerPiningEntityService: Provider = { provide: 'UserBannerPiningEntityService', useExisting: UserBannerPiningEntityService };
const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
@ -315,6 +323,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteCreateService,
NoteDeleteService,
NotePiningService,
UserBannerPiningService,
UserBannerService,
NoteReadService,
NotificationService,
PollService,
@ -393,6 +403,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SigninEntityService,
UserEntityService,
UserListEntityService,
UserBannerEntityService,
UserBannerPiningEntityService,
FlashEntityService,
FlashLikeEntityService,
RoleEntityService,
@ -451,6 +463,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteCreateService,
$NoteDeleteService,
$NotePiningService,
$UserBannerService,
$UserBannerPiningService,
$NoteReadService,
$NotificationService,
$PollService,
@ -529,6 +543,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SigninEntityService,
$UserEntityService,
$UserListEntityService,
$UserBannerEntityService,
$UserBannerPiningEntityService,
$FlashEntityService,
$FlashLikeEntityService,
$RoleEntityService,
@ -588,6 +604,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteCreateService,
NoteDeleteService,
NotePiningService,
UserBannerService,
UserBannerPiningService,
NoteReadService,
NotificationService,
PollService,
@ -665,6 +683,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SigninEntityService,
UserEntityService,
UserListEntityService,
UserBannerEntityService,
UserBannerPiningEntityService,
FlashEntityService,
FlashLikeEntityService,
RoleEntityService,
@ -723,6 +743,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteCreateService,
$NoteDeleteService,
$NotePiningService,
$UserBannerService,
$UserBannerPiningService,
$NoteReadService,
$NotificationService,
$PollService,
@ -800,6 +822,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SigninEntityService,
$UserEntityService,
$UserListEntityService,
$UserBannerEntityService,
$UserBannerPiningEntityService,
$FlashEntityService,
$FlashLikeEntityService,
$RoleEntityService,

View File

@ -0,0 +1,55 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { MiUser } from '@/models/User.js';
import { MiUserBanner } from '@/models/UserBanner.js';
import type { MiUserBannerPining, UserBannerPiningRepository, UserBannerRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class UserBannerPiningService {
constructor(
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.userBannerPiningRepository)
private userBannerPiningRepository: UserBannerPiningRepository,
private idService: IdService,
) {
}
/**
*
* @param userId
* @param bannerIds
*/
public async addPinned(userId: MiUser['id'], bannerIds: MiUserBanner['id'][]) {
const pinsToInsert = bannerIds.map(bannerId => ({
id: this.idService.gen(),
userId,
pinnedBannerId: bannerId,
} as MiUserBannerPining));
await this.userBannerPiningRepository
.createQueryBuilder()
.insert()
.values(pinsToInsert)
.orIgnore()
.execute();
}
/**
*
* @param userId
* @param bannerIds
*/
@bindThis
public async removePinned(userId:MiUser['id'], bannerIds:MiUserBanner['id'][]) {
await this.userBannerPiningRepository.delete({
userId,
pinnedBannerId: In(bannerIds),
});
}
}

View File

@ -0,0 +1,115 @@
import { Inject, Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { MiUser } from '@/models/User.js';
import { MiUserBanner } from '@/models/UserBanner.js';
import type { DriveFilesRepository, MiDriveFile, UserBannerRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class UserBannerService {
constructor(
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private idService: IdService,
) {
}
/**
*
* @param userId
* @param description
* @param url
* @param fileId
*/
@bindThis
public async create(userId: MiUser['id'], description: string | null, url: string, fileId: MiDriveFile['id']) {
const banner = await this.userBannerRepository.findOneBy({
userId,
});
if (banner) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'Already exists.');
const file = await this.driveFilesRepository.findOneBy({
id: fileId,
});
if (file == null) throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.');
return await this.userBannerRepository.insert({
id: this.idService.gen(),
userId,
description: description ?? null,
fileId: file.id,
url: url,
} as MiUserBanner);
}
/**
*
* @param userId
* @param bannerId
* @param description
* @param url
* @param fileId
*/
@bindThis
public async update(userId: MiUser['id'], bannerId: MiUserBanner['id'], description: string | null, url: string | null, fileId: MiDriveFile['id'] ) {
const banner = await this.userBannerRepository.findOneBy({
id: bannerId,
});
if (banner == null) {
throw new IdentifiableError('ac26da32-1659-4fbb-82c2-fc11a494799f', 'No such banner.');
}
if (banner.userId !== userId) {
throw new IdentifiableError('dfe79730-96f7-4d65-8c2a-b0975bf3524c', 'Not this user banner.');
}
const file = await this.driveFilesRepository.findOneBy({
id: fileId,
});
if (file == null) {
throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.');
}
await this.userBannerRepository.update({
id: bannerId,
}, {
description: description ?? null,
fileId: file.id,
url: url ?? null,
});
}
/**
*
* @param userId
* @param bannerId
*/
@bindThis
public async delete(userId: MiUser['id'], bannerId: MiUserBanner['id']) {
const banner = await this.userBannerRepository.findOneBy({
id: bannerId,
});
if (banner == null) {
throw new IdentifiableError('f4b158a5-610f-4ed3-b228-3507ebe1bba6', 'No such banner.');
}
if (banner.userId !== userId) {
throw new IdentifiableError('ad84053d-0cf4-4446-ac72-209adef15835', 'Not this user banner.');
}
await this.userBannerRepository.delete({
id: bannerId,
});
}
}

View File

@ -0,0 +1,56 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { bindThis } from '@/decorators.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, MiUserBanner, UserBannerRepository } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { Packed } from '@/misc/json-schema.js';
@Injectable()
export class UserBannerEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
constructor(
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private moduleRef: ModuleRef,
) {
}
async onModuleInit() {
this.userEntityService = this.moduleRef.get(UserEntityService.name);
}
@bindThis
public async pack(
src: MiUserBanner | MiUserBanner['id'] | null | undefined,
me: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'UserBanner'>> {
if (!src) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'No such banner.');
const banner = typeof src === 'object' ? src : await this.userBannerRepository.findOneByOrFail({ id: src });
const file = await this.driveFilesRepository.findOneByOrFail({ id: banner.fileId });
return {
id: banner.id,
user: await this.userEntityService.pack(banner.userId, me),
description: banner.description,
imgUrl: file.url,
url: banner.url,
fileId: file.id,
};
}
@bindThis
public async packMany(
src: MiUserBanner[] | MiUserBanner['id'][],
me: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'UserBanner'>[]> {
return (await Promise.allSettled(src.map(x => this.pack(x, me))))
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<'UserBanner'>>).value);
}
}

View File

@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserBannerPining } from '@/models/_.js';
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
import { Packed } from '@/misc/json-schema.js';
@Injectable()
export class UserBannerPiningEntityService {
constructor(
private userBannerEntityService: UserBannerEntityService,
) {}
@bindThis
public async packMany(
src: MiUserBannerPining[],
me: { id: MiUser['id'] } | null | undefined,
) : Promise<Packed<'UserBanner'>[]> {
return (await Promise.allSettled(src.map(pining => this.userBannerEntityService.pack(pining.pinnedBannerId, me))))
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<'UserBanner'>>).value);
}
}

View File

@ -28,16 +28,20 @@ import type {
FollowingsRepository,
FollowRequestsRepository,
MiFollowing,
MiUserBanner,
MiUserNotePining,
MiUserProfile,
MutingsRepository,
NoteUnreadsRepository,
RenoteMutingsRepository,
UserBannerRepository,
UserBannerPiningRepository,
UserMemoRepository,
UserNotePiningsRepository,
UserProfilesRepository,
UserSecurityKeysRepository,
UsersRepository,
MiUserBannerPining,
} from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@ -49,9 +53,10 @@ import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
const Ajv = _Ajv.default;
@ -130,11 +135,19 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.userNotePiningsRepository)
private userNotePiningsRepository: UserNotePiningsRepository,
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.userBannerPiningRepository)
private userBannerPiningRepository: UserBannerPiningRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository,
private userBannerEntityService: UserBannerEntityService,
private userBannerPiningEntityService: UserBannerPiningEntityService,
) {
}
@ -444,6 +457,8 @@ export class UserEntityService implements OnModuleInit {
}
let pins: MiUserNotePining[] = [];
let myMutualBanner: MiUserBanner | null = null;
let mutualBanners: MiUserBannerPining[] = [];
if (isDetailed) {
if (opts.pinNotes) {
pins = opts.pinNotes.get(user.id) ?? [];
@ -454,6 +469,12 @@ export class UserEntityService implements OnModuleInit {
.orderBy('pin.id', 'DESC')
.getMany();
}
if (user.id) {
[myMutualBanner, mutualBanners] = await Promise.all([
this.userBannerRepository.findOneBy({ userId: user.id }),
this.userBannerPiningRepository.findBy({ userId: user.id }),
]);
}
}
const followingCount = profile == null ? null :
@ -534,6 +555,8 @@ export class UserEntityService implements OnModuleInit {
lang: profile!.lang,
fields: profile!.fields,
verifiedLinks: profile!.verifiedLinks,
mutualBanners: mutualBanners.length > 0 ? this.userBannerPiningEntityService.packMany(mutualBanners, me) : [],
myMutualBanner: myMutualBanner ? this.userBannerEntityService.pack(myMutualBanner, me) : null,
followersCount: followersCount ?? 0,
followingCount: followingCount ?? 0,
notesCount: user.notesCount,
@ -563,7 +586,7 @@ export class UserEntityService implements OnModuleInit {
isModerator: role.isModerator,
isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder,
}))
})),
),
memo: memo,
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
@ -704,7 +727,7 @@ export class UserEntityService implements OnModuleInit {
}
}
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
}

View File

@ -28,6 +28,8 @@ export const DI = {
pollsRepository: Symbol('pollsRepository'),
pollVotesRepository: Symbol('pollVotesRepository'),
userProfilesRepository: Symbol('userProfilesRepository'),
userBannerRepository: Symbol('userBannerRepository'),
userBannerPiningRepository: Symbol('userBannerPiningRepository'),
userKeypairsRepository: Symbol('userKeypairsRepository'),
userPendingsRepository: Symbol('userPendingsRepository'),
userSecurityKeysRepository: Symbol('userSecurityKeysRepository'),

View File

@ -11,6 +11,7 @@ import {
packedMeDetailedSchema,
packedUserDetailedSchema,
packedUserSchema,
packedUserBannerSchema,
} from '@/models/json-schema/user.js';
import { packedAbuseUserReportSchema } from '@/models/json-schema/abuse-user-report.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@ -68,6 +69,7 @@ export const refs = {
MeDetailed: packedMeDetailedSchema,
UserDetailed: packedUserDetailedSchema,
User: packedUserSchema,
UserBanner: packedUserBannerSchema,
UserList: packedUserListSchema,
UserListMembership: packedUserListMembershipSchema,

View File

@ -76,6 +76,8 @@ import {
MiWebhook,
MiBubbleGameRecord,
MiReversiGame,
MiUserBannerPining,
MiUserBanner,
} from './_.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@ -206,6 +208,18 @@ const $userNotePiningsRepository: Provider = {
inject: [DI.db],
};
const $userBannerRepository: Provider = {
provide: DI.userBannerRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserBanner),
inject: [DI.db],
};
const $userBannerPiningRepository: Provider = {
provide: DI.userBannerPiningRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserBannerPining),
inject: [DI.db],
};
const $userIpsRepository: Provider = {
provide: DI.userIpsRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserIp),
@ -525,6 +539,8 @@ const $abuseReportResolversRepository: Provider = {
$userListFavoritesRepository,
$userListMembershipsRepository,
$userNotePiningsRepository,
$userBannerPiningRepository,
$userBannerRepository,
$userIpsRepository,
$usedUsernamesRepository,
$followingsRepository,
@ -597,6 +613,8 @@ const $abuseReportResolversRepository: Provider = {
$userListFavoritesRepository,
$userListMembershipsRepository,
$userNotePiningsRepository,
$userBannerPiningRepository,
$userBannerRepository,
$userIpsRepository,
$usedUsernamesRepository,
$followingsRepository,

View File

@ -0,0 +1,42 @@
import { Entity, Column, Index, JoinColumn, PrimaryColumn, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiDriveFile } from './DriveFile.js';
@Entity('user_banner')
export class MiUserBanner {
@PrimaryColumn(id())
public id: string;
@Index()
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public description: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public url: string | null;
@Column({
...id(),
})
public fileId: MiDriveFile['id'];
@ManyToOne(type => MiDriveFile, {
onDelete: 'CASCADE',
})
@JoinColumn()
public file: MiDriveFile;
}

View File

@ -0,0 +1,32 @@
import { Entity, Column, Index, JoinColumn, PrimaryColumn, ManyToOne, OneToOne } from 'typeorm';
import { MiUserBanner } from '@/models/UserBanner.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@Entity('user_banner_pining')
@Index(['userId', 'pinnedBannerId'], { unique: true })
export class MiUserBannerPining {
@PrimaryColumn(id())
public id: string;
@Index()
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Column({
...id(),
})
public pinnedBannerId: MiUserBanner['id'];
@ManyToOne(type => MiUserBanner, {
onDelete: 'CASCADE',
})
@JoinColumn()
public pinnedBanner: MiUserBanner;
}

View File

@ -4,7 +4,7 @@
*/
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
import { followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiPage } from './Page.js';

View File

@ -60,6 +60,8 @@ import { MiUserListMembership } from '@/models/UserListMembership.js';
import { MiUserNotePining } from '@/models/UserNotePining.js';
import { MiUserPending } from '@/models/UserPending.js';
import { MiUserProfile } from '@/models/UserProfile.js';
import { MiUserBanner } from '@/models/UserBanner.js';
import { MiUserBannerPining } from '@/models/UserBannerPining.js';
import { MiUserPublickey } from '@/models/UserPublickey.js';
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
import { MiUserMemo } from '@/models/UserMemo.js';
@ -134,6 +136,8 @@ export {
MiUserNotePining,
MiUserPending,
MiUserProfile,
MiUserBanner,
MiUserBannerPining,
MiUserPublickey,
MiUserSecurityKey,
MiWebhook,
@ -206,6 +210,8 @@ export type UserListMembershipsRepository = Repository<MiUserListMembership>;
export type UserNotePiningsRepository = Repository<MiUserNotePining>;
export type UserPendingsRepository = Repository<MiUserPending>;
export type UserProfilesRepository = Repository<MiUserProfile>;
export type UserBannerRepository = Repository<MiUserBanner>;
export type UserBannerPiningRepository = Repository<MiUserBannerPining>;
export type UserPublickeysRepository = Repository<MiUserPublickey>;
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey>;
export type WebhooksRepository = Repository<MiWebhook>;

View File

@ -179,7 +179,7 @@ export const packedUserLiteSchema = {
behavior: {
type: 'string',
nullable: false, optional: true,
}
},
},
},
},
@ -428,6 +428,80 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: true,
},
mutualBanners: {
type: 'array',
nullable: true, optional: false,
items: {
type: 'object',
nullable: false, optional: false,
properties: {
id: {
type: 'string',
format: 'id',
nullable: false, optional: false,
},
user: {
type: 'object',
nullable: false, optional: false,
ref: 'UserLite',
},
description: {
type: 'string',
nullable: true, optional: false,
},
imgUrl: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
url: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
fileId: {
type: 'string',
format: 'id',
nullable: false, optional: false,
},
},
},
},
myMutualBanner: {
type: 'object',
nullable: true, optional: false,
properties: {
id: {
type: 'string',
format: 'id',
nullable: false, optional: false,
},
user: {
type: 'object',
nullable: false, optional: false,
ref: 'UserLite',
},
description: {
type: 'string',
nullable: true, optional: false,
},
imgUrl: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
url: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
fileId: {
type: 'string',
format: 'id',
nullable: false, optional: false,
},
},
},
//#endregion
},
} as const;
@ -713,3 +787,37 @@ export const packedUserSchema = {
},
],
} as const;
export const packedUserBannerSchema = {
type: 'object',
properties: {
id: {
type: 'string',
nullable: false, optional: false,
format: 'id',
},
user: {
type: 'object',
nullable: false, optional: false,
ref: 'UserLite',
},
description: {
type: 'string',
nullable: true, optional: false,
},
imgUrl: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
url: {
type: 'string',
nullable: true, optional: false,
},
fileId: {
type: 'string',
format: 'id',
nullable: false, optional: false,
},
},
} as const;

View File

@ -83,6 +83,8 @@ import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserMemo } from '@/models/UserMemo.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
import { MiUserBanner } from '@/models/UserBanner.js';
import { MiUserBannerPining } from '@/models/UserBannerPining.js';
import { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
@ -203,6 +205,8 @@ export const entities = [
MiFlash,
MiFlashLike,
MiUserMemo,
MiUserBanner,
MiUserBannerPining,
MiBubbleGameRecord,
MiReversiGame,
...charts,

View File

@ -31,6 +31,7 @@ import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-dec
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
import * as ep___admin_unsetUserMutualBanner from './endpoints/admin/unset-user-mutual-banner.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
import * as ep___admin_drive_deleteAllFilesOfAUser from './endpoints/admin/drive/delete-all-files-of-a-user.js';
@ -421,6 +422,7 @@ const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-deco
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
const $admin_unsetUserMutualBanner: Provider = { provide: 'ep:admin/unset-user-mutual-banner', useClass: ep___admin_unsetUserMutualBanner.default };
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
const $admin_drive_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/drive/delete-all-files-of-a-user', useClass: ep___admin_drive_deleteAllFilesOfAUser.default };
@ -815,6 +817,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_avatarDecorations_update,
$admin_unsetUserAvatar,
$admin_unsetUserBanner,
$admin_unsetUserMutualBanner,
$admin_drive_cleanRemoteFiles,
$admin_drive_cleanup,
$admin_drive_deleteAllFilesOfAUser,
@ -1203,6 +1206,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_avatarDecorations_update,
$admin_unsetUserAvatar,
$admin_unsetUserBanner,
$admin_unsetUserMutualBanner,
$admin_drive_cleanRemoteFiles,
$admin_drive_cleanup,
$admin_drive_deleteAllFilesOfAUser,

View File

@ -18,6 +18,17 @@ const ajv = new Ajv({
});
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
ajv.addFormat('url', {
type: 'string',
validate: (url: string) => {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
},
});
export type Response = Record<string, any> | void;

View File

@ -31,6 +31,7 @@ import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-dec
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
import * as ep___admin_unsetUserMutualBanner from './endpoints/admin/unset-user-mutual-banner.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
import * as ep___admin_drive_deleteAllFilesOfAUser from './endpoints/admin/drive/delete-all-files-of-a-user.js';
@ -419,6 +420,7 @@ const eps = [
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
['admin/unset-user-banner', ep___admin_unsetUserBanner],
['admin/unset-user-mutual-banner', ep___admin_unsetUserMutualBanner],
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
['admin/drive/cleanup', ep___admin_drive_cleanup],
['admin/drive/delete-all-files-of-a-user', ep___admin_drive_deleteAllFilesOfAUser],

View File

@ -0,0 +1,58 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserBannerRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:unset-user-mutual-banner',
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
const mutualBanner = await this.userBannerRepository.findOneBy({ userId: user.id });
if (mutualBanner == null) return;
await this.userBannerRepository.delete({
id: mutualBanner.id,
});
this.moderationLogService.log(me, 'unsetUserMutualBanner', {
userId: user.id,
userUsername: user.username,
userBannerDescription: mutualBanner.description,
userBannerUrl: mutualBanner.url,
fileId: mutualBanner.fileId,
});
});
}
}

View File

@ -11,7 +11,14 @@ import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type {
UsersRepository,
DriveFilesRepository,
UserProfilesRepository,
PagesRepository,
UserBannerRepository,
UserBannerPiningRepository,
} from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
@ -33,6 +40,8 @@ import type { Config } from '@/config.js';
import { safeForSql } from '@/misc/safe-for-sql.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
import { UserBannerService } from '@/core/UserBannerService.js';
import { UserBannerPiningService } from '@/core/UserBannerPiningService.js';
import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js';
@ -56,6 +65,12 @@ export const meta = {
id: '539f3a45-f215-4f81-a9a8-31293640207f',
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'e0f0d3c7-e704-4314-a0b5-04286d69a65c',
},
noSuchBanner: {
message: 'No such banner file.',
code: 'NO_SUCH_BANNER',
@ -68,6 +83,12 @@ export const meta = {
id: 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191',
},
fileNotAnImage: {
message: 'The specified file is not an image.',
code: 'FILE_NOT_AN_IMAGE',
id: '2851568b-5ad1-4031-bf0d-5320afebf3a9',
},
bannerNotAnImage: {
message: 'The file specified as a banner is not an image.',
code: 'BANNER_NOT_AN_IMAGE',
@ -178,8 +199,8 @@ export const paramDef = {
mutedWords: { type: 'array', items: {
oneOf: [
{ type: 'array', items: { type: 'string' } },
{ type: 'string' }
]
{ type: 'string' },
],
} },
mutedInstances: { type: 'array', items: {
type: 'string',
@ -213,6 +234,24 @@ export const paramDef = {
uniqueItems: true,
items: { type: 'string' },
},
mutualBannerPining: {
type: 'array',
nullable: true,
items: {
type: 'string',
format: 'misskey:id',
},
},
myMutualBanner: {
type: 'object',
nullable: true,
properties: {
fileId: { type: 'string', format: 'misskey:id' },
description: { type: 'string' },
url: { type: 'string', nullable: true, format: 'url' },
},
required: ['fileId'],
},
},
} as const;
@ -231,10 +270,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.userBannerRepository)
private userBannerRepository: UserBannerRepository,
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
@Inject(DI.userBannerPiningRepository)
private userBannerPiningRepository: UserBannerPiningRepository,
private userEntityService: UserEntityService,
private userBannerService: UserBannerService,
private driveFileEntityService: DriveFileEntityService,
private globalEventService: GlobalEventService,
private userFollowingService: UserFollowingService,
@ -246,6 +292,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService,
private httpRequestService: HttpRequestService,
private avatarDecorationService: AvatarDecorationService,
private userBannerPiningService: UserBannerPiningService,
) {
super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@ -322,6 +369,50 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.avatarBlurhash = null;
}
if (ps.mutualBannerPining) {
const bannerPiningNow = await this.userBannerPiningRepository.findBy({ userId: user.id });
const bannerPiningNowIds = new Set(bannerPiningNow.map(b => b.pinnedBannerId));
const mutualBannerPiningIds = new Set(ps.mutualBannerPining);
const bannersToAdd = [...mutualBannerPiningIds].filter(bannerId => !bannerPiningNowIds.has(bannerId));
const bannersToRemove = [...bannerPiningNowIds].filter(bannerId => !mutualBannerPiningIds.has(bannerId));
if (bannersToAdd.length > 0) {
await this.userBannerPiningService.addPinned(user.id, bannersToAdd);
}
if (bannersToRemove.length > 0) {
await this.userBannerPiningService.removePinned(user.id, bannersToRemove);
}
}
if (ps.myMutualBanner) {
const banner = await this.userBannerRepository.findOneBy({
userId: user.id,
});
const file = await this.driveFilesRepository.findOneBy({ id: ps.myMutualBanner.fileId });
const profileUrl = this.config.url + '/@' + user.username;
if (file === null) throw new ApiError(meta.errors.noSuchFile);
if (!file.type.startsWith('image/')) throw new ApiError(meta.errors.fileNotAnImage);
if (banner) {
await this.userBannerService.update(user.id, banner.id, ps.myMutualBanner.description ?? null, ps.myMutualBanner.url ?? profileUrl, ps.myMutualBanner.fileId);
} else {
await this.userBannerService.create(user.id, ps.myMutualBanner.description ?? null, ps.myMutualBanner.url ?? profileUrl, ps.myMutualBanner.fileId);
}
}
if (ps.myMutualBanner === null) {
const banner = await this.userBannerRepository.findOneBy({
userId: user.id,
});
if (banner) {
await this.userBannerService.delete(user.id, banner.id);
}
}
if (ps.bannerId) {
if (!policy.canUpdateBanner) throw new ApiError(meta.errors.restrictedByRole);
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });

View File

@ -96,6 +96,7 @@ export const moderationLogTypes = [
'deleteAvatarDecoration',
'unsetUserAvatar',
'unsetUserBanner',
'unsetUserMutualBanner',
] as const;
export type ModerationLogPayloads = {
@ -314,6 +315,13 @@ export type ModerationLogPayloads = {
userHost: string | null;
fileId: string;
};
unsetUserMutualBanner: {
userId: string;
userUsername: string;
userBannerDescription: string | null;
userBannerUrl: string | null;
fileId: string;
}
};
export type Serialized<T> = {

View File

@ -74,6 +74,8 @@ describe('ユーザー', () => {
lang: user.lang,
fields: user.fields,
verifiedLinks: user.verifiedLinks,
myMutualBanner: user.myMutualBanner,
mutualBanners: user.mutualBanners,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,

View File

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@click="(ev: MouseEvent) => warningExternalWebsite(ev, url)"
>
<slot></slot>
<i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i>
<i v-if="target === '_blank' && !hideIcon" class="ti ti-external-link" :class="$style.icon"></i>
</component>
</template>
@ -34,7 +34,9 @@ const props = withDefaults(defineProps<{
url: string;
rel?: null | string;
navigationBehavior?: MkABehavior;
hideIcon?: boolean;
}>(), {
hideIcon: false,
});
const self = props.url.startsWith(local);

View File

@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="user.host == null" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<MkButton inline danger @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
<MkButton inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
<MkButton inline danger @click="unsetUserMutualBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserMutualBanner }}</MkButton>
</div>
</MkFolder>
@ -292,7 +293,7 @@ function createFetcher() {
watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', {
userId: user.value.id, text: moderationNote.value
userId: user.value.id, text: moderationNote.value,
}).then(refreshUser);
});
});
@ -304,7 +305,7 @@ function refreshUser() {
async function updateRemoteUser() {
await os.apiWithDialog('federation/update-remote-user', {
userId: user.value.id
userId: user.value.id,
}).then(refreshUser);
}
@ -335,7 +336,7 @@ async function toggleSuspend(v) {
suspended.value = !v;
} else {
await misskeyApi(v ? 'admin/suspend-user' : 'admin/unsuspend-user', {
userId: user.value.id
userId: user.value.id,
}).then(refreshUser);
}
}
@ -348,7 +349,7 @@ async function unsetUserAvatar() {
if (confirm.canceled) return;
await os.apiWithDialog('admin/unset-user-avatar', {
userId: user.value.id
userId: user.value.id,
}).then(refreshUser);
}
@ -360,7 +361,19 @@ async function unsetUserBanner() {
if (confirm.canceled) return;
await os.apiWithDialog('admin/unset-user-banner', {
userId: user.value.id
userId: user.value.id,
}).then(refreshUser);
}
async function unsetUserMutualBanner() {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unsetUserMutualBannerConfirm,
});
if (confirm.canceled) return;
await os.apiWithDialog('admin/unset-user-mutual-banner', {
userId: user.value.id,
}).then(refreshUser);
}
@ -378,7 +391,7 @@ async function deleteAllFiles() {
if (typed.result === user.value?.username) {
await os.apiWithDialog('admin/drive/delete-all-files-of-a-user', {
userId: user.value.id
userId: user.value.id,
}).then(refreshUser);
} else {
os.alert({
@ -447,7 +460,7 @@ async function assignRole() {
: null;
await os.apiWithDialog('admin/roles/assign', {
roleId, userId: user.value.id, expiresAt
roleId, userId: user.value.id, expiresAt,
}).then(refreshUser);
}
@ -458,7 +471,7 @@ async function unassignRole(role, ev) {
danger: true,
action: async () => {
await os.apiWithDialog('admin/roles/unassign', {
roleId: role.id, userId: user.value.id
roleId: role.id, userId: user.value.id,
}).then(refreshUser);
},
}], ev.currentTarget ?? ev.target);

View File

@ -87,6 +87,33 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
</FormSlot>
<FormSlot>
<MkFolder class="_margin">
<template #icon><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._profile.myMutualBanner }}</template>
<MkInput v-model="myMutualBanner.description" class="_margin" small>
<template #label>{{ i18n.ts._profile.mutualBannerDescriptionEdit }}</template>
</MkInput>
<MkInput v-model="myMutualBanner.url" class="_margin" type="url" small>
<template #label>URL</template>
</MkInput>
<div>
<p>{{ i18n.ts._profile.mutualBanner }}</p>
<img class="_margin" :class="$style.mutualBannerImg" :src="myMutualBanner.imgUrl">
<MkButton class="_button _margin" @click="ev => changeMutualBannerFile(ev)">{{ i18n.ts.selectFile }}</MkButton>
</div>
<MkButton class="_button" primary @click="saveMyMutualBanner">{{ i18n.ts.save }}</MkButton>
<MkButton class="_button" primary @click="deleteMyMutualBanner">{{ i18n.ts.delete }}</MkButton>
</MkFolder>
<template #caption>
<span>{{ i18n.ts._profile.myMutualBannerDescription }}<br>
{{ i18n.ts.recommended }} 200x40<br>
{{ i18n.ts.maximum }} 300x60
</span>
</template>
</FormSlot>
<MkFolder>
<template #label>{{ i18n.ts.advancedSettings }}</template>
@ -152,6 +179,12 @@ watch(() => profile, () => {
});
const fields = ref($i.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
const myMutualBanner = ref<{ fileId: string; description?: string; url?: string | null; imgUrl?: string; }>({
fileId: $i.myMutualBanner?.fileId ?? '',
description: $i.myMutualBanner?.description ?? '',
url: $i.myMutualBanner?.url ?? '',
imgUrl: $i.myMutualBanner?.imgUrl ?? '',
});
const fieldEditMode = ref(false);
function addField() {
@ -177,6 +210,40 @@ function saveFields() {
globalEvents.emit('requestClearPageCache');
}
function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch (_) {
return false;
}
}
function saveMyMutualBanner() {
if ( myMutualBanner.value.fileId === '' || myMutualBanner.value.url && !isValidUrl(myMutualBanner.value.url)) {
os.alert({
type: 'error',
title: i18n.ts.invalidParamError,
text: i18n.ts.invalidParamErrorDescription,
});
return;
}
os.apiWithDialog('i/update', {
myMutualBanner: {
fileId: myMutualBanner.value.fileId,
description: myMutualBanner.value.description,
url: myMutualBanner.value.url === '' ? null : myMutualBanner.value.url,
},
});
}
function deleteMyMutualBanner() {
os.apiWithDialog('i/update', {
myMutualBanner: null,
});
myMutualBanner.value = { fileId: '', description: '', url: '' };
}
function save() {
os.apiWithDialog('i/update', {
// null??使
@ -203,6 +270,13 @@ function save() {
}
}
function changeMutualBannerFile(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.mutualBanner).then(async (file) => {
myMutualBanner.value.imgUrl = file.url;
myMutualBanner.value.fileId = file.id;
});
}
function changeAvatar(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => {
let originalOrCropped = file;
@ -350,4 +424,13 @@ definePageMetadata(() => ({
.dragItemForm {
flex-grow: 1;
}
.mutualBannerImg {
max-width: 300px;
min-width: 200px;
max-height: 60px;
min-height: 40px;
object-fit: contain;
}
</style>

View File

@ -143,6 +143,28 @@ SPDX-License-Identifier: AGPL-3.0-only
</dd>
</dl>
</div>
<div v-if="user?.myMutualBanner" class="fields">
{{ i18n.ts.mutualBannerThisUser }}
<div :class="$style.myMutualBanner">
<MkLink :hideIcon="true" :url="user.myMutualBanner.url">
<img :class="$style.mutualBannerImg" :src="user.myMutualBanner.imgUrl" :alt="user.myMutualBanner.description"/>
</MkLink>
<span>{{ (user.myMutualBanner?.description === '' || user.myMutualBanner?.description === null) ? i18n.ts.noDescription : user.myMutualBanner?.description }}</span>
<MkButton v-if="$i && $i?.id !== user.id && !$i?.mutualBanners?.some(banner => banner.id === user.myMutualBanner?.id) " @click="mutualBannerFollow(user.myMutualBanner?.id)">{{ i18n.ts.follow }}</MkButton>
<MkButton v-else-if="$i && $i?.id !== user.id" @click="mutualBannerUnFollow(user.myMutualBanner?.id)">{{ i18n.ts.unfollow }}</MkButton>
</div>
</div>
<div v-if="user?.mutualBanners && user?.mutualBanners.length > 0" class="fields">
{{ i18n.ts.mutualBanner }}
<div :class="$style.mutualBanner">
<div v-for="(mutualBanner, i) in mutualBanners?.slice(0, 9)" :key="i">
<MkLink :hideIcon="true" :url="mutualBanner.url">
<img :class="$style.mutualBannerImg" :src="mutualBanner.imgUrl" :alt="mutualBanner.description"/>
</MkLink>
</div>
</div>
</div>
<div class="status">
<MkA :to="userPage(user)">
<b>{{ number(user.notesCount) }}</b>
@ -212,6 +234,7 @@ import { confetti } from '@/scripts/confetti.js';
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
import { useRouter } from '@/router/supplier.js';
import MkLink from '@/components/MkLink.vue';
function calcAge(birthdate: string): number {
const date = new Date(birthdate);
@ -253,6 +276,7 @@ const memoDraft = ref(props.user.memo);
const isEditingMemo = ref(false);
const moderationNote = ref(props.user.moderationNote);
const editModerationNote = ref(false);
const mutualBanners = ref(props.user.mutualBanners);
watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
@ -299,6 +323,23 @@ function showMemoTextarea() {
});
}
function mutualBannerFollow(id: string) {
os.apiWithDialog('i/update', {
mutualBannerPining: [
...($i?.mutualBanners?.map(banner => banner.id) ?? []),
id,
],
});
}
function mutualBannerUnFollow(id:string) {
os.apiWithDialog('i/update', {
mutualBannerPining: [
...($i?.mutualBanners?.map(banner => banner.id) ?? []).filter(bannerId => bannerId !== id),
],
});
}
function adjustMemoTextarea() {
if (!memoTextareaEl.value) return;
memoTextareaEl.value.style.height = '0px';
@ -787,4 +828,28 @@ onUnmounted(() => {
color: rgb(255, 255, 255);
background-color: rgb(54, 54, 54);
}
.myMutualBanner {
display: flex;
justify-content: space-around;
align-items: center;
flex-flow: column wrap;
padding: 16px;
}
.mutualBanner {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
padding: 16px;
}
.mutualBannerImg {
max-width: 300px;
min-width: 200px;
max-height: 60px;
min-height: 40px;
object-fit: contain;
}
</style>

View File

@ -11,9 +11,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<option :value="null">{{ i18n.ts.notes }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="files">{{ i18n.ts.withFiles }}</option>
<option value="mutualBanners">{{ i18n.ts.mutualBanner }}</option>
</MkTab>
</template>
<MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
<MkNotes v-if="tab !== 'mutualBanners'" :noGap="true" :pagination="pagination" :class="$style.tl"/>
<div v-else>
<div v-if="mutualBanners && mutualBanners.length > 0" :class="$style.mutualBanners">
<div v-for="(mutualBanner, i) in mutualBanners" :key="i" class="_margin">
<MkLink :hideIcon="true" :url="mutualBanner.url">
<img :class="$style.banner" :src="mutualBanner.imgUrl"/>
</MkLink>
<p>{{ (mutualBanner.description === '' || mutualBanner.description === null) ? i18n.ts.noDescription : mutualBanner.description }}</p>
<MkButton v-if="$i?.id === user.id" @click="mutualBannerUnFollow(mutualBanner.id)">{{ i18n.ts.unfollow }}</MkButton>
</div>
</div>
<div v-else>
<p>{{ i18n.ts.nothing }}</p>
</div>
</div>
</MkStickyContainer>
</template>
@ -23,11 +38,17 @@ import * as Misskey from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue';
import MkTab from '@/components/MkTab.vue';
import { i18n } from '@/i18n.js';
import MkLink from '@/components/MkLink.vue';
import MkButton from '@/components/MkButton.vue';
import { $i } from '@/account.js';
import * as os from '@/os.js';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
}>();
const mutualBanners = ref(props.user.mutualBanners);
const tab = ref<string | null>('all');
const pagination = computed(() => tab.value === 'featured' ? {
@ -47,6 +68,17 @@ const pagination = computed(() => tab.value === 'featured' ? {
withChannelNotes: true,
},
});
function mutualBannerUnFollow(id:string) {
os.apiWithDialog('i/update', {
mutualBannerPining: [
...($i?.mutualBanners?.map(banner => banner.id) ?? []).filter(bannerId => bannerId !== id),
],
});
if (mutualBanners.value) {
mutualBanners.value = mutualBanners.value.filter(banner => banner.id !== id);
}
}
</script>
<style lang="scss" module>
@ -60,4 +92,20 @@ const pagination = computed(() => tab.value === 'featured' ? {
border-radius: var(--radius);
overflow: clip;
}
.banner {
max-width: 300px;
min-width: 200px;
max-height: 60px;
min-height: 40px;
object-fit: contain;
}
.mutualBanners{
display: flex;
flex-wrap: wrap;
justify-content: space-around;
background: var(--panel);
border-radius: var(--radius);
}
</style>

View File

@ -376,6 +376,9 @@ type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requ
// @public (undocumented)
type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminUnsetUserMutualBannerRequest = operations['admin___unset-user-mutual-banner']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
@ -1232,6 +1235,7 @@ declare namespace entities {
AdminAvatarDecorationsUpdateRequest,
AdminUnsetUserAvatarRequest,
AdminUnsetUserBannerRequest,
AdminUnsetUserMutualBannerRequest,
AdminDriveDeleteAllFilesOfAUserRequest,
AdminDriveFilesRequest,
AdminDriveFilesResponse,
@ -1783,6 +1787,7 @@ declare namespace entities {
MeDetailed,
UserDetailed,
User,
UserBanner,
UserList,
UserListMembership,
Ad,
@ -2502,7 +2507,7 @@ type ModerationLog = {
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createSSOServiceProvider", "updateSSOServiceProvider", "deleteSSOServiceProvider", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createSSOServiceProvider", "updateSSOServiceProvider", "deleteSSOServiceProvider", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "unsetUserMutualBanner"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@ -2754,7 +2759,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
function parse(acct: string): Acct;
// @public (undocumented)
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
// @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
@ -3053,6 +3058,9 @@ function toString_2(acct: Acct): string;
// @public (undocumented)
type User = components['schemas']['User'];
// @public (undocumented)
type UserBanner = components['schemas']['UserBanner'];
// @public (undocumented)
type UserDetailed = components['schemas']['UserDetailed'];

View File

@ -278,6 +278,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-banner*
*/
request<E extends 'admin/unset-user-mutual-banner', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View File

@ -37,6 +37,7 @@ import type {
AdminAvatarDecorationsUpdateRequest,
AdminUnsetUserAvatarRequest,
AdminUnsetUserBannerRequest,
AdminUnsetUserMutualBannerRequest,
AdminDriveDeleteAllFilesOfAUserRequest,
AdminDriveFilesRequest,
AdminDriveFilesResponse,
@ -608,6 +609,7 @@ export type Endpoints = {
'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse };
'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse };
'admin/unset-user-mutual-banner': { req: AdminUnsetUserMutualBannerRequest; res: EmptyResponse };
'admin/drive/clean-remote-files': { req: EmptyRequest; res: EmptyResponse };
'admin/drive/cleanup': { req: EmptyRequest; res: EmptyResponse };
'admin/drive/delete-all-files-of-a-user': { req: AdminDriveDeleteAllFilesOfAUserRequest; res: EmptyResponse };

View File

@ -40,6 +40,7 @@ export type AdminAvatarDecorationsListResponse = operations['admin___avatar-deco
export type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json'];
export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json'];
export type AdminUnsetUserMutualBannerRequest = operations['admin___unset-user-mutual-banner']['requestBody']['content']['application/json'];
export type AdminDriveDeleteAllFilesOfAUserRequest = operations['admin___drive___delete-all-files-of-a-user']['requestBody']['content']['application/json'];
export type AdminDriveFilesRequest = operations['admin___drive___files']['requestBody']['content']['application/json'];
export type AdminDriveFilesResponse = operations['admin___drive___files']['responses']['200']['content']['application/json'];

View File

@ -7,6 +7,7 @@ export type UserDetailedNotMe = components['schemas']['UserDetailedNotMe'];
export type MeDetailed = components['schemas']['MeDetailed'];
export type UserDetailed = components['schemas']['UserDetailed'];
export type User = components['schemas']['User'];
export type UserBanner = components['schemas']['UserBanner'];
export type UserList = components['schemas']['UserList'];
export type UserListMembership = components['schemas']['UserListMembership'];
export type Ad = components['schemas']['Ad'];

View File

@ -237,6 +237,15 @@ export type paths = {
*/
post: operations['admin___unset-user-banner'];
};
'/admin/unset-user-mutual-banner': {
/**
* admin/unset-user-mutual-banner
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-banner*
*/
post: operations['admin___unset-user-mutual-banner'];
};
'/admin/drive/clean-remote-files': {
/**
* admin/drive/clean-remote-files
@ -3844,6 +3853,30 @@ export type components = {
/** @enum {string} */
notify?: 'normal' | 'none';
withReplies?: boolean;
mutualBanners: (({
/** Format: id */
id: string;
user: components['schemas']['UserLite'];
description: string | null;
/** Format: url */
imgUrl: string;
/** Format: url */
url: string;
/** Format: id */
fileId: string;
})[]) | null;
myMutualBanner: ({
/** Format: id */
id: string;
user: components['schemas']['UserLite'];
description: string | null;
/** Format: url */
imgUrl: string;
/** Format: url */
url: string;
/** Format: id */
fileId: string;
}) | null;
};
MeDetailedOnly: {
/** Format: id */
@ -4028,6 +4061,17 @@ export type components = {
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
UserDetailed: components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed'];
User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'];
UserBanner: {
/** Format: id */
id: string;
user: components['schemas']['UserLite'];
description: string | null;
/** Format: url */
imgUrl: string;
url: string | null;
/** Format: id */
fileId: string;
};
UserList: {
/**
* Format: id
@ -6819,6 +6863,58 @@ export type operations = {
};
};
};
/**
* admin/unset-user-mutual-banner
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-banner*
*/
'admin___unset-user-mutual-banner': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* admin/drive/clean-remote-files
* @description No description provided.
@ -20212,6 +20308,14 @@ export type operations = {
};
emailNotificationTypes?: string[];
alsoKnownAs?: string[];
mutualBannerPining?: string[] | null;
myMutualBanner?: ({
/** Format: misskey:id */
fileId: string;
description?: string;
/** Format: url */
url?: string | null;
}) | null;
};
};
};

View File

@ -62,6 +62,7 @@ export const permissions = [
'write:admin:suspend-user',
'write:admin:unset-user-avatar',
'write:admin:unset-user-banner',
'write:admin:unset-user-mutual-banner',
'write:admin:unsuspend-user',
'write:admin:meta',
'write:admin:user-note',
@ -144,6 +145,7 @@ export const moderationLogTypes = [
'deleteAvatarDecoration',
'unsetUserAvatar',
'unsetUserBanner',
'unsetUserMutualBanner',
] as const;
export type ModerationLogPayloads = {
@ -362,4 +364,11 @@ export type ModerationLogPayloads = {
userHost: string | null;
fileId: string;
};
unsetUserMutualBanner: {
userId: string;
userUsername: string;
userBannerDescription: string | null;
userBannerUrl: string;
fileId: string;
};
};