feat: ローカルユーザー追加時に自動でホームタイムライン等を作成するように

This commit is contained in:
usbharu 2024-08-16 02:49:48 +09:00
parent 711084e366
commit 8d244b74c1
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
22 changed files with 328 additions and 42 deletions

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
data class RegisterHomeTimeline(
val userDetailId: Long
)

View File

@ -1,9 +1,23 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
class RegisterLocalUserSetHomeTimelineSubscriber(private val domainEventSubscriber: DomainEventSubscriber) :
import dev.usbharu.hideout.core.domain.event.userdetail.UserDetailEvent
import dev.usbharu.hideout.core.domain.event.userdetail.UserDetailEventBody
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import org.springframework.stereotype.Component
@Component
class RegisterLocalUserSetHomeTimelineSubscriber(
domainEventSubscriber: DomainEventSubscriber,
private val userRegisterHomeTimelineApplicationService: UserRegisterHomeTimelineApplicationService
) :
Subscriber {
init {
domainEventSubscriber.subscribe<>()
domainEventSubscriber.subscribe<UserDetailEventBody>(UserDetailEvent.CREATE.eventName) {
userRegisterHomeTimelineApplicationService.execute(
RegisterHomeTimeline(it.body.getUserDetail().id),
Anonymous
)
}
}
}

View File

@ -0,0 +1,21 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
import dev.usbharu.hideout.core.application.timeline.SetTimelineToTimelineStoreApplicationService
import dev.usbharu.hideout.core.application.timeline.SetTimleineStore
import dev.usbharu.hideout.core.domain.event.timeline.TimelineEvent
import dev.usbharu.hideout.core.domain.event.timeline.TimelineEventBody
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import org.springframework.stereotype.Component
@Component
class RegisterTimelineSetTimelineStoreSubscriber(
domainEventSubscriber: DomainEventSubscriber,
private val setTimelineToTimelineStoreApplicationService: SetTimelineToTimelineStoreApplicationService
) :
Subscriber {
init {
domainEventSubscriber.subscribe<TimelineEventBody>(TimelineEvent.CREATE.eventName) {
setTimelineToTimelineStoreApplicationService.execute(SetTimleineStore(it.body.getTimelineId()), Anonymous)
}
}
}

View File

