summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/core/AvatarDecorationService.ts129
-rw-r--r--packages/backend/src/core/CoreModule.ts6
-rw-r--r--packages/backend/src/core/GlobalEventService.ts5
-rw-r--r--packages/backend/src/core/RoleService.ts6
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts13
-rw-r--r--packages/backend/src/di-symbols.ts1
-rw-r--r--packages/backend/src/models/AvatarDecoration.ts39
-rw-r--r--packages/backend/src/models/RepositoryModule.ts10
-rw-r--r--packages/backend/src/models/User.ts5
-rw-r--r--packages/backend/src/models/_.ts3
-rw-r--r--packages/backend/src/models/json-schema/user.ts20
-rw-r--r--packages/backend/src/postgres.ts2
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts20
-rw-r--r--packages/backend/src/server/api/endpoints.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts44
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts101
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts50
-rw-r--r--packages/backend/src/server/api/endpoints/get-avatar-decorations.ts79
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts16
-rw-r--r--packages/backend/src/types.ts16
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> = {