From 65a2ea6a74fbcb56216d667a9feb8259f744d30d Mon Sep 17 00:00:00 2001 From: Mar0xy Date: Thu, 30 Nov 2023 02:11:47 +0100 Subject: [PATCH] upd: improve post editing on polls Fixes not being able to edit post if poll expiry was set and now checks properly if poll was edited or not --- packages/backend/src/core/NoteEditService.ts | 24 +-- .../src/server/api/endpoints/notes/edit.ts | 163 +++++++++--------- .../frontend/src/components/MkPostForm.vue | 4 +- packages/misskey-js/src/entities.ts | 1 + 4 files changed, 95 insertions(+), 97 deletions(-) diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index dfcff0e5b..e5918e6c2 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -14,18 +14,15 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf 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 } from '@/models/_.js'; +import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, 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'; import { IdService } from '@/core/IdService.js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { MiPoll, type IPoll } from '@/models/Poll.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; import type { MiChannel } from '@/models/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MemorySingleCache } from '@/misc/cache.js'; -import type { MiUserProfile } from '@/models/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -35,7 +32,6 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { WebhookService } from '@/core/WebhookService.js'; -import { HashtagService } from '@/core/HashtagService.js'; import { QueueService } from '@/core/QueueService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -48,11 +44,7 @@ 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 { FeaturedService } from '@/core/FeaturedService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; -import { AntennaService } from './AntennaService.js'; -import NotesChart from './chart/charts/notes.js'; -import PerUserNotesChart from './chart/charts/per-user-notes.js'; import { UtilityService } from '@/core/UtilityService.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -191,6 +183,9 @@ export class NoteEditService implements OnApplicationShutdown { @Inject(DI.noteEditRepository) private noteEditRepository: NoteEditRepository, + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -201,18 +196,13 @@ export class NoteEditService implements OnApplicationShutdown { private notificationService: NotificationService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, - private hashtagService: HashtagService, - private antennaService: AntennaService, private webhookService: WebhookService, - private featuredService: FeaturedService, private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, private metaService: MetaService, private searchService: SearchService, - private notesChart: NotesChart, - private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, private instanceChart: InstanceChart, private utilityService: UtilityService, @@ -385,6 +375,10 @@ export class NoteEditService implements OnApplicationShutdown { update.hasPoll = !!data.poll; } + const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id }); + + const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null; + if (Object.keys(update).length > 0) { const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id }); @@ -456,7 +450,7 @@ export class NoteEditService implements OnApplicationShutdown { })); } - if (data.poll != null) { + if (data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll)) { // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.update(MiNote, oldnote.id, note); diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 6140c80a5..49fa4c3bd 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -14,7 +14,7 @@ import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { - tags: ["notes"], + tags: ['notes'], requireCredential: true, @@ -23,99 +23,99 @@ export const meta = { max: 300, }, - kind: "write:notes", + kind: 'write:notes', res: { - type: "object", + type: 'object', optional: false, nullable: false, properties: { createdNote: { - type: "object", + type: 'object', optional: false, nullable: false, - ref: "Note", + ref: 'Note', }, }, }, errors: { noSuchRenoteTarget: { - message: "No such renote target.", - code: "NO_SUCH_RENOTE_TARGET", - id: "b5c90186-4ab0-49c8-9bba-a1f76c282ba4", + message: 'No such renote target.', + code: 'NO_SUCH_RENOTE_TARGET', + id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4', }, cannotReRenote: { - message: "You can not Renote a pure Renote.", - code: "CANNOT_RENOTE_TO_A_PURE_RENOTE", - id: "fd4cc33e-2a37-48dd-99cc-9b806eb2031a", + message: 'You can not Renote a pure Renote.', + code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', + id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', }, noSuchReplyTarget: { - message: "No such reply target.", - code: "NO_SUCH_REPLY_TARGET", - id: "749ee0f6-d3da-459a-bf02-282e2da4292c", + message: 'No such reply target.', + code: 'NO_SUCH_REPLY_TARGET', + id: '749ee0f6-d3da-459a-bf02-282e2da4292c', }, cannotReplyToPureRenote: { - message: "You can not reply to a pure Renote.", - code: "CANNOT_REPLY_TO_A_PURE_RENOTE", - id: "3ac74a84-8fd5-4bb0-870f-01804f82ce15", + message: 'You can not reply to a pure Renote.', + code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', }, cannotCreateAlreadyExpiredPoll: { - message: "Poll is already expired.", - code: "CANNOT_CREATE_ALREADY_EXPIRED_POLL", - id: "04da457d-b083-4055-9082-955525eda5a5", + message: 'Poll is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', + id: '04da457d-b083-4055-9082-955525eda5a5', }, noSuchChannel: { - message: "No such channel.", - code: "NO_SUCH_CHANNEL", - id: "b1653923-5453-4edc-b786-7c4f39bb0bbb", + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb', }, youHaveBeenBlocked: { - message: "You have been blocked by this user.", - code: "YOU_HAVE_BEEN_BLOCKED", - id: "b390d7e1-8a5e-46ed-b625-06271cafd3d3", + message: 'You have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', }, accountLocked: { - message: "You migrated. Your account is now locked.", - code: "ACCOUNT_LOCKED", - id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3", + message: 'You migrated. Your account is now locked.', + code: 'ACCOUNT_LOCKED', + id: 'd390d7e1-8a5e-46ed-b625-06271cafd3d3', }, needsEditId: { - message: "You need to specify `editId`.", - code: "NEEDS_EDIT_ID", - id: "d697edc8-8c73-4de8-bded-35fd198b79e5", + message: 'You need to specify `editId`.', + code: 'NEEDS_EDIT_ID', + id: 'd697edc8-8c73-4de8-bded-35fd198b79e5', }, noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "eef6c173-3010-4a23-8674-7c4fcaeba719", + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'eef6c173-3010-4a23-8674-7c4fcaeba719', }, youAreNotTheAuthor: { - message: "You are not the author of this note.", - code: "YOU_ARE_NOT_THE_AUTHOR", - id: "c6e61685-411d-43d0-b90a-a448d2539001", + message: 'You are not the author of this note.', + code: 'YOU_ARE_NOT_THE_AUTHOR', + id: 'c6e61685-411d-43d0-b90a-a448d2539001', }, cannotPrivateRenote: { - message: "You can not perform a private renote.", - code: "CANNOT_PRIVATE_RENOTE", - id: "19a50f1c-84fa-4e33-81d3-17834ccc0ad8", + message: 'You can not perform a private renote.', + code: 'CANNOT_PRIVATE_RENOTE', + id: '19a50f1c-84fa-4e33-81d3-17834ccc0ad8', }, notLocalUser: { - message: "You are not a local user.", - code: "NOT_LOCAL_USER", - id: "b907f407-2aa0-4283-800b-a2c56290b822", + message: 'You are not a local user.', + code: 'NOT_LOCAL_USER', + id: 'b907f407-2aa0-4283-800b-a2c56290b822', }, cannotRenoteOutsideOfChannel: { @@ -127,60 +127,63 @@ export const meta = { } as const; export const paramDef = { - type: "object", + type: 'object', properties: { - editId: { type: "string", format: "misskey:id" }, - visibility: { type: "string", enum: ['public', 'home', 'followers', 'specified'], default: "public" }, + editId: { type: 'string', format: 'misskey:id' }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, visibleUserIds: { - type: "array", + type: 'array', uniqueItems: true, items: { - type: "string", - format: "misskey:id", + type: 'string', + format: 'misskey:id', }, }, - text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, - cw: { type: "string", nullable: true, minLength: 1, maxLength: 250 }, - localOnly: { type: "boolean", default: false }, - noExtractMentions: { type: "boolean", default: false }, - noExtractHashtags: { type: "boolean", default: false }, - noExtractEmojis: { type: "boolean", default: false }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 250 }, + localOnly: { type: 'boolean', default: false }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + noExtractMentions: { type: 'boolean', default: false }, + noExtractHashtags: { type: 'boolean', default: false }, + noExtractEmojis: { type: 'boolean', default: false }, + replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, + channelId: { type: 'string', format: 'misskey:id', nullable: true }, + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: true, + }, fileIds: { - type: "array", + type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, - items: { type: "string", format: "misskey:id" }, + items: { type: 'string', format: 'misskey:id' }, }, mediaIds: { - deprecated: true, - description: - "Use `fileIds` instead. If both are specified, this property is discarded.", - type: "array", + type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, - items: { type: "string", format: "misskey:id" }, + items: { type: 'string', format: 'misskey:id' }, }, - replyId: { type: "string", format: "misskey:id", nullable: true }, - renoteId: { type: "string", format: "misskey:id", nullable: true }, - channelId: { type: "string", format: "misskey:id", nullable: true }, poll: { - type: "object", + type: 'object', nullable: true, properties: { choices: { - type: "array", + type: 'array', uniqueItems: true, minItems: 2, maxItems: 10, - items: { type: "string", minLength: 1, maxLength: 50 }, + items: { type: 'string', minLength: 1, maxLength: 50 }, }, - multiple: { type: "boolean", default: false }, - expiresAt: { type: "integer", nullable: true }, - expiredAfter: { type: "integer", nullable: true, minimum: 1 }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, }, - required: ["choices"], + required: ['choices'], }, }, anyOf: [ @@ -188,32 +191,32 @@ export const paramDef = { // (re)note with text, files and poll are optional properties: { text: { - type: "string", + type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false, }, }, - required: ["text"], + required: ['text'], }, { // (re)note with files, text and poll are optional - required: ["fileIds"], + required: ['fileIds'], }, { // (re)note with files, text and poll are optional - required: ["mediaIds"], + required: ['mediaIds'], }, { // (re)note with poll, text and files are optional properties: { - poll: { type: "object", nullable: false }, + poll: { type: 'object', nullable: false }, }, - required: ["poll"], + required: ['poll'], }, { // pure renote - required: ["renoteId"], + required: ['renoteId'], }, ], } as const; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index f72e4ddfc..74038cd62 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -931,8 +931,8 @@ onMounted(() => { poll = { choices: init.poll.choices.map(x => x.text), multiple: init.poll.multiple, - expiresAt: init.poll.expiresAt, - expiredAfter: init.poll.expiredAfter, + expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null, + expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null, }; } visibility = init.visibility; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index ec9a3d45c..10b9dd5eb 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -217,6 +217,7 @@ export type Note = { clippedCount?: number; poll?: { expiresAt: DateString | null; + expiredAfter: DateString | null; multiple: boolean; choices: { isVoted: boolean;