@ -1,19 +1,21 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
import dev.usbharu.hideout.core.application.timeline.AddPost
import dev.usbharu.hideout.core.application.timeline.TimelineAddPostApplicationService
import dev.usbharu.hideout.core.domain.event.post.PostEvent
import dev.usbharu.hideout.core.domain.event.post.PostEventBody
import dev.usbharu.hideout.core.external.timeline.TimelineStore
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class TimelinePostCreateSubscriber(
private val timelineStore: TimelineStore,
domainEventSubscriber: DomainEventSubscriber
private val timelineAddPostApplicationService: TimelineAddPostApplicationService,
domainEventSubscriber: DomainEventSubscriber,
) : Subscriber {
init {
domainEventSubscriber.subscribe<PostEventBody>(PostEvent.CREATE.eventName) {
timelineStore.addPost(it.body.getPost())
timelineAddPostApplicationService.execute(AddPost(it.body.getPostId()), Anonymous)
}
}

View File

@ -32,7 +32,7 @@ class TimelineRelationshipFollowSubscriber(
AddTimelineRelationship(
TimelineRelationship(
TimelineRelationshipId(idGenerateService.generateId()),
userDetail.homeTimelineId,
userDetail.homeTimelineId!!,
relationship.targetActorId,
Visible.FOLLOWERS
)

View File

@ -0,0 +1,40 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.timeline.*
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class UserRegisterHomeTimelineApplicationService(
private val userDetailRepository: UserDetailRepository,
private val timelineRepository: TimelineRepository,
private val idGenerateService: IdGenerateService, transaction: Transaction,
) : AbstractApplicationService<RegisterHomeTimeline, Unit>(transaction, logger) {
override suspend fun internalExecute(command: RegisterHomeTimeline, principal: Principal) {
val userDetail = (userDetailRepository.findById(UserDetailId(command.userDetailId))
?: throw IllegalArgumentException("UserDetail ${command.userDetailId} not found."))
val timeline = Timeline.create(
TimelineId(idGenerateService.generateId()),
UserDetailId(command.userDetailId),
TimelineName("System-LocalUser-HomeTimeline-${command.userDetailId}"),
TimelineVisibility.PRIVATE,
true
)
timelineRepository.save(timeline)
userDetail.homeTimelineId = timeline.id
userDetailRepository.save(userDetail)
}
companion object {
private val logger = LoggerFactory.getLogger(UserRegisterHomeTimelineApplicationService::class.java)
}
}

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.domain.model.post.PostId
data class AddPost(val postId: PostId)

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineVisibility
data class RegisterTimeline(
val timelineName: String,
val visibility: TimelineVisibility
)

View File

@ -0,0 +1,29 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import dev.usbharu.hideout.core.external.timeline.TimelineStore
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class SetTimelineToTimelineStoreApplicationService(
transaction: Transaction,
private val timelineStore: TimelineStore,
private val timelineRepository: TimelineRepository
) :
AbstractApplicationService<SetTimleineStore, Unit>(
transaction, logger
) {
override suspend fun internalExecute(command: SetTimleineStore, principal: Principal) {
val findById = timelineRepository.findById(command.timelineId)
?: throw IllegalArgumentException("Timeline ${command.timelineId} not found")
timelineStore.addTimeline(findById, emptyList())
}
companion object {
private val logger = LoggerFactory.getLogger(SetTimelineToTimelineStoreApplicationService::class.java)
}
}

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
data class SetTimleineStore(val timelineId: TimelineId)

View File

@ -0,0 +1,29 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.external.timeline.TimelineStore
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class TimelineAddPostApplicationService(
private val timelineStore: TimelineStore,
private val postRepository: PostRepository,
transaction: Transaction
) : AbstractApplicationService<AddPost, Unit>(
transaction,
logger
) {
override suspend fun internalExecute(command: AddPost, principal: Principal) {
val findById = postRepository.findById(command.postId)
?: throw IllegalArgumentException("Post ${command.postId} not found.")
timelineStore.addPost(findById)
}
companion object {
private val logger = LoggerFactory.getLogger(TimelineAddPostApplicationService::class.java)
}
}

View File

@ -0,0 +1,37 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineName
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class UserRegisterTimelineApplicationService(
private val idGenerateService: IdGenerateService,
private val timelineRepository: TimelineRepository,
transaction: Transaction
) :
LocalUserAbstractApplicationService<RegisterTimeline, TimelineId>(transaction, logger) {
override suspend fun internalExecute(command: RegisterTimeline, principal: LocalUser): TimelineId {
val timeline = Timeline.create(
id = TimelineId(idGenerateService.generateId()),
userDetailId = principal.userDetailId,
name = TimelineName(command.timelineName),
visibility = command.visibility,
isSystem = false
)
timelineRepository.save(timeline)
return timeline.id
}
companion object {
private val logger = LoggerFactory.getLogger(UserRegisterTimelineApplicationService::class.java)
}
}

View File

@ -17,6 +17,7 @@
package dev.usbharu.hideout.core.domain.event.actor
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
@ -24,13 +25,13 @@ class ActorDomainEventFactory(private val actor: Actor) {
fun createEvent(actorEvent: ActorEvent): DomainEvent<ActorEventBody> {
return DomainEvent.create(
actorEvent.eventName,
ActorEventBody(actor),
ActorEventBody(actor.id),
actorEvent.collectable
)
}
}
class ActorEventBody(actor: Actor) : DomainEventBody(
class ActorEventBody(actor: ActorId) : DomainEventBody(
mapOf(
"actor" to actor
),

View File

@ -17,7 +17,9 @@
package dev.usbharu.hideout.core.domain.event.post
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
@ -25,14 +27,14 @@ class PostDomainEventFactory(private val post: Post, private val actor: Actor? =
fun createEvent(postEvent: PostEvent): DomainEvent<PostEventBody> {
return DomainEvent.create(
postEvent.eventName,
PostEventBody(post, actor)
PostEventBody(post.id, actor?.id)
)
}
}
class PostEventBody(post: Post, actor: Actor?) : DomainEventBody(mapOf("post" to post, "actor" to actor)) {
fun getPost(): Post = toMap()["post"] as Post
fun getActor(): Actor? = toMap()["actor"] as Actor?
class PostEventBody(post: PostId, actor: ActorId?) : DomainEventBody(mapOf("post" to post, "actor" to actor)) {
fun getPostId(): PostId = toMap()["post"] as PostId
fun getActorId(): ActorId? = toMap()["actor"] as ActorId?
}
enum class PostEvent(val eventName: String) {

View File

@ -1,16 +1,22 @@
package dev.usbharu.hideout.core.domain.event.timeline
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class TimelineEventFactory(private val timeline: Timeline) {
fun createEvent(timelineEvent: TimelineEvent): DomainEvent<TimelineEventBody> =
DomainEvent.create(timelineEvent.eventName, TimelineEventBody(timeline))
DomainEvent.create(timelineEvent.eventName, TimelineEventBody(timeline.id))
}
class TimelineEventBody(timeline: Timeline) : DomainEventBody(mapOf("timeline" to timeline))
class TimelineEventBody(timelineId: TimelineId) : DomainEventBody(mapOf("timeline" to timelineId)) {
fun getTimelineId(): TimelineId {
return toMap()["timeline"] as TimelineId
}
}
enum class TimelineEvent(val eventName: String) {
CHANGE_VISIBILITY("ChangeVisibility")
CHANGE_VISIBILITY("ChangeVisibility"),
CREATE("TimelineCreate")
}

View File

@ -0,0 +1,29 @@
package dev.usbharu.hideout.core.domain.event.userdetail
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class UserDetailDomainEventFactory(private val userDetail: UserDetail) {
fun createEvent(userDetailEvent: UserDetailEvent): DomainEvent<UserDetailEventBody> {
return DomainEvent.create(
userDetailEvent.eventName,
UserDetailEventBody(userDetail.id)
)
}
}
class UserDetailEventBody(userDetail: UserDetailId) : DomainEventBody(
mapOf(
"userDetail" to userDetail
)
) {
fun getUserDetail(): UserDetailId {
return toMap()["userDetail"] as UserDetailId
}
}
enum class UserDetailEvent(val eventName: String) {
CREATE("UserDetailCreate"),
}

View File

@ -25,4 +25,24 @@ class Timeline(
var name = name
private set
companion object {
fun create(
id: TimelineId,
userDetailId: UserDetailId,
name: TimelineName,
visibility: TimelineVisibility,
isSystem: Boolean
): Timeline {
val timeline = Timeline(
id = id,
userDetailId = userDetailId,
name = name,
visibility = visibility,
isSystem = isSystem
)
timeline.addDomainEvent(TimelineEventFactory(timeline).createEvent(TimelineEvent.CREATE))
return timeline
}
}
}

View File

@ -16,18 +16,21 @@
package dev.usbharu.hideout.core.domain.model.userdetails
import dev.usbharu.hideout.core.domain.event.userdetail.UserDetailDomainEventFactory
import dev.usbharu.hideout.core.domain.event.userdetail.UserDetailEvent
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable
import java.time.Instant
class UserDetail private constructor(
class UserDetail(
val id: UserDetailId,
val actorId: ActorId,
var password: UserDetailHashedPassword,
var autoAcceptFolloweeFollowRequest: Boolean,
var lastMigration: Instant? = null,
val homeTimelineId: TimelineId?
) {
var homeTimelineId: TimelineId?
) : DomainEventStorable() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -49,7 +52,7 @@ class UserDetail private constructor(
lastMigration: Instant? = null,
homeTimelineId: TimelineId? = null
): UserDetail {
return UserDetail(
val userDetail = UserDetail(
id,
actorId,
password,
@ -57,6 +60,8 @@ class UserDetail private constructor(
lastMigration,
homeTimelineId
)
userDetail.addDomainEvent(UserDetailDomainEventFactory(userDetail).createEvent(UserDetailEvent.CREATE))
return userDetail
}
}
}

View File

@ -2,12 +2,16 @@ package dev.usbharu.hideout.core.domain.shared.repository
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.springframework.stereotype.Repository
@Repository
interface DomainEventPublishableRepository<T : DomainEventStorable> {
val domainEventPublisher: DomainEventPublisher
suspend fun update(entity: T) {
println(entity.getDomainEvents().joinToString())
val current = TransactionManager.current()
current.registerInterceptor()
entity.getDomainEvents().distinctBy {
if (it.collectable) {
it.name

View File

@ -22,6 +22,8 @@ import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher
import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.javatime.timestamp
@ -30,11 +32,13 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
@Repository
class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() {
class UserDetailRepositoryImpl(override val domainEventPublisher: DomainEventPublisher) : UserDetailRepository,
AbstractRepository(), DomainEventPublishableRepository<UserDetail> {
override val logger: Logger
get() = Companion.logger
override suspend fun save(userDetail: UserDetail): UserDetail = query {
override suspend fun save(userDetail: UserDetail): UserDetail {
val userDetail1 = query {
val singleOrNull =
UserDetails.selectAll().where { UserDetails.id eq userDetail.id.id }.forUpdate().singleOrNull()
if (singleOrNull == null) {
@ -53,12 +57,19 @@ class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() {
it[lastMigration] = userDetail.lastMigration
}
}
return@query userDetail
userDetail
}
update(userDetail)
return userDetail1
}
override suspend fun delete(userDetail: UserDetail): Unit = query {
override suspend fun delete(userDetail: UserDetail): Unit {
query {
UserDetails.deleteWhere { id eq userDetail.id.id }
}
update(userDetail)
}
override suspend fun findByActorId(actorId: Long): UserDetail? = query {
return@query UserDetails
@ -89,7 +100,7 @@ class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() {
}
}
private fun userDetail(it: ResultRow) = UserDetail.create(
private fun userDetail(it: ResultRow) = UserDetail(
UserDetailId(it[UserDetails.id]),
ActorId(it[UserDetails.actorId]),
UserDetailHashedPassword(it[UserDetails.password]),

View File

@ -18,6 +18,7 @@ package dev.usbharu.hideout.core.infrastructure.springframework.domainevent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component
@ -25,6 +26,11 @@ import org.springframework.stereotype.Component
class SpringFrameworkDomainEventPublisher(private val applicationEventPublisher: ApplicationEventPublisher) :
DomainEventPublisher {
override suspend fun publishEvent(domainEvent: DomainEvent<*>) {
logger.trace("Publish ${domainEvent.id} ${domainEvent.name}")
applicationEventPublisher.publishEvent(domainEvent)
}
companion object {
private val logger = LoggerFactory.getLogger(SpringFrameworkDomainEventPublisher::class.java)
}
}

View File

@ -4,6 +4,7 @@ import dev.usbharu.hideout.core.application.domainevent.subscribers.DomainEventC
import dev.usbharu.hideout.core.application.domainevent.subscribers.DomainEventSubscriber
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
import org.slf4j.LoggerFactory
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
@ -18,11 +19,17 @@ class SpringFrameworkDomainEventSubscriber : DomainEventSubscriber {
@EventListener
suspend fun onDomainEventPublished(domainEvent: DomainEvent<*>) {
logger.trace("Domain Event Published: $domainEvent")
map[domainEvent.name]?.forEach {
try {
it.invoke(domainEvent)
} catch (e: Exception) {
logger.error("", e)
}
}
}
companion object {
private val logger = LoggerFactory.getLogger(SpringFrameworkDomainEventSubscriber::class.java)
}
}