Merge pull request #547 from usbharu/timeline2

ホームタイムラインを見れるように
This commit is contained in:
usbharu 2024-08-17 20:09:26 +09:00 committed by GitHub
commit 65d8502a18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 904 additions and 127 deletions

View File

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

View File

@ -0,0 +1,24 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
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<UserDetailEventBody>(UserDetailEvent.CREATE.eventName) {
userRegisterHomeTimelineApplicationService.execute(
RegisterHomeTimeline(it.body.getUserDetail().id),
Anonymous
)
}
}
}
// todo userdetailにdomain event付けて createのイベントで反応させる タイムラインを新しく一つ作って userdetailのhometimelineに紐づけて自分自身をtimleine relationshipに入れる

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,18 +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.domain.model.support.principal.Anonymous
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class TimelinePostCreateSubscriber(domainEventSubscriber: DomainEventSubscriber) : Subscriber {
class TimelinePostCreateSubscriber(
private val timelineAddPostApplicationService: TimelineAddPostApplicationService,
domainEventSubscriber: DomainEventSubscriber,
) : Subscriber {
init {
domainEventSubscriber.subscribe<PostEventBody>(PostEvent.CREATE.eventName) {
val post = it.body.getPost()
val actor = it.body.getActor()
logger.info("New Post! : {}", post)
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,56 @@
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.timelinerelationship.TimelineRelationship
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipId
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository
import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible
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,
private val timelineRelationshipRepository: TimelineRelationshipRepository
) : 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
val timelineRelationship = TimelineRelationship(
TimelineRelationshipId(idGenerateService.generateId()),
timeline.id,
userDetail.actorId,
Visible.DIRECT
)
timelineRelationshipRepository.save(timelineRelationship)
userDetailRepository.save(userDetail)
}
companion object {
private val logger = LoggerFactory.getLogger(UserRegisterHomeTimelineApplicationService::class.java)
}
}

View File

@ -1,14 +1,12 @@
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.instance.Instance
import dev.usbharu.hideout.core.domain.model.media.Media
import java.net.URI
data class ActorDetail(
val actorId: Long,
val instanceId: Long,
val instanceName: String,
val name: String,
val domain: String,
val screenName: String,
@ -17,11 +15,10 @@ data class ActorDetail(
val icon: URI?,
) {
companion object {
fun of(actor: Actor, instance: Instance, iconMedia: Media?): ActorDetail {
fun of(actor: Actor, iconMedia: Media?): ActorDetail {
return ActorDetail(
actor.id.id,
actor.instance.instanceId,
instance.name.name,
actor.name.name,
actor.domain.domain,
actor.screenName.screenName,

View File

@ -5,8 +5,6 @@ import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.instance.Instance
import dev.usbharu.hideout.core.domain.model.instance.InstanceRepository
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
import dev.usbharu.hideout.core.domain.model.post.PostId
@ -21,7 +19,6 @@ class GetPostDetailApplicationService(
transaction: Transaction,
private val postRepository: PostRepository,
private val actorRepository: ActorRepository,
private val instanceRepository: InstanceRepository,
private val mediaRepository: MediaRepository,
private val iPostReadAccessControl: IPostReadAccessControl
) : AbstractApplicationService<GetPostDetail, PostDetail>(
@ -36,8 +33,6 @@ class GetPostDetailApplicationService(
}
val actor =
actorRepository.findById(post.actorId) ?: throw InternalServerException("Actor ${post.actorId} not found.")
val instance = instanceRepository.findById(post.instanceId)
?: throw InternalServerException("Instance ${post.instanceId} not found.")
val iconMedia = actor.icon?.let { mediaRepository.findById(it) }
@ -46,19 +41,17 @@ class GetPostDetailApplicationService(
return PostDetail.of(
post,
actor,
instance,
iconMedia,
mediaList,
post.replyId?.let { fetchChild(it, actor, instance, iconMedia, principal) },
post.repostId?.let { fetchChild(it, actor, instance, iconMedia, principal) },
post.moveTo?.let { fetchChild(it, actor, instance, iconMedia, principal) },
post.replyId?.let { fetchChild(it, actor, iconMedia, principal) },
post.repostId?.let { fetchChild(it, actor, iconMedia, principal) },
post.moveTo?.let { fetchChild(it, actor, iconMedia, principal) },
)
}
private suspend fun fetchChild(
postId: PostId,
actor: Actor,
instance: Instance,
iconMedia: Media?,
principal: Principal
): PostDetail? {
@ -68,21 +61,16 @@ class GetPostDetailApplicationService(
return null
}
val (first, second: Instance, third) = if (actor.id != post.actorId) {
Triple(
actorRepository.findById(post.actorId) ?: return null,
instanceRepository.findById(actor.instance) ?: return null,
actor.icon?.let { mediaRepository.findById(it) }
)
val (first, third) = if (actor.id != post.actorId) {
(actorRepository.findById(post.actorId) ?: return null) to actor.icon?.let { mediaRepository.findById(it) }
} else {
Triple(actor, instance, iconMedia)
actor to iconMedia
}
val mediaList = mediaRepository.findByIds(post.mediaIds)
return PostDetail.of(
post,
first,
second,
third,
mediaList
)

View File

@ -1,7 +1,6 @@
package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.instance.Instance
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.Visibility
@ -30,7 +29,6 @@ data class PostDetail(
fun of(
post: Post,
actor: Actor,
instance: Instance,
iconMedia: Media?,
mediaList: List<Media>,
reply: PostDetail? = null,
@ -39,7 +37,7 @@ data class PostDetail(
): PostDetail {
return PostDetail(
id = post.id.id,
actor = ActorDetail.of(actor, instance, iconMedia),
actor = ActorDetail.of(actor, iconMedia),
overview = post.overview?.overview,
text = post.text,
content = post.content.content,

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,11 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.domain.model.support.page.Page
data class ReadTimeline(
val timelineId: Long,
val mediaOnly: Boolean,
val localOnly: Boolean,
val remoteOnly: Boolean,
val page: Page
)

View File

@ -0,0 +1,82 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.application.post.PostDetail
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.support.page.PaginationList
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption
import dev.usbharu.hideout.core.external.timeline.TimelineStore
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class ReadTimelineApplicationService(
private val timelineStore: TimelineStore,
private val timelineRepository: TimelineRepository,
transaction: Transaction
) :
AbstractApplicationService<ReadTimeline, PaginationList<PostDetail, PostId>>(transaction, logger) {
override suspend fun internalExecute(
command: ReadTimeline,
principal: Principal
): PaginationList<PostDetail, PostId> {
val findById = timelineRepository.findById(TimelineId(command.timelineId))
?: throw IllegalArgumentException("Timeline ${command.timelineId} not found.")
val readTimelineOption = ReadTimelineOption(
command.mediaOnly,
command.localOnly,
command.remoteOnly
)
val timeline = timelineStore.readTimeline(
findById,
readTimelineOption,
command.page,
principal,
)
val postDetailList = timeline.map {
val reply = if (it.replyPost != null) {
PostDetail.of(
it.replyPost,
it.replyPostActor!!,
it.replyPostActorIconMedia,
it.replyPostMedias.orEmpty()
)
} else {
null
}
val repost = if (it.repostPost != null) {
PostDetail.of(
it.repostPost,
it.repostPostActor!!,
it.repostPostActorIconMedia,
it.repostPostMedias.orEmpty()
)
} else {
null
}
PostDetail.of(
it.post,
it.postActor,
it.postActorIconMedia,
it.postMedias,
reply,
repost
)
}
return PaginationList(postDetailList, timeline.next, timeline.prev)
}
companion object {
private val logger = LoggerFactory.getLogger(ReadTimelineApplicationService::class.java)
}
}

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,30 @@
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

@ -52,7 +52,7 @@ import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
@Configuration
@EnableWebSecurity(debug = true)
@EnableWebSecurity(debug = false)
class SecurityConfig {
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

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

@ -8,6 +8,5 @@ interface FilterRepository {
suspend fun findByFilterKeywordId(filterKeywordId: FilterKeywordId): Filter?
suspend fun findByFilterId(filterId: FilterId): Filter?
suspend fun findByUserDetailId(userDetailId: UserDetailId): List<Filter>
}

View File

@ -22,6 +22,18 @@ interface RelationshipRepository {
suspend fun save(relationship: Relationship): Relationship
suspend fun delete(relationship: Relationship)
suspend fun findByActorIdAndTargetId(actorId: ActorId, targetId: ActorId): Relationship?
suspend fun findByActorIdsAndTargetIdAndBlocking(
actorIds: List<ActorId>,
targetId: ActorId,
blocking: Boolean
): List<Relationship>
suspend fun findByActorIdAndTargetIdsAndFollowing(
actorId: ActorId,
targetIds: List<ActorId>,
following: Boolean
): List<Relationship>
suspend fun findByTargetId(
targetId: ActorId,
option: FindRelationshipOption? = null,

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject
@ -14,11 +15,17 @@ data class TimelineObjectDetail(
val postId: PostId,
val timelineUserDetail: UserDetail,
val post: Post,
val postMedias: List<Media>,
val postActor: Actor,
val postActorIconMedia: Media?,
val replyPost: Post?,
val replyPostMedias: List<Media>?,
val replyPostActor: Actor?,
val replyPostActorIconMedia: Media?,
val repostPost: Post?,
val repostPostMedias: List<Media>?,
val repostPostActor: Actor?,
val repostPostActorIconMedia: Media?,
val isPureRepost: Boolean,
val lastUpdateAt: Instant,
val hasMediaInRepost: Boolean,
@ -29,27 +36,39 @@ data class TimelineObjectDetail(
timelineObject: TimelineObject,
timelineUserDetail: UserDetail,
post: Post,
postMedias: List<Media>,
postActor: Actor,
postActorIconMedia: Media?,
replyPost: Post?,
replyPostMedias: List<Media>?,
replyPostActor: Actor?,
replyPostActorIconMedia: Media?,
repostPost: Post?,
repostPostMedias: List<Media>?,
repostPostActor: Actor?,
repostPostActorIconMedia: Media?,
warnFilter: List<TimelineObjectWarnFilter>
): TimelineObjectDetail {
return TimelineObjectDetail(
timelineObject.id,
post.id,
timelineUserDetail,
post,
postActor,
replyPost,
replyPostActor,
repostPost,
repostPostActor,
timelineObject.isPureRepost,
timelineObject.lastUpdatedAt,
timelineObject.hasMediaInRepost,
warnFilter
id = timelineObject.id,
postId = post.id,
timelineUserDetail = timelineUserDetail,
post = post,
postMedias = postMedias,
postActor = postActor,
postActorIconMedia = postActorIconMedia,
replyPost = replyPost,
replyPostMedias = replyPostMedias,
replyPostActor = replyPostActor,
replyPostActorIconMedia = replyPostActorIconMedia,
repostPost = repostPost,
repostPostMedias = repostPostMedias,
repostPostActor = repostPostActor,
repostPostActorIconMedia = repostPostActorIconMedia,
isPureRepost = timelineObject.isPureRepost,
lastUpdateAt = timelineObject.lastUpdatedAt,
hasMediaInRepost = timelineObject.hasMediaInRepost,
warnFilter = warnFilter
)
}
}

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

@ -10,6 +10,7 @@ import org.springframework.stereotype.Component
interface IPostReadAccessControl {
suspend fun isAllow(post: Post, principal: Principal): Boolean
suspend fun areAllows(postList: List<Post>, principal: Principal): List<Post>
}
@Component
@ -57,4 +58,47 @@ class DefaultPostReadAccessControl(private val relationshipRepository: Relations
// その他の場合は見れない
return false
}
override suspend fun areAllows(postList: List<Post>, principal: Principal): List<Post> {
val actorIds = postList.map { it.actorId }
val relationshipList =
relationshipRepository.findByActorIdsAndTargetIdAndBlocking(actorIds, principal.actorId, true)
.map { it.actorId }
val inverseRelationshipList =
relationshipRepository.findByActorIdAndTargetIdsAndFollowing(principal.actorId, actorIds, true)
.map { it.actorId }
fun internalAllow(post: Post): Boolean {
// ポスト主は無条件で見れる
if (post.actorId == principal.actorId) {
return true
}
if (relationshipList.contains(post.actorId)) {
return false
}
if (post.visibility == Visibility.PUBLIC || post.visibility == Visibility.UNLISTED) {
return true
}
if (principal is Anonymous) {
return false
}
if (post.visibility == Visibility.DIRECT && post.visibleActors.contains(principal.actorId)) {
return true
}
if (post.visibility == Visibility.FOLLOWERS && inverseRelationshipList.contains(principal.actorId)) {
return true
}
return false
}
return postList
.filter {
internalAllow(it)
}
}
}

View File

@ -4,6 +4,7 @@ import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.support.page.Page
import dev.usbharu.hideout.core.domain.model.support.page.PaginationList
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail.TimelineObjectDetail
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
@ -22,6 +23,7 @@ interface TimelineStore {
suspend fun readTimeline(
timeline: Timeline,
option: ReadTimelineOption? = null,
page: Page? = null
page: Page? = null,
principal: Principal
): PaginationList<TimelineObjectDetail, PostId>
}

View File

@ -17,6 +17,10 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.exception.SpringDataAccessExceptionSQLExceptionTranslator
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.slf4j.MDCContext
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.statements.StatementInterceptor
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.slf4j.Logger
import org.springframework.beans.factory.annotation.Value
@ -35,7 +39,20 @@ abstract class AbstractRepository {
@Value("\${hideout.debug.trace-query-call:false}")
private var traceQueryCall: Boolean = false
protected suspend fun <T> query(block: () -> T): T = try {
class TransactionInterceptor(private val transaction: Transaction) {
fun onComplete(block: suspend (transaction: Transaction) -> Unit) {
transaction.registerInterceptor(object : StatementInterceptor {
override fun afterCommit(transaction: Transaction) {
runBlocking(MDCContext()) {
block(transaction)
}
}
})
}
}
protected suspend fun <T> query(block: TransactionInterceptor.() -> T): T = try {
if (traceQueryCall) {
@Suppress("ThrowingExceptionsWithoutMessageOrCause")
logger.trace(
@ -49,7 +66,7 @@ ${Throwable().stackTrace.joinToString("\n")}
)
}
block.invoke()
block.invoke(TransactionInterceptor(TransactionManager.current()))
} catch (e: SQLException) {
if (traceQueryException) {
logger.trace("FAILED EXECUTE SQL", e)

View File

@ -45,8 +45,11 @@ class ExposedActorInstanceRelationshipRepository(override val domainEventPublish
it[muting] = actorInstanceRelationship.muting
it[doNotSendPrivate] = actorInstanceRelationship.doNotSendPrivate
}
onComplete {
update(actorInstanceRelationship)
}
}
update(actorInstanceRelationship)
return actorInstanceRelationship
}
@ -56,8 +59,10 @@ class ExposedActorInstanceRelationshipRepository(override val domainEventPublish
actorId eq actorInstanceRelationship.actorId.id and
(instanceId eq actorInstanceRelationship.instanceId.instanceId)
}
onComplete {
update(actorInstanceRelationship)
}
}
update(actorInstanceRelationship)
}
override suspend fun findByActorIdAndInstanceId(

View File

@ -59,8 +59,11 @@ class ExposedActorRepository(
this[ActorsAlsoKnownAs.actorId] = actor.id.id
this[ActorsAlsoKnownAs.alsoKnownAs] = it.id
}
onComplete {
update(actor)
}
}
update(actor)
return actor
}
@ -68,8 +71,10 @@ class ExposedActorRepository(
query {
Actors.deleteWhere { id eq actor.id.id }
ActorsAlsoKnownAs.deleteWhere { actorId eq actor.id.id }
onComplete {
update(actor)
}
}
update(actor)
}
override suspend fun findById(id: ActorId): Actor? {

View File

@ -59,22 +59,22 @@ class ExposedFilterRepository(private val filterQueryMapper: QueryMapper<Filter>
Filters.deleteWhere { id eq filter.id.id }
}
override suspend fun findByFilterKeywordId(filterKeywordId: FilterKeywordId): Filter? {
override suspend fun findByFilterKeywordId(filterKeywordId: FilterKeywordId): Filter? = query {
val filterId = FilterKeywords
.selectAll()
.where { FilterKeywords.id eq filterKeywordId.id }
.firstOrNull()?.get(FilterKeywords.filterId) ?: return null
.firstOrNull()?.get(FilterKeywords.filterId) ?: return@query null
val where = Filters.selectAll().where { Filters.id eq filterId }
return filterQueryMapper.map(where).firstOrNull()
return@query filterQueryMapper.map(where).firstOrNull()
}
override suspend fun findByFilterId(filterId: FilterId): Filter? {
override suspend fun findByFilterId(filterId: FilterId): Filter? = query {
val where = Filters.selectAll().where { Filters.id eq filterId.id }
return filterQueryMapper.map(where).firstOrNull()
return@query filterQueryMapper.map(where).firstOrNull()
}
override suspend fun findByUserDetailId(userDetailId: UserDetailId): List<Filter> {
return Filters.selectAll().where { Filters.userId eq userDetailId.id }.let(filterQueryMapper::map)
override suspend fun findByUserDetailId(userDetailId: UserDetailId): List<Filter> = query {
return@query Filters.selectAll().where { Filters.userId eq userDetailId.id }.let(filterQueryMapper::map)
}
companion object {

View File

@ -98,8 +98,11 @@ class ExposedPostRepository(
this[PostsVisibleActors.postId] = post.id.id
this[PostsVisibleActors.actorId] = it.id
}
onComplete {
update(post)
}
}
update(post)
return post
}
@ -148,9 +151,11 @@ class ExposedPostRepository(
this[PostsVisibleActors.postId] = it.first
this[PostsVisibleActors.actorId] = it.second
}
}
posts.forEach {
update(it)
onComplete {
posts.forEach {
update(it)
}
}
}
return posts
}
@ -195,8 +200,10 @@ class ExposedPostRepository(
Posts.deleteWhere {
id eq post.id.id
}
onComplete {
update(post)
}
}
update(post)
}
override suspend fun findByActorIdAndVisibilityInList(

View File

@ -47,8 +47,11 @@ class ExposedRelationshipRepository(override val domainEventPublisher: DomainEve
it[followRequesting] = relationship.followRequesting
it[mutingFollowRequest] = relationship.mutingFollowRequest
}
onComplete {
update(relationship)
}
}
update(relationship)
return relationship
}
@ -57,8 +60,10 @@ class ExposedRelationshipRepository(override val domainEventPublisher: DomainEve
Relationships.deleteWhere {
actorId eq relationship.actorId.id and (targetActorId eq relationship.targetActorId.id)
}
onComplete {
update(relationship)
}
}
update(relationship)
}
override suspend fun findByActorIdAndTargetId(actorId: ActorId, targetId: ActorId): Relationship? = query {
@ -67,6 +72,26 @@ class ExposedRelationshipRepository(override val domainEventPublisher: DomainEve
}.singleOrNull()?.toRelationships()
}
override suspend fun findByActorIdsAndTargetIdAndBlocking(
actorIds: List<ActorId>,
targetId: ActorId,
blocking: Boolean
): List<Relationship> = query {
Relationships.selectAll().where {
Relationships.actorId inList actorIds.map { it.id } and (Relationships.targetActorId eq targetId.id)
}.map { it.toRelationships() }
}
override suspend fun findByActorIdAndTargetIdsAndFollowing(
actorId: ActorId,
targetIds: List<ActorId>,
following: Boolean
): List<Relationship> = query {
Relationships.selectAll().where {
Relationships.actorId eq actorId.id and (Relationships.targetActorId inList targetIds.map { it.id })
}.map { it.toRelationships() }
}
override suspend fun findByTargetId(
targetId: ActorId,
option: FindRelationshipOption?,

View File

@ -24,8 +24,11 @@ class ExposedTimelineRepository(override val domainEventPublisher: DomainEventPu
it[visibility] = timeline.visibility.name
it[isSystem] = timeline.isSystem
}
onComplete {
update(timeline)
}
}
update(timeline)
return timeline
}
@ -34,8 +37,10 @@ class ExposedTimelineRepository(override val domainEventPublisher: DomainEventPu
Timelines.deleteWhere {
Timelines.id eq timeline.id.value
}
onComplete {
update(timeline)
}
}
update(timeline)
}
override suspend fun findByIds(ids: List<TimelineId>): List<Timeline> {

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,34 +32,51 @@ 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 {
val singleOrNull =
UserDetails.selectAll().where { UserDetails.id eq userDetail.id.id }.forUpdate().singleOrNull()
if (singleOrNull == null) {
UserDetails.insert {
it[id] = userDetail.id.id
it[actorId] = userDetail.actorId.id
it[password] = userDetail.password.password
it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest
it[lastMigration] = userDetail.lastMigration
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) {
UserDetails.insert {
it[id] = userDetail.id.id
it[actorId] = userDetail.actorId.id
it[password] = userDetail.password.password
it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest
it[lastMigration] = userDetail.lastMigration
it[homeTimelineId] = userDetail.homeTimelineId?.value
}
} else {
UserDetails.update({ UserDetails.id eq userDetail.id.id }) {
it[actorId] = userDetail.actorId.id
it[password] = userDetail.password.password
it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest
it[lastMigration] = userDetail.lastMigration
it[homeTimelineId] = userDetail.homeTimelineId?.value
}
}
} else {
UserDetails.update({ UserDetails.id eq userDetail.id.id }) {
it[actorId] = userDetail.actorId.id
it[password] = userDetail.password.password
it[autoAcceptFolloweeFollowRequest] = userDetail.autoAcceptFolloweeFollowRequest
it[lastMigration] = userDetail.lastMigration
onComplete {
update(userDetail)
}
userDetail
}
return@query userDetail
return userDetail1
}
override suspend fun delete(userDetail: UserDetail): Unit = query {
UserDetails.deleteWhere { id eq userDetail.id.id }
override suspend fun delete(userDetail: UserDetail) {
query {
UserDetails.deleteWhere { id eq userDetail.id.id }
onComplete {
update(userDetail)
}
}
}
override suspend fun findByActorId(actorId: Long): UserDetail? = query {
@ -89,7 +108,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

@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import org.springframework.data.annotation.Id
import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.mapping.Document
@ -61,6 +62,22 @@ class MongoInternalTimelineObjectRepository(
springDataMongoTimelineObjectRepository.deleteByTimelineId(timelineId.value)
}
override suspend fun findByTimelineIdAndPostIdGT(timelineId: TimelineId, postId: PostId): TimelineObject? {
return springDataMongoTimelineObjectRepository.findFirstByTimelineIdAndPostIdGreaterThanOrderByIdAsc(
timelineId.value,
postId.id
)
?.toTimelineObject()
}
override suspend fun findByTimelineIdAndPostIdLT(timelineId: TimelineId, postId: PostId): TimelineObject? {
return springDataMongoTimelineObjectRepository.findFirstByTimelineIdAndPostIdLessThanOrderByIdDesc(
timelineId.value,
postId.id
)
?.toTimelineObject()
}
override suspend fun findByTimelineId(
timelineId: TimelineId,
internalTimelineObjectOption: InternalTimelineObjectOption?,
@ -70,12 +87,12 @@ class MongoInternalTimelineObjectRepository(
if (page?.minId != null) {
query.with(Sort.by(Sort.Direction.ASC, "postCreatedAt"))
page.minId?.let { query.addCriteria(Criteria.where("id").gt(it)) }
page.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) }
page.minId?.let { query.addCriteria(Criteria.where("postId").gt(it)) }
page.maxId?.let { query.addCriteria(Criteria.where("postId").lt(it)) }
} else {
query.with(Sort.by(Sort.Direction.DESC, "postCreatedAt"))
page?.sinceId?.let { query.addCriteria(Criteria.where("id").gt(it)) }
page?.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) }
page?.sinceId?.let { query.addCriteria(Criteria.where("postId").gt(it)) }
page?.maxId?.let { query.addCriteria(Criteria.where("postId").lt(it)) }
}
page?.limit?.let { query.limit(it) }
@ -83,16 +100,23 @@ class MongoInternalTimelineObjectRepository(
val timelineObjects =
mongoTemplate.find(query, SpringDataMongoTimelineObject::class.java).map { it.toTimelineObject() }
val objectList = if (page?.minId != null) {
timelineObjects.reversed()
} else {
timelineObjects
}
return PaginationList(
timelineObjects,
timelineObjects.lastOrNull()?.postId,
timelineObjects.firstOrNull()?.postId
objectList,
objectList.lastOrNull()?.postId,
objectList.firstOrNull()?.postId
)
}
}
@Document
data class SpringDataMongoTimelineObject(
@Id
val id: Long,
val userDetailId: Long,
val timelineId: Long,
@ -194,4 +218,14 @@ interface SpringDataMongoTimelineObjectRepository : CoroutineCrudRepository<Spri
suspend fun deleteByTimelineId(timelineId: Long)
suspend fun findByTimelineId(timelineId: TimelineId): Flow<SpringDataMongoTimelineObject>
suspend fun findFirstByTimelineIdAndPostIdGreaterThanOrderByIdAsc(
timelineId: Long,
postId: Long
): SpringDataMongoTimelineObject?
suspend fun findFirstByTimelineIdAndPostIdLessThanOrderByIdDesc(
timelineId: Long,
postId: Long
): SpringDataMongoTimelineObject?
}

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)
}
}

View File

@ -4,11 +4,14 @@ 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.filter.Filter
import dev.usbharu.hideout.core.domain.model.filter.FilteredPost
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.support.page.Page
import dev.usbharu.hideout.core.domain.model.support.page.PaginationList
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail.TimelineObjectDetail
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
@ -105,7 +108,7 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
protected abstract suspend fun getPostsByTimelineRelationshipList(timelineRelationshipList: List<TimelineRelationship>): List<Post>
protected abstract suspend fun getPostsByPostId(postIds: List<PostId>): List<Post>
protected abstract suspend fun getPostsByPostId(postIds: List<PostId>, principal: Principal): List<Post>
protected abstract suspend fun getTimelineObject(
timelineId: TimelineId,
@ -202,12 +205,21 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
removeTimelineObject(timeline.id)
}
protected abstract suspend fun getNextPaging(
timelineId: TimelineId,
page: Page?
): PaginationList<TimelineObjectDetail, PostId>
override suspend fun readTimeline(
timeline: Timeline,
option: ReadTimelineOption?,
page: Page?
page: Page?,
principal: Principal
): PaginationList<TimelineObjectDetail, PostId> {
val timelineObjectList = getTimelineObject(timeline.id, option, page)
if (timelineObjectList.isEmpty()) {
return getNextPaging(timeline.id, page)
}
val lastUpdatedAt = timelineObjectList.minBy { it.lastUpdatedAt }.lastUpdatedAt
val newerFilters = getNewerFilters(timeline.userDetailId, lastUpdatedAt)
@ -216,7 +228,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
getPostsByPostId(
timelineObjectList.map {
it.postId
} + timelineObjectList.mapNotNull { it.repostId } + timelineObjectList.mapNotNull { it.replyId }
} + timelineObjectList.mapNotNull { it.repostId } + timelineObjectList.mapNotNull { it.replyId },
principal
)
val userDetails = getUserDetails(timelineObjectList.map { it.userDetailId })
@ -232,24 +245,35 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
post.id to applyFilters(post, newerFilters)
}
val mediaMap = getMedias(posts.flatMap { it.mediaIds } + actors.mapNotNull { it.value.icon })
return PaginationList(
timelineObjectList.mapNotNull<TimelineObject, TimelineObjectDetail> {
val timelineUserDetail = userDetails[it.userDetailId] ?: return@mapNotNull null
val actor = actors[it.postActorId] ?: return@mapNotNull null
val post = postMap[it.postId] ?: return@mapNotNull null
val postMedias = post.post.mediaIds.mapNotNull { mediaId -> mediaMap[mediaId] }
val reply = postMap[it.replyId]
val replyMedias = reply?.post?.mediaIds?.mapNotNull { mediaId -> mediaMap[mediaId] }
val replyActor = actors[it.replyActorId]
val repost = postMap[it.repostId]
val repostMedias = repost?.post?.mediaIds?.mapNotNull { mediaId -> mediaMap[mediaId] }
val repostActor = actors[it.repostActorId]
TimelineObjectDetail.of(
timelineObject = it,
timelineUserDetail = timelineUserDetail,
post = post.post,
postMedias = postMedias,
postActor = actor,
postActorIconMedia = mediaMap[actor.icon],
replyPost = reply?.post,
replyPostMedias = replyMedias,
replyPostActor = replyActor,
replyPostActorIconMedia = mediaMap[replyActor?.icon],
repostPost = repost?.post,
repostPostMedias = repostMedias,
repostPostActor = repostActor,
repostPostActorIconMedia = mediaMap[repostActor?.icon],
warnFilter = it.warnFilters + post.filterResults.map {
TimelineObjectWarnFilter(
it.filter.id,
@ -265,5 +289,7 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
protected abstract suspend fun getActors(actorIds: List<ActorId>): Map<ActorId, Actor>
protected abstract suspend fun getMedias(mediaIds: List<MediaId>): Map<MediaId, Media>
protected abstract suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail>
}

View File

@ -8,12 +8,17 @@ import dev.usbharu.hideout.core.domain.model.filter.Filter
import dev.usbharu.hideout.core.domain.model.filter.FilterContext
import dev.usbharu.hideout.core.domain.model.filter.FilterRepository
import dev.usbharu.hideout.core.domain.model.filter.FilteredPost
import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.support.page.Page
import dev.usbharu.hideout.core.domain.model.support.page.PaginationList
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail.TimelineObjectDetail
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.TimelineRepository
@ -24,6 +29,7 @@ import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import dev.usbharu.hideout.core.domain.service.filter.FilterDomainService
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption
import org.springframework.stereotype.Component
@ -40,7 +46,9 @@ open class DefaultTimelineStore(
private val defaultTimelineStoreConfig: DefaultTimelineStoreConfig,
private val internalTimelineObjectRepository: InternalTimelineObjectRepository,
private val userDetailRepository: UserDetailRepository,
private val actorRepository: ActorRepository
private val actorRepository: ActorRepository,
private val mediaRepository: MediaRepository,
private val postIPostReadAccessControl: IPostReadAccessControl
) : AbstractTimelineStore(idGenerateService) {
override suspend fun getTimelines(actorId: ActorId): List<Timeline> {
return timelineRepository.findByIds(
@ -60,7 +68,7 @@ open class DefaultTimelineStore(
}
override suspend fun getNewerFilters(userDetailId: UserDetailId, lastUpdateAt: Instant): List<Filter> {
TODO("Not yet implemented")
return filterRepository.findByUserDetailId(userDetailId)
}
override suspend fun applyFilters(post: Post, filters: List<Filter>): FilteredPost {
@ -99,8 +107,9 @@ open class DefaultTimelineStore(
return timelineRelationshipList.flatMap { getActorPost(it.actorId, visibilities(it)) }
}
override suspend fun getPostsByPostId(postIds: List<PostId>): List<Post> {
return postRepository.findAllById(postIds)
override suspend fun getPostsByPostId(postIds: List<PostId>, principal: Principal): List<Post> {
val findAllById = postRepository.findAllById(postIds)
return postIPostReadAccessControl.areAllows(findAllById, principal)
}
override suspend fun getTimelineObject(
@ -127,10 +136,36 @@ open class DefaultTimelineStore(
)
}
override suspend fun getNextPaging(
timelineId: TimelineId,
page: Page?
): PaginationList<TimelineObjectDetail, PostId> {
if (page?.maxId != null) {
return PaginationList(
emptyList(),
null,
internalTimelineObjectRepository.findByTimelineIdAndPostIdLT(timelineId, PostId(page.maxId!!))?.postId
?: PostId(0)
)
} else if (page?.minId != null) {
return PaginationList(
emptyList(),
internalTimelineObjectRepository.findByTimelineIdAndPostIdGT(timelineId, PostId(page.minId!!))?.postId
?: PostId(Long.MAX_VALUE),
null
)
}
return PaginationList(emptyList(), page?.maxId?.let { PostId(it) }, page?.minId?.let { PostId(it) })
}
override suspend fun getActors(actorIds: List<ActorId>): Map<ActorId, Actor> {
return actorRepository.findAllById(actorIds).associateBy { it.id }
}
override suspend fun getMedias(mediaIds: List<MediaId>): Map<MediaId, Media> {
return mediaRepository.findByIds(mediaIds).associateBy { it.id }
}
override suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail> {
return userDetailRepository.findAllById(userDetailIdList).associateBy { it.id }
}

View File

@ -19,6 +19,17 @@ interface InternalTimelineObjectRepository {
suspend fun deleteByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId)
suspend fun deleteByTimelineId(timelineId: TimelineId)
/**
* 指定したTimelineIdより大きく近いものを返す
*/
suspend fun findByTimelineIdAndPostIdGT(timelineId: TimelineId, postId: PostId): TimelineObject?
/**
* 指定したTimelineIdより小さく近いものを返す
*/
suspend fun findByTimelineIdAndPostIdLT(timelineId: TimelineId, postId: PostId): TimelineObject?
suspend fun findByTimelineId(
timelineId: TimelineId,
internalTimelineObjectOption: InternalTimelineObjectOption? = null,

View File

@ -0,0 +1,56 @@
package dev.usbharu.hideout.core.interfaces.web.timeline
import dev.usbharu.hideout.core.application.exception.InternalServerException
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.application.timeline.ReadTimeline
import dev.usbharu.hideout.core.application.timeline.ReadTimelineApplicationService
import dev.usbharu.hideout.core.domain.model.support.page.Page
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
@Controller
class TimelineController(
private val readTimelineApplicationService: ReadTimelineApplicationService,
private val userDetailRepository: UserDetailRepository,
private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder,
private val transaction: Transaction
) {
@GetMapping("/home")
suspend fun homeTimeline(
model: Model,
@RequestParam sinceId: String?,
@RequestParam maxId: String?,
@RequestParam minId: String?
): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
val userDetail = transaction.transaction {
userDetailRepository.findByActorId(principal.actorId.id)
?: throw InternalServerException("UserDetail not found.")
}
val homeTimelineId = userDetail.homeTimelineId!!
val execute = readTimelineApplicationService.execute(
ReadTimeline(
timelineId = homeTimelineId.value,
mediaOnly = false,
localOnly = false,
remoteOnly = false,
page = Page.of(
maxId = maxId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(),
minId = minId?.toLongOrNull(),
limit = 20
)
),
principal
)
model.addAttribute("timeline", execute)
return "homeTimeline"
}
}

View File

@ -20,6 +20,12 @@ hideout:
spring:
data:
mongodb:
auto-index-creation: true
host: localhost
port: 27017
database: hideout
jmx:
enabled: false
jackson:
@ -31,12 +37,6 @@ spring:
url: "jdbc:postgresql:hideout"
username: "postgres"
password: "password"
data:
mongodb:
auto-index-creation: true
host: localhost
port: 27017
database: hideout
servlet:
multipart:
max-file-size: 40MB

View File

@ -81,7 +81,7 @@ create table timelines
);
create table if not exists user_details
(
id bigserial primary key,
id bigint primary key,
actor_id bigint not null unique,
password varchar(255) not null,
auto_accept_followee_follow_request boolean not null,
@ -286,4 +286,34 @@ create table if not exists actor_instance_relationships
PRIMARY KEY (actor_id, instance_id),
constraint fk_actor_instance_relationships_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade,
constraint fk_actor_instance_relationships_instance_id__id foreign key (instance_id) references instance (id) on delete cascade on update cascade
);
);
create table if not exists timeline_relationships
(
id bigint primary key,
timeline_id bigint not null,
actor_id bigint not null,
visible varchar(100) not null,
constraint fk_timeline_relationships_timeline_id__id foreign key (timeline_id) references timelines (id) on delete cascade on update cascade,
constraint fk_timeline_relationships_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade
);
create table if not exists filters
(
id bigint primary key,
user_id bigint not null,
name varchar(255) not null,
context varchar(500) not null,
action varchar(255) not null,
constraint fk_filters_user_id__id foreign key (user_id) references user_details (id) on delete cascade on update cascade
);
create table if not exists filter_keywords
(
id bigint primary key,
filter_id bigint not null,
keyword varchar(1000) not null,
mode varchar(100) not null,
constraint fk_filter_keywords_filter_id__id foreign key (filter_id) references filters (id) on delete cascade on update cascade
)

View File

@ -6,12 +6,14 @@
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<Root level="DEBUG">
<AppenderRef ref="Console"/>
</Root>
<Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/>
<Logger name="org.mongodb.driver.cluster" level="WARN"/>
<!-- <Logger name="org.mongodb.driver.cluster" level=""/>-->
<Logger name="org.apache.tomcat.util.net.NioEndpoint" level="INFO"/>
<Logger name="Exposed" level="DEBUG"/>
<Logger name="sun.rmi" level="INFO"/>
<Logger name="javax.management.remote.rmi" level="INFO"/>
</Loggers>
</Configuration>

View File

@ -1,6 +1,8 @@
common.audio=\u30AA\u30FC\u30C7\u30A3\u30AA
common.audio-download-link=\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.empty=\u8868\u793A\u3059\u308B\u3082\u306E\u304C\u3042\u308A\u307E\u305B\u3093
common.media-original-link=\u30AA\u30EA\u30B8\u30CA\u30EB
common.paging-load=\u3082\u3063\u3068\u898B\u308B
common.thumbnail=\u30B5\u30E0\u30CD\u30A4\u30EB
common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F
common.video=\u52D5\u753B

View File

@ -1,6 +1,8 @@
common.audio=Audio
common.audio-download-link=Download the audio.
common.empty=Empty
common.media-original-link=original
common.paging-load=Show more
common.thumbnail=thumbnail
common.unknwon-file-type=Unknown filetype
common.video=Video

View File

@ -1,6 +1,8 @@
common.audio=\u30AA\u30FC\u30C7\u30A3\u30AA
common.audio-download-link=\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.empty=\u8868\u793A\u3059\u308B\u3082\u306E\u304C\u3042\u308A\u307E\u305B\u3093
common.media-original-link=\u30AA\u30EA\u30B8\u30CA\u30EB
common.paging-load=\u3082\u3063\u3068\u898B\u308B
common.thumbnail=\u30B5\u30E0\u30CD\u30A4\u30EB
common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F
common.video=\u52D5\u753B

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<th:block th:fragment="simple-timline(timelineObject,href)">
<!--/*@thymesVar id="timelineObject" type="dev.usbharu.hideout.core.domain.model.support.page.PaginationList<dev.usbharu.hideout.core.application.post.PostDetail,dev.usbharu.hideout.core.domain.model.post.PostId>"*/-->
<div th:if="${timelineObject.prev != null}">
<a th:href="${href + '?minId=' + timelineObject.prev.id}" th:text="#{common.paging-load}">Show more</a>
</div>
<div th:if="${timelineObject.isEmpty()}" th:text="#{common.empty}"></div>
<div th:each="postDetail : ${timelineObject}">
<th:block th:replace="~{fragments-post :: single-simple-post(${postDetail})}"></th:block>
<th:block th:replace="~{fragments-post :: single-post-controller(${postDetail})}"></th:block>
</div>
<div th:if="${timelineObject.next != null}">
<a th:href="${href + '?maxId=' + timelineObject.next.id}" th:text="#{common.paging-load}">Show more</a>
</div>
</th:block>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<noscript>
<div th:replace="fragments-timeline :: simple-timline(${timeline},'/home')"></div>
</noscript>
</body>
</html>

View File

@ -3,6 +3,7 @@ package dev.usbharu.hideout.core.infrastructure.timeline
import dev.usbharu.hideout.core.config.DefaultTimelineStoreConfig
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.filter.*
import dev.usbharu.hideout.core.domain.model.media.MediaRepository
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.post.TestPostFactory
import dev.usbharu.hideout.core.domain.model.post.Visibility
@ -16,6 +17,7 @@ import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import dev.usbharu.hideout.core.domain.service.filter.FilterDomainService
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
@ -56,6 +58,12 @@ class DefaultTimelineStoreTest {
@Mock
lateinit var actorRepository: ActorRepository
@Mock
lateinit var mediaRepository: MediaRepository
@Mock
lateinit var iPostReadAccessControl: IPostReadAccessControl
@Spy
val defaultTimelineStoreConfig = DefaultTimelineStoreConfig(500)