feat(profile): 相互リンク機能の追加 (MisskeyIO#675)
This commit is contained in:
parent
b059162324
commit
b6a5a36eaa
37 changed files with 1130 additions and 20 deletions
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue