summaryrefslogtreecommitdiff
path: root/packages/backend/src/core/LatestNoteService.ts
blob: c3798055066fd14ae5245eb85c1d27c8f0fd025c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { Inject, Injectable } from '@nestjs/common';
import { Not } from 'typeorm';
import { MiNote } from '@/models/Note.js';
import { isPureRenote } from '@/misc/is-renote.js';
import { SkLatestNote } from '@/models/LatestNote.js';
import { DI } from '@/di-symbols.js';
import type { LatestNotesRepository, NotesRepository } from '@/models/_.js';
import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';

@Injectable()
export class LatestNoteService {
	private readonly logger: Logger;

	constructor(
		@Inject(DI.notesRepository)
		private notesRepository: NotesRepository,

		@Inject(DI.latestNotesRepository)
		private latestNotesRepository: LatestNotesRepository,

		loggerService: LoggerService,
	) {
		this.logger = loggerService.getLogger('LatestNoteService');
	}

	handleUpdatedNoteBG(before: MiNote, after: MiNote): void {
		this
			.handleUpdatedNote(before, after)
			.catch(err => this.logger.error('Unhandled exception while updating latest_note (after update):', err));
	}

	async handleUpdatedNote(before: MiNote, after: MiNote): Promise<void> {
		// If the key didn't change, then there's nothing to update
		if (SkLatestNote.areEquivalent(before, after)) return;

		// Simulate update as delete + create
		await this.handleDeletedNote(before);
		await this.handleCreatedNote(after);
	}

	handleCreatedNoteBG(note: MiNote): void {
		this
			.handleCreatedNote(note)
			.catch(err => this.logger.error('Unhandled exception while updating latest_note (after create):', err));
	}

	async handleCreatedNote(note: MiNote): Promise<void> {
		// Ignore DMs.
		// Followers-only posts are *included*, as this table is used to back the "following" feed.
		if (note.visibility === 'specified') return;

		// Ignore pure renotes
		if (isPureRenote(note)) return;

		// Compute the compound key of the entry to check
		const key = SkLatestNote.keyFor(note);

		// Make sure that this isn't an *older* post.
		// We can get older posts through replies, lookups, updates, etc.
		const currentLatest = await this.latestNotesRepository.findOneBy(key);
		if (currentLatest != null && currentLatest.noteId >= note.id) return;

		// Record this as the latest note for the given user
		const latestNote = new SkLatestNote({
			...key,
			noteId: note.id,
		});
		await this.latestNotesRepository.upsert(latestNote, ['userId', 'isPublic', 'isReply', 'isQuote']);
	}

	handleDeletedNoteBG(note: MiNote): void {
		this
			.handleDeletedNote(note)
			.catch(err => this.logger.error('Unhandled exception while updating latest_note (after delete):', err));
	}

	async handleDeletedNote(note: MiNote): Promise<void> {
		// If it's a DM, then it can't possibly be the latest note so we can safely skip this.
		if (note.visibility === 'specified') return;

		// If it's a pure renote, then it can't possibly be the latest note so we can safely skip this.
		if (isPureRenote(note)) return;

		// Compute the compound key of the entry to check
		const key = SkLatestNote.keyFor(note);

		// Check if the deleted note was possibly the latest for the user
		const existingLatest = await this.latestNotesRepository.findOneBy(key);
		if (existingLatest == null || existingLatest.noteId !== note.id) return;

		// Find the newest remaining note for the user.
		// We exclude DMs and pure renotes.
		const nextLatest = await this.notesRepository
			.createQueryBuilder('note')
			.select()
			.where({
				userId: key.userId,
				visibility: key.isPublic
					? 'public'
					: Not('specified'),
				replyId: key.isReply
					? Not(null)
					: null,
				renoteId: key.isQuote
					? Not(null)
					: null,
			})
			.andWhere(`
				(
					note."renoteId" IS NULL
					OR note.text IS NOT NULL
					OR note.cw IS NOT NULL
					OR note."replyId" IS NOT NULL
					OR note."hasPoll"
					OR note."fileIds" != '{}'
				)
			`)
			.orderBy({ id: 'DESC' })
			.getOne();
		if (!nextLatest) return;

		// Record it as the latest
		const latestNote = new SkLatestNote({
			...key,
			noteId: nextLatest.id,
		});

		// When inserting the latest note, it's possible that another worker has "raced" the insert and already added a newer note.
		// We must use orIgnore() to ensure that the query ignores conflicts, otherwise an exception may be thrown.
		await this.latestNotesRepository
			.createQueryBuilder('latest')
			.insert()
			.into(SkLatestNote)
			.values(latestNote)
			.orIgnore()
			.execute();
	}
}