mirror of https://github.com/usbharu/Hideout.git
				
				
				
			feat: 削除済みポストとActorを復元できるように
This commit is contained in:
		
							parent
							
								
									4232be3962
								
							
						
					
					
						commit
						c8294c4cb8
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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, | ||||
| ) | ||||
|  |  | |||
|  | @ -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 | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										33
									
								
								hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/job/UpdateActorTask.kt
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										33
									
								
								hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/job/UpdateActorTask.kt
								
								
								
									vendored
								
								
									Normal 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 | ||||
| } | ||||
|  | @ -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], | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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.") | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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() | ||||
|         ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue