feat: 削除済みポストとActorを復元できるように

This commit is contained in:
usbharu 2024-05-17 21:31:08 +09:00
parent 4232be3962
commit c8294c4cb8
9 changed files with 96 additions and 110 deletions

View File

@ -71,6 +71,11 @@ class APUndoProcessor(
return
}
"Delete" -> {
delete(undo)
return
}
else -> {}
}
TODO()
@ -124,6 +129,12 @@ class APUndoProcessor(
postService.deleteRemote(findByApId)
}
private suspend fun delete(undo: Undo) {
val announce = undo.apObject as Delete
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Undo
override fun type(): Class<Undo> = Undo::class.java

View File

@ -22,6 +22,7 @@ data class DeletedActor(
val id: Long,
val name: String,
val domain: String,
val apiId: String,
val publicKey: String,
val deletedAt: Instant
val deletedAt: Instant,
)

View File

@ -43,7 +43,7 @@ data class Post private constructor(
@get:URL
val apId: String = url,
val mediaIds: List<Long> = emptyList(),
val delted: Boolean = false,
val deleted: Boolean = false,
val emojiIds: List<Long> = emptyList(),
) {
@ -67,7 +67,8 @@ data class Post private constructor(
sensitive: Boolean = false,
apId: String = url,
mediaIds: List<Long> = emptyList(),
emojiIds: List<Long> = emptyList()
emojiIds: List<Long> = emptyList(),
deleted: Boolean = false,
): Post {
require(id >= 0) { "id must be greater than or equal to 0." }
@ -109,7 +110,7 @@ data class Post private constructor(
sensitive = sensitive,
apId = apId,
mediaIds = mediaIds,
delted = false,
deleted = deleted,
emojiIds = emojiIds
)
@ -119,6 +120,10 @@ data class Post private constructor(
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
if (post.deleted) {
return post.delete()
}
return post
}
@ -130,7 +135,7 @@ data class Post private constructor(
createdAt: Instant,
url: String,
repost: Post,
apId: String
apId: String,
): Post {
// リポストの公開範囲は元のポストより広くてはいけない
val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) {
@ -139,16 +144,11 @@ data class Post private constructor(
visibility
}
require(id >= 0) { "id must be greater than or equal to 0." }
require(actorId >= 0) { "actorId must be greater than or equal to 0." }
val post = Post(
val post = of(
id = id,
actorId = actorId,
overview = null,
content = "",
text = "",
createdAt = createdAt.toEpochMilli(),
visibility = fixedVisibility,
url = url,
@ -157,16 +157,9 @@ data class Post private constructor(
sensitive = false,
apId = apId,
mediaIds = emptyList(),
delted = false,
deleted = false,
emojiIds = emptyList()
)
val validate = validator.validate(post)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return post
}
@ -184,7 +177,7 @@ data class Post private constructor(
sensitive: Boolean = false,
apId: String = url,
mediaIds: List<Long> = emptyList(),
emojiIds: List<Long> = emptyList()
emojiIds: List<Long> = emptyList(),
): Post {
// リポストの公開範囲は元のポストより広くてはいけない
val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) {
@ -193,37 +186,11 @@ data class Post private constructor(
visibility
}
require(id >= 0) { "id must be greater than or equal to 0." }
require(actorId >= 0) { "actorId must be greater than or equal to 0." }
val limitedOverview = if ((overview?.length ?: 0) >= characterLimit.post.overview) {
overview?.substring(0, characterLimit.post.overview)
} else {
overview
}
val limitedText = if (content.length >= characterLimit.post.text) {
content.substring(0, characterLimit.post.text)
} else {
content
}
val (html, content1) = postContentFormatter.format(limitedText)
require(url.isNotBlank()) { "url must contain non-blank characters" }
require(url.length <= characterLimit.general.url) {
"url must not exceed ${characterLimit.general.url} characters."
}
require((replyId ?: 0) >= 0) { "replyId must be greater then or equal to 0." }
val post = Post(
val post = of(
id = id,
actorId = actorId,
overview = limitedOverview,
content = html,
text = content1,
overview = overview,
content = content,
createdAt = createdAt.toEpochMilli(),
visibility = fixedVisibility,
url = url,
@ -232,70 +199,37 @@ data class Post private constructor(
sensitive = sensitive,
apId = apId,
mediaIds = mediaIds,
delted = false,
deleted = false,
emojiIds = emojiIds
)
val validate = validator.validate(post)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return post
}
@Suppress("LongParameterList")
fun deleteOf(
id: Long,
visibility: Visibility,
url: String,
repostId: Long?,
replyId: Long?,
apId: String
): Post {
return Post(
id = id,
actorId = 0,
overview = null,
content = "",
text = "",
createdAt = Instant.EPOCH.toEpochMilli(),
visibility = visibility,
url = url,
repostId = repostId,
replyId = replyId,
sensitive = false,
apId = apId,
mediaIds = emptyList(),
delted = true
)
}
}
fun isPureRepost(): Boolean =
this.text.isEmpty() &&
this.content.isEmpty() &&
this.overview == null &&
this.replyId == null &&
this.repostId != null
this.content.isEmpty() &&
this.overview == null &&
this.replyId == null &&
this.repostId != null
fun delete(): Post {
return Post(
id = this.id,
actorId = 0,
overview = null,
content = "",
text = "",
createdAt = Instant.EPOCH.toEpochMilli(),
actorId = actorId,
overview = overview,
content = content,
text = text,
createdAt = createdAt,
visibility = visibility,
url = url,
repostId = repostId,
replyId = replyId,
sensitive = false,
sensitive = sensitive,
apId = apId,
mediaIds = emptyList(),
delted = true
mediaIds = mediaIds,
deleted = true
)
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.external.job
import dev.usbharu.owl.common.task.Task
import dev.usbharu.owl.common.task.TaskDefinition
import org.springframework.stereotype.Component
data class UpdateActorTask(
val id: Long,
val apId: String,
) : Task()
@Component
data object UpdateActorTaskDef : TaskDefinition<UpdateActorTask> {
override val type: Class<UpdateActorTask>
get() = UpdateActorTask::class.java
}

View File

@ -26,16 +26,6 @@ import org.springframework.stereotype.Component
@Component
class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRowMapper<Post> {
override fun map(resultRow: ResultRow): Post {
if (resultRow[Posts.deleted]) {
return postBuilder.deleteOf(
id = resultRow[Posts.id],
visibility = Visibility.values().first { it.ordinal == resultRow[Posts.visibility] },
url = resultRow[Posts.url],
repostId = resultRow[Posts.repostId],
replyId = resultRow[Posts.replyId],
apId = resultRow[Posts.apId]
)
}
return postBuilder.of(
id = resultRow[Posts.id],
@ -49,6 +39,7 @@ class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRow
replyId = resultRow[Posts.replyId],
sensitive = resultRow[Posts.sensitive],
apId = resultRow[Posts.apId],
deleted = resultRow[Posts.deleted],
)
}
}

View File

@ -52,7 +52,7 @@ class PostRepositoryImpl(
it[replyId] = post.replyId
it[sensitive] = post.sensitive
it[apId] = post.apId
it[deleted] = post.delted
it[deleted] = post.deleted
}
PostsMedia.batchInsert(post.mediaIds) {
this[PostsMedia.postId] = post.id
@ -89,7 +89,7 @@ class PostRepositoryImpl(
it[replyId] = post.replyId
it[sensitive] = post.sensitive
it[apId] = post.apId
it[deleted] = post.delted
it[deleted] = post.deleted
}
}
return@query post

View File

@ -59,7 +59,7 @@ class PostServiceImpl(
}
override suspend fun deleteLocal(post: Post) {
if (post.delted) {
if (post.deleted) {
return
}
reactionRepository.deleteByPostId(post.id)
@ -73,7 +73,7 @@ class PostServiceImpl(
}
override suspend fun deleteRemote(post: Post) {
if (post.delted) {
if (post.deleted) {
return
}
reactionRepository.deleteByPostId(post.id)
@ -86,7 +86,7 @@ class PostServiceImpl(
}
override suspend fun deleteByActor(actorId: Long) {
postRepository.findByActorId(actorId).filterNot { it.delted }.forEach { postRepository.save(it.delete()) }
postRepository.findByActorId(actorId).filterNot { it.deleted }.forEach { postRepository.save(it.delete()) }
val actor = actorRepository.findById(actorId)
?: throw IllegalStateException("actor: $actorId was not found.")

View File

@ -32,6 +32,8 @@ interface UserService {
suspend fun deleteRemoteActor(actorId: Long)
suspend fun restorationRemoteActor(actorId: Long)
suspend fun deleteLocalUser(userId: Long)
suspend fun updateUserStatistics(userId: Long)

View File

@ -29,8 +29,10 @@ import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import dev.usbharu.hideout.core.external.job.UpdateActorTask
import dev.usbharu.hideout.core.service.instance.InstanceService
import dev.usbharu.hideout.core.service.post.PostService
import dev.usbharu.owl.producer.api.OwlProducer
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.Instant
@ -50,6 +52,7 @@ class UserServiceImpl(
private val postService: PostService,
private val apSendDeleteService: APSendDeleteService,
private val postRepository: PostRepository,
private val owlProducer: OwlProducer,
) :
UserService {
@ -156,6 +159,7 @@ class UserServiceImpl(
actor.id,
actor.name,
actor.domain,
actor.url,
actor.publicKey,
Instant.now()
)
@ -169,6 +173,15 @@ class UserServiceImpl(
deletedActorRepository.save(deletedActor)
}
override suspend fun restorationRemoteActor(actorId: Long) {
val deletedActor = deletedActorRepository.findById(actorId)
?: return
deletedActorRepository.delete(deletedActor)
owlProducer.publishTask(UpdateActorTask(deletedActor.id, deletedActor.apiId))
}
override suspend fun deleteLocalUser(userId: Long) {
val actor = actorRepository.findByIdWithLock(userId) ?: throw UserNotFoundException.withId(userId)
apSendDeleteService.sendDeleteActor(actor)
@ -176,6 +189,7 @@ class UserServiceImpl(
actor.id,
actor.name,
actor.domain,
actor.url,
actor.publicKey,
Instant.now()
)