diff options
Diffstat (limited to 'packages/backend/src')
21 files changed, 608 insertions, 6 deletions
diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts new file mode 100644 index 0000000000..e97946f9dc --- /dev/null +++ b/packages/backend/src/core/AvatarDecorationService.ts @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import type { AvatarDecorationsRepository, MiAvatarDecoration, MiUser } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { MemorySingleCache } from '@/misc/cache.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +@Injectable() +export class AvatarDecorationService implements OnApplicationShutdown { + public cache: MemorySingleCache<MiAvatarDecoration[]>; + + constructor( + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, + + @Inject(DI.avatarDecorationsRepository) + private avatarDecorationsRepository: AvatarDecorationsRepository, + + private idService: IdService, + private moderationLogService: ModerationLogService, + private globalEventService: GlobalEventService, + ) { + this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); + + this.redisForSub.on('message', this.onMessage); + } + + @bindThis + private async onMessage(_: string, data: string): Promise<void> { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'avatarDecorationCreated': + case 'avatarDecorationUpdated': + case 'avatarDecorationDeleted': { + this.cache.delete(); + break; + } + default: + break; + } + } + } + + @bindThis + public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> { + const created = await this.avatarDecorationsRepository.insert({ + id: this.idService.gen(), + ...options, + }).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('avatarDecorationCreated', created); + + if (moderator) { + this.moderationLogService.log(moderator, 'createAvatarDecoration', { + avatarDecorationId: created.id, + avatarDecoration: created, + }); + } + + return created; + } + + @bindThis + public async update(id: MiAvatarDecoration['id'], params: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<void> { + const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id }); + + const date = new Date(); + await this.avatarDecorationsRepository.update(avatarDecoration.id, { + updatedAt: date, + ...params, + }); + + const updated = await this.avatarDecorationsRepository.findOneByOrFail({ id: avatarDecoration.id }); + this.globalEventService.publishInternalEvent('avatarDecorationUpdated', updated); + + if (moderator) { + this.moderationLogService.log(moderator, 'updateAvatarDecoration', { + avatarDecorationId: avatarDecoration.id, + before: avatarDecoration, + after: updated, + }); + } + } + + @bindThis + public async delete(id: MiAvatarDecoration['id'], moderator?: MiUser): Promise<void> { + const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id }); + + await this.avatarDecorationsRepository.delete({ id: avatarDecoration.id }); + this.globalEventService.publishInternalEvent('avatarDecorationDeleted', avatarDecoration); + + if (moderator) { + this.moderationLogService.log(moderator, 'deleteAvatarDecoration', { + avatarDecorationId: avatarDecoration.id, + avatarDecoration: avatarDecoration, + }); + } + } + + @bindThis + public async getAll(noCache = false): Promise<MiAvatarDecoration[]> { + if (noCache) { + this.cache.delete(); + } + return this.cache.fetch(() => this.avatarDecorationsRepository.find()); + } + + @bindThis + public dispose(): void { + this.redisForSub.off('message', this.onMessage); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index e7e66646fc..b46afb1909 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -11,6 +11,7 @@ import { AnnouncementService } from './AnnouncementService.js'; import { AntennaService } from './AntennaService.js'; import { AppLockService } from './AppLockService.js'; import { AchievementService } from './AchievementService.js'; +import { AvatarDecorationService } from './AvatarDecorationService.js'; import { CaptchaService } from './CaptchaService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CustomEmojiService } from './CustomEmojiService.js'; @@ -140,6 +141,7 @@ const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExis const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; +const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; @@ -273,6 +275,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AntennaService, AppLockService, AchievementService, + AvatarDecorationService, CaptchaService, CreateSystemUserService, CustomEmojiService, @@ -399,6 +402,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AntennaService, $AppLockService, $AchievementService, + $AvatarDecorationService, $CaptchaService, $CreateSystemUserService, $CustomEmojiService, @@ -526,6 +530,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AntennaService, AppLockService, AchievementService, + AvatarDecorationService, CaptchaService, CreateSystemUserService, CustomEmojiService, @@ -651,6 +656,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AntennaService, $AppLockService, $AchievementService, + $AvatarDecorationService, $CaptchaService, $CreateSystemUserService, $CustomEmojiService, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index b74fbbe584..bfbdecf688 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -18,7 +18,7 @@ import type { MiSignin } from '@/models/Signin.js'; import type { MiPage } from '@/models/Page.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { MiMeta } from '@/models/Meta.js'; -import { MiRole, MiRoleAssignment } from '@/models/_.js'; +import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -188,6 +188,9 @@ export interface InternalEventTypes { antennaCreated: MiAntenna; antennaDeleted: MiAntenna; antennaUpdated: MiAntenna; + avatarDecorationCreated: MiAvatarDecoration; + avatarDecorationDeleted: MiAvatarDecoration; + avatarDecorationUpdated: MiAvatarDecoration; metaUpdated: MiMeta; followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 2c2ff7af1d..ef05920d50 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -228,6 +228,12 @@ export class RoleService implements OnApplicationShutdown { } @bindThis + public async getRoles() { + const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); + return roles; + } + + @bindThis public async getUserAssigns(userId: MiUser['id']) { const now = Date.now(); let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index b0577fc1a0..66facce4c2 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -21,9 +21,10 @@ import { RoleService } from '@/core/RoleService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { IdService } from '@/core/IdService.js'; +import type { AnnouncementService } from '@/core/AnnouncementService.js'; +import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import type { OnModuleInit } from '@nestjs/common'; -import type { AnnouncementService } from '../AnnouncementService.js'; -import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { NoteEntityService } from './NoteEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; @@ -62,6 +63,7 @@ export class UserEntityService implements OnModuleInit { private roleService: RoleService; private federatedInstanceService: FederatedInstanceService; private idService: IdService; + private avatarDecorationService: AvatarDecorationService; constructor( private moduleRef: ModuleRef, @@ -126,6 +128,7 @@ export class UserEntityService implements OnModuleInit { this.roleService = this.moduleRef.get('RoleService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.idService = this.moduleRef.get('IdService'); + this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService'); } //#region Validators @@ -328,8 +331,6 @@ export class UserEntityService implements OnModuleInit { ...announcement, })) : null; - const falsy = opts.detail ? false : undefined; - const packed = { id: user.id, name: user.name, @@ -337,6 +338,10 @@ export class UserEntityService implements OnModuleInit { host: user.host, avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user), avatarBlurhash: user.avatarBlurhash, + avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => decorations.filter(decoration => user.avatarDecorations.includes(decoration.id)).map(decoration => ({ + id: decoration.id, + url: decoration.url, + }))) : [], isBot: user.isBot, isCat: user.isCat, instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index edcdd21d60..8411cb8229 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -18,6 +18,7 @@ export const DI = { announcementsRepository: Symbol('announcementsRepository'), announcementReadsRepository: Symbol('announcementReadsRepository'), appsRepository: Symbol('appsRepository'), + avatarDecorationsRepository: Symbol('avatarDecorationsRepository'), noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'), diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts new file mode 100644 index 0000000000..08ebbdeac1 --- /dev/null +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from './util/id.js'; + +@Entity('avatar_decoration') +export class MiAvatarDecoration { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + nullable: true, + }) + public updatedAt: Date | null; + + @Column('varchar', { + length: 1024, + }) + public url: string; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Column('varchar', { + length: 2048, + }) + public description: string; + + // TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする + @Column('varchar', { + array: true, length: 128, default: '{}', + }) + public roleIdsThatCanBeUsedThisDecoration: string[]; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 9efd6841b1..866fdfe6d4 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -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, MiAvatarDecoration, 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 type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -39,6 +39,12 @@ const $appsRepository: Provider = { inject: [DI.db], }; +const $avatarDecorationsRepository: Provider = { + provide: DI.avatarDecorationsRepository, + useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration), + inject: [DI.db], +}; + const $noteFavoritesRepository: Provider = { provide: DI.noteFavoritesRepository, useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite), @@ -402,6 +408,7 @@ const $userMemosRepository: Provider = { $announcementsRepository, $announcementReadsRepository, $appsRepository, + $avatarDecorationsRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, @@ -468,6 +475,7 @@ const $userMemosRepository: Provider = { $announcementsRepository, $announcementReadsRepository, $appsRepository, + $avatarDecorationsRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 796d7c8356..c98426a7b6 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -138,6 +138,11 @@ export class MiUser { }) public bannerBlurhash: string | null; + @Column('varchar', { + length: 512, array: true, default: '{}', + }) + public avatarDecorations: string[]; + @Index() @Column('varchar', { length: 128, array: true, default: '{}', diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index f974f95ed8..d7c327f164 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -10,6 +10,7 @@ import { MiAnnouncement } from '@/models/Announcement.js'; import { MiAnnouncementRead } from '@/models/AnnouncementRead.js'; import { MiAntenna } from '@/models/Antenna.js'; import { MiApp } from '@/models/App.js'; +import { MiAvatarDecoration } from '@/models/AvatarDecoration.js'; import { MiAuthSession } from '@/models/AuthSession.js'; import { MiBlocking } from '@/models/Blocking.js'; import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; @@ -77,6 +78,7 @@ export { MiAnnouncementRead, MiAntenna, MiApp, + MiAvatarDecoration, MiAuthSession, MiBlocking, MiChannelFollowing, @@ -143,6 +145,7 @@ export type AnnouncementsRepository = Repository<MiAnnouncement>; export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>; export type AntennasRepository = Repository<MiAntenna>; export type AppsRepository = Repository<MiApp>; +export type AvatarDecorationsRepository = Repository<MiAvatarDecoration>; export type AuthSessionsRepository = Repository<MiAuthSession>; export type BlockingsRepository = Repository<MiBlocking>; export type ChannelFollowingsRepository = Repository<MiChannelFollowing>; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 57d2d976ff..bf283fbeb2 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -37,6 +37,26 @@ export const packedUserLiteSchema = { type: 'string', nullable: true, optional: false, }, + avatarDecorations: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + properties: { + id: { + type: 'string', + nullable: false, optional: false, + format: 'id', + }, + url: { + type: 'string', + format: 'url', + nullable: false, optional: false, + }, + }, + }, + }, isAdmin: { type: 'boolean', nullable: false, optional: true, diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index d4c6ad82ce..cd611839a4 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -18,6 +18,7 @@ import { MiAnnouncement } from '@/models/Announcement.js'; import { MiAnnouncementRead } from '@/models/AnnouncementRead.js'; import { MiAntenna } from '@/models/Antenna.js'; import { MiApp } from '@/models/App.js'; +import { MiAvatarDecoration } from '@/models/AvatarDecoration.js'; import { MiAuthSession } from '@/models/AuthSession.js'; import { MiBlocking } from '@/models/Blocking.js'; import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; @@ -129,6 +130,7 @@ export const entities = [ MiMeta, MiInstance, MiApp, + MiAvatarDecoration, MiAuthSession, MiAccessToken, MiUser, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index f834561456..f234a2637d 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -18,6 +18,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; +import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js'; +import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js'; +import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; +import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.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'; @@ -176,6 +180,7 @@ import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js'; import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; +import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js'; import * as ep___hashtags_list from './endpoints/hashtags/list.js'; import * as ep___hashtags_search from './endpoints/hashtags/search.js'; import * as ep___hashtags_show from './endpoints/hashtags/show.js'; @@ -368,6 +373,10 @@ const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default }; const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default }; const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default }; +const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-decorations/create', useClass: ep___admin_avatarDecorations_create.default }; +const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default }; +const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default }; +const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default }; const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.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 }; @@ -526,6 +535,7 @@ const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useCla const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default }; const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default }; const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default }; +const $getAvatarDecorations: Provider = { provide: 'ep:get-avatar-decorations', useClass: ep___getAvatarDecorations.default }; const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default }; const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default }; const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default }; @@ -722,6 +732,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_announcements_delete, $admin_announcements_list, $admin_announcements_update, + $admin_avatarDecorations_create, + $admin_avatarDecorations_delete, + $admin_avatarDecorations_list, + $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, @@ -880,6 +894,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $gallery_posts_unlike, $gallery_posts_update, $getOnlineUsersCount, + $getAvatarDecorations, $hashtags_list, $hashtags_search, $hashtags_show, @@ -1070,6 +1085,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_announcements_delete, $admin_announcements_list, $admin_announcements_update, + $admin_avatarDecorations_create, + $admin_avatarDecorations_delete, + $admin_avatarDecorations_list, + $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, @@ -1228,6 +1247,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $gallery_posts_unlike, $gallery_posts_update, $getOnlineUsersCount, + $getAvatarDecorations, $hashtags_list, $hashtags_search, $hashtags_show, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index d12a035afa..8d34edca9d 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -18,6 +18,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; +import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js'; +import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js'; +import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; +import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.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'; @@ -176,6 +180,7 @@ import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js'; import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js'; import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js'; import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js'; +import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js'; import * as ep___hashtags_list from './endpoints/hashtags/list.js'; import * as ep___hashtags_search from './endpoints/hashtags/search.js'; import * as ep___hashtags_show from './endpoints/hashtags/show.js'; @@ -366,6 +371,10 @@ const eps = [ ['admin/announcements/delete', ep___admin_announcements_delete], ['admin/announcements/list', ep___admin_announcements_list], ['admin/announcements/update', ep___admin_announcements_update], + ['admin/avatar-decorations/create', ep___admin_avatarDecorations_create], + ['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete], + ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list], + ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/cleanup', ep___admin_drive_cleanup], @@ -524,6 +533,7 @@ const eps = [ ['gallery/posts/unlike', ep___gallery_posts_unlike], ['gallery/posts/update', ep___gallery_posts_update], ['get-online-users-count', ep___getOnlineUsersCount], + ['get-avatar-decorations', ep___getAvatarDecorations], ['hashtags/list', ep___hashtags_list], ['hashtags/search', ep___hashtags_search], ['hashtags/show', ep___hashtags_show], diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts new file mode 100644 index 0000000000..c1869b141a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1 }, + description: { type: 'string' }, + url: { type: 'string', minLength: 1 }, + roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { + type: 'string', + } }, + }, + required: ['name', 'description', 'url'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private avatarDecorationService: AvatarDecorationService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.avatarDecorationService.create({ + name: ps.name, + description: ps.description, + url: ps.url, + roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + }, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts new file mode 100644 index 0000000000..5aba24b426 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts @@ -0,0 +1,39 @@ +/* + * 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 { DI } from '@/di-symbols.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + }, + required: ['id'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private avatarDecorationService: AvatarDecorationService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.avatarDecorationService.delete(ps.id, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts new file mode 100644 index 0000000000..9a32a59081 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js'; +import type { MiAnnouncement } from '@/models/Announcement.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + updatedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + description: { + type: 'string', + optional: false, nullable: false, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + roleIdsThatCanBeUsedThisDecoration: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id', nullable: true }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private avatarDecorationService: AvatarDecorationService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const avatarDecorations = await this.avatarDecorationService.getAll(true); + + return avatarDecorations.map(avatarDecoration => ({ + id: avatarDecoration.id, + createdAt: this.idService.parse(avatarDecoration.id).date.toISOString(), + updatedAt: avatarDecoration.updatedAt?.toISOString() ?? null, + name: avatarDecoration.name, + description: avatarDecoration.description, + url: avatarDecoration.url, + roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration, + })); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts new file mode 100644 index 0000000000..564014a3df --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -0,0 +1,50 @@ +/* + * 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 { DI } from '@/di-symbols.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { type: 'string', format: 'misskey:id' }, + name: { type: 'string', minLength: 1 }, + description: { type: 'string' }, + url: { type: 'string', minLength: 1 }, + roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { + type: 'string', + } }, + }, + required: ['id'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private avatarDecorationService: AvatarDecorationService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.avatarDecorationService.update(ps.id, { + name: ps.name, + description: ps.description, + url: ps.url, + roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + }, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts new file mode 100644 index 0000000000..ec602a0dc5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -0,0 +1,79 @@ +/* + * 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 { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + description: { + type: 'string', + optional: false, nullable: false, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + roleIdsThatCanBeUsedThisDecoration: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + }, + }, + }, +} 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( + private avatarDecorationService: AvatarDecorationService, + ) { + super(meta, paramDef, async (ps, me) => { + const decorations = await this.avatarDecorationService.getAll(true); + + return decorations.map(decoration => ({ + id: decoration.id, + name: decoration.name, + description: decoration.description, + url: decoration.url, + roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration, + })); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 431bb4c60a..f1837e7082 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -32,6 +32,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { Config } from '@/config.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -131,6 +132,9 @@ export const paramDef = { birthday: { ...birthdaySchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, + avatarDecorations: { type: 'array', maxItems: 1, items: { + type: 'string', + } }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, fields: { type: 'array', @@ -207,6 +211,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private roleService: RoleService, private cacheService: CacheService, private httpRequestService: HttpRequestService, + private avatarDecorationService: AvatarDecorationService, ) { super(meta, paramDef, async (ps, _user, token) => { const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; @@ -296,6 +301,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- updates.bannerBlurhash = null; } + if (ps.avatarDecorations) { + const decorations = await this.avatarDecorationService.getAll(true); + const myRoles = await this.roleService.getUserRoles(user.id); + const allRoles = await this.roleService.getRoles(); + const decorationIds = decorations + .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) + .map(d => d.id); + + updates.avatarDecorations = ps.avatarDecorations.filter(id => decorationIds.includes(id)); + } + if (ps.pinnedPageId) { const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 316073c992..69224360b3 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -60,6 +60,9 @@ export const moderationLogTypes = [ 'createAd', 'updateAd', 'deleteAd', + 'createAvatarDecoration', + 'updateAvatarDecoration', + 'deleteAvatarDecoration', ] as const; export type ModerationLogPayloads = { @@ -221,6 +224,19 @@ export type ModerationLogPayloads = { adId: string; ad: any; }; + createAvatarDecoration: { + avatarDecorationId: string; + avatarDecoration: any; + }; + updateAvatarDecoration: { + avatarDecorationId: string; + before: any; + after: any; + }; + deleteAvatarDecoration: { + avatarDecorationId: string; + avatarDecoration: any; + }; }; export type Serialized<T> = { |