Merge remote-tracking branch 'mi-dev/develop' into emoji
# Conflicts: # packages/backend/src/models/RepositoryModule.ts # packages/misskey-js/etc/misskey-js.api.md
This commit is contained in:
commit
27a7d0bbf7
75 changed files with 2799 additions and 686 deletions
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AvatarDecoration1697847397844 {
|
||||
name = 'AvatarDecoration1697847397844'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "avatar_decoration" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "url" character varying(1024) NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(2048) NOT NULL, "roleIdsThatCanBeUsedThisDecoration" character varying(128) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_b6de9296f6097078e1dc53f7603" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" character varying(512) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`);
|
||||
await queryRunner.query(`DROP TABLE "avatar_decoration"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
"@types/js-yaml": "4.0.8",
|
||||
"@types/jsdom": "21.1.4",
|
||||
"@types/jsonld": "1.5.11",
|
||||
"@types/jsrsasign": "10.5.10",
|
||||
"@types/jsrsasign": "10.5.11",
|
||||
"@types/mime-types": "2.1.3",
|
||||
"@types/ms": "0.7.33",
|
||||
"@types/node": "20.8.7",
|
||||
|
|
|
|||
129
packages/backend/src/core/AvatarDecorationService.ts
Normal file
129
packages/backend/src/core/AvatarDecorationService.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -141,6 +142,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 };
|
||||
|
|
@ -275,6 +277,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
AntennaService,
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
AvatarDecorationService,
|
||||
CaptchaService,
|
||||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
|
|
@ -402,6 +405,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$AntennaService,
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$AvatarDecorationService,
|
||||
$CaptchaService,
|
||||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
|
|
@ -530,6 +534,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
AntennaService,
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
AvatarDecorationService,
|
||||
CaptchaService,
|
||||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
|
|
@ -656,6 +661,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$AntennaService,
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$AvatarDecorationService,
|
||||
$CaptchaService,
|
||||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
|
|
|
|||
|
|
@ -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']; };
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ import { MetaService } from '@/core/MetaService.js';
|
|||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||
import { nyaize } from '@/misc/nyaize.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
|
|
|||
|
|
@ -229,6 +229,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();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiInstance } from '@/models/Instance.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
|
||||
@bindThis
|
||||
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) {
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
let hide = false;
|
||||
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
|
|
@ -83,7 +83,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
|
||||
|
||||
if (specified) {
|
||||
|
|
@ -360,12 +360,14 @@ export class NoteEntityService implements OnModuleInit {
|
|||
|
||||
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
||||
detail: false,
|
||||
skipHide: opts.skipHide,
|
||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||
_hint_: options?._hint_,
|
||||
}) : undefined,
|
||||
|
||||
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
|
||||
detail: true,
|
||||
skipHide: opts.skipHide,
|
||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||
_hint_: options?._hint_,
|
||||
}) : undefined,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -322,9 +325,11 @@ export class UserEntityService implements OnModuleInit {
|
|||
|
||||
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
|
||||
const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null;
|
||||
const unreadAnnouncements = isMe && opts.detail ? await this.announcementService.getUnreadAnnouncements(user) : null;
|
||||
|
||||
const falsy = opts.detail ? false : undefined;
|
||||
const unreadAnnouncements = isMe && opts.detail ?
|
||||
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
|
||||
createdAt: this.idService.parse(announcement.id).date.toISOString(),
|
||||
...announcement,
|
||||
})) : null;
|
||||
|
||||
const packed = {
|
||||
id: user.id,
|
||||
|
|
@ -333,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 ? {
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function nyaize(text: string): string {
|
||||
return text
|
||||
// ja-JP
|
||||
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
|
||||
// en-US
|
||||
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
|
||||
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
|
||||
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
|
||||
// ko-KR
|
||||
.replace(/[나-낳]/g, match => String.fromCharCode(
|
||||
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
|
||||
))
|
||||
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
|
||||
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
|
||||
}
|
||||
39
packages/backend/src/models/AvatarDecoration.ts
Normal file
39
packages/backend/src/models/AvatarDecoration.ts
Normal file
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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, 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 { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, 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';
|
||||
|
||||
|
|
@ -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),
|
||||
|
|
@ -408,6 +414,7 @@ const $userMemosRepository: Provider = {
|
|||
$announcementsRepository,
|
||||
$announcementReadsRepository,
|
||||
$appsRepository,
|
||||
$avatarDecorationsRepository,
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
|
|
@ -475,6 +482,7 @@ const $userMemosRepository: Provider = {
|
|||
$announcementsRepository,
|
||||
$announcementReadsRepository,
|
||||
$appsRepository,
|
||||
$avatarDecorationsRepository,
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
|
|
|
|||
|
|
@ -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: '{}',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -78,6 +79,7 @@ export {
|
|||
MiAnnouncementRead,
|
||||
MiAntenna,
|
||||
MiApp,
|
||||
MiAvatarDecoration,
|
||||
MiAuthSession,
|
||||
MiBlocking,
|
||||
MiChannelFollowing,
|
||||
|
|
@ -145,6 +147,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>;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -130,6 +131,7 @@ export const entities = [
|
|||
MiMeta,
|
||||
MiInstance,
|
||||
MiApp,
|
||||
MiAvatarDecoration,
|
||||
MiAuthSession,
|
||||
MiAccessToken,
|
||||
MiUser,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -163,6 +167,7 @@ import * as ep___federation_stats from './endpoints/federation/stats.js';
|
|||
import * as ep___following_create from './endpoints/following/create.js';
|
||||
import * as ep___following_delete from './endpoints/following/delete.js';
|
||||
import * as ep___following_update from './endpoints/following/update.js';
|
||||
import * as ep___following_update_all from './endpoints/following/update-all.js';
|
||||
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
|
||||
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
|
||||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
||||
|
|
@ -178,6 +183,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';
|
||||
|
|
@ -354,6 +360,7 @@ import * as ep___users_show from './endpoints/users/show.js';
|
|||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
|
||||
import * as ep___retention from './endpoints/retention.js';
|
||||
import { GetterService } from './GetterService.js';
|
||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||
|
|
@ -371,6 +378,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 };
|
||||
|
|
@ -516,6 +527,7 @@ const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass:
|
|||
const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
|
||||
const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
|
||||
const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default };
|
||||
const $following_update_all: Provider = { provide: 'ep:following/update-all', useClass: ep___following_update_all.default };
|
||||
const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
|
||||
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
|
||||
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
|
||||
|
|
@ -531,6 +543,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 };
|
||||
|
|
@ -707,6 +720,7 @@ const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_s
|
|||
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
|
||||
const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
|
||||
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
|
||||
const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
|
||||
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
|
||||
|
||||
@Module({
|
||||
|
|
@ -728,6 +742,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,
|
||||
|
|
@ -873,6 +891,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$following_create,
|
||||
$following_delete,
|
||||
$following_update,
|
||||
$following_update_all,
|
||||
$following_invalidate,
|
||||
$following_requests_accept,
|
||||
$following_requests_cancel,
|
||||
|
|
@ -888,6 +907,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$gallery_posts_unlike,
|
||||
$gallery_posts_update,
|
||||
$getOnlineUsersCount,
|
||||
$getAvatarDecorations,
|
||||
$hashtags_list,
|
||||
$hashtags_search,
|
||||
$hashtags_show,
|
||||
|
|
@ -1064,6 +1084,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$users_achievements,
|
||||
$users_updateMemo,
|
||||
$fetchRss,
|
||||
$fetchExternalResources,
|
||||
$retention,
|
||||
],
|
||||
exports: [
|
||||
|
|
@ -1079,6 +1100,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,
|
||||
|
|
@ -1224,6 +1249,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$following_create,
|
||||
$following_delete,
|
||||
$following_update,
|
||||
$following_update_all,
|
||||
$following_invalidate,
|
||||
$following_requests_accept,
|
||||
$following_requests_cancel,
|
||||
|
|
@ -1239,6 +1265,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$gallery_posts_unlike,
|
||||
$gallery_posts_update,
|
||||
$getOnlineUsersCount,
|
||||
$getAvatarDecorations,
|
||||
$hashtags_list,
|
||||
$hashtags_search,
|
||||
$hashtags_show,
|
||||
|
|
@ -1412,6 +1439,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$users_achievements,
|
||||
$users_updateMemo,
|
||||
$fetchRss,
|
||||
$fetchExternalResources,
|
||||
$retention,
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -163,6 +167,7 @@ import * as ep___federation_stats from './endpoints/federation/stats.js';
|
|||
import * as ep___following_create from './endpoints/following/create.js';
|
||||
import * as ep___following_delete from './endpoints/following/delete.js';
|
||||
import * as ep___following_update from './endpoints/following/update.js';
|
||||
import * as ep___following_update_all from './endpoints/following/update-all.js';
|
||||
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
|
||||
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
|
||||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
||||
|
|
@ -178,6 +183,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';
|
||||
|
|
@ -354,6 +360,7 @@ import * as ep___users_show from './endpoints/users/show.js';
|
|||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
|
||||
import * as ep___retention from './endpoints/retention.js';
|
||||
|
||||
const eps = [
|
||||
|
|
@ -369,6 +376,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],
|
||||
|
|
@ -514,6 +525,7 @@ const eps = [
|
|||
['following/create', ep___following_create],
|
||||
['following/delete', ep___following_delete],
|
||||
['following/update', ep___following_update],
|
||||
['following/update-all', ep___following_update_all],
|
||||
['following/invalidate', ep___following_invalidate],
|
||||
['following/requests/accept', ep___following_requests_accept],
|
||||
['following/requests/cancel', ep___following_requests_cancel],
|
||||
|
|
@ -529,6 +541,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],
|
||||
|
|
@ -705,6 +718,7 @@ const eps = [
|
|||
['users/achievements', ep___users_achievements],
|
||||
['users/update-memo', ep___users_updateMemo],
|
||||
['fetch-rss', ep___fetchRss],
|
||||
['fetch-external-resources', ep___fetchExternalResources],
|
||||
['retention', ep___retention],
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
import ms from 'ms';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { ApiError } from '../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 50,
|
||||
},
|
||||
|
||||
errors: {
|
||||
invalidSchema: {
|
||||
message: 'External resource returned invalid schema.',
|
||||
code: 'EXT_RESOURCE_RETURNED_INVALID_SCHEMA',
|
||||
id: 'bb774091-7a15-4a70-9dc5-6ac8cf125856',
|
||||
},
|
||||
hashUnmached: {
|
||||
message: 'Hash did not match.',
|
||||
code: 'EXT_RESOURCE_HASH_DIDNT_MATCH',
|
||||
id: '693ba8ba-b486-40df-a174-72f8279b56a4',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' },
|
||||
hash: { type: 'string' },
|
||||
},
|
||||
required: ['url', 'hash'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private httpRequestService: HttpRequestService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const res = await this.httpRequestService.getJson<{
|
||||
type: string;
|
||||
data: string;
|
||||
}>(ps.url);
|
||||
|
||||
if (!res.data || !res.type) {
|
||||
throw new ApiError(meta.errors.invalidSchema);
|
||||
}
|
||||
|
||||
const resHash = createHash('sha512').update(res.data.replace(/\r\n/g, '\n')).digest('hex');
|
||||
if (resHash !== ps.hash) {
|
||||
throw new ApiError(meta.errors.hashUnmached);
|
||||
}
|
||||
|
||||
return {
|
||||
type: res.type,
|
||||
data: res.data,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { FollowingsRepository } from '@/models/_.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['following', 'users'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:following',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
notify: { type: 'string', enum: ['normal', 'none'] },
|
||||
withReplies: { type: 'boolean' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.followingsRepository.update({
|
||||
followerId: me.id,
|
||||
}, {
|
||||
notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined,
|
||||
withReplies: ps.withReplies != null ? ps.withReplies : undefined,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
|
||||
|
|
@ -421,9 +437,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const myLink = `${this.config.url}/@${user.username}`;
|
||||
|
||||
const includesMyLink = Array.from(doc.getElementsByTagName('a')).some(a => a.href === myLink);
|
||||
const aEls = Array.from(doc.getElementsByTagName('a'));
|
||||
const linkEls = Array.from(doc.getElementsByTagName('link'));
|
||||
|
||||
if (includesMyLink) {
|
||||
const includesMyLink = aEls.some(a => a.href === myLink);
|
||||
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
|
||||
|
||||
if (includesMyLink || includesRelMeLinks) {
|
||||
await this.userProfilesRepository.createQueryBuilder('profile').update()
|
||||
.where('userId = :userId', { userId: user.id })
|
||||
.set({
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.js';
|
||||
import type { NotesRepository, UserListsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
|
@ -67,9 +64,6 @@ export const paramDef = {
|
|||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.redisForTimelines)
|
||||
private redisForTimelines: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
|
|
|
|||
|
|
@ -39,20 +39,22 @@ class HomeTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user!.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
|
||||
if (note.channelId) {
|
||||
if (!this.followingChannels.has(note.channelId)) return;
|
||||
} else {
|
||||
// その投稿のユーザーをフォローしていなかったら弾く
|
||||
if ((this.user!.id !== note.userId) && !Object.hasOwn(this.following, note.userId)) return;
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
}
|
||||
|
||||
// Ignore notes from instances the user has muted
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances))) return;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
if (!Object.hasOwn(this.following, note.userId)) return;
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
} else if (note.visibility === 'specified') {
|
||||
if (!note.visibleUserIds!.includes(this.user!.id)) return;
|
||||
}
|
||||
|
|
@ -61,7 +63,7 @@ class HomeTimelineChannel extends Channel {
|
|||
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ class HybridTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user!.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
|
||||
// チャンネルの投稿ではなく、自分自身の投稿 または
|
||||
|
|
@ -56,14 +58,14 @@ class HybridTimelineChannel extends Channel {
|
|||
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
|
||||
// フォローしているチャンネルの投稿 の場合だけ
|
||||
if (!(
|
||||
(note.channelId == null && this.user!.id === note.userId) ||
|
||||
(note.channelId == null && isMe) ||
|
||||
(note.channelId == null && Object.hasOwn(this.following, note.userId)) ||
|
||||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
|
||||
(note.channelId != null && this.followingChannels.has(note.channelId))
|
||||
)) return;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
if (!Object.hasOwn(this.following, note.userId)) return;
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
} else if (note.visibility === 'specified') {
|
||||
if (!note.visibleUserIds!.includes(this.user!.id)) return;
|
||||
}
|
||||
|
|
@ -75,7 +77,7 @@ class HybridTimelineChannel extends Channel {
|
|||
if (note.reply && !this.following[note.userId]?.withReplies && !this.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
|
|
|||
|
|
@ -78,12 +78,14 @@ class UserListChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user!.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
|
||||
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
if (!Object.hasOwn(this.following, note.userId)) return;
|
||||
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
|
||||
} else if (note.visibility === 'specified') {
|
||||
if (!note.visibleUserIds!.includes(this.user!.id)) return;
|
||||
}
|
||||
|
|
@ -92,7 +94,7 @@ class UserListChannel extends Channel {
|
|||
if (note.reply && !this.membershipsMap[note.userId]?.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ export const moderationLogTypes = [
|
|||
'createAd',
|
||||
'updateAd',
|
||||
'deleteAd',
|
||||
'createAvatarDecoration',
|
||||
'updateAvatarDecoration',
|
||||
'deleteAvatarDecoration',
|
||||
] as const;
|
||||
|
||||
export type ModerationLogPayloads = {
|
||||
|
|
@ -226,6 +229,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> = {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,16 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
test('自分の visibility: followers な投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:Home
|
||||
() => api('notes/create', { text: 'foo', visibility: 'followers' }, ayano), // ayano posts
|
||||
msg => msg.type === 'note' && msg.body.text === 'foo',
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
test('フォローしているユーザーの投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
|
|
@ -125,6 +135,30 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
test('フォローしているユーザーの visibility: followers な投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), // kyoko posts
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
/* なんか失敗する
|
||||
test('フォローしているユーザーの visibility: followers な投稿への返信が流れる', async () => {
|
||||
const note = await api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko);
|
||||
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
() => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.body.id }, kyoko), // kyoko posts
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo',
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
*/
|
||||
|
||||
test('フォローしていないユーザーの投稿は流れない', async () => {
|
||||
const fired = await waitFire(
|
||||
kyoko, 'homeTimeline', // kyoko:home
|
||||
|
|
@ -241,6 +275,16 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
test('自分の visibility: followers な投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'hybridTimeline',
|
||||
() => api('notes/create', { text: 'foo', visibility: 'followers' }, ayano), // ayano posts
|
||||
msg => msg.type === 'note' && msg.body.text === 'foo',
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
test('フォローしていないローカルユーザーの投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'hybridTimeline', // ayano:Hybrid
|
||||
|
|
@ -293,6 +337,16 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
test('フォローしているユーザーの visibility: followers な投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'hybridTimeline', // ayano:Hybrid
|
||||
() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko),
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
test('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'hybridTimeline', // ayano:Hybrid
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ describe('ユーザー', () => {
|
|||
host: user.host,
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
avatarDecorations: user.avatarDecorations,
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
instance: user.instance,
|
||||
|
|
@ -349,6 +350,7 @@ describe('ユーザー', () => {
|
|||
assert.strictEqual(response.host, null);
|
||||
assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
|
||||
assert.strictEqual(response.avatarBlurhash, null);
|
||||
assert.deepStrictEqual(response.avatarDecorations, []);
|
||||
assert.strictEqual(response.isBot, false);
|
||||
assert.strictEqual(response.isCat, false);
|
||||
assert.strictEqual(response.instance, undefined);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
|
|||
onlineStatus: 'unknown',
|
||||
avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
|
||||
avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
|
||||
avatarDecorations: [],
|
||||
emojis: [],
|
||||
bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog',
|
||||
bannerColor: '#000000',
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
"@tabler/icons-webfont": "2.37.0",
|
||||
"@vitejs/plugin-vue": "4.4.0",
|
||||
"@vue-macros/reactivity-transform": "0.3.23",
|
||||
"@vue/compiler-sfc": "3.3.4",
|
||||
"@vue/compiler-sfc": "3.3.5",
|
||||
"astring": "1.8.6",
|
||||
"autosize": "6.0.1",
|
||||
"broadcast-channel": "5.5.0",
|
||||
|
|
@ -73,29 +73,29 @@
|
|||
"v-code-diff": "1.7.1",
|
||||
"vanilla-tilt": "1.8.1",
|
||||
"vite": "4.5.0",
|
||||
"vue": "3.3.4",
|
||||
"vue": "3.3.5",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.5.0",
|
||||
"@storybook/addon-essentials": "7.5.0",
|
||||
"@storybook/addon-interactions": "7.5.0",
|
||||
"@storybook/addon-links": "7.5.0",
|
||||
"@storybook/addon-storysource": "7.5.0",
|
||||
"@storybook/addons": "7.5.0",
|
||||
"@storybook/blocks": "7.5.0",
|
||||
"@storybook/core-events": "7.5.0",
|
||||
"@storybook/addon-actions": "7.5.1",
|
||||
"@storybook/addon-essentials": "7.5.1",
|
||||
"@storybook/addon-interactions": "7.5.1",
|
||||
"@storybook/addon-links": "7.5.1",
|
||||
"@storybook/addon-storysource": "7.5.1",
|
||||
"@storybook/addons": "7.5.1",
|
||||
"@storybook/blocks": "7.5.1",
|
||||
"@storybook/core-events": "7.5.1",
|
||||
"@storybook/jest": "0.2.3",
|
||||
"@storybook/manager-api": "7.5.0",
|
||||
"@storybook/preview-api": "7.5.0",
|
||||
"@storybook/react": "7.5.0",
|
||||
"@storybook/react-vite": "7.5.0",
|
||||
"@storybook/manager-api": "7.5.1",
|
||||
"@storybook/preview-api": "7.5.1",
|
||||
"@storybook/react": "7.5.1",
|
||||
"@storybook/react-vite": "7.5.1",
|
||||
"@storybook/testing-library": "0.2.2",
|
||||
"@storybook/theming": "7.5.0",
|
||||
"@storybook/types": "7.5.0",
|
||||
"@storybook/vue3": "7.5.0",
|
||||
"@storybook/vue3-vite": "7.5.0",
|
||||
"@storybook/theming": "7.5.1",
|
||||
"@storybook/types": "7.5.1",
|
||||
"@storybook/vue3": "7.5.1",
|
||||
"@storybook/vue3-vite": "7.5.1",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.2",
|
||||
"@types/estree": "1.0.3",
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||
"@typescript-eslint/parser": "6.8.0",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.3.4",
|
||||
"@vue/runtime-core": "3.3.5",
|
||||
"acorn": "8.10.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.3.2",
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.1",
|
||||
"storybook": "7.5.0",
|
||||
"storybook": "7.5.1",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
|
||||
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
|
||||
<img :class="$style.icon" :src="avatarUrl" alt="">
|
||||
<span>
|
||||
<span>@{{ username }}</span>
|
||||
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span>
|
||||
|
|
@ -15,11 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { toUnicode } from 'punycode';
|
||||
import { } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { host as localHost } from '@/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
|
||||
const props = defineProps<{
|
||||
username: string;
|
||||
|
|
@ -37,6 +38,11 @@ const isMe = $i && (
|
|||
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
|
||||
bg.setAlpha(0.1);
|
||||
const bgCss = bg.toRgbString();
|
||||
|
||||
const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
|
||||
: `/avatar/@${props.username}@${props.host}`,
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<footer>
|
||||
<div :class="$style.noteFooterInfo">
|
||||
<MkA :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
||||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div :class="$style.info">
|
||||
<MkA :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
<MkTime :time="note.createdAt" colored/>
|
||||
</MkA>
|
||||
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
|
|
|
|||
|
|
@ -283,6 +283,12 @@ useTooltip(reactionRef, (showing) => {
|
|||
|
||||
.quote:first-child {
|
||||
margin-right: 4px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.quote:last-child {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
|||
|
||||
const pagination: Paging = {
|
||||
endpoint: 'i/notifications' as const,
|
||||
limit: 10,
|
||||
limit: 20,
|
||||
params: computed(() => ({
|
||||
excludeTypes: props.excludeTypes ?? undefined,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
|
||||
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
|
||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||
<div v-if="user.isCat" :class="[$style.ears]">
|
||||
<div :class="$style.earLeft">
|
||||
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img v-if="decoration || user.avatarDecorations.length > 0" :class="[$style.decoration]" :src="decoration ?? user.avatarDecorations[0].url" alt="">
|
||||
</component>
|
||||
</template>
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ const props = withDefaults(defineProps<{
|
|||
link?: boolean;
|
||||
preview?: boolean;
|
||||
indicator?: boolean;
|
||||
decoration?: string;
|
||||
}>(), {
|
||||
target: null,
|
||||
link: false,
|
||||
|
|
@ -134,7 +136,7 @@ watch(() => props.user.avatarBlurhash, () => {
|
|||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 20%;
|
||||
|
|
@ -278,4 +280,13 @@ watch(() => props.user.avatarBlurhash, () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.decoration {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<time :title="absolute">
|
||||
<time :title="absolute" :class="{ [$style.old1]: colored && (ago > 60 * 60 * 24 * 90), [$style.old2]: colored && (ago > 60 * 60 * 24 * 180) }">
|
||||
<template v-if="invalid">{{ i18n.ts._ago.invalid }}</template>
|
||||
<template v-else-if="mode === 'relative'">{{ relative }}</template>
|
||||
<template v-else-if="mode === 'absolute'">{{ absolute }}</template>
|
||||
|
|
@ -22,6 +22,7 @@ const props = withDefaults(defineProps<{
|
|||
time: Date | string | number | null;
|
||||
origin?: Date | null;
|
||||
mode?: 'relative' | 'absolute' | 'detail';
|
||||
colored?: boolean;
|
||||
}>(), {
|
||||
origin: isChromatic() ? new Date('2023-04-01T00:00:00Z') : null,
|
||||
mode: 'relative',
|
||||
|
|
@ -75,3 +76,13 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod
|
|||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.old1 {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.old1.old2 {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -31,23 +31,28 @@ import * as os from '@/os.js';
|
|||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
url: string;
|
||||
rel?: string;
|
||||
}>();
|
||||
showUrlPreview?: boolean;
|
||||
}>(), {
|
||||
showUrlPreview: true,
|
||||
});
|
||||
|
||||
const self = props.url.startsWith(local);
|
||||
const url = new URL(props.url);
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
const el = ref();
|
||||
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
|
||||
showing,
|
||||
url: props.url,
|
||||
source: el.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
if (props.showUrlPreview) {
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
|
||||
showing,
|
||||
url: props.url,
|
||||
source: el.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
const schema = url.protocol;
|
||||
const hostname = decodePunycode(url.hostname);
|
||||
|
|
|
|||
103
packages/frontend/src/pages/admin/avatar-decorations.vue
Normal file
103
packages/frontend/src/pages/admin/avatar-decorations.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
||||
<template #label>{{ avatarDecoration.name }}</template>
|
||||
<template #caption>{{ avatarDecoration.description }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="avatarDecoration.name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="avatarDecoration.description">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="avatarDecoration.url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<div class="buttons _buttons">
|
||||
<MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
let avatarDecorations: any[] = $ref([]);
|
||||
|
||||
function add() {
|
||||
avatarDecorations.unshift({
|
||||
_id: Math.random().toString(36),
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
url: '',
|
||||
});
|
||||
}
|
||||
|
||||
function del(avatarDecoration) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
avatarDecorations = avatarDecorations.filter(x => x !== avatarDecoration);
|
||||
os.api('admin/avatar-decorations/delete', avatarDecoration);
|
||||
});
|
||||
}
|
||||
|
||||
async function save(avatarDecoration) {
|
||||
if (avatarDecoration.id == null) {
|
||||
await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
|
||||
load();
|
||||
} else {
|
||||
os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
os.api('admin/avatar-decorations/list').then(_avatarDecorations => {
|
||||
avatarDecorations = _avatarDecorations;
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.add,
|
||||
handler: add,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.avatarDecorations,
|
||||
icon: 'ti ti-sparkles',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -115,6 +115,11 @@ const menuDef = $computed(() => [{
|
|||
text: i18n.ts.customEmojis,
|
||||
to: '/admin/emojis',
|
||||
active: currentPage?.route.name === 'emojis',
|
||||
}, {
|
||||
icon: 'ti ti-sparkles',
|
||||
text: i18n.ts.avatarDecorations,
|
||||
to: '/admin/avatar-decorations',
|
||||
active: currentPage?.route.name === 'avatarDecorations',
|
||||
}, {
|
||||
icon: 'ti ti-whirl',
|
||||
text: i18n.ts.federation,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>
|
||||
<b
|
||||
:class="{
|
||||
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation'].includes(log.type),
|
||||
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
|
||||
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
|
||||
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd'].includes(log.type)
|
||||
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
|
||||
}"
|
||||
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
|
|
@ -37,6 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="log.type === 'deleteUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'deleteNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'deleteDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'createAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
|
||||
<span v-else-if="log.type === 'updateAvatarDecoration'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<MkAvatar :user="log.user" :class="$style.avatar"/>
|
||||
|
|
@ -102,6 +105,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateAvatarDecoration'">
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<details>
|
||||
<summary>raw</summary>
|
||||
|
|
|
|||
354
packages/frontend/src/pages/install-extentions.vue
Normal file
354
packages/frontend/src/pages/install-extentions.vue
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="500">
|
||||
<MkLoading v-if="uiPhase === 'fetching'"/>
|
||||
<div v-else-if="uiPhase === 'confirm' && data" class="_gaps_m" :class="$style.extInstallerRoot">
|
||||
<div :class="$style.extInstallerIconWrapper">
|
||||
<i v-if="data.type === 'plugin'" class="ti ti-plug"></i>
|
||||
<i v-else-if="data.type === 'theme'" class="ti ti-palette"></i>
|
||||
<i v-else class="ti ti-download"></i>
|
||||
</div>
|
||||
<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}</h2>
|
||||
<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
|
||||
<MkInfo v-if="data.type === 'plugin'" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._externalResourceInstaller[`_${data.type}`].metaTitle }}</template>
|
||||
<div class="_gaps_s">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.name }}</template>
|
||||
<template #value>{{ data.meta?.name }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.author }}</template>
|
||||
<template #value>{{ data.meta?.author }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<MkKeyValue v-if="data.type === 'plugin'">
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ data.meta?.description }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="data.type === 'plugin'">
|
||||
<template #key>{{ i18n.ts.version }}</template>
|
||||
<template #value>{{ data.meta?.version }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="data.type === 'plugin'">
|
||||
<template #key>{{ i18n.ts.permission }}</template>
|
||||
<template #value>
|
||||
<ul :class="$style.extInstallerKVList">
|
||||
<li v-for="permission in data.meta?.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="data.type === 'theme' && data.meta?.base">
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
|
||||
<template #value>{{ i18n.ts[data.meta.base] }}</template>
|
||||
</MkKeyValue>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
|
||||
|
||||
<MkCode :code="data.raw ?? ''"/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
|
||||
<template #value><MkUrl :url="url ?? ''" :showUrlPreview="false"></MkUrl></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
|
||||
<template #value>
|
||||
<!--この画面が出ている時点でハッシュの検証には成功している-->
|
||||
<i class="ti ti-check" style="color: var(--accent)"></i>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</FormSection>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]">
|
||||
<div :class="$style.extInstallerIconWrapper">
|
||||
<i class="ti ti-circle-x"></i>
|
||||
</div>
|
||||
<h2 :class="$style.extInstallerTitle">{{ errorKV?.title }}</h2>
|
||||
<div :class="$style.extInstallerNormDesc">{{ errorKV?.description }}</div>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton @click="goBack()">{{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton @click="goToMisskey()">{{ i18n.ts.goToMisskey }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import MkLoading from '@/components/global/MkLoading.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkCode from '@/components/MkCode.vue';
|
||||
import MkUrl from '@/components/global/MkUrl.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
|
||||
import { parseThemeCode, installTheme } from '@/scripts/install-theme.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const uiPhase = ref<'fetching' | 'confirm' | 'error'>('fetching');
|
||||
const errorKV = ref<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
}>({
|
||||
title: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const url = urlParams.get('url');
|
||||
const hash = urlParams.get('hash');
|
||||
|
||||
const data = ref<{
|
||||
type: 'plugin' | 'theme';
|
||||
raw: string;
|
||||
meta?: {
|
||||
// Plugin & Theme Common
|
||||
name: string;
|
||||
author: string;
|
||||
|
||||
// Plugin
|
||||
description?: string;
|
||||
version?: string;
|
||||
permissions?: string[];
|
||||
config?: Record<string, any>;
|
||||
|
||||
// Theme
|
||||
base?: 'light' | 'dark';
|
||||
};
|
||||
} | null>(null);
|
||||
|
||||
function goBack(): void {
|
||||
history.back();
|
||||
}
|
||||
|
||||
function goToMisskey(): void {
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
if (!url || !hash) {
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._invalidParams.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._invalidParams.description,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
const res = await os.api('fetch-external-resources', {
|
||||
url,
|
||||
hash,
|
||||
}).catch((err) => {
|
||||
switch (err.id) {
|
||||
case 'bb774091-7a15-4a70-9dc5-6ac8cf125856':
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._failedToFetch.parseErrorDescription,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
break;
|
||||
case '693ba8ba-b486-40df-a174-72f8279b56a4':
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._hashUnmatched.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._hashUnmatched.description,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
break;
|
||||
default:
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._failedToFetch.fetchErrorDescription,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
break;
|
||||
}
|
||||
throw new Error(err.code);
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._failedToFetch.fetchErrorDescription,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
switch (res.type) {
|
||||
case 'plugin':
|
||||
try {
|
||||
const meta = await parsePluginMeta(res.data);
|
||||
data.value = {
|
||||
type: 'plugin',
|
||||
meta,
|
||||
raw: res.data,
|
||||
};
|
||||
} catch (err) {
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._pluginParseFailed.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._pluginParseFailed.description,
|
||||
};
|
||||
console.error(err);
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'theme':
|
||||
try {
|
||||
const metaRaw = parseThemeCode(res.data);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { id, props, desc: description, ...meta } = metaRaw;
|
||||
data.value = {
|
||||
type: 'theme',
|
||||
meta: {
|
||||
description,
|
||||
...meta,
|
||||
},
|
||||
raw: res.data,
|
||||
};
|
||||
} catch (err) {
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._themeParseFailed.title,
|
||||
description: i18n.ts._theme.alreadyInstalled,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._themeParseFailed.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._themeParseFailed.description,
|
||||
};
|
||||
break;
|
||||
}
|
||||
console.error(err);
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._resourceTypeNotSupported.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._resourceTypeNotSupported.description,
|
||||
};
|
||||
uiPhase.value = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
uiPhase.value = 'confirm';
|
||||
}
|
||||
|
||||
async function install() {
|
||||
if (!data.value) return;
|
||||
|
||||
switch (data.value.type) {
|
||||
case 'plugin':
|
||||
if (!data.value.meta) return;
|
||||
try {
|
||||
await installPlugin(data.value.raw, data.value.meta as AiScriptPluginMeta);
|
||||
os.success();
|
||||
nextTick(() => {
|
||||
unisonReload('/');
|
||||
});
|
||||
} catch (err) {
|
||||
errorKV.value = {
|
||||
title: i18n.ts._externalResourceInstaller._errors._pluginInstallFailed.title,
|
||||
description: i18n.ts._externalResourceInstaller._errors._pluginInstallFailed.description,
|
||||
};
|
||||
console.error(err);
|
||||
uiPhase.value = 'error';
|
||||
}
|
||||
break;
|
||||
case 'theme':
|
||||
if (!data.value.meta) return;
|
||||
await installTheme(data.value.raw);
|
||||
os.success();
|
||||
nextTick(() => {
|
||||
location.href = '/settings/theme';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetch();
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts._externalResourceInstaller.title,
|
||||
icon: 'ti ti-download',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.extInstallerRoot {
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.extInstallerIconWrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
line-height: 48px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.error .extInstallerIconWrapper {
|
||||
background-color: rgba(255, 42, 42, .15);
|
||||
color: #ff2a2a;
|
||||
}
|
||||
|
||||
.extInstallerTitle {
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.extInstallerNormDesc {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.extInstallerKVList {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -30,6 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
|
||||
<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
|
||||
<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
|
||||
<MkButton danger @click="updateRepliesAll(true)"><i class="ti ti-messages"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
|
||||
<MkButton danger @click="updateRepliesAll(false)"><i class="ti ti-messages-off"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.pinnedList }}</template>
|
||||
<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
|
||||
|
|
@ -332,6 +334,15 @@ async function setPinnedList() {
|
|||
defaultStore.set('pinnedUserLists', [list]);
|
||||
}
|
||||
|
||||
async function updateRepliesAll(withReplies: boolean) {
|
||||
const { canceled } = os.confirm({
|
||||
type: 'warning',
|
||||
text: withReplies ? i18n.ts.confirmShowRepliesAll : i18n.ts.confirmHideRepliesAll,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.api('following/update-all', { withReplies });
|
||||
}
|
||||
|
||||
function removePinnedList() {
|
||||
defaultStore.set('pinnedUserLists', []);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,130 +18,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, nextTick, ref } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { ColdDeviceStorage } from '@/store.js';
|
||||
import { installPlugin } from '@/scripts/install-plugin.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const parser = new Parser();
|
||||
const code = ref(null);
|
||||
|
||||
function installPlugin({ id, meta, src, token }) {
|
||||
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
|
||||
...meta,
|
||||
id,
|
||||
active: true,
|
||||
configData: {},
|
||||
token: token,
|
||||
src: src,
|
||||
}));
|
||||
}
|
||||
|
||||
function isSupportedAiScriptVersion(version: string): boolean {
|
||||
try {
|
||||
return (compareVersions(version, '0.12.0') >= 0);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const code = ref<string | null>(null);
|
||||
|
||||
async function install() {
|
||||
if (code.value == null) return;
|
||||
if (!code.value) return;
|
||||
|
||||
const lv = utils.getLangVersion(code.value);
|
||||
if (lv == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No language version annotation found :(',
|
||||
});
|
||||
return;
|
||||
} else if (!isSupportedAiScriptVersion(lv)) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: `aiscript version '${lv}' is not supported :(`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = parser.parse(code.value);
|
||||
await installPlugin(code.value);
|
||||
os.success();
|
||||
|
||||
nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Syntax error :(',
|
||||
title: 'Install failed',
|
||||
text: err.toString() ?? null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = Interpreter.collectMetadata(ast);
|
||||
if (meta == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No metadata found :(',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = meta.get(null);
|
||||
if (metadata == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'No metadata found :(',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, version, author, description, permissions, config } = metadata;
|
||||
if (name == null || version == null || author == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Required property not found :(',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
|
||||
title: i18n.ts.tokenRequested,
|
||||
information: i18n.ts.pluginTokenRequestedDescription,
|
||||
initialName: name,
|
||||
initialPermissions: permissions,
|
||||
}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
res(token);
|
||||
},
|
||||
}, 'closed');
|
||||
});
|
||||
|
||||
installPlugin({
|
||||
id: uuid(),
|
||||
meta: {
|
||||
name, version, author, description, permissions, config,
|
||||
},
|
||||
token,
|
||||
src: code.value,
|
||||
});
|
||||
|
||||
os.success();
|
||||
|
||||
nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||
</FormSlot>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-sparkles"></i></template>
|
||||
<template #label>{{ i18n.ts.avatarDecorations }}</template>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-gap: 12px;">
|
||||
<div
|
||||
v-for="avatarDecoration in avatarDecorations"
|
||||
:key="avatarDecoration.id"
|
||||
:class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]"
|
||||
@click="toggleDecoration(avatarDecoration)"
|
||||
>
|
||||
<div :class="$style.avatarDecorationName">{{ avatarDecoration.name }}</div>
|
||||
<MkAvatar style="width: 64px; height: 64px;" :user="$i" :decoration="avatarDecoration.url"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
||||
|
||||
|
|
@ -126,6 +143,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
|
||||
let avatarDecorations: any[] = $ref([]);
|
||||
|
||||
const profile = reactive({
|
||||
name: $i.name,
|
||||
|
|
@ -146,6 +164,10 @@ watch(() => profile, () => {
|
|||
const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
|
||||
const fieldEditMode = ref(false);
|
||||
|
||||
os.api('get-avatar-decorations').then(_avatarDecorations => {
|
||||
avatarDecorations = _avatarDecorations;
|
||||
});
|
||||
|
||||
function addField() {
|
||||
fields.value.push({
|
||||
id: Math.random().toString(),
|
||||
|
|
@ -244,6 +266,20 @@ function changeBanner(ev) {
|
|||
});
|
||||
}
|
||||
|
||||
function toggleDecoration(avatarDecoration) {
|
||||
if ($i.avatarDecorations.some(x => x.id === avatarDecoration.id)) {
|
||||
os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [],
|
||||
});
|
||||
$i.avatarDecorations = [];
|
||||
} else {
|
||||
os.apiWithDialog('i/update', {
|
||||
avatarDecorations: [avatarDecoration.id],
|
||||
});
|
||||
$i.avatarDecorations.push(avatarDecoration);
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
|
@ -338,4 +374,23 @@ definePageMetadata({
|
|||
.dragItemForm {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.avatarDecoration {
|
||||
cursor: pointer;
|
||||
padding: 16px 16px 24px 16px;
|
||||
border: solid 2px var(--divider);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatarDecorationActive {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.avatarDecorationName {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkTextarea>
|
||||
|
||||
<div class="_buttons">
|
||||
<MkButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
|
||||
<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
|
||||
<MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -18,60 +18,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { applyTheme, validateTheme } from '@/scripts/theme.js';
|
||||
import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import { addTheme, getThemes } from '@/theme-store';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
let installThemeCode = $ref(null);
|
||||
|
||||
function parseThemeCode(code: string) {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (getThemes().some(t => t.id === theme.id)) {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts._theme.alreadyInstalled,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
function preview(code: string): void {
|
||||
const theme = parseThemeCode(code);
|
||||
if (theme) applyTheme(theme, false);
|
||||
}
|
||||
|
||||
async function install(code: string): Promise<void> {
|
||||
const theme = parseThemeCode(code);
|
||||
if (!theme) return;
|
||||
await addTheme(theme);
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.t('_theme.installed', { name: theme.name }),
|
||||
});
|
||||
try {
|
||||
const theme = parseThemeCode(code);
|
||||
await installTheme(code);
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.t('_theme.installed', { name: theme.name }),
|
||||
});
|
||||
} catch (err) {
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.ts._theme.alreadyInstalled,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts._theme.invalid,
|
||||
});
|
||||
break;
|
||||
}
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
|
|
|||
|
|
@ -322,6 +322,10 @@ export const routes = [{
|
|||
}, {
|
||||
path: '/registry',
|
||||
component: page(() => import('./pages/registry.vue')),
|
||||
}, {
|
||||
path: '/install-extentions',
|
||||
component: page(() => import('./pages/install-extentions.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/admin/user/:userId',
|
||||
component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')),
|
||||
|
|
@ -343,6 +347,10 @@ export const routes = [{
|
|||
path: '/emojis',
|
||||
name: 'emojis',
|
||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('./pages/admin/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/queue',
|
||||
name: 'queue',
|
||||
|
|
|
|||
129
packages/frontend/src/scripts/install-plugin.ts
Normal file
129
packages/frontend/src/scripts/install-plugin.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||
import type { Plugin } from '@/store.js';
|
||||
import { ColdDeviceStorage } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
export type AiScriptPluginMeta = {
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
description?: string;
|
||||
permissions?: string[];
|
||||
config?: Record<string, any>;
|
||||
};
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
export function savePlugin({ id, meta, src, token }: {
|
||||
id: string;
|
||||
meta: AiScriptPluginMeta;
|
||||
src: string;
|
||||
token: string;
|
||||
}) {
|
||||
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
|
||||
...meta,
|
||||
id,
|
||||
active: true,
|
||||
configData: {},
|
||||
token: token,
|
||||
src: src,
|
||||
} as Plugin));
|
||||
}
|
||||
|
||||
export function isSupportedAiScriptVersion(version: string): boolean {
|
||||
try {
|
||||
return (compareVersions(version, '0.12.0') >= 0);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> {
|
||||
if (!code) {
|
||||
throw new Error('code is required');
|
||||
}
|
||||
|
||||
const lv = utils.getLangVersion(code);
|
||||
if (lv == null) {
|
||||
throw new Error('No language version annotation found');
|
||||
} else if (!isSupportedAiScriptVersion(lv)) {
|
||||
throw new Error(`Aiscript version '${lv}' is not supported`);
|
||||
}
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = parser.parse(code);
|
||||
} catch (err) {
|
||||
throw new Error('Aiscript syntax error');
|
||||
}
|
||||
|
||||
const meta = Interpreter.collectMetadata(ast);
|
||||
if (meta == null) {
|
||||
throw new Error('Meta block not found');
|
||||
}
|
||||
|
||||
const metadata = meta.get(null);
|
||||
if (metadata == null) {
|
||||
throw new Error('Metadata not found');
|
||||
}
|
||||
|
||||
const { name, version, author, description, permissions, config } = metadata;
|
||||
if (name == null || version == null || author == null) {
|
||||
throw new Error('Required property not found');
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
author,
|
||||
description,
|
||||
permissions,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
|
||||
if (!code) return;
|
||||
|
||||
let realMeta: AiScriptPluginMeta;
|
||||
if (!meta) {
|
||||
realMeta = await parsePluginMeta(code);
|
||||
} else {
|
||||
realMeta = meta;
|
||||
}
|
||||
|
||||
const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
|
||||
title: i18n.ts.tokenRequested,
|
||||
information: i18n.ts.pluginTokenRequestedDescription,
|
||||
initialName: realMeta.name,
|
||||
initialPermissions: realMeta.permissions,
|
||||
}, {
|
||||
done: async result => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api('miauth/gen-token', {
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
});
|
||||
res(token);
|
||||
},
|
||||
}, 'closed');
|
||||
});
|
||||
|
||||
savePlugin({
|
||||
id: uuid(),
|
||||
meta: realMeta,
|
||||
token,
|
||||
src: code,
|
||||
});
|
||||
}
|
||||
37
packages/frontend/src/scripts/install-theme.ts
Normal file
37
packages/frontend/src/scripts/install-theme.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import JSON5 from 'json5';
|
||||
import { addTheme, getThemes } from '@/theme-store.js';
|
||||
import { Theme, applyTheme, validateTheme } from '@/scripts/theme.js';
|
||||
|
||||
export function parseThemeCode(code: string): Theme {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (err) {
|
||||
throw new Error('Failed to parse theme json');
|
||||
}
|
||||
if (!validateTheme(theme)) {
|
||||
throw new Error('This theme is invaild');
|
||||
}
|
||||
if (getThemes().some(t => t.id === theme.id)) {
|
||||
throw new Error('This theme is already installed');
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
export function previewTheme(code: string): void {
|
||||
const theme = parseThemeCode(code);
|
||||
if (theme) applyTheme(theme, false);
|
||||
}
|
||||
|
||||
export async function installTheme(code: string): Promise<void> {
|
||||
const theme = parseThemeCode(code);
|
||||
if (!theme) return;
|
||||
await addTheme(theme);
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest';
|
|||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||
import './init';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import { directives } from '@/directives';
|
||||
import { components } from '@/components/index';
|
||||
import { directives } from '@/directives/index.js';
|
||||
import { components } from '@/components/index.js';
|
||||
import XHome from '@/pages/user/home.vue';
|
||||
|
||||
describe('XHome', () => {
|
||||
|
|
@ -34,6 +34,8 @@ describe('XHome', () => {
|
|||
createdAt: '1970-01-01T00:00:00.000Z',
|
||||
fields: [],
|
||||
pinnedNotes: [],
|
||||
avatarUrl: 'https://example.com',
|
||||
avatarDecorations: [],
|
||||
});
|
||||
|
||||
const anchor = home.container.querySelector<HTMLAnchorElement>('a[href^="https://example.com/"]');
|
||||
|
|
@ -54,6 +56,8 @@ describe('XHome', () => {
|
|||
createdAt: '1970-01-01T00:00:00.000Z',
|
||||
fields: [],
|
||||
pinnedNotes: [],
|
||||
avatarUrl: 'https://example.com',
|
||||
avatarDecorations: [],
|
||||
});
|
||||
|
||||
const anchor = home.container.querySelector<HTMLAnchorElement>('a[href^="https://example.com/"]');
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest';
|
|||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||
import './init';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import { components } from '@/components';
|
||||
import { directives } from '@/directives';
|
||||
import { components } from '@/components/index.js';
|
||||
import { directives } from '@/directives/index.js';
|
||||
import MkMediaImage from '@/components/MkMediaImage.vue';
|
||||
|
||||
describe('MkMediaImage', () => {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest';
|
|||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||
import './init';
|
||||
import type { summaly } from 'summaly';
|
||||
import { components } from '@/components';
|
||||
import { directives } from '@/directives';
|
||||
import { components } from '@/components/index.js';
|
||||
import { directives } from '@/directives/index.js';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
|
||||
type SummalyResult = Awaited<ReturnType<typeof summaly>>;
|
||||
|
|
|
|||
|
|
@ -2230,6 +2230,22 @@ export type Endpoints = {
|
|||
};
|
||||
};
|
||||
};
|
||||
'fetch-rss': {
|
||||
req: {
|
||||
url: string;
|
||||
};
|
||||
res: TODO;
|
||||
};
|
||||
'fetch-external-resources': {
|
||||
req: {
|
||||
url: string;
|
||||
hash: string;
|
||||
};
|
||||
res: {
|
||||
type: string;
|
||||
data: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
declare namespace entities {
|
||||
|
|
@ -2635,10 +2651,22 @@ type ModerationLog = {
|
|||
} | {
|
||||
type: 'deleteAd';
|
||||
info: ModerationLogPayloads['deleteAd'];
|
||||
} | {
|
||||
type: 'createAvatarDecoration';
|
||||
info: ModerationLogPayloads['createAvatarDecoration'];
|
||||
} | {
|
||||
type: 'updateAvatarDecoration';
|
||||
info: ModerationLogPayloads['updateAvatarDecoration'];
|
||||
} | {
|
||||
type: 'deleteAvatarDecoration';
|
||||
info: ModerationLogPayloads['deleteAvatarDecoration'];
|
||||
} | {
|
||||
type: 'resolveAbuseReport';
|
||||
info: ModerationLogPayloads['resolveAbuseReport'];
|
||||
});
|
||||
|
||||
// @public (undocumented)
|
||||
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd"];
|
||||
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration"];
|
||||
|
||||
// @public (undocumented)
|
||||
export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
|
||||
|
|
@ -2966,6 +2994,10 @@ type UserLite = {
|
|||
onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
|
||||
avatarUrl: string;
|
||||
avatarBlurhash: string;
|
||||
avatarDecorations: {
|
||||
id: ID;
|
||||
url: string;
|
||||
}[];
|
||||
emojis: {
|
||||
name: string;
|
||||
url: string;
|
||||
|
|
@ -2990,8 +3022,6 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
|||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:109:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:606:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
|
|
|||
|
|
@ -639,4 +639,11 @@ export type Endpoints = {
|
|||
$default: UserDetailed;
|
||||
};
|
||||
}; };
|
||||
|
||||
// fetching external data
|
||||
'fetch-rss': { req: { url: string; }; res: TODO; };
|
||||
'fetch-external-resources': {
|
||||
req: { url: string; hash: string; };
|
||||
res: { type: string; data: string; };
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ export const moderationLogTypes = [
|
|||
'createAd',
|
||||
'updateAd',
|
||||
'deleteAd',
|
||||
'createAvatarDecoration',
|
||||
'updateAvatarDecoration',
|
||||
'deleteAvatarDecoration',
|
||||
] as const;
|
||||
|
||||
export type ModerationLogPayloads = {
|
||||
|
|
@ -239,4 +242,17 @@ export type ModerationLogPayloads = {
|
|||
adId: string;
|
||||
ad: any;
|
||||
};
|
||||
createAvatarDecoration: {
|
||||
avatarDecorationId: string;
|
||||
avatarDecoration: any;
|
||||
};
|
||||
updateAvatarDecoration: {
|
||||
avatarDecorationId: string;
|
||||
before: any;
|
||||
after: any;
|
||||
};
|
||||
deleteAvatarDecoration: {
|
||||
avatarDecorationId: string;
|
||||
avatarDecoration: any;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ export type UserLite = {
|
|||
onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
|
||||
avatarUrl: string;
|
||||
avatarBlurhash: string;
|
||||
avatarDecorations: {
|
||||
id: ID;
|
||||
url: string;
|
||||
}[];
|
||||
emojis: {
|
||||
name: string;
|
||||
url: string;
|
||||
|
|
@ -694,4 +698,16 @@ export type ModerationLog = {
|
|||
} | {
|
||||
type: 'deleteAd';
|
||||
info: ModerationLogPayloads['deleteAd'];
|
||||
} | {
|
||||
type: 'createAvatarDecoration';
|
||||
info: ModerationLogPayloads['createAvatarDecoration'];
|
||||
} | {
|
||||
type: 'updateAvatarDecoration';
|
||||
info: ModerationLogPayloads['updateAvatarDecoration'];
|
||||
} | {
|
||||
type: 'deleteAvatarDecoration';
|
||||
info: ModerationLogPayloads['deleteAvatarDecoration'];
|
||||
} | {
|
||||
type: 'resolveAbuseReport';
|
||||
info: ModerationLogPayloads['resolveAbuseReport'];
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue