enhance: 禁止ワードチェック強化 (#27)
* enhance: 禁止ワードチェック強化 * リモートの禁止ワードチェックを添付ファイルとユーザーを登録する前に行うなど Resolve https://github.com/misskey-dev/misskey/issues/13374 * 禁止ワートの対象の見直し * performActivityで特定のエラーが出た際にDelayedに追加しないように * use IdentifiableError * NoteCreateService.checkProhibitedWords * https://github.com/misskey-dev/misskey-private/pull/27/files#r1507416135 * remove comment
This commit is contained in:
parent
d1bf432e14
commit
eb60460d28
|
@ -263,7 +263,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
|
const hasProhibitedWords = await this.checkProhibitedWordsContain({
|
||||||
|
cw: data.cw,
|
||||||
|
text: data.text,
|
||||||
|
pollChoices: data.poll?.choices,
|
||||||
|
}, meta.prohibitedWords);
|
||||||
|
|
||||||
|
if (hasProhibitedWords) {
|
||||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -995,6 +1001,23 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
|
||||||
|
if (prohibitedWords == null) {
|
||||||
|
prohibitedWords = (await this.metaService.fetch()).prohibitedWords;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.utilityService.isKeyWordIncluded(
|
||||||
|
this.utilityService.concatNoteContentsForKeyWordCheck(content),
|
||||||
|
prohibitedWords,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.#shutdownController.abort();
|
this.#shutdownController.abort();
|
||||||
|
|
|
@ -42,6 +42,20 @@ export class UtilityService {
|
||||||
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public concatNoteContentsForKeyWordCheck(content: {
|
||||||
|
cw?: string | null;
|
||||||
|
text?: string | null;
|
||||||
|
pollChoices?: string[] | null;
|
||||||
|
others?: string[] | null;
|
||||||
|
}): string {
|
||||||
|
/**
|
||||||
|
* ノートの内容を結合してキーワードチェック用の文字列を生成する
|
||||||
|
* cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする
|
||||||
|
*/
|
||||||
|
return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
|
public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
|
||||||
if (keyWords.length === 0) return false;
|
if (keyWords.length === 0) return false;
|
||||||
|
|
|
@ -36,7 +36,6 @@ import { ApResolverService } from './ApResolverService.js';
|
||||||
import { ApAudienceService } from './ApAudienceService.js';
|
import { ApAudienceService } from './ApAudienceService.js';
|
||||||
import { ApPersonService } from './models/ApPersonService.js';
|
import { ApPersonService } from './models/ApPersonService.js';
|
||||||
import { ApQuestionService } from './models/ApQuestionService.js';
|
import { ApQuestionService } from './models/ApQuestionService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { Resolver } from './ApResolverService.js';
|
import type { Resolver } from './ApResolverService.js';
|
||||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
|
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
|
||||||
|
|
|
@ -24,6 +24,8 @@ import { StatusError } from '@/misc/status-error.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApMfmService } from '../ApMfmService.js';
|
import { ApMfmService } from '../ApMfmService.js';
|
||||||
|
@ -37,7 +39,6 @@ import { ApQuestionService } from './ApQuestionService.js';
|
||||||
import { ApImageService } from './ApImageService.js';
|
import { ApImageService } from './ApImageService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IPost } from '../type.js';
|
import type { IObject, IPost } from '../type.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApNoteService {
|
export class ApNoteService {
|
||||||
|
@ -152,11 +153,47 @@ export class ApNoteService {
|
||||||
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
|
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser;
|
const uri = getOneApId(note.attributedTo);
|
||||||
|
|
||||||
// 投稿者が凍結されていたらスキップ
|
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
||||||
|
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
|
||||||
|
if (cachedActor && cachedActor.isSuspended) {
|
||||||
|
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
||||||
|
}
|
||||||
|
|
||||||
|
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
||||||
|
const apHashtags = extractApHashtags(note.tag);
|
||||||
|
|
||||||
|
const cw = note.summary === '' ? null : note.summary;
|
||||||
|
|
||||||
|
// テキストのパース
|
||||||
|
let text: string | null = null;
|
||||||
|
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
|
||||||
|
text = note.source.content;
|
||||||
|
} else if (typeof note._misskey_content !== 'undefined') {
|
||||||
|
text = note._misskey_content;
|
||||||
|
} else if (typeof note.content === 'string') {
|
||||||
|
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||||
|
|
||||||
|
//#region Contents Check
|
||||||
|
// 添付ファイルとユーザーをこのサーバーで登録する前に内容をチェックする
|
||||||
|
/**
|
||||||
|
* 禁止ワードチェック
|
||||||
|
*/
|
||||||
|
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||||
|
if (hasProhibitedWords) {
|
||||||
|
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
||||||
|
|
||||||
|
// 解決した投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
throw new Error('actor has been suspended');
|
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
||||||
|
@ -171,9 +208,6 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
|
||||||
const apHashtags = extractApHashtags(note.tag);
|
|
||||||
|
|
||||||
// 添付ファイル
|
// 添付ファイル
|
||||||
// TODO: attachmentは必ずしもImageではない
|
// TODO: attachmentは必ずしもImageではない
|
||||||
// TODO: attachmentは必ずしも配列ではない
|
// TODO: attachmentは必ずしも配列ではない
|
||||||
|
@ -233,18 +267,6 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cw = note.summary === '' ? null : note.summary;
|
|
||||||
|
|
||||||
// テキストのパース
|
|
||||||
let text: string | null = null;
|
|
||||||
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
|
|
||||||
text = note.source.content;
|
|
||||||
} else if (typeof note._misskey_content !== 'undefined') {
|
|
||||||
text = note._misskey_content;
|
|
||||||
} else if (typeof note.content === 'string') {
|
|
||||||
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// vote
|
// vote
|
||||||
if (reply && reply.hasPoll) {
|
if (reply && reply.hasPoll) {
|
||||||
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
|
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
|
||||||
|
@ -274,8 +296,6 @@ export class ApNoteService {
|
||||||
|
|
||||||
const apEmojis = emojis.map(emoji => emoji.name);
|
const apEmojis = emojis.map(emoji => emoji.name);
|
||||||
|
|
||||||
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.noteCreateService.create(actor, {
|
return await this.noteCreateService.create(actor, {
|
||||||
createdAt: note.published ? new Date(note.published) : null,
|
createdAt: note.published ? new Date(note.published) : null,
|
||||||
|
|
|
@ -185,7 +185,10 @@ export class InboxProcessorService {
|
||||||
await this.apInboxService.performActivity(authUser.user, activity);
|
await this.apInboxService.performActivity(authUser.user, activity);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof IdentifiableError) {
|
if (e instanceof IdentifiableError) {
|
||||||
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') return 'blocked notes with prohibited words';
|
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
|
||||||
|
return 'blocked notes with prohibited words';
|
||||||
|
}
|
||||||
|
if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') return 'actor has been suspended';
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue