Merge branch 'develop' into re-ed25519
This commit is contained in:
commit
4477f956f8
78 changed files with 2181 additions and 586 deletions
|
|
@ -4,12 +4,15 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { Not, IsNull } from 'typeorm';
|
||||
import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccountService {
|
||||
|
|
@ -17,9 +20,14 @@ export class DeleteAccountService {
|
|||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private userSuspendService: UserSuspendService,
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private apRendererService: ApRendererService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -27,16 +35,52 @@ export class DeleteAccountService {
|
|||
public async deleteAccount(user: {
|
||||
id: string;
|
||||
host: string | null;
|
||||
}): Promise<void> {
|
||||
}, moderator?: MiUser): Promise<void> {
|
||||
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
|
||||
if (_user.isRoot) throw new Error('cannot delete a root account');
|
||||
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||
if (moderator != null) {
|
||||
this.moderationLogService.log(moderator, 'deleteAccount', {
|
||||
userId: user.id,
|
||||
userUsername: _user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
}
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: [
|
||||
{ followerSharedInbox: Not(IsNull()) },
|
||||
{ followeeSharedInbox: Not(IsNull()) },
|
||||
],
|
||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||
});
|
||||
|
||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||
|
||||
for (const inbox of inboxes) {
|
||||
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
||||
}
|
||||
|
||||
for (const inbox of queue) {
|
||||
this.queueService.deliver(user, content, inbox, true);
|
||||
}
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
} else {
|
||||
// リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: true,
|
||||
});
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import isSvg from 'is-svg';
|
|||
import probeImageSize from 'probe-image-size';
|
||||
import { type predictionType } from 'nsfwjs';
|
||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import { encode } from 'blurhash';
|
||||
import * as blurhash from 'blurhash';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { AiService } from '@/core/AiService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
|
|
@ -452,7 +452,7 @@ export class FileInfoService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calculate average color of image
|
||||
* Calculate blurhash string of image
|
||||
*/
|
||||
@bindThis
|
||||
private getBlurhash(path: string, type: string): Promise<string> {
|
||||
|
|
@ -467,7 +467,7 @@ export class FileInfoService {
|
|||
let hash;
|
||||
|
||||
try {
|
||||
hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
|
||||
hash = blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import type { ModerationLogsRepository } from '@/models/_.js';
|
|||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';
|
||||
import type { ModerationLogPayloads } from '@/types.js';
|
||||
import { moderationLogTypes } from '@/types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ModerationLogService {
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
const meta = await this.metaService.fetch();
|
||||
|
||||
this.notesChart.update(note, true);
|
||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) {
|
||||
this.perUserNotesChart.update(user, note, true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export class NoteDeleteService {
|
|||
this.deliverToConcerned(user, note, content);
|
||||
}
|
||||
|
||||
// also deliever delete activity to cascaded notes
|
||||
// also deliver delete activity to cascaded notes
|
||||
const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
|
||||
for (const cascadingNote of federatedLocalCascadingNotes) {
|
||||
if (!cascadingNote.user) continue;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { reversiUpdateKeys } from 'misskey-js';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import { IsNull, LessThan, MoreThan } from 'typeorm';
|
||||
import type {
|
||||
|
|
@ -399,7 +400,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: string, value: any) {
|
||||
public isValidReversiUpdateKey(key: unknown): key is typeof reversiUpdateKeys[number] {
|
||||
if (typeof key !== 'string') return false;
|
||||
return (reversiUpdateKeys as string[]).includes(key);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isValidReversiUpdateValue<K extends typeof reversiUpdateKeys[number]>(key: K, value: unknown): value is MiReversiGame[K] {
|
||||
switch (key) {
|
||||
case 'map':
|
||||
return Array.isArray(value) && value.every(row => typeof row === 'string');
|
||||
case 'bw':
|
||||
return typeof value === 'string' && ['random', '1', '2'].includes(value);
|
||||
case 'isLlotheo':
|
||||
return typeof value === 'boolean';
|
||||
case 'canPutEverywhere':
|
||||
return typeof value === 'boolean';
|
||||
case 'loopedBoard':
|
||||
return typeof value === 'boolean';
|
||||
case 'timeLimitForEachTurn':
|
||||
return typeof value === 'number' && value >= 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateSettings<K extends typeof reversiUpdateKeys[number]>(gameId: MiReversiGame['id'], user: MiUser, key: K, value: MiReversiGame[K]) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
if (game.isStarted) return;
|
||||
|
|
@ -407,10 +434,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
if ((game.user1Id === user.id) && game.user1Ready) return;
|
||||
if ((game.user2Id === user.id) && game.user2Ready) return;
|
||||
|
||||
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard', 'timeLimitForEachTurn'].includes(key)) return;
|
||||
|
||||
// TODO: より厳格なバリデーション
|
||||
|
||||
const updatedGame = {
|
||||
...game,
|
||||
[key]: value,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Not, IsNull } from 'typeorm';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
|
|
@ -11,22 +14,76 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { UserKeypairService } from './UserKeypairService.js';
|
||||
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
|
||||
import { RelationshipJobData } from '@/queue/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserSuspendService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async doPostSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
|
||||
public async suspend(user: MiUser, moderator: MiUser): Promise<void> {
|
||||
await this.usersRepository.update(user.id, {
|
||||
isSuspended: true,
|
||||
});
|
||||
|
||||
this.moderationLogService.log(moderator, 'suspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await this.postSuspend(user).catch(e => {});
|
||||
await this.unFollowAll(user).catch(e => {});
|
||||
})();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unsuspend(user: MiUser, moderator: MiUser): Promise<void> {
|
||||
await this.usersRepository.update(user.id, {
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
this.moderationLogService.log(moderator, 'unsuspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await this.postUnsuspend(user).catch(e => {});
|
||||
})();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async postSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
||||
|
||||
this.followRequestsRepository.delete({
|
||||
followeeId: user.id,
|
||||
});
|
||||
this.followRequestsRepository.delete({
|
||||
followerId: user.id,
|
||||
});
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
const manager = this.apDeliverManagerService.createDeliverManager(user, content);
|
||||
|
|
@ -38,7 +95,7 @@ export class UserSuspendService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async doPostUnsuspend(user: MiUser): Promise<void> {
|
||||
private async postUnsuspend(user: MiUser): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
|
|
@ -50,4 +107,26 @@ export class UserSuspendService {
|
|||
manager.execute({ privateKey });
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async unFollowAll(follower: MiUser) {
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const jobs: RelationshipJobData[] = [];
|
||||
for (const following of followings) {
|
||||
if (following.followeeId && following.followerId) {
|
||||
jobs.push({
|
||||
from: { id: following.followerId },
|
||||
to: { id: following.followeeId },
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.queueService.createUnfollowJob(jobs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,9 +78,10 @@ export class ApNoteService {
|
|||
@bindThis
|
||||
public validateNote(object: IObject, uri: string): Error | null {
|
||||
const expectHost = this.utilityService.extractDbHost(uri);
|
||||
const apType = getApType(object);
|
||||
|
||||
if (!validPost.includes(getApType(object))) {
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
|
||||
if (apType == null || !validPost.includes(apType)) {
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`);
|
||||
}
|
||||
|
||||
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
|||
import type { ApLoggerService } from '../ApLoggerService.js';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import type { ApImageService } from './ApImageService.js';
|
||||
import type { IActor, IKey, IObject } from '../type.js';
|
||||
import type { IActor, IKey, IObject, ICollection, IOrderedCollection } from '../type.js';
|
||||
|
||||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
|
|
@ -350,6 +350,21 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
|
||||
|
||||
const [followingVisibility, followersVisibility] = await Promise.all(
|
||||
[
|
||||
this.isPublicCollection(person.following, resolver),
|
||||
this.isPublicCollection(person.followers, resolver),
|
||||
].map((p): Promise<'public' | 'private'> => p
|
||||
.then(isPublic => isPublic ? 'public' : 'private')
|
||||
.catch(err => {
|
||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||
}
|
||||
return 'private';
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
|
@ -411,6 +426,8 @@ export class ApPersonService implements OnModuleInit {
|
|||
description: _description,
|
||||
url,
|
||||
fields,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
userHost: host,
|
||||
|
|
@ -522,6 +539,23 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
||||
|
||||
const [followingVisibility, followersVisibility] = await Promise.all(
|
||||
[
|
||||
this.isPublicCollection(person.following, resolver),
|
||||
this.isPublicCollection(person.followers, resolver),
|
||||
].map((p): Promise<'public' | 'private' | undefined> => p
|
||||
.then(isPublic => isPublic ? 'public' : 'private')
|
||||
.catch(err => {
|
||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||
// Do not update the visibiility on transient errors.
|
||||
return undefined;
|
||||
}
|
||||
return 'private';
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
|
@ -608,6 +642,8 @@ export class ApPersonService implements OnModuleInit {
|
|||
url,
|
||||
fields,
|
||||
description: _description,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
});
|
||||
|
|
@ -779,4 +815,16 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver): Promise<boolean> {
|
||||
if (collection) {
|
||||
const resolved = await resolver.resolveCollection(collection);
|
||||
if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,11 +60,14 @@ export function getApId(value: string | IObject): string {
|
|||
|
||||
/**
|
||||
* Get ActivityStreams Object type
|
||||
*
|
||||
* タイプ判定ができなかった場合に、あえてエラーではなくnullを返すようにしている。
|
||||
* 詳細: https://github.com/misskey-dev/misskey/issues/14239
|
||||
*/
|
||||
export function getApType(value: IObject): string {
|
||||
export function getApType(value: IObject): string | null {
|
||||
if (typeof value.type === 'string') return value.type;
|
||||
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
|
||||
throw new Error('cannot detect type');
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
|
||||
|
|
@ -97,19 +100,23 @@ export interface IActivity extends IObject {
|
|||
export interface ICollection extends IObject {
|
||||
type: 'Collection';
|
||||
totalItems: number;
|
||||
items: ApObject;
|
||||
first?: IObject | string;
|
||||
items?: ApObject;
|
||||
}
|
||||
|
||||
export interface IOrderedCollection extends IObject {
|
||||
type: 'OrderedCollection';
|
||||
totalItems: number;
|
||||
orderedItems: ApObject;
|
||||
first?: IObject | string;
|
||||
orderedItems?: ApObject;
|
||||
}
|
||||
|
||||
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
|
||||
|
||||
export const isPost = (object: IObject): object is IPost =>
|
||||
validPost.includes(getApType(object));
|
||||
export const isPost = (object: IObject): object is IPost => {
|
||||
const type = getApType(object);
|
||||
return type != null && validPost.includes(type);
|
||||
};
|
||||
|
||||
export interface IPost extends IObject {
|
||||
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
|
||||
|
|
@ -156,8 +163,10 @@ export const isTombstone = (object: IObject): object is ITombstone =>
|
|||
|
||||
export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application'];
|
||||
|
||||
export const isActor = (object: IObject): object is IActor =>
|
||||
validActor.includes(getApType(object));
|
||||
export const isActor = (object: IObject): object is IActor => {
|
||||
const type = getApType(object);
|
||||
return type != null && validActor.includes(type);
|
||||
};
|
||||
|
||||
export interface IActor extends IObject {
|
||||
type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application';
|
||||
|
|
@ -239,12 +248,16 @@ export interface IKey extends IObject {
|
|||
publicKeyPem: string;
|
||||
}
|
||||
|
||||
export const validDocumentTypes = ['Audio', 'Document', 'Image', 'Page', 'Video'];
|
||||
|
||||
export interface IApDocument extends IObject {
|
||||
type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
|
||||
}
|
||||
|
||||
export const isDocument = (object: IObject): object is IApDocument =>
|
||||
['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
|
||||
export const isDocument = (object: IObject): object is IApDocument => {
|
||||
const type = getApType(object);
|
||||
return type != null && validDocumentTypes.includes(type);
|
||||
};
|
||||
|
||||
export interface IApImage extends IApDocument {
|
||||
type: 'Image';
|
||||
|
|
@ -322,7 +335,10 @@ export const isAccept = (object: IObject): object is IAccept => getApType(object
|
|||
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
|
||||
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
|
||||
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
|
||||
export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact';
|
||||
export const isLike = (object: IObject): object is ILike => {
|
||||
const type = getApType(object);
|
||||
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
|
||||
};
|
||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export class FlashEntityService {
|
|||
title: flash.title,
|
||||
summary: flash.summary,
|
||||
script: flash.script,
|
||||
visibility: flash.visibility,
|
||||
likedCount: flash.likedCount,
|
||||
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,8 +64,9 @@ export class InstanceEntityService {
|
|||
@bindThis
|
||||
public packMany(
|
||||
instances: MiInstance[],
|
||||
me?: { id: MiUser['id']; } | null | undefined,
|
||||
) {
|
||||
return Promise.all(instances.map(x => this.pack(x)));
|
||||
return Promise.all(instances.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -454,12 +454,12 @@ export class UserEntityService implements OnModuleInit {
|
|||
}
|
||||
|
||||
const followingCount = profile == null ? null :
|
||||
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
|
||||
(profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount :
|
||||
(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
|
||||
null;
|
||||
|
||||
const followersCount = profile == null ? null :
|
||||
(profile.followersVisibility === 'public') || isMe ? user.followersCount :
|
||||
(profile.followersVisibility === 'public') || isMe || iAmModerator ? user.followersCount :
|
||||
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
|
||||
null;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,3 +6,7 @@
|
|||
export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
|
||||
export type JsonObject = {[K in string]?: JsonValue};
|
||||
export type JsonArray = JsonValue[];
|
||||
|
||||
export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ export const packedFlashSchema = {
|
|||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
visibility: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['private', 'public'],
|
||||
},
|
||||
likedCount: {
|
||||
type: 'number',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
|
@ -33,9 +33,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private queueService: QueueService,
|
||||
private userSuspendService: UserSuspendService,
|
||||
private deleteAccoountService: DeleteAccountService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
|
@ -48,22 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new Error('cannot delete a root account');
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
await this.userSuspendService.doPostSuspend(user).catch(err => {});
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
} else {
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
|
||||
});
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
await this.deleteAccoountService.deleteAccount(user);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
|
|
@ -33,12 +34,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
private metaService: MetaService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const before = await this.metaService.fetch(true);
|
||||
|
||||
await this.metaService.update({
|
||||
policies: ps.policies,
|
||||
});
|
||||
this.globalEventService.publishInternalEvent('policiesUpdated', ps.policies);
|
||||
|
||||
const after = await this.metaService.fetch(true);
|
||||
|
||||
this.globalEventService.publishInternalEvent('policiesUpdated', after.policies);
|
||||
this.moderationLogService.log(me, 'updateServerSettings', {
|
||||
before: before.policies,
|
||||
after: after.policies,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, FollowingsRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { RelationshipJobData } from '@/queue/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
|
@ -38,13 +32,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private userSuspendService: UserSuspendService,
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
|
@ -57,42 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new Error('cannot suspend moderator account');
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isSuspended: true,
|
||||
});
|
||||
|
||||
this.moderationLogService.log(me, 'suspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||
await this.unFollowAll(user).catch(e => {});
|
||||
})();
|
||||
await this.userSuspendService.suspend(user, me);
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async unFollowAll(follower: MiUser) {
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const jobs: RelationshipJobData[] = [];
|
||||
for (const following of followings) {
|
||||
if (following.followeeId && following.followerId) {
|
||||
jobs.push({
|
||||
from: { id: following.followerId },
|
||||
to: { id: following.followeeId },
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.queueService.createUnfollowJob(jobs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
|
|
@ -33,7 +32,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private usersRepository: UsersRepository,
|
||||
|
||||
private userSuspendService: UserSuspendService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
|
@ -42,17 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
this.moderationLogService.log(me, 'unsuspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
this.userSuspendService.doPostUnsuspend(user);
|
||||
await this.userSuspendService.unsuspend(user, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const instances = await query.limit(ps.limit).offset(ps.offset).getMany();
|
||||
|
||||
return await this.instanceEntityService.packMany(instances);
|
||||
return await this.instanceEntityService.packMany(instances, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,9 +107,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
|
||||
|
||||
return await awaitAll({
|
||||
topSubInstances: this.instanceEntityService.packMany(topSubInstances),
|
||||
topSubInstances: this.instanceEntityService.packMany(topSubInstances, me),
|
||||
otherFollowersCount: Math.max(0, allSubCount - gotSubCount),
|
||||
topPubInstances: this.instanceEntityService.packMany(topPubInstances),
|
||||
topPubInstances: this.instanceEntityService.packMany(topPubInstances, me),
|
||||
otherFollowingCount: Math.max(0, allPubCount - gotPubCount),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { QueryService } from '@/core/QueryService.js';
|
|||
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -81,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private utilityService: UtilityService,
|
||||
private followingEntityService: FollowingEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy(ps.userId != null
|
||||
|
|
@ -93,23 +95,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
if (profile.followersVisibility === 'private') {
|
||||
if (me == null || (me.id !== user.id)) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
} else if (profile.followersVisibility === 'followers') {
|
||||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
if (!isFollowing) {
|
||||
if (profile.followersVisibility !== 'public' && !await this.roleService.isModerator(me)) {
|
||||
if (profile.followersVisibility === 'private') {
|
||||
if (me == null || (me.id !== user.id)) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
} else if (profile.followersVisibility === 'followers') {
|
||||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
if (!isFollowing) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { QueryService } from '@/core/QueryService.js';
|
|||
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -90,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private utilityService: UtilityService,
|
||||
private followingEntityService: FollowingEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy(ps.userId != null
|
||||
|
|
@ -102,23 +104,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
if (profile.followingVisibility === 'private') {
|
||||
if (me == null || (me.id !== user.id)) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
} else if (profile.followingVisibility === 'followers') {
|
||||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
if (!isFollowing) {
|
||||
if (profile.followingVisibility !== 'public' && !await this.roleService.isModerator(me)) {
|
||||
if (profile.followingVisibility === 'private') {
|
||||
if (me == null || (me.id !== user.id)) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
} else if (profile.followingVisibility === 'followers') {
|
||||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
if (!isFollowing) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
||||
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { isJsonObject } from '@/misc/json-value.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type Channel from './channel.js';
|
||||
|
||||
const MAX_CHANNELS_PER_CONNECTION = 32;
|
||||
|
||||
/**
|
||||
* Main stream connection
|
||||
*/
|
||||
|
|
@ -112,8 +115,6 @@ export default class Connection {
|
|||
|
||||
const { type, body } = obj;
|
||||
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
|
||||
switch (type) {
|
||||
case 'readNotification': this.onReadNotification(body); break;
|
||||
case 'subNote': this.onSubscribeNote(body); break;
|
||||
|
|
@ -154,7 +155,8 @@ export default class Connection {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private readNote(body: JsonObject) {
|
||||
private readNote(body: JsonValue | undefined) {
|
||||
if (!isJsonObject(body)) return;
|
||||
const id = body.id;
|
||||
|
||||
const note = this.cachedNotes.find(n => n.id === id);
|
||||
|
|
@ -166,7 +168,7 @@ export default class Connection {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private onReadNotification(payload: JsonObject) {
|
||||
private onReadNotification(payload: JsonValue | undefined) {
|
||||
this.notificationService.readAllNotification(this.user!.id);
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +176,8 @@ export default class Connection {
|
|||
* 投稿購読要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onSubscribeNote(payload: JsonObject) {
|
||||
private onSubscribeNote(payload: JsonValue | undefined) {
|
||||
if (!isJsonObject(payload)) return;
|
||||
if (!payload.id || typeof payload.id !== 'string') return;
|
||||
|
||||
const current = this.subscribingNotes[payload.id] ?? 0;
|
||||
|
|
@ -190,7 +193,8 @@ export default class Connection {
|
|||
* 投稿購読解除要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onUnsubscribeNote(payload: JsonObject) {
|
||||
private onUnsubscribeNote(payload: JsonValue | undefined) {
|
||||
if (!isJsonObject(payload)) return;
|
||||
if (!payload.id || typeof payload.id !== 'string') return;
|
||||
|
||||
const current = this.subscribingNotes[payload.id];
|
||||
|
|
@ -216,12 +220,13 @@ export default class Connection {
|
|||
* チャンネル接続要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onChannelConnectRequested(payload: JsonObject) {
|
||||
private onChannelConnectRequested(payload: JsonValue | undefined) {
|
||||
if (!isJsonObject(payload)) return;
|
||||
const { channel, id, params, pong } = payload;
|
||||
if (typeof id !== 'string') return;
|
||||
if (typeof channel !== 'string') return;
|
||||
if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return;
|
||||
if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return;
|
||||
if (typeof params !== 'undefined' && !isJsonObject(params)) return;
|
||||
this.connectChannel(id, params, channel, pong ?? undefined);
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +234,8 @@ export default class Connection {
|
|||
* チャンネル切断要求時
|
||||
*/
|
||||
@bindThis
|
||||
private onChannelDisconnectRequested(payload: JsonObject) {
|
||||
private onChannelDisconnectRequested(payload: JsonValue | undefined) {
|
||||
if (!isJsonObject(payload)) return;
|
||||
const { id } = payload;
|
||||
if (typeof id !== 'string') return;
|
||||
this.disconnectChannel(id);
|
||||
|
|
@ -251,6 +257,10 @@ export default class Connection {
|
|||
*/
|
||||
@bindThis
|
||||
public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
|
||||
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channelService = this.channelsService.getChannelService(channel);
|
||||
|
||||
if (channelService.requireCredential && this.user == null) {
|
||||
|
|
@ -297,7 +307,8 @@ export default class Connection {
|
|||
* @param data メッセージ
|
||||
*/
|
||||
@bindThis
|
||||
private onChannelMessageRequested(data: JsonObject) {
|
||||
private onChannelMessageRequested(data: JsonValue | undefined) {
|
||||
if (!isJsonObject(data)) return;
|
||||
if (typeof data.id !== 'string') return;
|
||||
if (typeof data.type !== 'string') return;
|
||||
if (typeof data.body === 'undefined') return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import Xev from 'xev';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isJsonObject } from '@/misc/json-value.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ class QueueStatsChannel extends Channel {
|
|||
public onMessage(type: string, body: JsonValue) {
|
||||
switch (type) {
|
||||
case 'requestLog':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
if (!isJsonObject(body)) return;
|
||||
if (typeof body.id !== 'string') return;
|
||||
if (typeof body.length !== 'number') return;
|
||||
ev.once(`queueStatsLog:${body.id}`, statsLog => {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import { DI } from '@/di-symbols.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { isJsonObject } from '@/misc/json-value.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
import { reversiUpdateKeys } from 'misskey-js';
|
||||
|
||||
class ReversiGameChannel extends Channel {
|
||||
public readonly chName = 'reversiGame';
|
||||
|
|
@ -44,16 +46,17 @@ class ReversiGameChannel extends Channel {
|
|||
this.ready(body);
|
||||
break;
|
||||
case 'updateSettings':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
if (typeof body.key !== 'string') return;
|
||||
if (typeof body.value !== 'object' || body.value === null || Array.isArray(body.value)) return;
|
||||
if (!isJsonObject(body)) return;
|
||||
if (!this.reversiService.isValidReversiUpdateKey(body.key)) return;
|
||||
if (!this.reversiService.isValidReversiUpdateValue(body.key, body.value)) return;
|
||||
|
||||
this.updateSettings(body.key, body.value);
|
||||
break;
|
||||
case 'cancel':
|
||||
this.cancelGame();
|
||||
break;
|
||||
case 'putStone':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
if (!isJsonObject(body)) return;
|
||||
if (typeof body.pos !== 'number') return;
|
||||
if (typeof body.id !== 'string') return;
|
||||
this.putStone(body.pos, body.id);
|
||||
|
|
@ -63,7 +66,7 @@ class ReversiGameChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async updateSettings(key: string, value: JsonObject) {
|
||||
private async updateSettings<K extends typeof reversiUpdateKeys[number]>(key: K, value: MiReversiGame[K]) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.reversiService.updateSettings(this.gameId!, this.user, key, value);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import Xev from 'xev';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isJsonObject } from '@/misc/json-value.js';
|
||||
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ class ServerStatsChannel extends Channel {
|
|||
public onMessage(type: string, body: JsonValue) {
|
||||
switch (type) {
|
||||
case 'requestLog':
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
|
||||
if (!isJsonObject(body)) return;
|
||||
ev.once(`serverStatsLog:${body.id}`, statsLog => {
|
||||
this.send('statsLog', statsLog);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ html
|
|||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||
meta(property='instance_url' content= instanceUrl)
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||
link(rel='manifest' href='/manifest.json')
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ export const moderationLogTypes = [
|
|||
'createAbuseReportNotificationRecipient',
|
||||
'updateAbuseReportNotificationRecipient',
|
||||
'deleteAbuseReportNotificationRecipient',
|
||||
'deleteAccount',
|
||||
] as const;
|
||||
|
||||
export type ModerationLogPayloads = {
|
||||
|
|
@ -314,6 +315,11 @@ export type ModerationLogPayloads = {
|
|||
recipientId: string;
|
||||
recipient: any;
|
||||
};
|
||||
deleteAccount: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type Serialized<T> = {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import { CoreModule } from '@/core/CoreModule.js';
|
|||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
|
||||
import { MiMeta, MiNote } from '@/models/_.js';
|
||||
import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
|
@ -86,6 +87,7 @@ async function createRandomRemoteUser(
|
|||
}
|
||||
|
||||
describe('ActivityPub', () => {
|
||||
let userProfilesRepository: UserProfilesRepository;
|
||||
let imageService: ApImageService;
|
||||
let noteService: ApNoteService;
|
||||
let personService: ApPersonService;
|
||||
|
|
@ -127,6 +129,8 @@ describe('ActivityPub', () => {
|
|||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||
|
||||
noteService = app.get<ApNoteService>(ApNoteService);
|
||||
personService = app.get<ApPersonService>(ApPersonService);
|
||||
rendererService = app.get<ApRendererService>(ApRendererService);
|
||||
|
|
@ -205,6 +209,53 @@ describe('ActivityPub', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Collection visibility', () => {
|
||||
test('Public following/followers', async () => {
|
||||
const actor = createRandomActor();
|
||||
actor.following = {
|
||||
id: `${actor.id}/following`,
|
||||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
first: `${actor.id}/following?page=1`,
|
||||
};
|
||||
actor.followers = `${actor.id}/followers`;
|
||||
|
||||
resolver.register(actor.id, actor);
|
||||
resolver.register(actor.followers, {
|
||||
id: actor.followers,
|
||||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
first: `${actor.followers}?page=1`,
|
||||
});
|
||||
|
||||
const user = await personService.createPerson(actor.id, resolver);
|
||||
const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
assert.deepStrictEqual(userProfile.followingVisibility, 'public');
|
||||
assert.deepStrictEqual(userProfile.followersVisibility, 'public');
|
||||
});
|
||||
|
||||
test('Private following/followers', async () => {
|
||||
const actor = createRandomActor();
|
||||
actor.following = {
|
||||
id: `${actor.id}/following`,
|
||||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
// first: …
|
||||
};
|
||||
actor.followers = `${actor.id}/followers`;
|
||||
|
||||
resolver.register(actor.id, actor);
|
||||
//resolver.register(actor.followers, { … });
|
||||
|
||||
const user = await personService.createPerson(actor.id, resolver);
|
||||
const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
assert.deepStrictEqual(userProfile.followingVisibility, 'private');
|
||||
assert.deepStrictEqual(userProfile.followersVisibility, 'private');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Renderer', () => {
|
||||
test('Render an announce with visibility: followers', () => {
|
||||
rendererService.renderAnnounce('https://example.com/notes/00example', {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { AISCRIPT_VERSION } from '@syuilo/aiscript';
|
||||
import type { entities } from 'misskey-js'
|
||||
|
||||
export function abuseUserReport() {
|
||||
|
|
@ -114,6 +115,40 @@ export function file(isSensitive = false) {
|
|||
};
|
||||
}
|
||||
|
||||
const script = `/// @ ${AISCRIPT_VERSION}
|
||||
|
||||
var name = ""
|
||||
|
||||
Ui:render([
|
||||
Ui:C:textInput({
|
||||
label: "Your name"
|
||||
onInput: @(v) { name = v }
|
||||
})
|
||||
Ui:C:button({
|
||||
text: "Hello"
|
||||
onClick: @() {
|
||||
Mk:dialog(null, \`Hello, {name}!\`)
|
||||
}
|
||||
})
|
||||
])
|
||||
`;
|
||||
|
||||
export function flash(): entities.Flash {
|
||||
return {
|
||||
id: 'someflashid',
|
||||
createdAt: '2016-12-28T22:49:51.000Z',
|
||||
updatedAt: '2016-12-28T22:49:51.000Z',
|
||||
userId: 'someuserid',
|
||||
user: userLite(),
|
||||
title: 'Some Play title',
|
||||
summary: 'Some Play summary',
|
||||
script,
|
||||
visibility: 'public',
|
||||
likedCount: 0,
|
||||
isLiked: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function folder(id = 'somefolderid', name = 'Some Folder', parentId: string | null = null): entities.DriveFolder {
|
||||
return {
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -398,6 +398,7 @@ function toStories(component: string): Promise<string> {
|
|||
glob('src/components/global/Mk*.vue'),
|
||||
glob('src/components/global/RouterView.vue'),
|
||||
glob('src/components/Mk[A-E]*.vue'),
|
||||
glob('src/components/MkFlashPreview.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
glob('src/components/MkSignupServerRules.vue'),
|
||||
glob('src/components/MkUserSetupDialog.vue'),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"@tabler/icons-webfont": "3.3.0",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@vitejs/plugin-vue": "5.1.0",
|
||||
"@vue/compiler-sfc": "3.4.34",
|
||||
"@vue/compiler-sfc": "3.4.37",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
|
||||
"astring": "1.8.6",
|
||||
"broadcast-channel": "7.0.0",
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
"uuid": "10.0.0",
|
||||
"v-code-diff": "1.12.0",
|
||||
"vite": "5.3.5",
|
||||
"vue": "3.4.34",
|
||||
"vue": "3.4.37",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"@vue/runtime-core": "3.4.34",
|
||||
"@vue/runtime-core": "3.4.37",
|
||||
"acorn": "8.12.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.13.1",
|
||||
|
|
|
|||
|
|
@ -231,17 +231,18 @@ export async function mainBoot() {
|
|||
claimAchievement('client60min');
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
const lastUsed = miLocalStorage.getItem('lastUsed');
|
||||
if (lastUsed) {
|
||||
const lastUsedDate = parseInt(lastUsed, 10);
|
||||
// 二時間以上前なら
|
||||
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
|
||||
toast(i18n.tsx.welcomeBackWithName({
|
||||
name: $i.name || $i.username,
|
||||
}));
|
||||
}
|
||||
}
|
||||
miLocalStorage.setItem('lastUsed', Date.now().toString());
|
||||
// 邪魔
|
||||
//const lastUsed = miLocalStorage.getItem('lastUsed');
|
||||
//if (lastUsed) {
|
||||
// const lastUsedDate = parseInt(lastUsed, 10);
|
||||
// // 二時間以上前なら
|
||||
// if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
|
||||
// toast(i18n.tsx.welcomeBackWithName({
|
||||
// name: $i.name || $i.username,
|
||||
// }));
|
||||
// }
|
||||
//}
|
||||
//miLocalStorage.setItem('lastUsed', Date.now().toString());
|
||||
|
||||
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkFlashPreview from './MkFlashPreview.vue';
|
||||
import { flash } from './../../.storybook/fakes.js';
|
||||
export const Public = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkFlashPreview,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkFlashPreview v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
flash: {
|
||||
...flash(),
|
||||
visibility: 'public',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
|
||||
}),
|
||||
],
|
||||
} satisfies StoryObj<typeof MkFlashPreview>;
|
||||
export const Private = {
|
||||
...Public,
|
||||
args: {
|
||||
flash: {
|
||||
...flash(),
|
||||
visibility: 'private',
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkFlashPreview>;
|
||||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel">
|
||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" :class="[{ gray: flash.visibility === 'private' }]">
|
||||
<article>
|
||||
<header>
|
||||
<h1 :title="flash.title">{{ flash.title }}</h1>
|
||||
|
|
@ -22,11 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { userName } from '@/filters/user.js';
|
||||
|
||||
const props = defineProps<{
|
||||
//flash: Misskey.entities.Flash;
|
||||
flash: any;
|
||||
flash: Misskey.entities.Flash;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
@ -91,6 +91,12 @@ const props = defineProps<{
|
|||
}
|
||||
}
|
||||
|
||||
&:global(.gray) {
|
||||
--c: var(--bg);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ import { host } from '@/config.js';
|
|||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -242,14 +243,7 @@ if (noteViewInterruptors.length > 0) {
|
|||
});
|
||||
}
|
||||
|
||||
const isRenote = (
|
||||
note.value.renote != null &&
|
||||
note.value.reply == null &&
|
||||
note.value.text == null &&
|
||||
note.value.cw == null &&
|
||||
note.value.fileIds && note.value.fileIds.length === 0 &&
|
||||
note.value.poll == null
|
||||
);
|
||||
const isRenote = Misskey.note.isPureRenote(note.value);
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
|
|
@ -257,7 +251,7 @@ const renoteButton = shallowRef<HTMLElement>();
|
|||
const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
const clipButton = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||
const appearNote = computed(() => getAppearNote(note.value));
|
||||
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(false);
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
|||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -267,14 +268,7 @@ if (noteViewInterruptors.length > 0) {
|
|||
});
|
||||
}
|
||||
|
||||
const isRenote = (
|
||||
note.value.renote != null &&
|
||||
note.value.reply == null &&
|
||||
note.value.text == null &&
|
||||
note.value.cw == null &&
|
||||
note.value.fileIds && note.value.fileIds.length === 0 &&
|
||||
note.value.poll == null
|
||||
);
|
||||
const isRenote = Misskey.note.isPureRenote(note.value);
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
|
|
@ -282,7 +276,7 @@ const renoteButton = shallowRef<HTMLElement>();
|
|||
const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
const clipButton = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||
const appearNote = computed(() => getAppearNote(note.value));
|
||||
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(false);
|
||||
|
|
|
|||
|
|
@ -264,6 +264,9 @@ const patronsWithIcon = [{
|
|||
}, {
|
||||
name: 'ささくれりょう',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/cf55022cee6c41da8e70a43587aaad9a.jpg',
|
||||
}, {
|
||||
name: 'Macop',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/ee052bf550014d36a643ce3dce595640.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||
<MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||
|
||||
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
|
||||
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
|
||||
<MkSpacer :contentMax="700" :marginMin="16">
|
||||
<div class="lxpfedzu">
|
||||
<div class="lxpfedzu _gaps">
|
||||
<div class="banner">
|
||||
<img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||
</div>
|
||||
|
|
@ -61,10 +61,10 @@ const narrow = ref(false);
|
|||
const view = ref(null);
|
||||
const el = ref<HTMLDivElement | null>(null);
|
||||
const pageProps = ref({});
|
||||
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
|
||||
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
|
||||
let noEmailServer = !instance.enableEmail;
|
||||
let noInquiryUrl = isEmpty(instance.inquiryUrl);
|
||||
const noMaintainerInformation = computed(() => isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail));
|
||||
const noBotProtection = computed(() => !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile && !instance.enableMcaptcha);
|
||||
const noEmailServer = computed(() => !instance.enableEmail);
|
||||
const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl));
|
||||
const thereIsUnresolvedAbuseReport = ref(false);
|
||||
const currentPage = computed(() => router.currentRef.value.child);
|
||||
|
||||
|
|
@ -235,25 +235,22 @@ const menuDef = computed(() => [{
|
|||
}],
|
||||
}]);
|
||||
|
||||
watch(narrow.value, () => {
|
||||
if (currentPage.value?.route.name == null && !narrow.value) {
|
||||
router.push('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
ro.observe(el.value);
|
||||
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
if (el.value != null) {
|
||||
ro.observe(el.value);
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
}
|
||||
if (currentPage.value?.route.name == null && !narrow.value) {
|
||||
router.push('/admin/overview');
|
||||
router.replace('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
if (el.value != null) {
|
||||
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||
}
|
||||
if (currentPage.value?.route.name == null && !narrow.value) {
|
||||
router.push('/admin/overview');
|
||||
router.replace('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
].includes(log.type),
|
||||
[$style.logYellow]: [
|
||||
'markSensitiveDriveFile',
|
||||
'resetPassword'
|
||||
'resetPassword',
|
||||
'suspendRemoteInstance',
|
||||
].includes(log.type),
|
||||
[$style.logRed]: [
|
||||
'suspend',
|
||||
'deleteRole',
|
||||
'suspendRemoteInstance',
|
||||
'deleteGlobalAnnouncement',
|
||||
'deleteUserAnnouncement',
|
||||
'deleteCustomEmoji',
|
||||
|
|
@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
'deleteAvatarDecoration',
|
||||
'deleteSystemWebhook',
|
||||
'deleteAbuseReportNotificationRecipient',
|
||||
'deleteAccount',
|
||||
].includes(log.type)
|
||||
}"
|
||||
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||
|
|
@ -72,6 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="log.type === 'createAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
|
||||
<span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
|
||||
<span v-else-if="log.type === 'deleteAccount'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<MkAvatar :user="log.user" :class="$style.avatar"/>
|
||||
|
|
@ -143,7 +145,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateRemoteInstanceNote'">
|
||||
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSelect>
|
||||
</div>
|
||||
<div :class="$style.inputs">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:modelValue="$refs.users.reload()">
|
||||
<MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
|
|
|
|||
|
|
@ -369,7 +369,6 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const flash = ref<Misskey.entities.Flash | null>(null);
|
||||
const visibility = ref<'private' | 'public'>('public');
|
||||
|
||||
if (props.id) {
|
||||
flash.value = await misskeyApi('flash/show', {
|
||||
|
|
@ -380,6 +379,7 @@ if (props.id) {
|
|||
const title = ref(flash.value?.title ?? 'New Play');
|
||||
const summary = ref(flash.value?.summary ?? '');
|
||||
const permissions = ref(flash.value?.permissions ?? []);
|
||||
const visibility = ref<'private' | 'public'>(flash.value?.visibility ?? 'public');
|
||||
const script = ref(flash.value?.script ?? PRESET_DEFAULT);
|
||||
|
||||
function selectPreset(ev: MouseEvent) {
|
||||
|
|
|
|||
15
packages/frontend/src/pages/page-editor/common.ts
Normal file
15
packages/frontend/src/pages/page-editor/common.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
export function getPageBlockList() {
|
||||
return [
|
||||
{ value: 'section', text: i18n.ts._pages.blocks.section },
|
||||
{ value: 'text', text: i18n.ts._pages.blocks.text },
|
||||
{ value: 'image', text: i18n.ts._pages.blocks.image },
|
||||
{ value: 'note', text: i18n.ts._pages.blocks.note },
|
||||
];
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { getPageBlockList } from '@/pages/page-editor/common.js';
|
||||
|
||||
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
|
||||
|
||||
|
|
@ -53,11 +54,9 @@ watch(children, () => {
|
|||
deep: true,
|
||||
});
|
||||
|
||||
const getPageBlockList = inject<(any) => any>('getPageBlockList');
|
||||
|
||||
async function rename() {
|
||||
const { canceled, result: title } = await os.inputText({
|
||||
title: 'Enter title',
|
||||
title: i18n.ts._pages.enterSectionTitle,
|
||||
default: props.modelValue.title,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { getPageBlockList } from '@/pages/page-editor/common.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initPageId?: string;
|
||||
|
|
@ -101,7 +102,6 @@ const alignCenter = ref(false);
|
|||
const hideTitleWhenPinned = ref(false);
|
||||
|
||||
provide('readonly', readonly.value);
|
||||
provide('getPageBlockList', getPageBlockList);
|
||||
|
||||
watch(eyeCatchingImageId, async () => {
|
||||
if (eyeCatchingImageId.value == null) {
|
||||
|
|
@ -216,15 +216,6 @@ async function add() {
|
|||
content.value.push({ id, type });
|
||||
}
|
||||
|
||||
function getPageBlockList() {
|
||||
return [
|
||||
{ value: 'section', text: i18n.ts._pages.blocks.section },
|
||||
{ value: 'text', text: i18n.ts._pages.blocks.text },
|
||||
{ value: 'image', text: i18n.ts._pages.blocks.image },
|
||||
{ value: 'note', text: i18n.ts._pages.blocks.note },
|
||||
];
|
||||
}
|
||||
|
||||
function setEyeCatchingImage(img) {
|
||||
selectFile(img.currentTarget ?? img.target, null).then(file => {
|
||||
eyeCatchingImageId.value = file.id;
|
||||
|
|
|
|||
|
|
@ -198,9 +198,6 @@ const menuDef = computed(() => [{
|
|||
}],
|
||||
}]);
|
||||
|
||||
watch(narrow, () => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
ro.observe(el.value);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, provide, shallowRef, ref } from 'vue';
|
||||
import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
|
||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
|
@ -53,15 +53,18 @@ import { deepMerge } from '@/scripts/merge.js';
|
|||
import { MenuItem } from '@/types/menu.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
|
||||
|
||||
import type { BasicTimelineType } from '@/timelines.js';
|
||||
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
|
||||
type TimelinePageSrc = BasicTimelineType | `list:${string}`;
|
||||
|
||||
const queue = ref(0);
|
||||
const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
|
||||
const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`>({
|
||||
const src = computed<TimelinePageSrc>({
|
||||
get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
|
||||
set: (x) => saveSrc(x),
|
||||
});
|
||||
|
|
@ -195,7 +198,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
|
|||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
|
||||
function saveSrc(newSrc: TimelinePageSrc): void {
|
||||
const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
|
||||
|
||||
if (newSrc.startsWith('userList:')) {
|
||||
|
|
@ -236,6 +239,19 @@ function closeTutorial(): void {
|
|||
defaultStore.set('timelineTutorials', before);
|
||||
}
|
||||
|
||||
function switchTlIfNeeded() {
|
||||
if (isBasicTimeline(src.value) && !availableBasicTimelines().includes(src.value)) {
|
||||
src.value = availableBasicTimelines()[0];
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
switchTlIfNeeded();
|
||||
});
|
||||
onActivated(() => {
|
||||
switchTlIfNeeded();
|
||||
});
|
||||
|
||||
const headerActions = computed(() => {
|
||||
const tmp = [
|
||||
{
|
||||
|
|
|
|||
10
packages/frontend/src/scripts/get-appear-note.ts
Normal file
10
packages/frontend/src/scripts/get-appear-note.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export function getAppearNote(note: Misskey.entities.Note) {
|
||||
return Misskey.note.isPureRenote(note) ? note.renote : note;
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import { clipsCache, favoritedChannelsCache } from '@/cache.js';
|
|||
import { MenuItem } from '@/types/menu.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
|
||||
export async function getNoteClipMenu(props: {
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -34,14 +35,7 @@ export async function getNoteClipMenu(props: {
|
|||
}
|
||||
}
|
||||
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||
const appearNote = getAppearNote(props.note);
|
||||
|
||||
const clips = await clipsCache.fetch();
|
||||
const menu: MenuItem[] = [...clips.map(clip => ({
|
||||
|
|
@ -164,14 +158,7 @@ export function getNoteMenu(props: {
|
|||
isDeleted: Ref<boolean>;
|
||||
currentClip?: Misskey.entities.Clip;
|
||||
}) {
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||
const appearNote = getAppearNote(props.note);
|
||||
|
||||
const cleanups = [] as (() => void)[];
|
||||
|
||||
|
|
@ -248,6 +235,7 @@ export function getNoteMenu(props: {
|
|||
}
|
||||
|
||||
async function unclip(): Promise<void> {
|
||||
if (!props.currentClip) return;
|
||||
os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
|
||||
props.isDeleted.value = true;
|
||||
}
|
||||
|
|
@ -267,8 +255,8 @@ export function getNoteMenu(props: {
|
|||
|
||||
function share(): void {
|
||||
navigator.share({
|
||||
title: i18n.tsx.noteOf({ user: appearNote.user.name }),
|
||||
text: appearNote.text,
|
||||
title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }),
|
||||
text: appearNote.text ?? '',
|
||||
url: `${url}/notes/${appearNote.id}`,
|
||||
});
|
||||
}
|
||||
|
|
@ -509,14 +497,7 @@ export function getRenoteMenu(props: {
|
|||
renoteButton: ShallowRef<HTMLElement | undefined>;
|
||||
mock?: boolean;
|
||||
}) {
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
|
||||
const appearNote = getAppearNote(props.note);
|
||||
|
||||
const channelRenoteItems: MenuItem[] = [];
|
||||
const normalRenoteItems: MenuItem[] = [];
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { $i } from '@/account.js';
|
||||
|
||||
export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
|
||||
if ($i && $i.id === user.id) return true;
|
||||
if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
|
||||
|
||||
if (user.followingVisibility === 'private') return false;
|
||||
if (user.followingVisibility === 'followers' && !user.isFollowing) return false;
|
||||
|
|
@ -15,7 +15,7 @@ export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): bo
|
|||
return true;
|
||||
}
|
||||
export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
|
||||
if ($i && $i.id === user.id) return true;
|
||||
if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
|
||||
|
||||
if (user.followersVisibility === 'private') return false;
|
||||
if (user.followersVisibility === 'followers' && !user.isFollowing) return false;
|
||||
|
|
|
|||
|
|
@ -1172,6 +1172,7 @@ declare namespace entities {
|
|||
export {
|
||||
ID,
|
||||
DateString,
|
||||
PureRenote,
|
||||
PageEvent,
|
||||
ModerationLog,
|
||||
ServerStats,
|
||||
|
|
@ -2277,6 +2278,9 @@ type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['co
|
|||
// @public (undocumented)
|
||||
type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
function isPureRenote(note: Note): note is PureRenote;
|
||||
|
||||
// @public (undocumented)
|
||||
type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
@ -2478,10 +2482,13 @@ type ModerationLog = {
|
|||
} | {
|
||||
type: 'deleteAbuseReportNotificationRecipient';
|
||||
info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
|
||||
} | {
|
||||
type: 'deleteAccount';
|
||||
info: ModerationLogPayloads['deleteAccount'];
|
||||
});
|
||||
|
||||
// @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", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient"];
|
||||
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", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount"];
|
||||
|
||||
// @public (undocumented)
|
||||
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
|
||||
|
|
@ -2510,6 +2517,13 @@ type MyAppsResponse = operations['my___apps']['responses']['200']['content']['ap
|
|||
// @public (undocumented)
|
||||
type Note = components['schemas']['Note'];
|
||||
|
||||
declare namespace note {
|
||||
export {
|
||||
isPureRenote
|
||||
}
|
||||
}
|
||||
export { note }
|
||||
|
||||
// @public (undocumented)
|
||||
type NoteFavorite = components['schemas']['NoteFavorite'];
|
||||
|
||||
|
|
@ -2750,6 +2764,15 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte
|
|||
// @public (undocumented)
|
||||
type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & {
|
||||
files: [];
|
||||
fileIds: [];
|
||||
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
||||
|
||||
// @public (undocumented)
|
||||
type QueueCount = components['schemas']['QueueCount'];
|
||||
|
||||
|
|
@ -2829,6 +2852,9 @@ type ReversiShowGameResponse = operations['reversi___show-game']['responses']['2
|
|||
// @public (undocumented)
|
||||
type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
export const reversiUpdateKeys: ["map", "bw", "isLlotheo", "canPutEverywhere", "loopedBoard", "timeLimitForEachTurn"];
|
||||
|
||||
// @public (undocumented)
|
||||
type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
@ -3226,7 +3252,7 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
|
|||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/entities.ts:35:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:49:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:220:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2024.7.0-rc.8",
|
||||
"version": "2024.8.0-beta.2",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
|||
|
|
@ -4673,6 +4673,8 @@ export type components = {
|
|||
title: string;
|
||||
summary: string;
|
||||
script: string;
|
||||
/** @enum {string} */
|
||||
visibility: 'private' | 'public';
|
||||
likedCount: number | null;
|
||||
isLiked?: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import type { operations } from './autogen/types.js';
|
||||
import type {
|
||||
AbuseReportNotificationRecipient, Ad,
|
||||
AbuseReportNotificationRecipient,
|
||||
Ad,
|
||||
Announcement,
|
||||
EmojiDetailed, InviteCode,
|
||||
EmojiDetailed,
|
||||
InviteCode,
|
||||
MetaDetailed,
|
||||
Note,
|
||||
Role, SystemWebhook, UserLite,
|
||||
Role,
|
||||
ReversiGameDetailed,
|
||||
SystemWebhook,
|
||||
UserLite,
|
||||
} from './autogen/models.js';
|
||||
|
||||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
|
||||
|
|
@ -149,6 +154,7 @@ export const moderationLogTypes = [
|
|||
'createAbuseReportNotificationRecipient',
|
||||
'updateAbuseReportNotificationRecipient',
|
||||
'deleteAbuseReportNotificationRecipient',
|
||||
'deleteAccount',
|
||||
] as const;
|
||||
|
||||
// See: packages/backend/src/core/ReversiService.ts@L410
|
||||
|
|
@ -159,7 +165,7 @@ export const reversiUpdateKeys = [
|
|||
'canPutEverywhere',
|
||||
'loopedBoard',
|
||||
'timeLimitForEachTurn',
|
||||
] as const;
|
||||
] as const satisfies (keyof ReversiGameDetailed)[];
|
||||
|
||||
export type ReversiUpdateKey = typeof reversiUpdateKeys[number];
|
||||
|
||||
|
|
@ -387,4 +393,9 @@ export type ModerationLogPayloads = {
|
|||
recipientId: string;
|
||||
recipient: AbuseReportNotificationRecipient;
|
||||
};
|
||||
deleteAccount: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
Announcement,
|
||||
EmojiDetailed,
|
||||
MeDetailed,
|
||||
Note,
|
||||
Page,
|
||||
Role,
|
||||
RolePolicies,
|
||||
|
|
@ -16,6 +17,19 @@ export * from './autogen/models.js';
|
|||
export type ID = string;
|
||||
export type DateString = string;
|
||||
|
||||
type NonNullableRecord<T> = {
|
||||
[P in keyof T]-?: NonNullable<T[P]>;
|
||||
};
|
||||
type AllNullRecord<T> = {
|
||||
[P in keyof T]: null;
|
||||
};
|
||||
|
||||
export type PureRenote =
|
||||
Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
|
||||
& AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>>
|
||||
& { files: []; fileIds: []; }
|
||||
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
||||
|
||||
export type PageEvent = {
|
||||
pageId: Page['id'];
|
||||
event: string;
|
||||
|
|
@ -159,6 +173,9 @@ export type ModerationLog = {
|
|||
} | {
|
||||
type: 'deleteAbuseReportNotificationRecipient';
|
||||
info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
|
||||
} | {
|
||||
type: 'deleteAccount';
|
||||
info: ModerationLogPayloads['deleteAccount'];
|
||||
});
|
||||
|
||||
export type ServerStats = {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export const mutedNoteReasons = consts.mutedNoteReasons;
|
|||
export const followingVisibilities = consts.followingVisibilities;
|
||||
export const followersVisibilities = consts.followersVisibilities;
|
||||
export const moderationLogTypes = consts.moderationLogTypes;
|
||||
export const reversiUpdateKeys = consts.reversiUpdateKeys;
|
||||
|
||||
// api extractor not supported yet
|
||||
//export * as api from './api.js';
|
||||
|
|
@ -29,4 +30,5 @@ export const moderationLogTypes = consts.moderationLogTypes;
|
|||
import * as api from './api.js';
|
||||
import * as entities from './entities.js';
|
||||
import * as acct from './acct.js';
|
||||
export { api, entities, acct };
|
||||
import * as note from './note.js';
|
||||
export { api, entities, acct, note };
|
||||
|
|
|
|||
12
packages/misskey-js/src/note.ts
Normal file
12
packages/misskey-js/src/note.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { Note, PureRenote } from './entities.js';
|
||||
|
||||
export function isPureRenote(note: Note): note is PureRenote {
|
||||
return (
|
||||
note.renote != null &&
|
||||
note.reply == null &&
|
||||
note.text == null &&
|
||||
note.cw == null &&
|
||||
(note.fileIds == null || note.fileIds.length === 0) &&
|
||||
note.poll == null
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ describe('API', () => {
|
|||
expectType<Misskey.entities.MetaResponse>(res);
|
||||
});
|
||||
|
||||
test('conditional respose type (meta)', async () => {
|
||||
test('conditional response type (meta)', async () => {
|
||||
const cli = new Misskey.api.APIClient({
|
||||
origin: 'https://misskey.test',
|
||||
credential: 'TOKEN'
|
||||
|
|
@ -30,7 +30,7 @@ describe('API', () => {
|
|||
expectType<Misskey.entities.MetaResponse>(res4);
|
||||
});
|
||||
|
||||
test('conditional respose type (users/show)', async () => {
|
||||
test('conditional response type (users/show)', async () => {
|
||||
const cli = new Misskey.api.APIClient({
|
||||
origin: 'https://misskey.test',
|
||||
credential: 'TOKEN'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue