summaryrefslogtreecommitdiff
path: root/packages/backend/src/core/NoteEditService.ts
diff options
context:
space:
mode:
authorJulia <julia@insertdomain.name>2024-11-05 03:59:23 +0000
committerJulia <julia@insertdomain.name>2024-11-05 03:59:23 +0000
commit680e3ac7a3084313ed4857ffca2c582c5b3c7348 (patch)
tree5621986847b8390b7c4f8e2f63cc53b180982b67 /packages/backend/src/core/NoteEditService.ts
parentmerge: Bump version (!635) (diff)
parentcomment out sharkey-specific crowdin link (diff)
downloadsharkey-680e3ac7a3084313ed4857ffca2c582c5b3c7348.tar.gz
sharkey-680e3ac7a3084313ed4857ffca2c582c5b3c7348.tar.bz2
sharkey-680e3ac7a3084313ed4857ffca2c582c5b3c7348.zip
merge: release 2024.9.1 (!733)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/733 Approved-by: Marie <github@yuugi.dev> Approved-by: Julia <julia@insertdomain.name>
Diffstat (limited to 'packages/backend/src/core/NoteEditService.ts')
-rw-r--r--packages/backend/src/core/NoteEditService.ts130
1 files changed, 77 insertions, 53 deletions
diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts
index 5ff0f26e2b..d31958e5d4 100644
--- a/packages/backend/src/core/NoteEditService.ts
+++ b/packages/backend/src/core/NoteEditService.ts
@@ -8,13 +8,12 @@ import * as mfm from '@transfem-org/sfm-js';
import { DataSource, In, IsNull, LessThan } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
-import RE2 from 're2';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
-import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository, PollsRepository } from '@/models/_.js';
+import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository, PollsRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js';
@@ -40,9 +39,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
import { NoteReadService } from '@/core/NoteReadService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js';
-import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
-import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { UtilityService } from '@/core/UtilityService.js';
@@ -52,6 +49,9 @@ import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { LatestNoteService } from '@/core/LatestNoteService.js';
+import { CollapsedQueue } from '@/misc/collapsed-queue.js';
+import { NoteCreateService } from '@/core/NoteCreateService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited';
@@ -145,11 +145,15 @@ type Option = {
@Injectable()
export class NoteEditService implements OnApplicationShutdown {
#shutdownController = new AbortController();
+ private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;
constructor(
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.db)
private db: DataSource,
@@ -207,14 +211,17 @@ export class NoteEditService implements OnApplicationShutdown {
private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService,
private roleService: RoleService,
- private metaService: MetaService,
private searchService: SearchService,
private activeUsersChart: ActiveUsersChart,
private instanceChart: InstanceChart,
private utilityService: UtilityService,
private userBlockingService: UserBlockingService,
private cacheService: CacheService,
- ) { }
+ private latestNoteService: LatestNoteService,
+ private noteCreateService: NoteCreateService,
+ ) {
+ this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
+ }
@bindThis
public async edit(user: {
@@ -247,6 +254,11 @@ export class NoteEditService implements OnApplicationShutdown {
data.reply = undefined;
}
+ // changing visibility on an edit is ill-defined, let's try to
+ // keep the same visibility as the original note
+ data.visibility = oldnote.visibility;
+ data.localOnly = oldnote.localOnly;
+
// チャンネル外にリプライしたら対象のスコープに合わせる
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
@@ -270,10 +282,8 @@ export class NoteEditService implements OnApplicationShutdown {
if (data.channel != null) data.localOnly = true;
if (data.updatedAt == null) data.updatedAt = new Date();
- const meta = await this.metaService.fetch();
-
if (data.visibility === 'public' && data.channel == null) {
- const sensitiveWords = meta.sensitiveWords;
+ const sensitiveWords = this.meta.sensitiveWords;
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
@@ -281,17 +291,17 @@ export class NoteEditService implements OnApplicationShutdown {
}
}
- const hasProhibitedWords = await this.checkProhibitedWordsContain({
+ const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({
cw: data.cw,
text: data.text,
pollChoices: data.poll?.choices,
- }, meta.prohibitedWords);
+ }, this.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);
+ const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
data.visibility = 'home';
@@ -354,9 +364,13 @@ export class NoteEditService implements OnApplicationShutdown {
data.localOnly = true;
}
+ const maxTextLength = user.host == null
+ ? this.config.maxNoteLength
+ : this.config.maxRemoteNoteLength;
+
if (data.text) {
- if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) {
- data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
+ if (data.text.length > maxTextLength) {
+ data.text = data.text.slice(0, maxTextLength);
}
data.text = data.text.trim();
if (data.text === '') {
@@ -366,6 +380,22 @@ export class NoteEditService implements OnApplicationShutdown {
data.text = null;
}
+ const maxCwLength = user.host == null
+ ? this.config.maxCwLength
+ : this.config.maxRemoteCwLength;
+
+ if (data.cw) {
+ if (data.cw.length > maxCwLength) {
+ data.cw = data.cw.slice(0, maxCwLength);
+ }
+ data.cw = data.cw.trim();
+ if (data.cw === '') {
+ data.cw = null;
+ }
+ } else {
+ data.cw = null;
+ }
+
let tags = data.apHashtags;
let emojis = data.apEmojis;
let mentionedUsers = data.apMentions;
@@ -388,7 +418,7 @@ export class NoteEditService implements OnApplicationShutdown {
}
// if the host is media-silenced, custom emojis are not allowed
- if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
+ if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32);
@@ -429,9 +459,6 @@ export class NoteEditService implements OnApplicationShutdown {
if (data.cw !== oldnote.cw) {
update.cw = data.cw;
}
- if (data.localOnly !== oldnote.localOnly) {
- update.localOnly = data.localOnly;
- }
if (oldnote.hasPoll !== !!data.poll) {
update.hasPoll = !!data.poll;
}
@@ -496,6 +523,7 @@ export class NoteEditService implements OnApplicationShutdown {
renoteUserId: data.renote ? data.renote.userId : null,
renoteUserHost: data.renote ? data.renote.userHost : null,
userHost: user.host,
+ reactionAndUserPairCache: oldnote.reactionAndUserPairCache,
});
if (data.uri != null) note.uri = data.uri;
@@ -544,7 +572,7 @@ export class NoteEditService implements OnApplicationShutdown {
}
setImmediate('post edited', { signal: this.#shutdownController.signal }).then(
- () => this.postNoteEdited(note, user, data, silent, tags!, mentionedUsers!),
+ () => this.postNoteEdited(note, oldnote, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ },
);
@@ -555,7 +583,7 @@ export class NoteEditService implements OnApplicationShutdown {
}
@bindThis
- private async postNoteEdited(note: MiNote, user: {
+ private async postNoteEdited(note: MiNote, oldNote: MiNote, user: {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
@@ -565,8 +593,8 @@ export class NoteEditService implements OnApplicationShutdown {
// Register host
if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.fetch(user.host).then(async i => {
- this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ this.updateNotesCountQueue.enqueue(i.id, 1);
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.updateNote(i.host, note, true);
}
});
@@ -752,6 +780,9 @@ export class NoteEditService implements OnApplicationShutdown {
});
}
+ // Update the Latest Note index / following feed
+ this.latestNoteService.handleUpdatedNoteBG(oldNote, note);
+
// Register to search database
if (!user.noindex) this.index(note);
}
@@ -852,15 +883,14 @@ export class NoteEditService implements OnApplicationShutdown {
@bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
- const meta = await this.metaService.fetch();
- if (!meta.enableFanoutTimeline) return;
+ if (!this.meta.enableFanoutTimeline) return;
const r = this.redisForTimelines.pipeline();
if (note.channelId) {
this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
- this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
const channelFollowings = await this.channelFollowingsRepository.find({
where: {
@@ -870,9 +900,9 @@ export class NoteEditService implements OnApplicationShutdown {
});
for (const channelFollowing of channelFollowings) {
- this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
}
}
} else {
@@ -910,9 +940,9 @@ export class NoteEditService implements OnApplicationShutdown {
if (!following.withReplies) continue;
}
- this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
}
}
@@ -929,25 +959,25 @@ export class NoteEditService implements OnApplicationShutdown {
if (!userListMembership.withReplies) continue;
}
- this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r);
}
}
// 自分自身のHTL
if (note.userHost == null) {
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
- this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r);
}
}
}
// 自分自身以外への返信
if (isReply(note)) {
- this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
if (note.visibility === 'public' && note.userHost == null) {
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
@@ -956,9 +986,9 @@ export class NoteEditService implements OnApplicationShutdown {
}
}
} else {
- this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
+ this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);
if (note.fileIds.length > 0) {
- this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
+ this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r);
}
if (note.visibility === 'public' && note.userHost == null) {
@@ -1017,30 +1047,24 @@ export class NoteEditService 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;
- }
+ @bindThis
+ private collapseNotesCount(oldValue: number, newValue: number) {
+ return oldValue + newValue;
+ }
- return false;
+ @bindThis
+ private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) {
+ await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy);
}
@bindThis
- public dispose(): void {
+ public async dispose(): Promise<void> {
this.#shutdownController.abort();
+ await this.updateNotesCountQueue.performAllNow();
}
@bindThis
- public onApplicationShutdown(signal?: string | undefined): void {
- this.dispose();
+ public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
+ await this.dispose();
}
}