diff --git a/src/misc/is-duplicate-key-value-error.ts b/src/misc/is-duplicate-key-value-error.ts index 23d8ceb1b7..f5343d187c 100644 --- a/src/misc/is-duplicate-key-value-error.ts +++ b/src/misc/is-duplicate-key-value-error.ts @@ -1,3 +1,5 @@ -export function isDuplicateKeyValueError(e: Error): boolean { - return e.message.startsWith('duplicate key value'); +import { QueryFailedError } from 'typeorm'; + +export function isDuplicateKeyValueError(e: unknown | Error): boolean { + return e instanceof QueryFailedError && e.driverError.code === '23505'; } diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts index ed38450bb2..8da79547d6 100644 --- a/src/models/entities/note-reaction.ts +++ b/src/models/entities/note-reaction.ts @@ -23,7 +23,7 @@ export class NoteReaction { onDelete: 'CASCADE' }) @JoinColumn() - public user: User | null; + public user?: User | null; @Index() @Column(id()) @@ -33,7 +33,7 @@ export class NoteReaction { onDelete: 'CASCADE' }) @JoinColumn() - public note: Note | null; + public note?: Note | null; @Column('varchar', { length: 260 diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts index a6f02a1f8f..ce92907e3a 100644 --- a/src/remote/activitypub/kernel/like.ts +++ b/src/remote/activitypub/kernel/like.ts @@ -11,6 +11,13 @@ export default async (actor: IRemoteUser, activity: ILike) => { await extractEmojis(activity.tag || [], actor.host).catch(() => null); - await create(actor, note, activity._misskey_reaction || activity.content || activity.name); + await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => { + if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { + return 'skip: already reacted'; + } else { + throw e; + } + }); + return `ok`; }; diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 67e39c4f50..747c6c114c 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -12,33 +12,44 @@ import { perUserReactionsChart } from '../../chart'; import { genId } from '../../../misc/gen-id'; import { createNotification } from '../../create-notification'; import deleteReaction from './delete'; +import { NoteReaction } from '../../../models/entities/note-reaction'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; +import { IdentifiableError } from '../../../misc/identifiable-error'; export default async (user: User, note: Note, reaction?: string) => { reaction = await toDbReaction(reaction, user.host); - const exist = await NoteReactions.findOne({ + const inserted: NoteReaction = { + id: genId(), + createdAt: new Date(), noteId: note.id, userId: user.id, - }); + reaction, + }; + + // Create reaction + try { + await NoteReactions.insert(inserted); + } catch (e) { + if (isDuplicateKeyValueError(e)) { + const exists = await NoteReactions.findOneOrFail({ + noteId: note.id, + userId: user.id, + }); - if (exist) { - if (exist.reaction !== reaction) { - // 別のリアクションがすでにされていたら置き換える - await deleteReaction(user, note); + if (exists.reaction !== reaction) { + // 別のリアクションがすでにされていたら置き換える + await deleteReaction(user, note); + await NoteReactions.insert(inserted); + } else { + // 同じリアクションがすでにされていたらエラー + throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); + } } else { - // 同じリアクションがすでにされていたら何もしない - return; + throw e; } } - // Create reaction - const inserted = await NoteReactions.save({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - reaction - }); // Increment reactions count const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;