Skip to content

Commit 421f8d4

Browse files
committed
Never return broken notifications misskey-dev#409
Since notifications are stored in Redis, we can't expect relational integrity: deleting a user will *not* delete notifications that mention it. But if we return notifications with missing bits (a `follow` without a `user`, for example), the frontend will get very confused and throw an exception while trying to render them. This change makes sure we never expose those broken notifications. For uniformity, I've applied the same logic to notes and roles mentioned in notifications, even if nobody reported breakage in those cases. Tested by creating a few types of notifications with a `notifierId`, then deleting their user.
1 parent 2d12513 commit 421f8d4

File tree

1 file changed

+59
-16
lines changed

1 file changed

+59
-16
lines changed

packages/backend/src/core/entities/NotificationEntityService.ts

+59-16
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,38 @@ export class NotificationEntityService implements OnModuleInit {
6464
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
6565
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
6666
},
67-
): Promise<Packed<'Notification'>> {
67+
): Promise<Packed<'Notification'> | null> {
6868
const notification = src;
69-
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
69+
const needsNote = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification;
70+
const noteIfNeed = needsNote ? (
7071
hint?.packedNotes != null
7172
? hint.packedNotes.get(notification.noteId)
7273
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
7374
detail: true,
7475
})
7576
) : undefined;
76-
const userIfNeed = 'notifierId' in notification ? (
77+
// if the note has been deleted, don't show this notification
78+
if (needsNote && !noteIfNeed) {
79+
return null;
80+
}
81+
82+
const needsUser = 'notifierId' in notification;
83+
const userIfNeed = needsUser ? (
7784
hint?.packedUsers != null
7885
? hint.packedUsers.get(notification.notifierId)
7986
: this.userEntityService.pack(notification.notifierId, { id: meId })
8087
) : undefined;
81-
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
88+
// if the user has been deleted, don't show this notification
89+
if (needsUser && !userIfNeed) {
90+
return null;
91+
}
92+
93+
const needsRole = notification.type === 'roleAssigned';
94+
const role = needsRole ? await this.roleEntityService.pack(notification.roleId) : undefined;
95+
// if the role has been deleted, don't show this notification
96+
if (needsRole && !role) {
97+
return null;
98+
}
8299

83100
return await awaitAll({
84101
id: notification.id,
@@ -141,10 +158,10 @@ export class NotificationEntityService implements OnModuleInit {
141158
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
142159
}
143160

144-
return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
161+
return (await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
145162
packedNotes,
146163
packedUsers,
147-
})));
164+
})))).filter(n => n);
148165
}
149166

150167
@bindThis
@@ -159,31 +176,47 @@ export class NotificationEntityService implements OnModuleInit {
159176
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
160177
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
161178
},
162-
): Promise<Packed<'Notification'>> {
179+
): Promise<Packed<'Notification'> | null> {
163180
const notification = src;
164-
const noteIfNeed = NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
181+
const needsNote = NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification;
182+
const noteIfNeed = needsNote ? (
165183
hint?.packedNotes != null
166184
? hint.packedNotes.get(notification.noteId)
167185
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
168186
detail: true,
169187
})
170188
) : undefined;
171-
const userIfNeed = 'notifierId' in notification ? (
189+
// if the note has been deleted, don't show this notification
190+
if (needsNote && !noteIfNeed) {
191+
return null;
192+
}
193+
194+
const needsUser = 'notifierId' in notification;
195+
const userIfNeed = needsUser ? (
172196
hint?.packedUsers != null
173197
? hint.packedUsers.get(notification.notifierId)
174198
: this.userEntityService.pack(notification.notifierId, { id: meId })
175199
) : undefined;
200+
// if the user has been deleted, don't show this notification
201+
if (needsUser && !userIfNeed) {
202+
return null;
203+
}
176204

177205
if (notification.type === 'reaction:grouped') {
178-
const reactions = await Promise.all(notification.reactions.map(async reaction => {
206+
const reactions = (await Promise.all(notification.reactions.map(async reaction => {
179207
const user = hint?.packedUsers != null
180208
? hint.packedUsers.get(reaction.userId)!
181209
: await this.userEntityService.pack(reaction.userId, { id: meId });
182210
return {
183211
user,
184212
reaction: reaction.reaction,
185213
};
186-
}));
214+
}))).filter(r => r.user);
215+
// if all users have been deleted, don't show this notification
216+
if (!reactions.length) {
217+
return null;
218+
}
219+
187220
return await awaitAll({
188221
id: notification.id,
189222
createdAt: new Date(notification.createdAt).toISOString(),
@@ -192,14 +225,19 @@ export class NotificationEntityService implements OnModuleInit {
192225
reactions,
193226
});
194227
} else if (notification.type === 'renote:grouped') {
195-
const users = await Promise.all(notification.userIds.map(userId => {
228+
const users = (await Promise.all(notification.userIds.map(userId => {
196229
const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null;
197230
if (packedUser) {
198231
return packedUser;
199232
}
200233

201234
return this.userEntityService.pack(userId, { id: meId });
202-
}));
235+
}))).filter(u => u);
236+
// if all users have been deleted, don't show this notification
237+
if (!users.length) {
238+
return null;
239+
}
240+
203241
return await awaitAll({
204242
id: notification.id,
205243
createdAt: new Date(notification.createdAt).toISOString(),
@@ -209,7 +247,12 @@ export class NotificationEntityService implements OnModuleInit {
209247
});
210248
}
211249

212-
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
250+
const needsRole = notification.type === 'roleAssigned';
251+
const role = needsRole ? await this.roleEntityService.pack(notification.roleId) : undefined;
252+
// if the role has been deleted, don't show this notification
253+
if (needsRole && !role) {
254+
return null;
255+
}
213256

214257
return await awaitAll({
215258
id: notification.id,
@@ -277,9 +320,9 @@ export class NotificationEntityService implements OnModuleInit {
277320
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
278321
}
279322

280-
return await Promise.all(validNotifications.map(x => this.packGrouped(x, meId, {}, {
323+
return (await Promise.all(validNotifications.map(x => this.packGrouped(x, meId, {}, {
281324
packedNotes,
282325
packedUsers,
283-
})));
326+
})))).filter(n => n);
284327
}
285328
}

0 commit comments

Comments
 (0)