summaryrefslogtreecommitdiff
path: root/packages/backend/src/core/NoteCreateService.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/core/NoteCreateService.ts')
-rw-r--r--packages/backend/src/core/NoteCreateService.ts102
1 files changed, 91 insertions, 11 deletions
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 3bc4a29b99..b985846f1c 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setImmediate } from 'node:timers/promises';
-import * as mfm from '@sharkey/sfm-js';
+import * as mfm from '@transfem-org/sfm-js';
import { In, DataSource, IsNull, LessThan } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
@@ -57,7 +57,12 @@ import { FeaturedService } from '@/core/FeaturedService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
+import { CacheService } from '@/core/CacheService.js';
import { isReply } from '@/misc/is-reply.js';
+import { trackPromise } from '@/misc/promise-tracker.js';
+import { isUserRelated } from '@/misc/is-user-related.js';
+import { isNotNull } from '@/misc/is-not-null.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -216,6 +221,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private instanceChart: InstanceChart,
private utilityService: UtilityService,
private userBlockingService: UserBlockingService,
+ private cacheService: CacheService,
) { }
@bindThis
@@ -253,7 +259,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
- if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
+ if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
@@ -324,6 +330,9 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
}
data.text = data.text.trim();
+ if (data.text === '') {
+ data.text = null;
+ }
} else {
data.text = null;
}
@@ -377,6 +386,10 @@ export class NoteCreateService implements OnApplicationShutdown {
});
}
+ if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) {
+ throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions');
+ }
+
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
@@ -422,13 +435,23 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
- if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
+ if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
}
}
+ 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');
+ }
+
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
@@ -493,6 +516,9 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
}
data.text = data.text.trim();
+ if (data.text === '') {
+ data.text = null;
+ }
} else {
data.text = null;
}
@@ -538,6 +564,10 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
+ if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) {
+ throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions');
+ }
+
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
@@ -785,14 +815,22 @@ export class NoteCreateService implements OnApplicationShutdown {
});
// 通知
if (data.reply.userHost === null) {
- const isThreadMuted = await this.noteThreadMutingsRepository.exist({
+ const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id,
},
});
- if (!isThreadMuted) {
+ const [
+ userIdsWhoMeMuting,
+ ] = data.reply.userId ? await Promise.all([
+ this.cacheService.userMutingsCache.fetch(data.reply.userId),
+ ]) : [new Set<string>()];
+
+ const muted = isUserRelated(note, userIdsWhoMeMuting);
+
+ if (!isThreadMuted && !muted) {
nm.push(data.reply.userId, 'reply');
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
@@ -812,7 +850,24 @@ export class NoteCreateService implements OnApplicationShutdown {
// Notify
if (data.renote.userHost === null) {
- nm.push(data.renote.userId, type);
+ const isThreadMuted = await this.noteThreadMutingsRepository.exists({
+ where: {
+ userId: data.renote.userId,
+ threadId: data.renote.threadId ?? data.renote.id,
+ },
+ });
+
+ const [
+ userIdsWhoMeMuting,
+ ] = data.renote.userId ? await Promise.all([
+ this.cacheService.userMutingsCache.fetch(data.renote.userId),
+ ]) : [new Set<string>()];
+
+ const muted = isUserRelated(note, userIdsWhoMeMuting);
+
+ if (!isThreadMuted && !muted) {
+ nm.push(data.renote.userId, type);
+ }
}
// Publish event
@@ -862,7 +917,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.relayService.deliverToRelays(user, noteActivity);
}
- dm.execute();
+ trackPromise(dm.execute());
})();
}
//#endregion
@@ -1022,14 +1077,22 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
- const isThreadMuted = await this.noteThreadMutingsRepository.exist({
+ const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: u.id,
threadId: note.threadId ?? note.id,
},
});
- if (isThreadMuted) {
+ const [
+ userIdsWhoMeMuting,
+ ] = u.id ? await Promise.all([
+ this.cacheService.userMutingsCache.fetch(u.id),
+ ]) : [new Set<string>()];
+
+ const muted = isUserRelated(note, userIdsWhoMeMuting);
+
+ if (isThreadMuted || muted) {
continue;
}
@@ -1092,7 +1155,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
- ))).filter(x => x != null) as MiUser[];
+ ))).filter(isNotNull);
// Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) =>
@@ -1266,6 +1329,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();