mirror of https://github.com/usbharu/Hideout.git
				
				
				
			feat: 削除された投稿はレコードを完全に削除せず、返信などの情報を維持したまま削除されるように
This commit is contained in:
		
							parent
							
								
									8d1f339a5d
								
							
						
					
					
						commit
						8a18780963
					
				|  | @ -8,15 +8,15 @@ import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext | |||
| import dev.usbharu.hideout.activitypub.service.common.ActivityType | ||||
| import dev.usbharu.hideout.application.external.Transaction | ||||
| import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException | ||||
| import dev.usbharu.hideout.core.domain.model.post.PostRepository | ||||
| import dev.usbharu.hideout.core.query.PostQueryService | ||||
| import dev.usbharu.hideout.core.service.post.PostService | ||||
| import org.springframework.stereotype.Service | ||||
| 
 | ||||
| @Service | ||||
| class APDeleteProcessor( | ||||
|     transaction: Transaction, | ||||
|     private val postQueryService: PostQueryService, | ||||
|     private val postRepository: PostRepository | ||||
|     private val postService: PostService | ||||
| ) : | ||||
|     AbstractActivityPubProcessor<Delete>(transaction) { | ||||
|     override suspend fun internalProcess(activity: ActivityPubProcessContext<Delete>) { | ||||
|  | @ -33,7 +33,7 @@ class APDeleteProcessor( | |||
|             return | ||||
|         } | ||||
| 
 | ||||
|         postRepository.delete(post.id) | ||||
|         postService.deleteRemote(post) | ||||
|     } | ||||
| 
 | ||||
|     override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Delete | ||||
|  |  | |||
|  | @ -0,0 +1,23 @@ | |||
| package dev.usbharu.hideout.activitypub.service.activity.delete | ||||
| 
 | ||||
| import dev.usbharu.hideout.activitypub.service.common.APRequestService | ||||
| import dev.usbharu.hideout.application.external.Transaction | ||||
| import dev.usbharu.hideout.core.external.job.DeliverDeleteJob | ||||
| import dev.usbharu.hideout.core.external.job.DeliverDeleteJobParam | ||||
| import dev.usbharu.hideout.core.query.ActorQueryService | ||||
| import dev.usbharu.hideout.core.service.job.JobProcessor | ||||
| import org.springframework.stereotype.Service | ||||
| 
 | ||||
| @Service | ||||
| class APDeliverDeleteJobProcessor( | ||||
|     private val apRequestService: APRequestService, | ||||
|     private val actorQueryService: ActorQueryService, | ||||
|     private val transaction: Transaction, | ||||
|     private val deliverDeleteJob: DeliverDeleteJob | ||||
| ) : JobProcessor<DeliverDeleteJobParam, DeliverDeleteJob> { | ||||
|     override suspend fun process(param: DeliverDeleteJobParam): Unit = transaction.transaction { | ||||
|         apRequestService.apPost(param.inbox, param.delete, actorQueryService.findById(param.signer)) | ||||
|     } | ||||
| 
 | ||||
|     override fun job(): DeliverDeleteJob = deliverDeleteJob | ||||
| } | ||||
|  | @ -0,0 +1,50 @@ | |||
| package dev.usbharu.hideout.activitypub.service.activity.delete | ||||
| 
 | ||||
| import dev.usbharu.hideout.activitypub.domain.model.Delete | ||||
| import dev.usbharu.hideout.activitypub.domain.model.Tombstone | ||||
| import dev.usbharu.hideout.application.config.ApplicationConfig | ||||
| import dev.usbharu.hideout.core.domain.model.post.Post | ||||
| import dev.usbharu.hideout.core.external.job.DeliverDeleteJob | ||||
| import dev.usbharu.hideout.core.external.job.DeliverDeleteJobParam | ||||
| import dev.usbharu.hideout.core.query.ActorQueryService | ||||
| import dev.usbharu.hideout.core.query.FollowerQueryService | ||||
| import dev.usbharu.hideout.core.service.job.JobQueueParentService | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.Instant | ||||
| 
 | ||||
| interface APSendDeleteService { | ||||
|     suspend fun sendDeleteNote(deletedPost: Post) | ||||
| } | ||||
| 
 | ||||
| @Service | ||||
| class APSendDeleteServiceImpl( | ||||
|     private val jobQueueParentService: JobQueueParentService, | ||||
|     private val delverDeleteJob: DeliverDeleteJob, | ||||
|     private val followerQueryService: FollowerQueryService, | ||||
|     private val actorQueryService: ActorQueryService, | ||||
|     private val applicationConfig: ApplicationConfig | ||||
| ) : APSendDeleteService { | ||||
|     override suspend fun sendDeleteNote(deletedPost: Post) { | ||||
| 
 | ||||
| 
 | ||||
|         val actor = actorQueryService.findById(deletedPost.actorId) | ||||
|         val followersById = followerQueryService.findFollowersById(deletedPost.actorId) | ||||
| 
 | ||||
|         val delete = Delete( | ||||
|             actor = actor.url, | ||||
|             id = "${applicationConfig.url}/delete/note/${deletedPost.id}", | ||||
|             published = Instant.now().toString(), | ||||
|             `object` = Tombstone(id = deletedPost.apId) | ||||
|         ) | ||||
| 
 | ||||
|         followersById.forEach { | ||||
|             val jobProps = DeliverDeleteJobParam( | ||||
|                 delete, | ||||
|                 it.inbox, | ||||
|                 actor.id | ||||
|             ) | ||||
|             jobQueueParentService.scheduleTypeSafe(delverDeleteJob, jobProps) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -2,6 +2,7 @@ package dev.usbharu.hideout.core.domain.model.post | |||
| 
 | ||||
| import dev.usbharu.hideout.application.config.CharacterLimit | ||||
| import org.springframework.stereotype.Component | ||||
| import java.time.Instant | ||||
| 
 | ||||
| data class Post private constructor( | ||||
|     val id: Long, | ||||
|  | @ -15,7 +16,8 @@ data class Post private constructor( | |||
|     val replyId: Long? = null, | ||||
|     val sensitive: Boolean = false, | ||||
|     val apId: String = url, | ||||
|     val mediaIds: List<Long> = emptyList() | ||||
|     val mediaIds: List<Long> = emptyList(), | ||||
|     val delted: Boolean = false | ||||
| ) { | ||||
| 
 | ||||
|     @Component | ||||
|  | @ -71,8 +73,52 @@ data class Post private constructor( | |||
|                 replyId = replyId, | ||||
|                 sensitive = sensitive, | ||||
|                 apId = apId, | ||||
|                 mediaIds = mediaIds | ||||
|                 mediaIds = mediaIds, | ||||
|                 delted = false | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         fun deleteOf( | ||||
|             id: Long, | ||||
|             visibility: Visibility, | ||||
|             url: String, | ||||
|             repostId: Long?, | ||||
|             replyId: Long?, | ||||
|             apId: String | ||||
|         ): Post { | ||||
|             return Post( | ||||
|                 id = id, | ||||
|                 actorId = 0, | ||||
|                 overview = null, | ||||
|                 text = "", | ||||
|                 createdAt = Instant.EPOCH.toEpochMilli(), | ||||
|                 visibility = visibility, | ||||
|                 url = url, | ||||
|                 repostId = repostId, | ||||
|                 replyId = replyId, | ||||
|                 sensitive = false, | ||||
|                 apId = apId, | ||||
|                 mediaIds = emptyList(), | ||||
|                 delted = true | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun delete(): Post { | ||||
|         return Post( | ||||
|             id = this.id, | ||||
|             actorId = 0, | ||||
|             overview = null, | ||||
|             text = "", | ||||
|             createdAt = Instant.EPOCH.toEpochMilli(), | ||||
|             visibility = visibility, | ||||
|             url = url, | ||||
|             repostId = repostId, | ||||
|             replyId = replyId, | ||||
|             sensitive = false, | ||||
|             apId = apId, | ||||
|             mediaIds = emptyList(), | ||||
|             delted = true | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -7,4 +7,6 @@ interface ReactionRepository { | |||
|     suspend fun generateId(): Long | ||||
|     suspend fun save(reaction: Reaction): Reaction | ||||
|     suspend fun delete(reaction: Reaction): Reaction | ||||
|     suspend fun deleteByPostId(postId: Long): Int | ||||
|     suspend fun deleteByActorId(actorId: Long): Int | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,36 @@ | |||
| package dev.usbharu.hideout.core.external.job | ||||
| 
 | ||||
| import com.fasterxml.jackson.databind.ObjectMapper | ||||
| import com.fasterxml.jackson.module.kotlin.readValue | ||||
| import dev.usbharu.hideout.activitypub.domain.model.Delete | ||||
| import kjob.core.dsl.ScheduleContext | ||||
| import kjob.core.job.JobProps | ||||
| import org.springframework.beans.factory.annotation.Qualifier | ||||
| import org.springframework.stereotype.Component | ||||
| 
 | ||||
| data class DeliverDeleteJobParam( | ||||
|     val delete: Delete, | ||||
|     val inbox: String, | ||||
|     val signer: Long | ||||
| ) | ||||
| 
 | ||||
| @Component | ||||
| class DeliverDeleteJob(@Qualifier("activitypub") private val objectMapper: ObjectMapper) : | ||||
|     HideoutJob<DeliverDeleteJobParam, DeliverDeleteJob>("DeliverDeleteJob") { | ||||
| 
 | ||||
|     val delete = string("delete") | ||||
|     val inbox = string("inbox") | ||||
|     val signer = long("signer") | ||||
| 
 | ||||
|     override fun convert(value: DeliverDeleteJobParam): ScheduleContext<DeliverDeleteJob>.(DeliverDeleteJob) -> Unit = { | ||||
|         props[delete] = objectMapper.writeValueAsString(value.delete) | ||||
|         props[inbox] = value.inbox | ||||
|         props[signer] = value.signer | ||||
|     } | ||||
| 
 | ||||
|     override fun convert(props: JobProps<DeliverDeleteJob>): DeliverDeleteJobParam = DeliverDeleteJobParam( | ||||
|         objectMapper.readValue(props[delete]), | ||||
|         props[inbox], | ||||
|         props[signer] | ||||
|     ) | ||||
| } | ||||
|  | @ -10,6 +10,17 @@ 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( | ||||
|                 resultRow[Posts.id], | ||||
|                 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], | ||||
|             actorId = resultRow[Posts.actorId], | ||||
|  |  | |||
|  | @ -85,6 +85,7 @@ object Posts : Table() { | |||
|     val replyId: Column<Long?> = long("reply_id").references(id).nullable() | ||||
|     val sensitive: Column<Boolean> = bool("sensitive").default(false) | ||||
|     val apId: Column<String> = varchar("ap_id", 100).uniqueIndex() | ||||
|     val deleted = bool("deleted").default(false) | ||||
|     override val primaryKey: PrimaryKey = PrimaryKey(id) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -42,6 +42,18 @@ class ReactionRepositoryImpl( | |||
|         } | ||||
|         return reaction | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun deleteByPostId(postId: Long): Int { | ||||
|         return Reactions.deleteWhere { | ||||
|             Reactions.postId eq postId | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun deleteByActorId(actorId: Long): Int { | ||||
|         return Reactions.deleteWhere { | ||||
|             Reactions.actorId eq actorId | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fun ResultRow.toReaction(): Reaction { | ||||
|  | @ -55,8 +67,10 @@ fun ResultRow.toReaction(): Reaction { | |||
| 
 | ||||
| object Reactions : LongIdTable("reactions") { | ||||
|     val emojiId: Column<Long> = long("emoji_id") | ||||
|     val postId: Column<Long> = long("post_id").references(Posts.id) | ||||
|     val actorId: Column<Long> = long("actor_id").references(Actors.id) | ||||
|     val postId: Column<Long> = long("post_id") | ||||
|         .references(Posts.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE) | ||||
|     val actorId: Column<Long> = long("actor_id") | ||||
|         .references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE) | ||||
| 
 | ||||
|     init { | ||||
|         uniqueIndex(emojiId, postId, actorId) | ||||
|  |  | |||
|  | @ -7,4 +7,6 @@ import org.springframework.stereotype.Service | |||
| interface PostService { | ||||
|     suspend fun createLocal(post: PostCreateDto): Post | ||||
|     suspend fun createRemote(post: Post): Post | ||||
|     suspend fun deleteLocal(post: Post) | ||||
|     suspend fun deleteRemote(post: Post) | ||||
| } | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import dev.usbharu.hideout.core.domain.exception.UserNotFoundException | |||
| import dev.usbharu.hideout.core.domain.model.actor.ActorRepository | ||||
| import dev.usbharu.hideout.core.domain.model.post.Post | ||||
| import dev.usbharu.hideout.core.domain.model.post.PostRepository | ||||
| import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository | ||||
| import dev.usbharu.hideout.core.query.PostQueryService | ||||
| import dev.usbharu.hideout.core.service.timeline.TimelineService | ||||
| import org.jetbrains.exposed.exceptions.ExposedSQLException | ||||
|  | @ -20,7 +21,8 @@ class PostServiceImpl( | |||
|     private val timelineService: TimelineService, | ||||
|     private val postQueryService: PostQueryService, | ||||
|     private val postBuilder: Post.PostBuilder, | ||||
|     private val apSendCreateService: ApSendCreateService | ||||
|     private val apSendCreateService: ApSendCreateService, | ||||
|     private val reactionRepository: ReactionRepository | ||||
| ) : PostService { | ||||
| 
 | ||||
|     override suspend fun createLocal(post: PostCreateDto): Post { | ||||
|  | @ -38,6 +40,16 @@ class PostServiceImpl( | |||
|         return createdPost | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun deleteLocal(post: Post) { | ||||
|         reactionRepository.deleteByPostId(post.id) | ||||
|         postRepository.save(post.delete()) | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun deleteRemote(post: Post) { | ||||
|         reactionRepository.deleteByPostId(post.id) | ||||
|         postRepository.save(post.delete()) | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun internalCreate(post: Post, isLocal: Boolean): Post { | ||||
|         return try { | ||||
|             if (postRepository.save(post)) { | ||||
|  |  | |||
|  | @ -67,7 +67,7 @@ create table if not exists meta_info | |||
| create table if not exists posts | ||||
| ( | ||||
|     id          bigint primary key, | ||||
|     actor_id bigint       not null, | ||||
|     actor_id bigint                not null, | ||||
|     overview    varchar(100)          null, | ||||
|     text        varchar(3000)         not null, | ||||
|     created_at  bigint                not null, | ||||
|  | @ -76,7 +76,8 @@ create table if not exists posts | |||
|     repost_id   bigint                null, | ||||
|     reply_id    bigint                null, | ||||
|     "sensitive" boolean default false not null, | ||||
|     ap_id    varchar(100) not null unique | ||||
|     ap_id    varchar(100)          not null unique, | ||||
|     deleted  boolean default false not null | ||||
| ); | ||||
| alter table posts | ||||
|     add constraint fk_posts_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict; | ||||
|  | @ -196,4 +197,8 @@ create table if not exists relationships | |||
|     constraint fk_relationships_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict, | ||||
|     constraint fk_relationships_target_actor_id__id foreign key (target_actor_id) references actors (id) on delete restrict on update restrict, | ||||
|     unique (actor_id, target_actor_id) | ||||
| ) | ||||
| ); | ||||
| 
 | ||||
| insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at, | ||||
|                     key_id, following, followers, instance, locked) | ||||
| values (0, 'ghost', '', '', '', '', '', '', '', null, 0, '', '', '', null, true); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue