Feat: 絵文字申請中のやつのテーブルを分けた

Signed-off-by: mattyatea <mattyacocacora0@gmail.com>
This commit is contained in:
mattyatea 2023-10-21 16:29:09 +09:00
parent 97590f2567
commit fe938bf8e6
No known key found for this signature in database
GPG key ID: 068E54E2C33BEF9A
29 changed files with 666 additions and 189 deletions

View file

@ -86,6 +86,7 @@ import { ClipEntityService } from './entities/ClipEntityService.js';
import { DriveFileEntityService } from './entities/DriveFileEntityService.js';
import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js';
import { EmojiEntityService } from './entities/EmojiEntityService.js';
import { EmojiDraftsEntityService } from './entities/EmojiDraftsEntityService.js';
import { FollowingEntityService } from './entities/FollowingEntityService.js';
import { FollowRequestEntityService } from './entities/FollowRequestEntityService.js';
import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js';
@ -217,6 +218,7 @@ const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting
const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService };
const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService };
const $EmojiEntityService: Provider = { provide: 'EmojiEntityService', useExisting: EmojiEntityService };
const $EmojiDraftsEntityService: Provider = { provide: 'EmojiDraftsEntityService', useExisting: EmojiDraftsEntityService };
const $FollowingEntityService: Provider = { provide: 'FollowingEntityService', useExisting: FollowingEntityService };
const $FollowRequestEntityService: Provider = { provide: 'FollowRequestEntityService', useExisting: FollowRequestEntityService };
const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService', useExisting: GalleryLikeEntityService };
@ -348,6 +350,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
DriveFileEntityService,
DriveFolderEntityService,
EmojiEntityService,
EmojiDraftsEntityService,
FollowingEntityService,
FollowRequestEntityService,
GalleryLikeEntityService,
@ -474,6 +477,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$DriveFileEntityService,
$DriveFolderEntityService,
$EmojiEntityService,
$EmojiDraftsEntityService,
$FollowingEntityService,
$FollowRequestEntityService,
$GalleryLikeEntityService,
@ -600,6 +604,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
DriveFileEntityService,
DriveFolderEntityService,
EmojiEntityService,
EmojiDraftsEntityService,
FollowingEntityService,
FollowRequestEntityService,
GalleryLikeEntityService,
@ -725,6 +730,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$DriveFileEntityService,
$DriveFolderEntityService,
$EmojiEntityService,
$EmojiDraftsEntityService,
$FollowingEntityService,
$FollowRequestEntityService,
$GalleryLikeEntityService,

View file

@ -12,12 +12,13 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiEmoji } from '@/models/Emoji.js';
import type { EmojisRepository, MiRole, MiUser, DriveFilesRepository } from '@/models/_.js';
import type { EmojisRepository, EmojiDraftsRepository, MiRole, MiUser } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { MiEmojiDraft } from '@/models/EmojiDraft.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
@ -33,8 +34,8 @@ export class CustomEmojiService implements OnApplicationShutdown {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.emojiDraftsRepository)
private emojiDraftsRepository: EmojiDraftsRepository,
private utilityService: UtilityService,
private idService: IdService,
@ -58,6 +59,40 @@ export class CustomEmojiService implements OnApplicationShutdown {
});
}
@bindThis
public async draft(data: {
driveFile: MiDriveFile;
name: string;
category: string | null;
aliases: string[];
license: string | null;
isSensitive: boolean;
localOnly: boolean;
}, me?: MiUser): Promise<MiEmojiDraft> {
const emoji = await this.emojiDraftsRepository.insert({
id: this.idService.gen(),
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
originalUrl: data.driveFile.url,
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
type: data.driveFile.webpublicType ?? data.driveFile.type,
license: data.license,
isSensitive: data.isSensitive,
localOnly: data.localOnly,
fileId: data.driveFile.id,
}).then(x => this.emojiDraftsRepository.findOneByOrFail(x.identifiers[0]));
if (me) {
this.moderationLogService.log(me, 'addCustomEmoji', {
emojiId: emoji.id,
emoji: emoji,
});
}
return emoji;
}
@bindThis
public async add(data: {
driveFile: MiDriveFile;
@ -68,7 +103,6 @@ export class CustomEmojiService implements OnApplicationShutdown {
license: string | null;
isSensitive: boolean;
localOnly: boolean;
draft: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
}, moderator?: MiUser): Promise<MiEmoji> {
const emoji = await this.emojisRepository.insert({
@ -85,7 +119,6 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive: data.isSensitive,
localOnly: data.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
draft: data.draft,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) {
@ -113,44 +146,27 @@ export class CustomEmojiService implements OnApplicationShutdown {
category?: string | null;
aliases?: string[];
license?: string | null;
fileId?: string | null;
isSensitive?: boolean;
localOnly?: boolean;
draft: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
}, moderator?: MiUser): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const driveFile = data.fileId !== null ? await this.driveFilesRepository.findOneBy({ id: data.fileId }) : null;
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
if (driveFile !== null) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
license: data.license,
isSensitive: data.isSensitive,
localOnly: data.localOnly,
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined,
type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
draft: data.draft,
});
} else {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
license: data.license,
isSensitive: data.isSensitive,
localOnly: data.localOnly,
draft: data.draft,
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
});
}
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
license: data.license,
isSensitive: data.isSensitive,
localOnly: data.localOnly,
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined,
type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
});
this.localEmojisCache.refresh();
@ -179,7 +195,35 @@ export class CustomEmojiService implements OnApplicationShutdown {
});
}
}
@bindThis
public async draftUpdate(id: MiEmoji['id'], data: {
driveFile?: MiDriveFile;
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
isSensitive?: boolean;
localOnly?: boolean;
}, moderator?: MiUser): Promise<void> {
const emoji = await this.emojiDraftsRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojiDraftsRepository.findOneBy({ name: data.name });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
await this.emojiDraftsRepository.update(emoji.id, {
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
license: data.license,
isSensitive: data.isSensitive,
localOnly: data.localOnly,
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined,
type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
});
this.localEmojisCache.refresh();
}
@bindThis
public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
@ -287,7 +331,12 @@ export class CustomEmojiService implements OnApplicationShutdown {
});
}
}
@bindThis
public async draftDelete(id: MiEmojiDraft['id']) {
const emoji = await this.emojiDraftsRepository.findOneByOrFail({ id: id });
await this.emojiDraftsRepository.delete(emoji.id);
}
@bindThis
public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) {
const emojis = await this.emojisRepository.findBy({
@ -408,12 +457,20 @@ export class CustomEmojiService implements OnApplicationShutdown {
public checkDuplicate(name: string): Promise<boolean> {
return this.emojisRepository.exist({ where: { name, host: IsNull() } });
}
@bindThis
public checkDraftDuplicate(name: string): Promise<boolean> {
return this.emojiDraftsRepository.exist({ where: { name } });
}
@bindThis
public getEmojiById(id: string): Promise<MiEmoji | null> {
return this.emojisRepository.findOneBy({ id });
}
@bindThis
public getEmojiDraftById(id: string): Promise<MiEmojiDraft | null> {
return this.emojiDraftsRepository.findOneBy({ id });
}
@bindThis
public dispose(): void {
this.cache.dispose();

View file

@ -133,7 +133,13 @@ export class DriveFileEntityService {
}
return url;
}
@bindThis
public async getFromUrl(url: string): Promise<MiDriveFile | null> {
const file = await this.driveFilesRepository.findOneBy({ url: url });
if (file === null ) return null;
return file;
}
@bindThis
public async calcDriveUsageOf(user: MiUser['id'] | { id: MiUser['id'] }): Promise<number> {
const id = typeof user === 'object' ? user.id : user;

View file

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { EmojiDraftsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
import { MiEmojiDraft } from '@/models/EmojiDraft.js';
@Injectable()
export class EmojiDraftsEntityService {
constructor(
@Inject(DI.emojiDraftsRepository)
private emojiDraftsRepository: EmojiDraftsRepository,
) {
}
@bindThis
public async packSimple(
src: MiEmojiDraft['id'] | MiEmojiDraft,
): Promise<Packed<'EmojiDraftSimple'>> {
const emoji = typeof src === 'object' ? src : await this.emojiDraftsRepository.findOneByOrFail({ id: src });
return {
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl,
isSensitive: emoji.isSensitive ? true : undefined,
};
}
@bindThis
public packSimpleMany(
emojis: any[],
) {
return Promise.all(emojis.map(x => this.packSimple(x)));
}
@bindThis
public async packDetailed(
src: MiEmojiDraft['id'] | MiEmojiDraft,
): Promise<Packed<'EmojiDraftDetailed'>> {
const emoji = typeof src === 'object' ? src : await this.emojiDraftsRepository.findOneByOrFail({ id: src });
return {
id: emoji.id,
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
url: emoji.publicUrl,
license: emoji.license,
isSensitive: emoji.isSensitive,
localOnly: emoji.localOnly,
fileId: emoji.fileId,
};
}
@bindThis
public packDetailedMany(
emojis: any[],
) {
return Promise.all(emojis.map(x => this.packDetailed(x)));
}
}

View file

@ -33,7 +33,6 @@ export class EmojiEntityService {
url: emoji.publicUrl || emoji.originalUrl,
isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
draft: emoji.draft,
};
}
@ -62,7 +61,6 @@ export class EmojiEntityService {
isSensitive: emoji.isSensitive,
localOnly: emoji.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
draft: emoji.draft,
};
}

View file

@ -39,6 +39,7 @@ export const DI = {
followRequestsRepository: Symbol('followRequestsRepository'),
instancesRepository: Symbol('instancesRepository'),
emojisRepository: Symbol('emojisRepository'),
emojiDraftsRepository: Symbol('emojiDraftsRepository'),
driveFilesRepository: Symbol('driveFilesRepository'),
driveFoldersRepository: Symbol('driveFoldersRepository'),
metasRepository: Symbol('metasRepository'),

View file

@ -33,7 +33,7 @@ import { packedClipSchema } from '@/models/json-schema/clip.js';
import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js';
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
import { packedEmojiDetailedSchema, packedEmojiDraftSimpleSchema, packedEmojiSimpleSchema, packedEmojiDraftDetailedSchema } from '@/models/json-schema/emoji.js';
import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
@ -69,7 +69,9 @@ export const refs = {
FederationInstance: packedFederationInstanceSchema,
GalleryPost: packedGalleryPostSchema,
EmojiSimple: packedEmojiSimpleSchema,
EmojiDraftSimple: packedEmojiDraftSimpleSchema,
EmojiDetailed: packedEmojiDetailedSchema,
EmojiDraftDetailed: packedEmojiDraftDetailedSchema,
Flash: packedFlashSchema,
};

View file

@ -81,10 +81,4 @@ export class MiEmoji {
array: true, length: 128, default: '{}',
})
public roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
@Column('boolean', {
default: false,
nullable: false,
})
public draft: boolean;
}

View file

@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { id } from './util/id.js';
@Entity('emoji_draft')
@Index(['name'], { unique: true })
export class MiEmojiDraft {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
nullable: true,
})
public updatedAt: Date | null;
@Index()
@Column('varchar', {
length: 128,
})
public name: string;
@Column('varchar', {
length: 128, nullable: true,
})
public category: string | null;
@Column('varchar', {
length: 512,
})
public originalUrl: string;
@Column('varchar', {
length: 512,
default: '',
})
public publicUrl: string;
// publicUrlの方のtypeが入る
@Column('varchar', {
length: 64, nullable: true,
})
public type: string | null;
@Column('varchar', {
array: true, length: 128, default: '{}',
})
public aliases: string[];
@Column('varchar', {
length: 1024, nullable: true,
})
public license: string | null;
@Column('varchar', {
length: 1024, nullable: false,
})
public fileId: string;
@Column('boolean', {
default: false,
})
public localOnly: boolean;
@Column('boolean', {
default: false,
})
public isSensitive: boolean;
}

View file

@ -5,7 +5,7 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiEmojiDraft, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@ -165,6 +165,12 @@ const $emojisRepository: Provider = {
inject: [DI.db],
};
const $emojiDraftsRepository: Provider = {
provide: DI.emojiDraftsRepository,
useFactory: (db: DataSource) => db.getRepository(MiEmojiDraft),
inject: [DI.db],
};
const $driveFilesRepository: Provider = {
provide: DI.driveFilesRepository,
useFactory: (db: DataSource) => db.getRepository(MiDriveFile),
@ -423,6 +429,7 @@ const $userMemosRepository: Provider = {
$followRequestsRepository,
$instancesRepository,
$emojisRepository,
$emojiDraftsRepository,
$driveFilesRepository,
$driveFoldersRepository,
$metasRepository,
@ -489,6 +496,7 @@ const $userMemosRepository: Provider = {
$followRequestsRepository,
$instancesRepository,
$emojisRepository,
$emojiDraftsRepository,
$driveFilesRepository,
$driveFoldersRepository,
$metasRepository,

View file

@ -67,6 +67,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiEmojiDraft } from '@/models/EmojiDraft.js';
import type { Repository } from 'typeorm';
export {
@ -87,6 +88,7 @@ export {
MiDriveFile,
MiDriveFolder,
MiEmoji,
MiEmojiDraft,
MiFollowing,
MiFollowRequest,
MiGalleryLike,
@ -153,6 +155,7 @@ export type ClipFavoritesRepository = Repository<MiClipFavorite>;
export type DriveFilesRepository = Repository<MiDriveFile>;
export type DriveFoldersRepository = Repository<MiDriveFolder>;
export type EmojisRepository = Repository<MiEmoji>;
export type EmojiDraftsRepository = Repository<MiEmojiDraft>;
export type FollowingsRepository = Repository<MiFollowing>;
export type FollowRequestsRepository = Repository<MiFollowRequest>;
export type GalleryLikesRepository = Repository<MiGalleryLike>;

View file

@ -40,10 +40,36 @@ export const packedEmojiSimpleSchema = {
format: 'id',
},
},
draft: {
type: 'boolean',
},
} as const;
export const packedEmojiDraftSimpleSchema = {
type: 'object',
properties: {
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
name: {
type: 'string',
optional: false, nullable: false,
},
category: {
type: 'string',
optional: false, nullable: true,
},
url: {
type: 'string',
optional: false, nullable: false,
},
isSensitive: {
type: 'boolean',
optional: true, nullable: false,
},
},
} as const;
@ -85,10 +111,6 @@ export const packedEmojiDetailedSchema = {
type: 'string',
optional: false, nullable: true,
},
draft: {
type: 'boolean',
optional: false, nullable: true,
},
isSensitive: {
type: 'boolean',
optional: false, nullable: false,
@ -108,3 +130,51 @@ export const packedEmojiDetailedSchema = {
},
},
} as const;
export const packedEmojiDraftDetailedSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
name: {
type: 'string',
optional: false, nullable: false,
},
category: {
type: 'string',
optional: false, nullable: true,
},
url: {
type: 'string',
optional: false, nullable: false,
},
license: {
type: 'string',
optional: false, nullable: true,
},
isSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
localOnly: {
type: 'boolean',
optional: false, nullable: false,
},
fileId: {
type: 'string',
optional: false, nullable: false,
},
},
} as const;

View file

@ -28,6 +28,7 @@ import { MiClipFavorite } from '@/models/ClipFavorite.js';
import { MiDriveFile } from '@/models/DriveFile.js';
import { MiDriveFolder } from '@/models/DriveFolder.js';
import { MiEmoji } from '@/models/Emoji.js';
import { MiEmojiDraft } from '@/models/EmojiDraft.js';
import { MiFollowing } from '@/models/Following.js';
import { MiFollowRequest } from '@/models/FollowRequest.js';
import { MiGalleryLike } from '@/models/GalleryLike.js';
@ -160,6 +161,7 @@ export const entities = [
MiPoll,
MiPollVote,
MiEmoji,
MiEmojiDraft,
MiHashtag,
MiSwSubscription,
MiAbuseUserReport,

View file

@ -37,6 +37,7 @@ import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-al
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_emoji_draftUpdate from './endpoints/admin/emoji/draft-update.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
@ -243,6 +244,7 @@ import * as ep___invite_list from './endpoints/invite/list.js';
import * as ep___invite_limit from './endpoints/invite/limit.js';
import * as ep___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___emojiDrafts from './endpoints/emoji-drafts.js';
import * as ep___emoji from './endpoints/emoji.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js';
@ -388,6 +390,7 @@ const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-ali
const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default };
const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default };
const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default };
const $admin_emoji_draftUpdate: Provider = { provide: 'ep:admin/emoji/draft-update', useClass: ep___admin_emoji_draftUpdate.default };
const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default };
const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default };
@ -594,6 +597,7 @@ const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invit
const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default };
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
const $emoji_drafts: Provider = { provide: 'ep:emoji-drafts', useClass: ep___emojiDrafts.default };
const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
@ -743,6 +747,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_emoji_setCategoryBulk,
$admin_emoji_setLicenseBulk,
$admin_emoji_update,
$admin_emoji_draftUpdate,
$admin_federation_deleteAllFiles,
$admin_federation_refreshRemoteInstanceMetadata,
$admin_federation_removeAllFollowing,
@ -949,6 +954,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$invite_limit,
$meta,
$emojis,
$emoji_drafts,
$emoji,
$miauth_genToken,
$mute_create,
@ -1092,6 +1098,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_emoji_setCategoryBulk,
$admin_emoji_setLicenseBulk,
$admin_emoji_update,
$admin_emoji_draftUpdate,
$admin_federation_deleteAllFiles,
$admin_federation_refreshRemoteInstanceMetadata,
$admin_federation_removeAllFollowing,
@ -1298,6 +1305,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$invite_limit,
$meta,
$emojis,
$emoji_drafts,
$emoji,
$miauth_genToken,
$mute_create,

View file

@ -37,6 +37,7 @@ import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-al
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_emoji_draftUpdate from './endpoints/admin/emoji/draft-update.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
@ -243,6 +244,7 @@ import * as ep___invite_list from './endpoints/invite/list.js';
import * as ep___invite_limit from './endpoints/invite/limit.js';
import * as ep___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___emojiDrafts from './endpoints/emoji-drafts.js';
import * as ep___emoji from './endpoints/emoji.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js';
@ -386,6 +388,7 @@ const eps = [
['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk],
['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
['admin/emoji/update', ep___admin_emoji_update],
['admin/emoji/draft-update', ep___admin_emoji_draftUpdate],
['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
@ -592,6 +595,7 @@ const eps = [
['invite/limit', ep___invite_limit],
['meta', ep___meta],
['emojis', ep___emojis],
['emoji-drafts', ep___emojiDrafts],
['emoji', ep___emoji],
['miauth/gen-token', ep___miauth_genToken],
['mute/create', ep___mute_create],

View file

@ -18,6 +18,11 @@ export const meta = {
code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
duplicateName: {
message: 'Duplicate name.',
code: 'DUPLICATE_NAME',
id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
},
},
} as const;
@ -55,11 +60,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
const isDraftDuplicate = await this.customEmojiService.checkDraftDuplicate(ps.name);
if (isDuplicate || isDraftDuplicate) throw new ApiError(meta.errors.duplicateName);
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
const emoji = await this.customEmojiService.add({
const emoji = await this.customEmojiService.draft({
driveFile,
name: ps.name,
category: ps.category ?? null,
@ -67,9 +76,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
license: ps.license ?? null,
isSensitive: ps.isSensitive ?? false,
localOnly: ps.localOnly ?? false,
host: null,
draft: true,
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
});
await this.moderationLogService.log(me, 'addCustomEmoji', {

View file

@ -51,7 +51,7 @@ export const paramDef = {
type: 'string',
} },
},
required: ['name', 'fileId', 'draft'],
required: ['name', 'fileId'],
} as const;
// TODO: ロジックをサービスに切り出す
@ -82,7 +82,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
license: ps.license ?? null,
isSensitive: ps.isSensitive ?? false,
localOnly: ps.localOnly ?? false,
draft: false,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
}, me);

View file

@ -36,7 +36,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
await this.customEmojiService.delete(ps.id, me);
const emoji = await this.customEmojiService.getEmojiById(ps.id);
const draftEmoji = await this.customEmojiService.getEmojiDraftById(ps.id);
if (emoji != null) {
await this.customEmojiService.delete(ps.id, me);
}
if (draftEmoji != null) {
await this.customEmojiService.draftDelete(ps.id);
}
});
}
}

View file

@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import type { DriveFilesRepository, EmojiDraftsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
errors: {
noSuchEmoji: {
message: 'No such emoji.',
code: 'NO_SUCH_EMOJI',
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d',
},
sameNameEmojiExists: {
message: 'Emoji that have same name already exists.',
code: 'SAME_NAME_EMOJI_EXISTS',
id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
fileId: { type: 'string', format: 'misskey:id' },
category: {
type: 'string',
nullable: true,
description: 'Use `null` to reset the category.',
},
aliases: { type: 'array', items: {
type: 'string',
} },
license: { type: 'string', nullable: true },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
type: 'string',
} },
draft: { type: 'boolean' },
},
required: ['id', 'name', 'aliases'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.emojiDraftsRepository)
private emojiDraftsRepository: EmojiDraftsRepository,
private customEmojiService: CustomEmojiService,
private driveFileEntityService: DriveFileEntityService,
) {
super(meta, paramDef, async (ps, me) => {
let driveFile;
const isDraft = !!ps.draft;
if (ps.fileId) {
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
}
const emoji = await this.customEmojiService.getEmojiDraftById(ps.id);
if (emoji != null) {
if (ps.name !== emoji.name) {
const isDuplicate = await this.customEmojiService.checkDraftDuplicate(ps.name);
if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
}
} else {
throw new ApiError(meta.errors.noSuchEmoji);
}
if (!isDraft) {
const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl);
if (file === null) throw new ApiError(meta.errors.noSuchFile);
await this.customEmojiService.add({
driveFile: file,
name: ps.name,
category: ps.category ?? null,
aliases: ps.aliases ?? [],
host: null,
license: ps.license ?? null,
isSensitive: ps.isSensitive ?? false,
localOnly: ps.localOnly ?? false,
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
}, me);
await this.customEmojiService.draftDelete(ps.id);
} else {
await this.customEmojiService.draftUpdate(ps.id, {
name: ps.name,
category: ps.category ?? null,
aliases: ps.aliases ?? [],
license: ps.license ?? null,
isSensitive: ps.isSensitive ?? false,
localOnly: ps.localOnly ?? false,
}, me);
}
});
}
}

View file

@ -5,8 +5,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import type { DriveFilesRepository } from '@/models/_.js';
import type { DriveFilesRepository, EmojiDraftsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@ -57,7 +58,7 @@ export const paramDef = {
} },
draft: { type: 'boolean' },
},
required: ['id', 'name', 'aliases', 'draft'],
required: ['id', 'name', 'aliases'],
} as const;
@Injectable()
@ -67,10 +68,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private driveFilesRepository: DriveFilesRepository,
private customEmojiService: CustomEmojiService,
private driveFileEntityService: DriveFileEntityService,
) {
super(meta, paramDef, async (ps, me) => {
let driveFile;
const isDraft = !!ps.draft;
if (ps.fileId) {
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
@ -85,18 +87,31 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchEmoji);
}
await this.customEmojiService.update(ps.id, {
driveFile,
name: ps.name,
category: ps.category ?? null,
aliases: ps.aliases,
license: ps.license ?? null,
isSensitive: ps.isSensitive,
localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
fileId: ps.fileId ?? null,
draft: ps.draft,
}, me);
if (!isDraft) {
await this.customEmojiService.update(ps.id, {
driveFile,
name: ps.name,
category: ps.category ?? null,
aliases: ps.aliases,
license: ps.license ?? null,
isSensitive: ps.isSensitive,
localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
}, me);
} else {
const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl);
if (file === null) throw new ApiError(meta.errors.noSuchFile);
await this.customEmojiService.draft({
driveFile: file,
name: ps.name,
category: ps.category ?? null,
aliases: ps.aliases ?? [],
license: ps.license ?? null,
isSensitive: ps.isSensitive ?? false,
localOnly: ps.localOnly ?? false,
}, me);
await this.customEmojiService.delete(ps.id);
}
});
}
}

View file

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { EmojiDraftsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { EmojiDraftsEntityService } from '@/core/entities/EmojiDraftsEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['meta'],
requireCredential: false,
allowGet: true,
cacheSec: 3600,
res: {
type: 'object',
optional: false, nullable: false,
properties: {
emojis: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'EmojiDraftDetailed',
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.emojiDraftsRepository)
private emojiDraftsRepository: EmojiDraftsRepository,
private emojiDraftsEntityService: EmojiDraftsEntityService,
) {
super(meta, paramDef, async () => {
const emojis = await this.emojiDraftsRepository.find({
order: {
category: 'ASC',
name: 'ASC',
},
});
return {
emojis: await this.emojiDraftsEntityService.packDetailedMany(emojis),
};
});
}
}

View file

@ -33,6 +33,7 @@ export const moderationLogTypes = [
'unsuspend',
'updateUserNote',
'addCustomEmoji',
'requestCustomEmoji',
'updateCustomEmoji',
'deleteCustomEmoji',
'assignRole',
@ -88,6 +89,10 @@ export type ModerationLogPayloads = {
emojiId: string;
emoji: any;
};
requestCustomEmoji: {
emojiId: string;
emoji: any;
};
updateCustomEmoji: {
emojiId: string;
before: any;