Merge remote-tracking branch 'misskey/master' into feature/misskey-2024.8
This commit is contained in:
commit
6151099f5b
81 changed files with 1428 additions and 567 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,
|
||||
|
|
|
@ -12,7 +12,7 @@ import FFmpeg from 'fluent-ffmpeg';
|
|||
import isSvg from 'is-svg';
|
||||
import probeImageSize from 'probe-image-size';
|
||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import { encode } from 'blurhash';
|
||||
import * as blurhash from 'blurhash';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
@ -283,7 +283,7 @@ export class FileInfoService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calculate average color of image
|
||||
* Calculate blurhash string of image
|
||||
*/
|
||||
@bindThis
|
||||
private getBlurhash(path: string, type: string): Promise<string> {
|
||||
|
@ -298,7 +298,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 {
|
||||
|
|
|
@ -707,7 +707,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);
|
||||
}
|
||||
|
||||
|
|
|
@ -99,7 +99,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,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Not, IsNull } from 'typeorm';
|
||||
import type { FollowingsRepository } from '@/models/_.js';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
@ -13,24 +13,75 @@ import { DI } from '@/di-symbols.js';
|
|||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.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 queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
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)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
|
@ -58,7 +109,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)) {
|
||||
|
@ -86,4 +137,26 @@ export class UserSuspendService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,6 +6,7 @@
|
|||
import * as crypto from 'node:crypto';
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Window } from 'happy-dom';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
|
@ -182,7 +183,8 @@ export class ApRequestService {
|
|||
* @param url URL to fetch
|
||||
*/
|
||||
@bindThis
|
||||
public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> {
|
||||
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
|
||||
const _followAlternate = followAlternate ?? true;
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const req = ApRequestCreator.createSignedGet({
|
||||
|
@ -200,9 +202,29 @@ export class ApRequestService {
|
|||
headers: req.request.headers,
|
||||
}, {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
validators: [validateContentTypeSetAsActivityPub],
|
||||
});
|
||||
|
||||
//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
|
||||
const contentType = res.headers.get('content-type');
|
||||
|
||||
if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
|
||||
const html = await res.text();
|
||||
const window = new Window();
|
||||
const document = window.document;
|
||||
document.documentElement.innerHTML = html;
|
||||
|
||||
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
||||
if (alternate) {
|
||||
const href = alternate.getAttribute('href');
|
||||
if (href) {
|
||||
return await this.signedGet(href, user, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
validateContentTypeSetAsActivityPub(res);
|
||||
|
||||
const finalUrl = res.url; // redirects may have been involved
|
||||
const activity = await res.json() as IObject;
|
||||
|
||||
|
|
|
@ -48,7 +48,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, IObject } from '../type.js';
|
||||
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
||||
|
||||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
|
@ -308,6 +308,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);
|
||||
|
@ -382,6 +397,8 @@ export class ApPersonService implements OnModuleInit {
|
|||
description: _description,
|
||||
url,
|
||||
fields,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
userHost: host,
|
||||
|
@ -490,6 +507,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);
|
||||
|
@ -561,6 +595,8 @@ export class ApPersonService implements OnModuleInit {
|
|||
url,
|
||||
fields,
|
||||
description: _description,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
listenbrainz: person.listenbrainz ?? null,
|
||||
|
@ -733,4 +769,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,13 +100,15 @@ 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'];
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -490,12 +490,12 @@ export class UserEntityService implements OnModuleInit {
|
|||
const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -72,6 +72,10 @@ export class RedisKVCache<T> {
|
|||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
|
||||
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
|
||||
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
|
||||
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
|
||||
*/
|
||||
@bindThis
|
||||
public async fetch(key: string): Promise<T> {
|
||||
|
@ -172,6 +176,10 @@ export class RedisSingleCache<T> {
|
|||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
|
||||
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
|
||||
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
|
||||
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
|
||||
*/
|
||||
@bindThis
|
||||
public async fetch(): Promise<T> {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { FlashsRepository } from '@/models/_.js';
|
||||
import type { FlashsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
|
||||
|
||||
if (flash == null) {
|
||||
throw new ApiError(meta.errors.noSuchFlash);
|
||||
}
|
||||
if (flash.userId !== me.id) {
|
||||
|
||||
if (!await this.roleService.isModerator(me) && flash.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
await this.flashsRepository.delete(flash.id);
|
||||
|
||||
if (flash.userId !== me.id) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: flash.userId });
|
||||
this.moderationLogService.log(me, 'deleteFlash', {
|
||||
flashId: flash.id,
|
||||
flashUserId: flash.userId,
|
||||
flashUserUsername: user.username,
|
||||
flash,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { GalleryPostsRepository } from '@/models/_.js';
|
||||
import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -22,6 +24,12 @@ export const meta = {
|
|||
code: 'NO_SUCH_POST',
|
||||
id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -38,18 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.galleryPostsRepository)
|
||||
private galleryPostsRepository: GalleryPostsRepository,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const post = await this.galleryPostsRepository.findOneBy({
|
||||
id: ps.postId,
|
||||
userId: me.id,
|
||||
});
|
||||
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
|
||||
|
||||
if (post == null) {
|
||||
throw new ApiError(meta.errors.noSuchPost);
|
||||
}
|
||||
|
||||
if (!await this.roleService.isModerator(me) && post.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
await this.galleryPostsRepository.delete(post.id);
|
||||
|
||||
if (post.userId !== me.id) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: post.userId });
|
||||
this.moderationLogService.log(me, 'deleteGalleryPost', {
|
||||
postId: post.id,
|
||||
postUserId: post.userId,
|
||||
postUserUsername: user.username,
|
||||
post,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { PagesRepository } from '@/models/_.js';
|
||||
import type { PagesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.pagesRepository)
|
||||
private pagesRepository: PagesRepository,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const page = await this.pagesRepository.findOneBy({ id: ps.pageId });
|
||||
|
||||
if (page == null) {
|
||||
throw new ApiError(meta.errors.noSuchPage);
|
||||
}
|
||||
if (page.userId !== me.id) {
|
||||
|
||||
if (!await this.roleService.isModerator(me) && page.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
await this.pagesRepository.delete(page.id);
|
||||
|
||||
if (page.userId !== me.id) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: page.userId });
|
||||
this.moderationLogService.log(me, 'deletePage', {
|
||||
pageId: page.id,
|
||||
pageUserId: page.userId,
|
||||
pageUserUsername: user.username,
|
||||
page,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +14,8 @@ 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';
|
||||
|
@ -23,6 +24,8 @@ import type Logger from '@/logger.js';
|
|||
|
||||
const MAX_CHANNELS_PER_CONNECTION = 32;
|
||||
|
||||
const MAX_CHANNELS_PER_CONNECTION = 32;
|
||||
|
||||
/**
|
||||
* Main stream connection
|
||||
*/
|
||||
|
@ -149,8 +152,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;
|
||||
|
@ -191,7 +192,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);
|
||||
|
@ -203,7 +205,7 @@ export default class Connection {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private onReadNotification(payload: JsonObject) {
|
||||
private onReadNotification(payload: JsonValue | undefined) {
|
||||
this.notificationService.readAllNotification(this.user!.id);
|
||||
}
|
||||
|
||||
|
@ -211,7 +213,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;
|
||||
|
@ -227,7 +230,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];
|
||||
|
@ -265,12 +269,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);
|
||||
}
|
||||
|
||||
|
@ -278,7 +283,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);
|
||||
|
@ -350,7 +356,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);
|
||||
});
|
||||
|
|
|
@ -32,6 +32,7 @@ html
|
|||
meta(property='og:site_name' content= instanceName || 'Sharkey')
|
||||
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')
|
||||
|
|
|
@ -98,6 +98,10 @@ export const moderationLogTypes = [
|
|||
'createAbuseReportNotificationRecipient',
|
||||
'updateAbuseReportNotificationRecipient',
|
||||
'deleteAbuseReportNotificationRecipient',
|
||||
'deleteAccount',
|
||||
'deletePage',
|
||||
'deleteFlash',
|
||||
'deleteGalleryPost',
|
||||
] as const;
|
||||
|
||||
export type ModerationLogPayloads = {
|
||||
|
@ -321,6 +325,29 @@ export type ModerationLogPayloads = {
|
|||
recipientId: string;
|
||||
recipient: any;
|
||||
};
|
||||
deleteAccount: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
deletePage: {
|
||||
pageId: string;
|
||||
pageUserId: string;
|
||||
pageUserUsername: string;
|
||||
page: any;
|
||||
};
|
||||
deleteFlash: {
|
||||
flashId: string;
|
||||
flashUserId: string;
|
||||
flashUserUsername: string;
|
||||
flash: any;
|
||||
};
|
||||
deleteGalleryPost: {
|
||||
postId: string;
|
||||
postUserId: string;
|
||||
postUserUsername: string;
|
||||
post: any;
|
||||
};
|
||||
};
|
||||
|
||||
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', {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue