Merge remote-tracking branch 'misskey/master' into feature/misskey-2024.8

This commit is contained in:
dakkar 2024-08-30 12:08:31 +01:00
commit 6151099f5b
81 changed files with 1428 additions and 567 deletions

View file

@ -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);
});
}
}

View file

@ -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,
});
});
}
}

View file

@ -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);
}
}

View file

@ -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);
});
}
}

View file

@ -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,
});
}
});
}
}

View file

@ -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,
});
}
});
}
}

View file

@ -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,
});
}
});
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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;

View file

@ -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 => {

View file

@ -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);

View file

@ -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);
});

View file

@ -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')