Merge branch 'develop' into ed25519
				
					
				
			This commit is contained in:
		@@ -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');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -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
 | 
			
		||||
	public dispose(): void {
 | 
			
		||||
		this.#shutdownController.abort();
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,20 @@ export class UtilityService {
 | 
			
		||||
		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
 | 
			
		||||
	public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
 | 
			
		||||
		if (keyWords.length === 0) return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,8 @@ import { StatusError } from '@/misc/status-error.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.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 { ApLoggerService } from '../ApLoggerService.js';
 | 
			
		||||
import { ApMfmService } from '../ApMfmService.js';
 | 
			
		||||
@@ -37,7 +39,6 @@ import { ApQuestionService } from './ApQuestionService.js';
 | 
			
		||||
import { ApImageService } from './ApImageService.js';
 | 
			
		||||
import type { Resolver } from '../ApResolverService.js';
 | 
			
		||||
import type { IObject, IPost } from '../type.js';
 | 
			
		||||
import { isNotNull } from '@/misc/is-not-null.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ApNoteService {
 | 
			
		||||
@@ -152,11 +153,47 @@ export class ApNoteService {
 | 
			
		||||
			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) {
 | 
			
		||||
			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);
 | 
			
		||||
@@ -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は必ずしも配列ではない
 | 
			
		||||
@@ -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
 | 
			
		||||
		if (reply && reply.hasPoll) {
 | 
			
		||||
			const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
 | 
			
		||||
@@ -274,8 +296,6 @@ export class ApNoteService {
 | 
			
		||||
 | 
			
		||||
		const apEmojis = emojis.map(emoji => emoji.name);
 | 
			
		||||
 | 
			
		||||
		const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			return await this.noteCreateService.create(actor, {
 | 
			
		||||
				createdAt: note.published ? new Date(note.published) : null,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,8 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import * as fs from 'node:fs/promises';
 | 
			
		||||
import type { PathLike } from 'node:fs';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,8 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { TransformStream } from 'node:stream/web';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -184,7 +184,10 @@ export class InboxProcessorService {
 | 
			
		||||
			await this.apInboxService.performActivity(authUser.user, activity);
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			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;
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@
 | 
			
		||||
 * achievementEarned - 実績を獲得
 | 
			
		||||
 * app - アプリ通知
 | 
			
		||||
 * test - テスト通知(サーバー側)
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export const notificationTypes = [
 | 
			
		||||
	'note',
 | 
			
		||||
 
 | 
			
		||||
@@ -117,6 +117,7 @@ describe('Mute', () => {
 | 
			
		||||
			assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
 | 
			
		||||
			assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
 | 
			
		||||
			const aliceNote = await post(alice, { text: 'hi' });
 | 
			
		||||
			await post(bob, { text: '@alice hi', replyId: aliceNote.id });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,8 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type EmojiDef = {
 | 
			
		||||
	emoji: string;
 | 
			
		||||
	name: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,28 +7,28 @@ import { assert, describe, test } from 'vitest';
 | 
			
		||||
import { searchEmoji } from '@/scripts/search-emoji.js';
 | 
			
		||||
 | 
			
		||||
describe('emoji autocomplete', () => {
 | 
			
		||||
  test('名前の完全一致は名前の前方一致より優先される', async () => {
 | 
			
		||||
    const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
 | 
			
		||||
    assert.equal(result[0].emoji, ':foooo:');
 | 
			
		||||
  });
 | 
			
		||||
	test('名前の完全一致は名前の前方一致より優先される', async () => {
 | 
			
		||||
		const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
 | 
			
		||||
		assert.equal(result[0].emoji, ':foooo:');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
  test('名前の前方一致は名前の部分一致より優先される', async () => {
 | 
			
		||||
    const result = searchEmoji('baaa', [{ emoji: ':baaar:', name: 'baaar' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
 | 
			
		||||
    assert.equal(result[0].emoji, ':baaar:');
 | 
			
		||||
  });
 | 
			
		||||
	test('名前の前方一致は名前の部分一致より優先される', async () => {
 | 
			
		||||
		const result = searchEmoji('baaa', [{ emoji: ':baaar:', name: 'baaar' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
 | 
			
		||||
		assert.equal(result[0].emoji, ':baaar:');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
  test('名前の完全一致はタグの完全一致より優先される', async () => {
 | 
			
		||||
    const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
 | 
			
		||||
    assert.equal(result[0].emoji, ':foooo:');
 | 
			
		||||
  });
 | 
			
		||||
	test('名前の完全一致はタグの完全一致より優先される', async () => {
 | 
			
		||||
		const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
 | 
			
		||||
		assert.equal(result[0].emoji, ':foooo:');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
  test('名前の前方一致はタグの前方一致より優先される', async () => {
 | 
			
		||||
    const result = searchEmoji('foo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
 | 
			
		||||
    assert.equal(result[0].emoji, ':foooo:');
 | 
			
		||||
  });
 | 
			
		||||
	test('名前の前方一致はタグの前方一致より優先される', async () => {
 | 
			
		||||
		const result = searchEmoji('foo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
 | 
			
		||||
		assert.equal(result[0].emoji, ':foooo:');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
  test('名前の部分一致はタグの部分一致より優先される', async () => {
 | 
			
		||||
    const result = searchEmoji('oooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
 | 
			
		||||
    assert.equal(result[0].emoji, ':foooo:');
 | 
			
		||||
  });
 | 
			
		||||
	test('名前の部分一致はタグの部分一致より優先される', async () => {
 | 
			
		||||
		const result = searchEmoji('oooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
 | 
			
		||||
		assert.equal(result[0].emoji, ':foooo:');
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "module",
 | 
			
		||||
	"name": "misskey-js",
 | 
			
		||||
	"version": "2024.2.0",
 | 
			
		||||
	"version": "2024.3.0",
 | 
			
		||||
	"description": "Misskey SDK for JavaScript",
 | 
			
		||||
	"types": "./built/dts/index.d.ts",
 | 
			
		||||
	"exports": {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user