Skip to content

Commit

Permalink
Merge pull request #107 from anatawa12/public-to-home-moderation
Browse files Browse the repository at this point in the history
feat: public to home moderation
  • Loading branch information
anatawa12 authored Nov 28, 2023
2 parents 092881e + 3060bca commit 7779c76
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
## 2023.x.x (unreleased)

### General
- publicノートをhomeノートにするモデレーションを追加
- Fix: 全体ハイライトでユーザーミュートが正常に機能しない問題

### Client
Expand Down
23 changes: 23 additions & 0 deletions packages/backend/src/core/FeaturedService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,29 @@ export class FeaturedService {
return true;
}

@bindThis
private removeNoteFromRankingOf(name: string, windowRange: number, element: string, redisPipeline: Redis.ChainableCommander) {
// removing from current & previous window is enough
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;

redisPipeline.zrem(`${name}:${currentWindow}`, element);
redisPipeline.zrem(`${name}:${previousWindow}`, element);
}

@bindThis
public async removeNote(note: MiNote): Promise<void> {
const redisPipeline = this.redisClient.pipeline();
this.removeNoteFromRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, note.id, redisPipeline);
this.removeNoteFromRankingOf(`featuredPerUserNotesRanking:${note.userId}`, PER_USER_NOTES_RANKING_WINDOW, note.id, redisPipeline);

if (note.channelId) {
this.removeNoteFromRankingOf(`featuredInChannelNotesRanking:${note.channelId}`, GLOBAL_NOTES_RANKING_WINDOW, note.id, redisPipeline);
}

await redisPipeline.exec();
}

@bindThis
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/core/FunoutTimelineService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export class FunoutTimelineService {
) {
}

@bindThis
public remove(tl: string, id: string, pipeline: Redis.ChainableCommander) {
pipeline.lrem('list:' + tl, 0, id);
}

@bindThis
public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) {
// リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/EndpointsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_notePublicToHome from './endpoints/admin/note-public-to-home.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
Expand Down Expand Up @@ -423,6 +424,7 @@ const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass:
const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default };
const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
const $admin_notePublicToHome: Provider = { provide: 'ep:admin/note-public-to-home', useClass: ep___admin_notePublicToHome.default };
const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
Expand Down Expand Up @@ -788,6 +790,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_relays_list,
$admin_relays_remove,
$admin_resetPassword,
$admin_notePublicToHome,
$admin_resolveAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
Expand Down Expand Up @@ -1147,6 +1150,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_relays_list,
$admin_relays_remove,
$admin_resetPassword,
$admin_notePublicToHome,
$admin_resolveAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/server/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_notePublicToHome from './endpoints/admin/note-public-to-home.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
Expand Down Expand Up @@ -421,6 +422,7 @@ const eps = [
['admin/relays/list', ep___admin_relays_list],
['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword],
['admin/note-public-to-home', ep___admin_notePublicToHome],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
['admin/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo],
Expand Down
123 changes: 123 additions & 0 deletions packages/backend/src/server/api/endpoints/admin/note-public-to-home.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import * as Redis from 'ioredis';
import type { NotesRepository } from '@/models/_.js';
import { MiNote, MiPoll } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '@/server/api/error.js';
import type { IEndpointMeta } from '@/server/api/endpoints.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FeaturedService } from '@/core/FeaturedService.js';

export const meta = {
tags: ['admin'],

requireCredential: true,
requireModerator: true,

errors: {
noteNotFound: {
message: 'Note not found.',
code: 'NOTE_NOT_FOUND',
id: 'b107f543-27fb-4bac-9549-9bbb64d95e85',
},
noteNotPublic: {
message: 'Note is not public',
code: 'NOTE_NOT_PUBLIC',
id: '561e3371-6ef1-457b-8fdc-736a6e914782',
},
},
} as const satisfies IEndpointMeta;

export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
},
required: ['noteId'],
} as const;

@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,

@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,

@Inject(DI.db)
private db: DataSource,

private moderationLogService: ModerationLogService,
private funoutTimelineService: FunoutTimelineService,
private featuredService: FeaturedService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.notesRepository.findOneBy({ id: ps.noteId });

if (note == null) {
throw new ApiError(meta.errors.noteNotFound);
}

if (note.visibility !== 'public') {
throw new ApiError(meta.errors.noteNotPublic);
}

// Note: by design, visibility of replies and quoted renotes are not changed
// replies and quoted renotes have their own text, so it's another moderation entity

await moderationLogService.log(me, 'makeNoteHome', { targetNoteId: note.id });

// update basic note info
await this.db.transaction(async transactionalEntityManager => {
// change visibility of the note
await transactionalEntityManager.update(MiNote, { id: note.id }, { visibility: 'home' });
await transactionalEntityManager.update(MiPoll, { noteId: note.id }, { noteVisibility: 'home' });

// change visibility of pure renotes
await transactionalEntityManager.createQueryBuilder()
.from(MiNote, 'note')
.update()
.where('renoteId = :renoteId', { renoteId: note.id })
.andWhere('text IS NULL')
.andWhere('fileIds = \'{}\'')
.andWhere('hasPoll = false')
.andWhere('visibility = \'public\'')
.set({ visibility: 'home' })
.execute();
});

// collect renotes after changing visibility of original note
const renotes = await this.notesRepository.createQueryBuilder('note')
.where('note.renoteId = :renoteId', { renoteId: note.id })
.andWhere('note.text IS NULL')
.andWhere('note.fileIds = \'{}\'')
.andWhere('note.hasPoll = false')
.andWhere('note.visibility = \'home\'')
.getMany();

// remove from funout local timeline
const redisPipeline = this.redisForTimelines.pipeline();
this.funoutTimelineService.remove('localTimeline', note.id, redisPipeline);
if (note.fileIds.length > 0) {
this.funoutTimelineService.remove('localTimelineWithFiles', note.id, redisPipeline);
}
for (const renote of renotes) {
this.funoutTimelineService.remove('localTimeline', renote.id, redisPipeline);
}
await redisPipeline.exec();

// remove from highlights
// since renotes are not included in featured, we don't need to remove them
await featuredService.removeNote(note);
});
}
}
4 changes: 4 additions & 0 deletions packages/backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const moderationLogTypes = [
'createAvatarDecoration',
'updateAvatarDecoration',
'deleteAvatarDecoration',
'makeNoteHome',
] as const;

export type ModerationLogPayloads = {
Expand Down Expand Up @@ -237,6 +238,9 @@ export type ModerationLogPayloads = {
avatarDecorationId: string;
avatarDecoration: any;
};
makeNoteHome: {
targetNoteId: string;
};
};

export type Serialized<T> = {
Expand Down
19 changes: 18 additions & 1 deletion packages/frontend/src/scripts/get-note-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ export function getNoteMenu(props: {

const cleanups = [] as (() => void)[];

function makeHome(): void {
os.confirm({
type: 'warning',
text: '本当にホーム投稿にしますか?',
}).then(({ canceled }) => {
if (canceled) return;

os.api('admin/note-public-to-home', { noteId: appearNote.id });
});
}

function del(): void {
os.confirm({
type: 'warning',
Expand Down Expand Up @@ -363,7 +374,13 @@ export function getNoteMenu(props: {
text: i18n.ts.delete,
danger: true,
action: del,
}]
},
$i.isModerator || $i.isAdmin ? {
icon: 'ti ti-home',
text: 'ホーム投稿にする', // めんどうなのでとりあえずハードコード
danger: true,
action: makeHome,
} : undefined]
: []
)]
.filter(x => x !== undefined);
Expand Down
12 changes: 9 additions & 3 deletions packages/misskey-js/etc/misskey-js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,12 @@ type DriveFolder = TODO_2;

// @public (undocumented)
export type Endpoints = {
'admin/note-public-to-home': {
req: {
noteId: Note['id'];
};
res: null;
};
'admin/abuse-user-reports': {
req: TODO;
res: TODO;
Expand Down Expand Up @@ -3047,9 +3053,9 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u

// Warnings were encountered during analysis:
//
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:635:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/api.types.ts:17:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
// src/api.types.ts:21:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:636:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/entities.ts:117:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
// src/entities.ts:628:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
Expand Down
1 change: 1 addition & 0 deletions packages/misskey-js/src/api.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ShowUserReq = { username: string; host?: string; } | { userId: User['id'];

export type Endpoints = {
// admin
'admin/note-public-to-home': { req: { noteId: Note['id'] }; res: null };
'admin/abuse-user-reports': { req: TODO; res: TODO; };
'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
'admin/delete-user-avatar': { req: { userId: User['id']; }; res: null; };
Expand Down

0 comments on commit 7779c76

Please sign in to comment.