summaryrefslogtreecommitdiff
path: root/packages/backend/src/core/NoteEditService.ts
diff options
context:
space:
mode:
authorJulia <julia@insertdomain.name>2025-05-29 00:07:38 +0000
committerJulia <julia@insertdomain.name>2025-05-29 00:07:38 +0000
commit6b554c178b81f13f83a69b19d44b72b282a0c119 (patch)
treef5537f1a56323a4dd57ba150b3cb84a2d8b5dc63 /packages/backend/src/core/NoteEditService.ts
parentmerge: Security fixes (!970) (diff)
parentbump version for release (diff)
downloadsharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.gz
sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.bz2
sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.zip
merge: release 2025.4.2 (!1051)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1051 Approved-by: Hazelnoot <acomputerdog@gmail.com> 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.ts128
1 files changed, 30 insertions, 98 deletions
diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts
index 7851af86b7..58233b90ee 100644
--- a/packages/backend/src/core/NoteEditService.ts
+++ b/packages/backend/src/core/NoteEditService.ts
@@ -8,6 +8,7 @@ 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 { UnrecoverableError } from 'bullmq';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
@@ -36,7 +37,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
-import { NoteReadService } from '@/core/NoteReadService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@@ -46,7 +46,7 @@ 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 { trackTask } 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';
@@ -203,7 +203,6 @@ export class NoteEditService implements OnApplicationShutdown {
private globalEventService: GlobalEventService,
private queueService: QueueService,
private fanoutTimelineService: FanoutTimelineService,
- private noteReadService: NoteReadService,
private notificationService: NotificationService,
private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService,
@@ -233,7 +232,7 @@ export class NoteEditService implements OnApplicationShutdown {
noindex: MiUser['noindex'];
}, editid: MiNote['id'], data: Option, silent = false): Promise<MiNote> {
if (!editid) {
- throw new Error('fail');
+ throw new UnrecoverableError('edit failed: missing editid');
}
const oldnote = await this.notesRepository.findOneBy({
@@ -241,11 +240,11 @@ export class NoteEditService implements OnApplicationShutdown {
});
if (oldnote == null) {
- throw new Error('no such note');
+ throw new UnrecoverableError(`edit failed for ${editid}: missing oldnote`);
}
if (oldnote.userId !== user.id) {
- throw new Error('not the author');
+ throw new UnrecoverableError(`edit failed for ${editid}: user is not the note author`);
}
// we never want to change the replyId, so fetch the original "parent"
@@ -276,12 +275,12 @@ export class NoteEditService implements OnApplicationShutdown {
data.channel = await this.channelsRepository.findOneBy({ id: data.reply.channelId });
}
+ if (data.updatedAt == null) data.updatedAt = new Date();
if (data.visibility == null) data.visibility = 'public';
if (data.localOnly == null) data.localOnly = false;
if (data.channel != null) data.visibility = 'public';
if (data.channel != null) data.visibleUsers = [];
if (data.channel != null) data.localOnly = true;
- if (data.updatedAt == null) data.updatedAt = new Date();
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = this.meta.sensitiveWords;
@@ -310,7 +309,7 @@ export class NoteEditService implements OnApplicationShutdown {
if (this.isRenote(data)) {
if (data.renote.id === oldnote.id) {
- throw new Error('A note can\'t renote itself');
+ throw new UnrecoverableError(`edit failed for ${oldnote.id}: cannot renote itself`);
}
switch (data.renote.visibility) {
@@ -406,10 +405,10 @@ export class NoteEditService implements OnApplicationShutdown {
// Parse MFM if needed
if (!tags || !emojis || !mentionedUsers) {
- const tokens = data.text ? mfm.parse(data.text)! : [];
- const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
+ const tokens = data.text ? mfm.parse(data.text) : [];
+ const cwTokens = data.cw ? mfm.parse(data.cw) : [];
const choiceTokens = data.poll && data.poll.choices
- ? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
+ ? concat(data.poll.choices.map(choice => mfm.parse(choice)))
: [];
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
@@ -424,7 +423,7 @@ export class NoteEditService implements OnApplicationShutdown {
// if the host is media-silenced, custom emojis are not allowed
if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = [];
- tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32);
+ tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId }));
@@ -467,10 +466,8 @@ export class NoteEditService implements OnApplicationShutdown {
update.hasPoll = !!data.poll;
}
- // technically we should check if the two sets of files are
- // different, or if their descriptions have changed. In practice
- // this is good enough.
- const filesChanged = oldnote.fileIds?.length || data.files?.length;
+ // TODO deep-compare files
+ const filesChanged = oldnote.fileIds.length || data.files?.length;
const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id });
@@ -564,7 +561,7 @@ export class NoteEditService implements OnApplicationShutdown {
noteVisibility: note.visibility,
userId: user.id,
userHost: user.host,
- channelId: data.channel ? data.channel.id : null,
+ channelId: data.channel?.id ?? null,
});
if (!oldnote.hasPoll) {
@@ -577,12 +574,15 @@ export class NoteEditService implements OnApplicationShutdown {
await this.notesRepository.update(oldnote.id, note);
}
+ // Re-fetch note to get the default values of null / unset fields.
+ const edited = await this.notesRepository.findOneByOrFail({ id: note.id });
+
setImmediate('post edited', { signal: this.#shutdownController.signal }).then(
- () => this.postNoteEdited(note, oldnote, user, data, silent, tags!, mentionedUsers!),
+ () => this.postNoteEdited(edited, oldnote, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ },
);
- return note;
+ return edited;
} else {
return oldnote;
}
@@ -610,6 +610,8 @@ export class NoteEditService implements OnApplicationShutdown {
}
}
+ this.usersRepository.update({ id: user.id }, { updatedAt: new Date() });
+
// ハッシュタグ更新
this.pushToTl(note, user);
@@ -628,44 +630,12 @@ export class NoteEditService implements OnApplicationShutdown {
if (!silent) {
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
- // 未読通知を作成
- if (data.visibility === 'specified') {
- if (data.visibleUsers == null) throw new Error('invalid param');
-
- for (const u of data.visibleUsers) {
- // ローカルユーザーのみ
- if (!this.userEntityService.isLocalUser(u)) continue;
-
- this.noteReadService.insertNoteUnread(u.id, note, {
- isSpecified: true,
- isMentioned: false,
- });
- }
- } else {
- for (const u of mentionedUsers) {
- // ローカルユーザーのみ
- if (!this.userEntityService.isLocalUser(u)) continue;
-
- this.noteReadService.insertNoteUnread(u.id, note, {
- isSpecified: false,
- isMentioned: true,
- });
- }
- }
-
// Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
- if (data.poll != null) {
- this.globalEventService.publishNoteStream(note.id, 'updated', {
- cw: note.cw,
- text: note.text!,
- });
- } else {
- this.globalEventService.publishNoteStream(note.id, 'updated', {
- cw: note.cw,
- text: note.text!,
- });
- }
+ this.globalEventService.publishNoteStream(note.id, 'updated', {
+ cw: note.cw,
+ text: note.text ?? '',
+ });
this.roleService.addNoteToRoleTimeline(noteObj);
@@ -673,8 +643,6 @@ export class NoteEditService implements OnApplicationShutdown {
const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
- //await this.createMentionedEvents(mentionedUsers, note, nm);
-
// If has in reply to note
if (data.reply) {
// 通知
@@ -697,7 +665,6 @@ export class NoteEditService implements OnApplicationShutdown {
if (!isThreadMuted && !muted) {
nm.push(data.reply.userId, 'edited');
this.globalEventService.publishMainStream(data.reply.userId, 'edited', noteObj);
-
this.webhookService.enqueueUserWebhook(data.reply.userId, 'reply', { note: noteObj });
}
}
@@ -707,7 +674,7 @@ export class NoteEditService implements OnApplicationShutdown {
//#region AP deliver
if (!data.localOnly && this.userEntityService.isLocalUser(user)) {
- (async () => {
+ trackTask(async () => {
const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user);
const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity);
@@ -751,12 +718,12 @@ export class NoteEditService implements OnApplicationShutdown {
}
}
+ await dm.execute();
+
if (['public'].includes(note.visibility)) {
- this.relayService.deliverToRelays(user, noteActivity);
+ await this.relayService.deliverToRelays(user, noteActivity);
}
-
- trackPromise(dm.execute());
- })();
+ });
}
//#endregion
}
@@ -803,41 +770,6 @@ export class NoteEditService implements OnApplicationShutdown {
(note.files != null && note.files.length > 0);
}
- // TODO why is this unused?
- @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.exists({
- where: {
- userId: u.id,
- threadId: note.threadId ?? note.id,
- },
- });
-
- 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;
- }
-
- const detailPackedNote = await this.noteEntityService.pack(note, u, {
- detail: true,
- });
-
- this.globalEventService.publishMainStream(u.id, 'edited', detailPackedNote);
- this.webhookService.enqueueUserWebhook(u.id, 'edited', { note: detailPackedNote });
-
- // Create notification
- nm.push(u.id, 'edited');
- }
- }
-
@bindThis
private async renderNoteOrRenoteActivity(data: Option, note: MiNote, user: MiUser) {
if (data.localOnly) return null;