Merge pull request #475 from usbharu/timeline

タイムラインの実装
This commit is contained in:
usbharu 2024-07-30 10:58:26 +09:00 committed by GitHub
commit b3a9be96c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
91 changed files with 1731 additions and 127 deletions

View File

@ -66,4 +66,5 @@ tasks.register("run") {
springBoot {
mainClass = "dev.usbharu.hideout.SpringApplicationKt"
}
}

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
version: "3"
services:
db:
image: postgres:16
ports:
- "5432:5432"
environment:
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "password"
POSTGRES_DB: "hideout"

View File

@ -86,7 +86,6 @@ dependencies {
implementation(libs.bundles.owl.broker)
implementation(libs.bundles.spring.boot.oauth2)
implementation(libs.bundles.spring.boot.data.mongodb)
implementation(libs.bundles.spring.boot.data.mongodb)
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")

View File

@ -63,6 +63,9 @@ class RegisterLocalActorApplicationService(
id = UserDetailId(idGenerateService.generateId()),
actorId = actor.id,
password = userDetailDomainService.hashPassword(command.password),
autoAcceptFolloweeFollowRequest = false,
lastMigration = null,
homeTimelineId = null
)
userDetailRepository.save(userDetail)
return actor.url

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
interface DomainEventSubscriber {
fun <T : DomainEventBody> subscribe(eventName: String, domainEventConsumer: DomainEventConsumer<T>)
}
typealias DomainEventConsumer<T> = suspend (DomainEvent<T>) -> Unit

View File

@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
interface Subscriber

View File

@ -0,0 +1,11 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component
@Component
class SubscriberRunner(subscribers: List<Subscriber>) : ApplicationRunner {
override fun run(args: ApplicationArguments?) {
}
}

View File

@ -0,0 +1,22 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
import dev.usbharu.hideout.core.domain.event.post.PostEvent
import dev.usbharu.hideout.core.domain.event.post.PostEventBody
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class TimelinePostCreateSubscriber(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)
}
}
companion object {
private val logger = LoggerFactory.getLogger(TimelinePostCreateSubscriber::class.java)
}
}

View File

@ -0,0 +1,57 @@
package dev.usbharu.hideout.core.application.domainevent.subscribers
import dev.usbharu.hideout.core.application.shared.DomainEventCommandExecutor
import dev.usbharu.hideout.core.application.shared.UserDetailGettableCommandExecutor
import dev.usbharu.hideout.core.application.timeline.AddTimelineRelationship
import dev.usbharu.hideout.core.application.timeline.UserAddTimelineRelationshipApplicationService
import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEvent
import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEventBody
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.Visible
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 TimelineRelationshipFollowSubscriber(
private val userAddTimelineRelationshipApplicationService: UserAddTimelineRelationshipApplicationService,
private val idGenerateService: IdGenerateService,
private val userDetailRepository: UserDetailRepository,
domainEventSubscriber: DomainEventSubscriber
) : Subscriber {
init {
domainEventSubscriber.subscribe<RelationshipEventBody>(RelationshipEvent.FOLLOW.eventName) {
val relationship = it.body.getRelationship()
val userDetail = userDetailRepository.findByActorId(relationship.actorId.id) ?: throw Exception()
if (userDetail.homeTimelineId == null) {
logger.warn("Home timeline for ${relationship.actorId} is not found")
return@subscribe
}
userAddTimelineRelationshipApplicationService.execute(
AddTimelineRelationship(
TimelineRelationship(
TimelineRelationshipId(idGenerateService.generateId()),
userDetail.homeTimelineId,
relationship.targetActorId,
Visible.FOLLOWERS
)
), DomainEventCommandExecutor("", object : UserDetailGettableCommandExecutor {
override val userDetailId: Long
get() = userDetail.id.id
override val executor: String
get() = userDetail.id.id.toString()
})
)
}
}
companion object {
private val logger = LoggerFactory.getLogger(TimelineRelationshipFollowSubscriber::class.java)
}
}

View File

@ -41,9 +41,9 @@ class RegisterLocalPostApplicationService(
override suspend fun internalExecute(command: RegisterLocalPost, executor: CommandExecutor): Long {
val actorId = (
userDetailRepository.findById(command.userDetailId)
?: throw IllegalStateException("actor not found")
).actorId
userDetailRepository.findById(command.userDetailId)
?: throw IllegalStateException("actor not found")
).actorId
val actor = actorRepository.findById(actorId)!!

View File

@ -48,14 +48,14 @@ class GetRelationshipApplicationService(
val targetId = ActorId(command.targetActorId)
val target = actorRepository.findById(targetId)!!
val relationship = (
relationshipRepository.findByActorIdAndTargetId(actor.id, targetId)
?: dev.usbharu.hideout.core.domain.model.relationship.Relationship.default(actor.id, targetId)
)
relationshipRepository.findByActorIdAndTargetId(actor.id, targetId)
?: dev.usbharu.hideout.core.domain.model.relationship.Relationship.default(actor.id, targetId)
)
val relationship1 = (
relationshipRepository.findByActorIdAndTargetId(targetId, actor.id)
?: dev.usbharu.hideout.core.domain.model.relationship.Relationship.default(targetId, actor.id)
)
relationshipRepository.findByActorIdAndTargetId(targetId, actor.id)
?: dev.usbharu.hideout.core.domain.model.relationship.Relationship.default(targetId, actor.id)
)
val actorInstanceRelationship =
actorInstanceRelationshipRepository.findByActorIdAndInstanceId(actor.id, target.instance)

View File

@ -23,3 +23,8 @@ interface CommandExecutor {
interface UserDetailGettableCommandExecutor : CommandExecutor {
val userDetailId: Long
}
data class DomainEventCommandExecutor(
override val executor: String,
val commandExecutor: CommandExecutor?
) : CommandExecutor

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
data class AddTimelineRelationship(
val timelineRelationship: TimelineRelationship
)

View File

@ -0,0 +1,26 @@
package dev.usbharu.hideout.core.application.timeline
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.CommandExecutor
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class UserAddTimelineRelationshipApplicationService(
private val timelineRelationshipRepository: TimelineRelationshipRepository,
transaction: Transaction
) :
AbstractApplicationService<AddTimelineRelationship, Unit>(
transaction, logger
) {
override suspend fun internalExecute(command: AddTimelineRelationship, executor: CommandExecutor) {
timelineRelationshipRepository.save(command.timelineRelationship)
}
companion object {
private val logger = LoggerFactory.getLogger(UserAddTimelineRelationshipApplicationService::class.java)
}
}

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.core.config
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("hideout.timeline.default")
data class DefaultTimelineStoreConfig(
val actorPostsCount: Int = 500
)

View File

@ -0,0 +1,16 @@
package dev.usbharu.hideout.core.config
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class FlywayConfig {
@Bean
fun cleanMigrateStrategy(): FlywayMigrationStrategy {
return FlywayMigrationStrategy { migrate ->
migrate.repair()
migrate.migrate()
}
}
}

View File

@ -21,7 +21,7 @@ import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class ActorDomainEventFactory(private val actor: Actor) {
fun createEvent(actorEvent: ActorEvent): DomainEvent {
fun createEvent(actorEvent: ActorEvent): DomainEvent<ActorEventBody> {
return DomainEvent.create(
actorEvent.eventName,
ActorEventBody(actor),

View File

@ -21,7 +21,9 @@ import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class ActorInstanceRelationshipDomainEventFactory(private val actorInstanceRelationship: ActorInstanceRelationship) {
fun createEvent(actorInstanceRelationshipEvent: ActorInstanceRelationshipEvent): DomainEvent {
fun createEvent(
actorInstanceRelationshipEvent: ActorInstanceRelationshipEvent
): DomainEvent<ActorInstanceRelationshipEventBody> {
return DomainEvent.create(
actorInstanceRelationshipEvent.eventName,
ActorInstanceRelationshipEventBody(actorInstanceRelationship)

View File

@ -21,7 +21,7 @@ import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class InstanceEventFactory(private val instance: Instance) {
fun createEvent(event: InstanceEvent): DomainEvent {
fun createEvent(event: InstanceEvent): DomainEvent<InstanceEventBody> {
return DomainEvent.create(
event.eventName,
InstanceEventBody(instance)

View File

@ -22,7 +22,7 @@ import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class PostDomainEventFactory(private val post: Post, private val actor: Actor? = null) {
fun createEvent(postEvent: PostEvent): DomainEvent {
fun createEvent(postEvent: PostEvent): DomainEvent<PostEventBody> {
return DomainEvent.create(
postEvent.eventName,
PostEventBody(post, actor)
@ -30,7 +30,10 @@ class PostDomainEventFactory(private val post: Post, private val actor: Actor? =
}
}
class PostEventBody(post: Post, actor: Actor?) : DomainEventBody(mapOf("post" to post, "actor" to actor))
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?
}
enum class PostEvent(val eventName: String) {
DELETE("PostDelete"),

View File

@ -21,11 +21,15 @@ import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class RelationshipEventFactory(private val relationship: Relationship) {
fun createEvent(relationshipEvent: RelationshipEvent): DomainEvent =
fun createEvent(relationshipEvent: RelationshipEvent): DomainEvent<RelationshipEventBody> =
DomainEvent.create(relationshipEvent.eventName, RelationshipEventBody(relationship))
}
class RelationshipEventBody(relationship: Relationship) : DomainEventBody(mapOf("relationship" to relationship))
class RelationshipEventBody(relationship: Relationship) : DomainEventBody(mapOf("relationship" to relationship)) {
fun getRelationship(): Relationship {
return toMap()["relationship"] as Relationship
}
}
enum class RelationshipEvent(val eventName: String) {
FOLLOW("RelationshipFollow"),

View File

@ -0,0 +1,16 @@
package dev.usbharu.hideout.core.domain.event.timeline
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
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))
}
class TimelineEventBody(timeline: Timeline) : DomainEventBody(mapOf("timeline" to timeline))
enum class TimelineEvent(val eventName: String) {
CHANGE_VISIBILITY("ChangeVisibility")
}

View File

@ -21,7 +21,7 @@ import dev.usbharu.hideout.core.domain.event.actor.ActorEvent.*
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable
import java.net.URI
import java.time.Instant

View File

@ -25,12 +25,12 @@ value class ActorPrivateKey(val privateKey: String) {
fun create(privateKey: PrivateKey): ActorPrivateKey {
return ActorPrivateKey(
"-----BEGIN PRIVATE KEY-----\n" +
Base64
.getEncoder()
.encodeToString(privateKey.encoded)
.chunked(64)
.joinToString("\n") +
"\n-----END PRIVATE KEY-----"
Base64
.getEncoder()
.encodeToString(privateKey.encoded)
.chunked(64)
.joinToString("\n") +
"\n-----END PRIVATE KEY-----"
)
}
}

View File

@ -25,12 +25,12 @@ value class ActorPublicKey(val publicKey: String) {
fun create(publicKey: PublicKey): ActorPublicKey {
return ActorPublicKey(
"-----BEGIN PUBLIC KEY-----\n" +
Base64
.getEncoder()
.encodeToString(publicKey.encoded)
.chunked(64)
.joinToString("\n") +
"\n-----END PUBLIC KEY-----"
Base64
.getEncoder()
.encodeToString(publicKey.encoded)
.chunked(64)
.joinToString("\n") +
"\n-----END PUBLIC KEY-----"
)
}
}

View File

@ -21,4 +21,5 @@ interface ActorRepository {
suspend fun delete(actor: Actor)
suspend fun findById(id: ActorId): Actor?
suspend fun findByNameAndDomain(name: String, domain: String): Actor?
suspend fun findAllById(actorIds: List<ActorId>): List<Actor>
}

View File

@ -20,5 +20,5 @@ enum class Role {
LOCAL,
MODERATOR,
ADMINISTRATOR,
REMOTE;
REMOTE
}

View File

@ -89,12 +89,12 @@ class ActorInstanceRelationship(
override fun toString(): String {
return "ActorInstanceRelationship(" +
"actorId=$actorId, " +
"instanceId=$instanceId, " +
"blocking=$blocking, " +
"muting=$muting, " +
"doNotSendPrivate=$doNotSendPrivate" +
")"
"actorId=$actorId, " +
"instanceId=$instanceId, " +
"blocking=$blocking, " +
"muting=$muting, " +
"doNotSendPrivate=$doNotSendPrivate" +
")"
}
companion object {

View File

@ -25,4 +25,4 @@ value class ApplicationName(val name: String) {
companion object {
const val LENGTH = 300
}
}
}

View File

@ -17,7 +17,7 @@
package dev.usbharu.hideout.core.domain.model.emoji
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import java.net.URI
import java.time.Instant

View File

@ -1,9 +1,7 @@
package dev.usbharu.hideout.core.domain.model.filter
class FilterName(name: String) {
val name = name.take(LENGTH)
companion object {

View File

@ -1,9 +1,13 @@
package dev.usbharu.hideout.core.domain.model.filter
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
interface FilterRepository {
suspend fun save(filter: Filter): Filter
suspend fun delete(filter: Filter)
suspend fun findByFilterKeywordId(filterKeywordId: FilterKeywordId): Filter?
suspend fun findByFilterId(filterId: FilterId): Filter?
suspend fun findByUserDetailId(userDetailId: UserDetailId): List<Filter>
}

View File

@ -0,0 +1,6 @@
package dev.usbharu.hideout.core.domain.model.followtimeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
class FollowTimeline(val userDetailId: UserDetailId, val timelineId: TimelineId)

View File

@ -0,0 +1,6 @@
package dev.usbharu.hideout.core.domain.model.followtimeline
interface FollowTimelineRepository {
suspend fun save(followTimeline: FollowTimeline): FollowTimeline
suspend fun delete(followTimeline: FollowTimeline)
}

View File

@ -37,15 +37,15 @@ class Media(
}
override fun toString(): String {
return "Media(" +
"id=$id, " +
"name=$name, " +
"url=$url, " +
"remoteUrl=$remoteUrl, " +
"thumbnailUrl=$thumbnailUrl, " +
"type=$type, " +
"mimeType=$mimeType, " +
"blurHash=$blurHash, " +
"description=$description" +
")"
"id=$id, " +
"name=$name, " +
"url=$url, " +
"remoteUrl=$remoteUrl, " +
"thumbnailUrl=$thumbnailUrl, " +
"type=$type, " +
"mimeType=$mimeType, " +
"blurHash=$blurHash, " +
"description=$description" +
")"
}
}

View File

@ -22,6 +22,7 @@ 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.actor.Role
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.post.Post.Companion.Action.*
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable
@ -32,6 +33,7 @@ import java.time.Instant
class Post(
val id: PostId,
actorId: ActorId,
val instanceId: InstanceId,
overview: PostOverview?,
content: PostContent,
val createdAt: Instant,
@ -227,6 +229,7 @@ class Post(
return Post(
id = id,
actorId = actorId,
instanceId = instanceId,
overview = overview,
content = PostContent(this.content.text, this.content.content, emojis),
createdAt = createdAt,
@ -244,11 +247,35 @@ class Post(
)
}
override fun toString(): String {
return "Post(" +
"id=$id, " +
"createdAt=$createdAt, " +
"url=$url, " +
"repostId=$repostId, " +
"replyId=$replyId, " +
"apId=$apId, " +
"actorId=$actorId, " +
"visibility=$visibility, " +
"visibleActors=$visibleActors, " +
"content=$content, " +
"overview=$overview, " +
"sensitive=$sensitive, " +
"text='$text', " +
"emojiIds=$emojiIds, " +
"mediaIds=$mediaIds, " +
"deleted=$deleted, " +
"hide=$hide, " +
"moveTo=$moveTo" +
")"
}
companion object {
@Suppress("LongParameterList")
fun create(
id: PostId,
actorId: ActorId,
instanceId: InstanceId,
overview: PostOverview? = null,
content: PostContent,
createdAt: Instant,
@ -277,6 +304,7 @@ class Post(
val post = Post(
id = id,
actorId = actorId,
instanceId = instanceId,
overview = overview,
content = content,
createdAt = createdAt,

View File

@ -17,11 +17,19 @@
package dev.usbharu.hideout.core.domain.model.post
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.support.page.Page
import dev.usbharu.hideout.core.domain.model.support.page.PaginationList
interface PostRepository {
suspend fun save(post: Post): Post
suspend fun saveAll(posts: List<Post>): List<Post>
suspend fun findById(id: PostId): Post?
suspend fun findByActorId(id: ActorId): List<Post>
suspend fun findAllById(ids: List<PostId>): List<Post>
suspend fun findByActorId(id: ActorId, page: Page? = null): PaginationList<Post, PostId>
suspend fun delete(post: Post)
suspend fun findByActorIdAndVisibilityInList(
actorId: ActorId,
visibilityList: List<Visibility>,
of: Page? = null
): PaginationList<Post, PostId>
}

View File

@ -22,4 +22,17 @@ interface RelationshipRepository {
suspend fun save(relationship: Relationship): Relationship
suspend fun delete(relationship: Relationship)
suspend fun findByActorIdAndTargetId(actorId: ActorId, targetId: ActorId): Relationship?
suspend fun findByTargetId(
targetId: ActorId,
option: FindRelationshipOption? = null,
inverseOption: FindRelationshipOption? = null
): List<Relationship>
}
data class FindRelationshipOption(
val follow: Boolean? = null,
val block: Boolean? = null,
val mute: Boolean? = null,
val followRequest: Boolean? = null,
val muteFollowRequest: Boolean? = null
)

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.shared
package dev.usbharu.hideout.core.domain.model.support.domain
@JvmInline
value class Domain(val domain: String) {

View File

@ -0,0 +1,46 @@
package dev.usbharu.hideout.core.domain.model.support.page
sealed class Page {
abstract val maxId: Long?
abstract val sinceId: Long?
abstract val minId: Long?
abstract val limit: Int?
data class PageByMaxId(
override val maxId: Long?,
override val sinceId: Long?,
override val limit: Int?
) : Page() {
override val minId: Long? = null
}
data class PageByMinId(
override val maxId: Long?,
override val minId: Long?,
override val limit: Int?
) : Page() {
override val sinceId: Long? = null
}
companion object {
fun of(
maxId: Long? = null,
sinceId: Long? = null,
minId: Long? = null,
limit: Int? = null
): Page =
if (minId != null) {
PageByMinId(
maxId,
minId,
limit
)
} else {
PageByMaxId(
maxId,
sinceId,
limit
)
}
}
}

View File

@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.domain.model.support.page
class PaginationList<T, ID>(list: List<T>, val next: ID?, val prev: ID?) : List<T> by list

View File

@ -0,0 +1,22 @@
package dev.usbharu.hideout.core.domain.model.support.postdetail
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.post.Post
data class PostDetail(
val post: Post,
val reply: Post? = null,
val repost: Post? = null,
val postActor: Actor,
val replyActor: Actor? = null,
val repostActor: Actor? = null
) {
init {
require(post.replyId == reply?.id)
require(post.repostId == repost?.id)
require(post.actorId == postActor.id)
require(reply?.actorId == replyActor?.id)
require(repost?.actorId == repostActor?.id)
}
}

View File

@ -0,0 +1,56 @@
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.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectId
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectWarnFilter
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail
import java.time.Instant
data class TimelineObjectDetail(
val id: TimelineObjectId,
val postId: PostId,
val timelineUserDetail: UserDetail,
val post: Post,
val postActor: Actor,
val replyPost: Post?,
val replyPostActor: Actor?,
val repostPost: Post?,
val repostPostActor: Actor?,
val isPureRepost: Boolean,
val lastUpdateAt: Instant,
val hasMediaInRepost: Boolean,
val warnFilter: List<TimelineObjectWarnFilter>
) {
companion object {
fun of(
timelineObject: TimelineObject,
timelineUserDetail: UserDetail,
post: Post,
postActor: Actor,
replyPost: Post?,
replyPostActor: Actor?,
repostPost: Post?,
repostPostActor: Actor?,
warnFilter: List<TimelineObjectWarnFilter>
): TimelineObjectDetail {
return TimelineObjectDetail(
timelineObject.id,
post.id,
timelineUserDetail,
post,
postActor,
replyPost,
replyPostActor,
repostPost,
repostPostActor,
timelineObject.isPureRepost,
timelineObject.lastUpdatedAt,
timelineObject.hasMediaInRepost,
warnFilter
)
}
}
}

View File

@ -0,0 +1,28 @@
package dev.usbharu.hideout.core.domain.model.timeline
import dev.usbharu.hideout.core.domain.event.timeline.TimelineEvent
import dev.usbharu.hideout.core.domain.event.timeline.TimelineEventFactory
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.DomainEventStorable
class Timeline(
val id: TimelineId,
val userDetailId: UserDetailId,
name: TimelineName,
visibility: TimelineVisibility,
val isSystem: Boolean
) : DomainEventStorable() {
var visibility = visibility
private set
fun setVisibility(visibility: TimelineVisibility, userDetail: UserDetail) {
check(isSystem.not())
require(userDetailId == userDetail.id)
this.visibility = visibility
addDomainEvent(TimelineEventFactory(this).createEvent(TimelineEvent.CHANGE_VISIBILITY))
}
var name = name
private set
}

View File

@ -0,0 +1,4 @@
package dev.usbharu.hideout.core.domain.model.timeline
@JvmInline
value class TimelineId(val value: Long)

View File

@ -0,0 +1,4 @@
package dev.usbharu.hideout.core.domain.model.timeline
@JvmInline
value class TimelineName(val value: String)

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.core.domain.model.timeline
interface TimelineRepository {
suspend fun save(timeline: Timeline): Timeline
suspend fun delete(timeline: Timeline)
suspend fun findByIds(ids: List<TimelineId>): List<Timeline>
suspend fun findById(id: TimelineId): Timeline?
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.core.domain.model.timeline
enum class TimelineVisibility {
PRIVATE,
UNLISTED,
PUBLIC
}

View File

@ -0,0 +1,140 @@
package dev.usbharu.hideout.core.domain.model.timelineobject
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.filter.FilterResult
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.PostContent
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.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import java.time.Instant
class TimelineObject(
val id: TimelineObjectId,
val userDetailId: UserDetailId,
val timelineId: TimelineId,
val postId: PostId,
val postActorId: ActorId,
val postCreatedAt: Instant,
val replyId: PostId?,
val replyActorId: ActorId?,
val repostId: PostId?,
val repostActorId: ActorId?,
visibility: Visibility,
isPureRepost: Boolean,
mediaIds: List<MediaId>,
emojiIds: List<EmojiId>,
visibleActors: List<ActorId>,
hasMediaInRepost: Boolean,
lastUpdatedAt: Instant,
var warnFilters: List<TimelineObjectWarnFilter>,
) {
var isPureRepost = isPureRepost
private set
var visibleActors = visibleActors
private set
var hasMediaInRepost = hasMediaInRepost
private set
val hasMedia
get() = mediaIds.isNotEmpty()
var lastUpdatedAt = lastUpdatedAt
private set
var visibility = visibility
private set
var mediaIds = mediaIds
private set
var emojiIds = emojiIds
private set
fun updateWith(post: Post, filterResults: List<FilterResult>) {
visibleActors = post.visibleActors.toList()
visibility = post.visibility
mediaIds = post.mediaIds.toList()
emojiIds = post.emojiIds.toList()
lastUpdatedAt = Instant.now()
isPureRepost =
post.repostId != null && post.replyId == null && post.text.isEmpty() && post.overview?.overview.isNullOrEmpty()
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) }
}
fun updateWith(post: Post, repost: Post, filterResults: List<FilterResult>) {
require(repost.id == post.repostId)
require(repostId == post.repostId)
updateWith(post, filterResults)
hasMediaInRepost = repost.mediaIds.isNotEmpty()
}
companion object {
fun create(
timelineObjectId: TimelineObjectId,
timeline: Timeline,
post: Post,
replyActorId: ActorId?,
filterResults: List<FilterResult>
): TimelineObject {
return TimelineObject(
id = timelineObjectId,
userDetailId = timeline.userDetailId,
timelineId = timeline.id,
postId = post.id,
postActorId = post.actorId,
postCreatedAt = post.createdAt,
replyId = post.replyId,
replyActorId = replyActorId,
repostId = null,
repostActorId = null,
visibility = post.visibility,
isPureRepost = true,
mediaIds = post.mediaIds,
emojiIds = post.emojiIds,
visibleActors = post.visibleActors.toList(),
hasMediaInRepost = false,
lastUpdatedAt = Instant.now(),
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) }
)
}
fun create(
timelineObjectId: TimelineObjectId,
timeline: Timeline,
post: Post,
replyActorId: ActorId?,
repost: Post,
filterResults: List<FilterResult>
): TimelineObject {
require(post.repostId == repost.id)
return TimelineObject(
id = timelineObjectId,
userDetailId = timeline.userDetailId,
timelineId = timeline.id,
postId = post.id,
postActorId = post.actorId,
postCreatedAt = post.createdAt,
replyId = post.replyId,
replyActorId = replyActorId,
repostId = repost.id,
repostActorId = repost.actorId,
visibility = post.visibility,
isPureRepost = repost.mediaIds.isEmpty() &&
repost.overview == null &&
repost.content == PostContent.empty &&
repost.replyId == null,
mediaIds = post.mediaIds,
emojiIds = post.emojiIds,
visibleActors = post.visibleActors.toList(),
hasMediaInRepost = repost.mediaIds.isNotEmpty(),
lastUpdatedAt = Instant.now(),
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) }
)
}
}
}

View File

@ -0,0 +1,4 @@
package dev.usbharu.hideout.core.domain.model.timelineobject
@JvmInline
value class TimelineObjectId(val value: Long)

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.domain.model.timelineobject
import dev.usbharu.hideout.core.domain.model.filter.FilterId
class TimelineObjectWarnFilter(val filterId: FilterId, val matchedKeyword: String)

View File

@ -0,0 +1,18 @@
package dev.usbharu.hideout.core.domain.model.timelinerelationship
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
class TimelineRelationship(
val id: TimelineRelationshipId,
val timelineId: TimelineId,
val actorId: ActorId,
val visible: Visible
)
enum class Visible {
PUBLIC,
UNLISTED,
FOLLOWERS,
DIRECT
}

View File

@ -0,0 +1,4 @@
package dev.usbharu.hideout.core.domain.model.timelinerelationship
@JvmInline
value class TimelineRelationshipId(val value: Long)

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.core.domain.model.timelinerelationship
import dev.usbharu.hideout.core.domain.model.actor.ActorId
interface TimelineRelationshipRepository {
suspend fun save(timelineRelationship: TimelineRelationship): TimelineRelationship
suspend fun delete(timelineRelationship: TimelineRelationship)
suspend fun findByActorId(actorId: ActorId): List<TimelineRelationship>
}

View File

@ -17,6 +17,7 @@
package dev.usbharu.hideout.core.domain.model.userdetails
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import java.time.Instant
class UserDetail private constructor(
@ -25,6 +26,7 @@ class UserDetail private constructor(
var password: UserDetailHashedPassword,
var autoAcceptFolloweeFollowRequest: Boolean,
var lastMigration: Instant? = null,
val homeTimelineId: TimelineId?
) {
override fun equals(other: Any?): Boolean {
@ -45,13 +47,15 @@ class UserDetail private constructor(
password: UserDetailHashedPassword,
autoAcceptFolloweeFollowRequest: Boolean = false,
lastMigration: Instant? = null,
homeTimelineId: TimelineId? = null
): UserDetail {
return UserDetail(
id,
actorId,
password,
autoAcceptFolloweeFollowRequest,
lastMigration
lastMigration,
homeTimelineId
)
}
}

View File

@ -21,4 +21,5 @@ interface UserDetailRepository {
suspend fun delete(userDetail: UserDetail)
suspend fun findByActorId(actorId: Long): UserDetail?
suspend fun findById(id: Long): UserDetail?
suspend fun findAllById(idList: List<UserDetailId>): List<UserDetail>
}

View File

@ -28,23 +28,23 @@ import java.util.*
* @property body ドメインイベントのボディ
* @property collectable trueで同じドメインイベント名でをまとめる
*/
data class DomainEvent(
data class DomainEvent<out T : DomainEventBody>(
val id: String,
val name: String,
val occurredOn: Instant,
val body: DomainEventBody,
val body: T,
val collectable: Boolean = false
) {
companion object {
fun create(name: String, body: DomainEventBody, collectable: Boolean = false): DomainEvent =
DomainEvent(UUID.randomUUID().toString(), name, Instant.now(), body, collectable)
fun <T : DomainEventBody> create(name: String, body: T, collectable: Boolean = false): DomainEvent<T> =
DomainEvent<T>(UUID.randomUUID().toString(), name, Instant.now(), body, collectable)
fun reconstruct(
fun <T : DomainEventBody> reconstruct(
id: String,
name: String,
occurredOn: Instant,
body: DomainEventBody,
body: T,
collectable: Boolean
): DomainEvent = DomainEvent(id, name, occurredOn, body, collectable)
): DomainEvent<T> = DomainEvent(id, name, occurredOn, body, collectable)
}
}

View File

@ -17,6 +17,6 @@
package dev.usbharu.hideout.core.domain.shared.domainevent
@Suppress("UnnecessaryAbstractClass")
abstract class DomainEventBody(val map: Map<String, Any?>) {
abstract class DomainEventBody(private val map: Map<String, Any?>) {
fun toMap(): Map<String, Any?> = map
}

View File

@ -1,5 +1,5 @@
package dev.usbharu.hideout.core.domain.shared.domainevent
interface DomainEventPublisher {
suspend fun publishEvent(domainEvent: DomainEvent)
suspend fun publishEvent(domainEvent: DomainEvent<*>)
}

View File

@ -18,13 +18,13 @@ package dev.usbharu.hideout.core.domain.shared.domainevent
@Suppress("UnnecessaryAbstractClass")
abstract class DomainEventStorable {
private val domainEvents: MutableList<DomainEvent> = mutableListOf()
private val domainEvents: MutableList<DomainEvent<*>> = mutableListOf()
protected fun addDomainEvent(domainEvent: DomainEvent) {
protected fun addDomainEvent(domainEvent: DomainEvent<*>) {
domainEvents.add(domainEvent)
}
fun clearDomainEvents() = domainEvents.clear()
fun getDomainEvents(): List<DomainEvent> = domainEvents.toList()
fun getDomainEvents(): List<DomainEvent<*>> = domainEvents.toList()
}

View File

@ -1,7 +0,0 @@
package dev.usbharu.hideout.core.domain.shared.domainevent
interface DomainEventSubscriber {
fun subscribe(eventName: String, domainEventConsumer: DomainEventConsumer)
}
typealias DomainEventConsumer = (DomainEvent) -> Unit

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.core.external.timeline
data class ReadTimelineOption(
val mediaOnly: Boolean = false,
val local: Boolean = false,
val remote: Boolean = false,
)

View File

@ -0,0 +1,27 @@
package dev.usbharu.hideout.core.external.timeline
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.timelineobjectdetail.TimelineObjectDetail
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
interface TimelineStore {
suspend fun addPost(post: Post)
suspend fun updatePost(post: Post)
suspend fun removePost(post: Post)
suspend fun addTimelineRelationship(timelineRelationship: TimelineRelationship)
suspend fun removeTimelineRelationship(timelineRelationship: TimelineRelationship)
suspend fun updateTimelineRelationship(timelineRelationship: TimelineRelationship)
suspend fun addTimeline(timeline: Timeline, timelineRelationshipList: List<TimelineRelationship>)
suspend fun removeTimeline(timeline: Timeline)
suspend fun readTimeline(
timeline: Timeline,
option: ReadTimelineOption? = null,
page: Page? = null
): PaginationList<TimelineObjectDetail, PostId>
}

View File

@ -20,7 +20,7 @@ import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors
import org.jetbrains.exposed.sql.ResultRow
import org.springframework.stereotype.Component

View File

@ -17,6 +17,7 @@
package dev.usbharu.hideout.core.infrastructure.exposed
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.post.*
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts
import org.jetbrains.exposed.sql.ResultRow
@ -29,6 +30,7 @@ class PostResultRowMapper : ResultRowMapper<Post> {
return Post(
id = PostId(resultRow[Posts.id]),
actorId = ActorId(resultRow[Posts.actorId]),
instanceId = InstanceId(resultRow[Posts.instanceId]),
overview = resultRow[Posts.overview]?.let { PostOverview(it) },
content = PostContent(resultRow[Posts.text], resultRow[Posts.content], emptyList()),
createdAt = resultRow[Posts.createdAt],

View File

@ -20,7 +20,7 @@ import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.javatime.CurrentTimestamp

View File

@ -54,7 +54,7 @@ class ExposedActorInstanceRelationshipRepository(override val domainEventPublish
query {
ActorInstanceRelationships.deleteWhere {
actorId eq actorInstanceRelationship.actorId.id and
(instanceId eq actorInstanceRelationship.instanceId.instanceId)
(instanceId eq actorInstanceRelationship.instanceId.instanceId)
}
}
update(actorInstanceRelationship)
@ -68,7 +68,7 @@ class ExposedActorInstanceRelationshipRepository(override val domainEventPublish
.selectAll()
.where {
ActorInstanceRelationships.actorId eq actorId.id and
(ActorInstanceRelationships.instanceId eq instanceId.instanceId)
(ActorInstanceRelationships.instanceId eq instanceId.instanceId)
}
.singleOrNull()
?.toActorInstanceRelationship()

View File

@ -1,7 +1,7 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher
import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository
import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper
@ -98,6 +98,18 @@ class ExposedActorRepository(
}
}
override suspend fun findAllById(actorIds: List<ActorId>): List<Actor> {
return query {
Actors
.leftJoin(ActorsAlsoKnownAs, onColumn = { id }, otherColumn = { actorId })
.selectAll()
.where {
Actors.id inList actorIds.map { it.id }
}
.let(actorQueryMapper::map)
}
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedActorRepository::class.java)
}

View File

@ -20,6 +20,7 @@ import dev.usbharu.hideout.core.domain.model.filter.Filter
import dev.usbharu.hideout.core.domain.model.filter.FilterId
import dev.usbharu.hideout.core.domain.model.filter.FilterKeywordId
import dev.usbharu.hideout.core.domain.model.filter.FilterRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
@ -72,6 +73,10 @@ class ExposedFilterRepository(private val filterQueryMapper: QueryMapper<Filter>
return filterQueryMapper.map(where).firstOrNull()
}
override suspend fun findByUserDetailId(userDetailId: UserDetailId): List<Filter> {
return Filters.selectAll().where { Filters.userId eq userDetailId.id }.let(filterQueryMapper::map)
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedFilterRepository::class.java)
}

View File

@ -18,6 +18,8 @@ package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.post.*
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.shared.domainevent.DomainEventPublisher
import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository
import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper
@ -160,15 +162,30 @@ class ExposedPostRepository(
.first()
}
override suspend fun findByActorId(id: ActorId): List<Post> = query {
Posts
.selectAll()
.where {
actorId eq id.id
}
.let(postQueryMapper::map)
override suspend fun findAllById(ids: List<PostId>): List<Post> {
return query {
Posts
.selectAll()
.where {
Posts.id inList ids.map { it.id }
}
.let(postQueryMapper::map)
}
}
override suspend fun findByActorId(id: ActorId, page: Page?): PaginationList<Post, PostId> = PaginationList(
query {
Posts
.selectAll()
.where {
actorId eq actorId
}
.let(postQueryMapper::map)
},
null,
null
)
override suspend fun delete(post: Post) {
query {
Posts.deleteWhere {
@ -178,6 +195,25 @@ class ExposedPostRepository(
update(post)
}
override suspend fun findByActorIdAndVisibilityInList(
actorId: ActorId,
visibilityList: List<Visibility>,
of: Page?
): PaginationList<Post, PostId> {
return PaginationList(
query {
Posts
.selectAll()
.where {
Posts.actorId eq actorId.id and (visibility inList visibilityList.map { it.name })
}
.let(postQueryMapper::map)
},
null,
null
)
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedPostRepository::class.java)
}
@ -186,6 +222,7 @@ class ExposedPostRepository(
object Posts : Table("posts") {
val id = long("id")
val actorId = long("actor_id").references(Actors.id)
val instanceId = long("instance_id").references(Instance.id)
val overview = varchar("overview", PostOverview.LENGTH).nullable()
val content = varchar("content", PostContent.CONTENT_LENGTH)
val text = varchar("text", PostContent.TEXT_LENGTH)

View File

@ -17,6 +17,7 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.relationship.FindRelationshipOption
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher
@ -66,11 +67,45 @@ class ExposedRelationshipRepository(override val domainEventPublisher: DomainEve
}.singleOrNull()?.toRelationships()
}
override suspend fun findByTargetId(
targetId: ActorId,
option: FindRelationshipOption?,
inverseOption: FindRelationshipOption?
): List<Relationship> {
val query1 = Relationships.selectAll().where { Relationships.actorId eq targetId.id }
inverseOption.apply(query1)
// todo 逆のほうがいいかも
val query = query1.alias("INV").selectAll().where {
Relationships.targetActorId eq targetId.id
}
option.apply(query)
return query.map(ResultRow::toRelationships)
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedRelationshipRepository::class.java)
}
}
fun FindRelationshipOption?.apply(query: Query) {
if (this?.follow != null) {
query.andWhere { Relationships.following eq this@apply.follow }
}
if (this?.mute != null) {
query.andWhere { Relationships.muting eq this@apply.mute }
}
if (this?.block != null) {
query.andWhere { Relationships.blocking eq this@apply.block }
}
if (this?.followRequest != null) {
query.andWhere { Relationships.followRequesting eq this@apply.followRequest }
}
if (this?.muteFollowRequest != null) {
query.andWhere { Relationships.mutingFollowRequest eq this@apply.muteFollowRequest }
}
}
fun ResultRow.toRelationships(): Relationship = Relationship(
actorId = ActorId(this[Relationships.actorId]),
targetActorId = ActorId(this[Relationships.targetActorId]),

View File

@ -0,0 +1,68 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
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 org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
@Repository
class ExposedTimelineRelationshipRepository : AbstractRepository(), TimelineRelationshipRepository {
override val logger: Logger
get() = Companion.logger
override suspend fun save(timelineRelationship: TimelineRelationship): TimelineRelationship {
query {
TimelineRelationships.insert {
it[id] = timelineRelationship.id.value
it[timelineId] = timelineRelationship.timelineId.value
it[actorId] = timelineRelationship.actorId.id
it[visible] = timelineRelationship.visible.name
}
}
return timelineRelationship
}
override suspend fun delete(timelineRelationship: TimelineRelationship) {
query {
TimelineRelationships.deleteWhere {
TimelineRelationships.id eq timelineRelationship.id.value
}
}
}
override suspend fun findByActorId(actorId: ActorId): List<TimelineRelationship> {
return query {
TimelineRelationships.selectAll().where {
TimelineRelationships.actorId eq actorId.id
}.map { it.toTimelineRelationship() }
}
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedTimelineRelationshipRepository::class.java)
}
}
fun ResultRow.toTimelineRelationship(): TimelineRelationship {
return TimelineRelationship(
TimelineRelationshipId(this[TimelineRelationships.id]),
TimelineId(this[TimelineRelationships.timelineId]),
ActorId(this[TimelineRelationships.actorId]),
Visible.valueOf(this[TimelineRelationships.visible])
)
}
object TimelineRelationships : Table("timeline_relationships") {
val id = long("id")
val timelineId = long("timeline_id").references(Timelines.id)
val actorId = long("actor_id").references(Actors.id)
val visible = varchar("visible", 100)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -0,0 +1,79 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.timeline.*
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
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.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
@Repository
class ExposedTimelineRepository(override val domainEventPublisher: DomainEventPublisher) :
TimelineRepository,
AbstractRepository(),
DomainEventPublishableRepository<Timeline> {
override suspend fun save(timeline: Timeline): Timeline {
query {
Timelines.insert {
it[id] = timeline.id.value
it[userDetailId] = timeline.userDetailId.id
it[name] = timeline.name.value
it[visibility] = timeline.visibility.name
it[isSystem] = timeline.isSystem
}
}
update(timeline)
return timeline
}
override suspend fun delete(timeline: Timeline) {
query {
Timelines.deleteWhere {
Timelines.id eq timeline.id.value
}
}
update(timeline)
}
override suspend fun findByIds(ids: List<TimelineId>): List<Timeline> {
return query {
Timelines.selectAll().where { Timelines.id inList ids.map { it.value } }.map { it.toTimeline() }
}
}
override suspend fun findById(id: TimelineId): Timeline? {
return query {
Timelines.selectAll().where { Timelines.id eq id.value }.firstOrNull()?.toTimeline()
}
}
companion object {
private val logger = LoggerFactory.getLogger(ExposedTimelineRepository::class.java.name)
}
override val logger: Logger
get() = Companion.logger
}
fun ResultRow.toTimeline(): Timeline {
return Timeline(
TimelineId(this[Timelines.id]),
UserDetailId(this[Timelines.userDetailId]),
TimelineName(this[Timelines.name]),
TimelineVisibility.valueOf(this[Timelines.visibility]),
this[Timelines.isSystem]
)
}
object Timelines : Table("timelines") {
val id = long("id")
val userDetailId = long("user_detail_id").references(UserDetails.id)
val name = varchar("name", 300)
val visibility = varchar("visibility", 100)
val isSystem = bool("is_system")
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -17,6 +17,7 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
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
@ -64,13 +65,7 @@ class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() {
.selectAll().where { UserDetails.actorId eq actorId }
.singleOrNull()
?.let {
UserDetail.create(
UserDetailId(it[UserDetails.id]),
ActorId(it[UserDetails.actorId]),
UserDetailHashedPassword(it[UserDetails.password]),
it[UserDetails.autoAcceptFolloweeFollowRequest],
it[UserDetails.lastMigration]
)
userDetail(it)
}
}
@ -79,16 +74,30 @@ class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() {
.selectAll().where { UserDetails.id eq id }
.singleOrNull()
?.let {
UserDetail.create(
UserDetailId(it[UserDetails.id]),
ActorId(it[UserDetails.actorId]),
UserDetailHashedPassword(it[UserDetails.password]),
it[UserDetails.autoAcceptFolloweeFollowRequest],
it[UserDetails.lastMigration]
)
userDetail(it)
}
}
override suspend fun findAllById(idList: List<UserDetailId>): List<UserDetail> {
return query {
UserDetails
.selectAll()
.where { UserDetails.id inList idList.map { it.id } }
.map {
userDetail(it)
}
}
}
private fun userDetail(it: ResultRow) = UserDetail.create(
UserDetailId(it[UserDetails.id]),
ActorId(it[UserDetails.actorId]),
UserDetailHashedPassword(it[UserDetails.password]),
it[UserDetails.autoAcceptFolloweeFollowRequest],
it[UserDetails.lastMigration],
it[UserDetails.homeTimelineId]?.let { it1 -> TimelineId(it1) }
)
companion object {
private val logger = LoggerFactory.getLogger(UserDetailRepositoryImpl::class.java)
}
@ -100,5 +109,6 @@ object UserDetails : Table("user_details") {
val password = varchar("password", 255)
val autoAcceptFolloweeFollowRequest = bool("auto_accept_followee_follow_request")
val lastMigration = timestamp("last_migration").nullable()
val homeTimelineId = long("home_timeline_id").references(Timelines.id).nullable()
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View File

@ -19,7 +19,7 @@ package dev.usbharu.hideout.core.infrastructure.factory
import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.springframework.stereotype.Component
import java.net.URI

View File

@ -52,6 +52,7 @@ class PostFactoryImpl(
return Post.create(
id = PostId(id),
actorId = actor.id,
instanceId = actor.instance,
overview = overview,
content = postContentFactoryImpl.create(content),
createdAt = Instant.now(),

View File

@ -0,0 +1,197 @@
package dev.usbharu.hideout.core.infrastructure.mongorepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.filter.FilterId
import dev.usbharu.hideout.core.domain.model.media.MediaId
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.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectId
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectWarnFilter
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.infrastructure.timeline.InternalTimelineObjectOption
import dev.usbharu.hideout.core.infrastructure.timeline.InternalTimelineObjectRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.data.mongodb.core.query.Query
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import org.springframework.stereotype.Repository
import java.time.Instant
@Repository
class MongoInternalTimelineObjectRepository(
private val springDataMongoTimelineObjectRepository: SpringDataMongoTimelineObjectRepository,
private val mongoTemplate: MongoTemplate
) :
InternalTimelineObjectRepository {
override suspend fun save(timelineObject: TimelineObject): TimelineObject {
springDataMongoTimelineObjectRepository.save(SpringDataMongoTimelineObject.of(timelineObject))
return timelineObject
}
override suspend fun saveAll(timelineObjectList: List<TimelineObject>): List<TimelineObject> {
springDataMongoTimelineObjectRepository.saveAll(timelineObjectList.map { SpringDataMongoTimelineObject.of(it) })
.collect()
return timelineObjectList
}
override suspend fun findByPostId(postId: PostId): List<TimelineObject> {
return springDataMongoTimelineObjectRepository.findByPostId(postId.id).map { it.toTimelineObject() }.toList()
}
override suspend fun deleteByPostId(postId: PostId) {
springDataMongoTimelineObjectRepository.deleteByPostId(postId.id)
}
override suspend fun deleteByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId) {
springDataMongoTimelineObjectRepository.deleteByTimelineIdAndPostActorId(timelineId.value, actorId.id)
}
override suspend fun deleteByTimelineId(timelineId: TimelineId) {
springDataMongoTimelineObjectRepository.deleteByTimelineId(timelineId.value)
}
override suspend fun findByTimelineId(
timelineId: TimelineId,
internalTimelineObjectOption: InternalTimelineObjectOption?,
page: Page?
): PaginationList<TimelineObject, PostId> {
val query = Query()
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)) }
} 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?.limit?.let { query.limit(it) }
val timelineObjects =
mongoTemplate.find(query, SpringDataMongoTimelineObject::class.java).map { it.toTimelineObject() }
return PaginationList(
timelineObjects,
timelineObjects.lastOrNull()?.postId,
timelineObjects.firstOrNull()?.postId
)
}
}
@Document
data class SpringDataMongoTimelineObject(
val id: Long,
val userDetailId: Long,
val timelineId: Long,
val postId: Long,
val postActorId: Long,
val postCreatedAt: Long,
val replyId: Long?,
val replyActorId: Long?,
val repostId: Long?,
val repostActorId: Long?,
val visibility: Visibility,
val isPureRepost: Boolean,
val mediaIds: List<Long>,
val emojiIds: List<Long>,
val visibleActors: List<Long>,
val hasMediaInRepost: Boolean,
val lastUpdatedAt: Long,
val warnFilters: List<SpringDataMongoTimelineObjectWarnFilter>
) {
fun toTimelineObject(): TimelineObject {
return TimelineObject(
TimelineObjectId(id),
UserDetailId(userDetailId),
TimelineId(timelineId),
PostId(postId),
ActorId(postActorId),
Instant.ofEpochSecond(postCreatedAt),
replyId?.let { PostId(it) },
replyActorId?.let { ActorId(it) },
repostId?.let { PostId(it) },
repostActorId?.let { ActorId(it) },
visibility,
isPureRepost,
mediaIds.map { MediaId(it) },
emojiIds.map { EmojiId(it) },
visibleActors.map { ActorId(it) },
hasMediaInRepost,
Instant.ofEpochSecond(lastUpdatedAt),
warnFilters.map { it.toTimelineObjectWarnFilter() }
)
}
companion object {
fun of(timelineObject: TimelineObject): SpringDataMongoTimelineObject {
return SpringDataMongoTimelineObject(
timelineObject.id.value,
timelineObject.userDetailId.id,
timelineObject.timelineId.value,
timelineObject.postId.id,
timelineObject.postActorId.id,
timelineObject.postCreatedAt.epochSecond,
timelineObject.replyId?.id,
timelineObject.replyActorId?.id,
timelineObject.repostId?.id,
timelineObject.repostActorId?.id,
timelineObject.visibility,
timelineObject.isPureRepost,
timelineObject.mediaIds.map { it.id },
timelineObject.emojiIds.map { it.emojiId },
timelineObject.visibleActors.map { it.id },
timelineObject.hasMediaInRepost,
timelineObject.lastUpdatedAt.epochSecond,
timelineObject.warnFilters.map { SpringDataMongoTimelineObjectWarnFilter.of(it) }
)
}
}
}
data class SpringDataMongoTimelineObjectWarnFilter(
val filterId: Long,
val matchedKeyword: String
) {
fun toTimelineObjectWarnFilter(): TimelineObjectWarnFilter {
return TimelineObjectWarnFilter(
FilterId(filterId),
matchedKeyword
)
}
companion object {
fun of(timelineObjectWarnFilter: TimelineObjectWarnFilter): SpringDataMongoTimelineObjectWarnFilter {
return SpringDataMongoTimelineObjectWarnFilter(
timelineObjectWarnFilter.filterId.id,
timelineObjectWarnFilter.matchedKeyword
)
}
}
}
interface SpringDataMongoTimelineObjectRepository : CoroutineCrudRepository<SpringDataMongoTimelineObject, Long> {
fun findByPostId(postId: Long): Flow<SpringDataMongoTimelineObject>
suspend fun deleteByPostId(postId: Long)
suspend fun deleteByTimelineIdAndPostActorId(timelineId: Long, postActorId: Long)
suspend fun deleteByTimelineId(timelineId: Long)
suspend fun findByTimelineId(timelineId: TimelineId): Flow<SpringDataMongoTimelineObject>
}

View File

@ -24,7 +24,7 @@ import org.springframework.stereotype.Component
@Component
class SpringFrameworkDomainEventPublisher(private val applicationEventPublisher: ApplicationEventPublisher) :
DomainEventPublisher {
override suspend fun publishEvent(domainEvent: DomainEvent) {
override suspend fun publishEvent(domainEvent: DomainEvent<*>) {
applicationEventPublisher.publishEvent(domainEvent)
}
}

View File

@ -0,0 +1,28 @@
package dev.usbharu.hideout.core.infrastructure.springframework.domainevent
import dev.usbharu.hideout.core.application.domainevent.subscribers.DomainEventConsumer
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.springframework.context.event.EventListener
import org.springframework.stereotype.Component
@Component
class SpringFrameworkDomainEventSubscriber : DomainEventSubscriber {
val map = mutableMapOf<String, MutableList<DomainEventConsumer<*>>>()
override fun <T : DomainEventBody> subscribe(eventName: String, domainEventConsumer: DomainEventConsumer<T>) {
map.getOrPut(eventName) { mutableListOf() }.add(domainEventConsumer as DomainEventConsumer<*>)
}
@EventListener
suspend fun onDomainEventPublished(domainEvent: DomainEvent<*>) {
map[domainEvent.name]?.forEach {
try {
it.invoke(domainEvent)
} catch (e: Exception) {
}
}
}
}

View File

@ -70,11 +70,11 @@ class HideoutUserDetails(
override fun toString(): String {
return "HideoutUserDetails(" +
"password='$password', " +
"username='$username', " +
"userDetailsId=$userDetailsId, " +
"authorities=$authorities" +
")"
"password='$password', " +
"username='$username', " +
"userDetailsId=$userDetailsId, " +
"authorities=$authorities" +
")"
}
companion object {

View File

@ -0,0 +1,269 @@
package dev.usbharu.hideout.core.infrastructure.timeline
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.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.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.TimelineVisibility
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectId
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObjectWarnFilter
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
import dev.usbharu.hideout.core.domain.model.timelinerelationship.Visible
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.id.IdGenerateService
import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption
import dev.usbharu.hideout.core.external.timeline.TimelineStore
import java.time.Instant
abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateService) : TimelineStore {
override suspend fun addPost(post: Post) {
val timelineList = getTimelines(post.actorId)
val repost = post.repostId?.let { getPost(it) }
val replyActorId = post.replyId?.let { getPost(it)?.actorId }
val timelineObjectList = timelineList.mapNotNull {
createTimelineObject(post, replyActorId, repost, it)
}
insertTimelineObject(timelineObjectList)
}
protected abstract suspend fun getTimelines(actorId: ActorId): List<Timeline>
protected abstract suspend fun getTimeline(timelineId: TimelineId): Timeline?
protected suspend fun createTimelineObject(
post: Post,
replyActorId: ActorId?,
repost: Post?,
timeline: Timeline
): TimelineObject? {
if (post.visibility == Visibility.DIRECT) {
return null
}
if (timeline.visibility == TimelineVisibility.PUBLIC && post.visibility != Visibility.PUBLIC) {
return null
}
if (timeline.visibility == TimelineVisibility.UNLISTED && (post.visibility != Visibility.PUBLIC || post.visibility != Visibility.UNLISTED)) {
return null
}
val filters = getFilters(timeline.userDetailId)
val applyFilters = applyFilters(post, filters)
if (repost != null) {
return TimelineObject.create(
TimelineObjectId(idGenerateService.generateId()),
timeline,
post,
replyActorId,
repost,
applyFilters.filterResults
)
}
return TimelineObject.create(
TimelineObjectId(idGenerateService.generateId()),
timeline,
post,
replyActorId,
applyFilters.filterResults
)
}
protected abstract suspend fun getFilters(userDetailId: UserDetailId): List<Filter>
protected abstract suspend fun getNewerFilters(userDetailId: UserDetailId, lastUpdateAt: Instant): List<Filter>
protected abstract suspend fun applyFilters(post: Post, filters: List<Filter>): FilteredPost
protected abstract suspend fun getPost(postId: PostId): Post?
protected abstract suspend fun insertTimelineObject(timelineObjectList: List<TimelineObject>)
protected abstract suspend fun updateTimelineObject(timelineObjectList: List<TimelineObject>)
protected abstract suspend fun getTimelineObjectByPostId(postId: PostId): List<TimelineObject>
protected abstract suspend fun removeTimelineObject(postId: PostId)
protected abstract suspend fun removeTimelineObject(timelineId: TimelineId, actorId: ActorId)
protected abstract suspend fun removeTimelineObject(timelineId: TimelineId)
protected abstract suspend fun getPostsByTimelineRelationshipList(timelineRelationshipList: List<TimelineRelationship>): List<Post>
protected abstract suspend fun getPostsByPostId(postIds: List<PostId>): List<Post>
protected abstract suspend fun getTimelineObject(
timelineId: TimelineId,
readTimelineOption: ReadTimelineOption?,
page: Page?
): PaginationList<TimelineObject, PostId>
override suspend fun updatePost(post: Post) {
val timelineObjectByPostId = getTimelineObjectByPostId(post.id)
val repost = post.repostId?.let { getPost(it) }
val timelineObjectList = if (repost != null) {
timelineObjectByPostId.map {
val filters = getFilters(it.userDetailId)
val applyFilters = applyFilters(post, filters)
it.updateWith(post, repost, applyFilters.filterResults)
it
}
} else {
timelineObjectByPostId.map {
val filters = getFilters(it.userDetailId)
val applyFilters = applyFilters(post, filters)
it.updateWith(post, applyFilters.filterResults)
it
}
}
updateTimelineObject(timelineObjectList)
}
protected abstract suspend fun getActorPost(actorId: ActorId, visibilityList: List<Visibility>): List<Post>
override suspend fun removePost(post: Post) {
removeTimelineObject(post.id)
}
override suspend fun addTimelineRelationship(timelineRelationship: TimelineRelationship) {
val visibilityList = visibilities(timelineRelationship)
val postList = getActorPost(timelineRelationship.actorId, visibilityList)
val timeline = getTimeline(timelineRelationship.timelineId) ?: return
val timelineObjects = postList.mapNotNull { post ->
val repost = post.repostId?.let { getPost(it) }
val replyActorId = post.replyId?.let { getPost(it)?.actorId }
createTimelineObject(post, replyActorId, repost, timeline)
}
insertTimelineObject(timelineObjects)
}
protected fun visibilities(timelineRelationship: TimelineRelationship): List<Visibility> {
val visibilityList = when (timelineRelationship.visible) {
Visible.PUBLIC -> {
listOf(Visibility.PUBLIC)
}
Visible.UNLISTED -> {
listOf(Visibility.PUBLIC, Visibility.UNLISTED)
}
Visible.FOLLOWERS -> {
listOf(Visibility.PUBLIC, Visibility.UNLISTED, Visibility.FOLLOWERS)
}
Visible.DIRECT -> {
listOf(Visibility.PUBLIC, Visibility.UNLISTED, Visibility.FOLLOWERS, Visibility.DIRECT)
}
}
return visibilityList
}
override suspend fun removeTimelineRelationship(timelineRelationship: TimelineRelationship) {
removeTimelineObject(timelineRelationship.timelineId, timelineRelationship.actorId)
}
override suspend fun updateTimelineRelationship(timelineRelationship: TimelineRelationship) {
removeTimelineRelationship(timelineRelationship)
addTimelineRelationship(timelineRelationship)
}
override suspend fun addTimeline(timeline: Timeline, timelineRelationshipList: List<TimelineRelationship>) {
val postList = getPostsByTimelineRelationshipList(timelineRelationshipList)
val timelineObjectList = postList.mapNotNull { post ->
val repost = post.repostId?.let { getPost(it) }
val replyActorId = post.replyId?.let { getPost(it)?.actorId }
createTimelineObject(post, replyActorId, repost, timeline)
}
insertTimelineObject(timelineObjectList)
}
override suspend fun removeTimeline(timeline: Timeline) {
removeTimelineObject(timeline.id)
}
override suspend fun readTimeline(
timeline: Timeline,
option: ReadTimelineOption?,
page: Page?
): PaginationList<TimelineObjectDetail, PostId> {
val timelineObjectList = getTimelineObject(timeline.id, option, page)
val lastUpdatedAt = timelineObjectList.minBy { it.lastUpdatedAt }.lastUpdatedAt
val newerFilters = getNewerFilters(timeline.userDetailId, lastUpdatedAt)
val posts =
getPostsByPostId(
timelineObjectList.map {
it.postId
} + timelineObjectList.mapNotNull { it.repostId } + timelineObjectList.mapNotNull { it.replyId }
)
val userDetails = getUserDetails(timelineObjectList.map { it.userDetailId })
val actors =
getActors(
timelineObjectList.map {
it.postActorId
} + timelineObjectList.mapNotNull { it.repostActorId } + timelineObjectList.mapNotNull { it.replyActorId }
)
val postMap = posts.associate { post ->
post.id to applyFilters(post, newerFilters)
}
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 reply = postMap[it.replyId]
val replyActor = actors[it.replyActorId]
val repost = postMap[it.repostId]
val repostActor = actors[it.repostActorId]
TimelineObjectDetail.of(
timelineObject = it,
timelineUserDetail = timelineUserDetail,
post = post.post,
postActor = actor,
replyPost = reply?.post,
replyPostActor = replyActor,
repostPost = repost?.post,
repostPostActor = repostActor,
warnFilter = it.warnFilters + post.filterResults.map {
TimelineObjectWarnFilter(
it.filter.id,
it.matchedKeyword
)
}
)
},
timelineObjectList.lastOrNull()?.postId,
timelineObjectList.firstOrNull()?.postId
)
}
abstract suspend fun getActors(actorIds: List<ActorId>): Map<ActorId, Actor>
abstract suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail>
}

View File

@ -0,0 +1,137 @@
package dev.usbharu.hideout.core.infrastructure.timeline
import dev.usbharu.hideout.core.config.DefaultTimelineStoreConfig
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.actor.ActorRepository
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.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.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationship
import dev.usbharu.hideout.core.domain.model.timelinerelationship.TimelineRelationshipRepository
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.shared.id.IdGenerateService
import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption
import org.springframework.stereotype.Component
import java.time.Instant
@Component
open class DefaultTimelineStore(
private val timelineRepository: TimelineRepository,
private val timelineRelationshipRepository: TimelineRelationshipRepository,
private val filterRepository: FilterRepository,
private val postRepository: PostRepository,
private val filterDomainService: FilterDomainService,
idGenerateService: IdGenerateService,
private val defaultTimelineStoreConfig: DefaultTimelineStoreConfig,
private val internalTimelineObjectRepository: InternalTimelineObjectRepository,
private val userDetailRepository: UserDetailRepository,
private val actorRepository: ActorRepository
) : AbstractTimelineStore(idGenerateService) {
override suspend fun getTimelines(actorId: ActorId): List<Timeline> {
return timelineRepository.findByIds(
timelineRelationshipRepository
.findByActorId(
actorId
).map { it.timelineId }
)
}
override suspend fun getTimeline(timelineId: TimelineId): Timeline? {
return timelineRepository.findById(timelineId)
}
override suspend fun getFilters(userDetailId: UserDetailId): List<Filter> {
return filterRepository.findByUserDetailId(userDetailId)
}
override suspend fun getNewerFilters(userDetailId: UserDetailId, lastUpdateAt: Instant): List<Filter> {
TODO("Not yet implemented")
}
override suspend fun applyFilters(post: Post, filters: List<Filter>): FilteredPost {
return filterDomainService.apply(post, FilterContext.HOME, filters)
}
override suspend fun getPost(postId: PostId): Post? {
return postRepository.findById(postId)
}
override suspend fun insertTimelineObject(timelineObjectList: List<TimelineObject>) {
internalTimelineObjectRepository.saveAll(timelineObjectList)
}
override suspend fun updateTimelineObject(timelineObjectList: List<TimelineObject>) {
internalTimelineObjectRepository.saveAll(timelineObjectList)
}
override suspend fun getTimelineObjectByPostId(postId: PostId): List<TimelineObject> {
return internalTimelineObjectRepository.findByPostId(postId)
}
override suspend fun removeTimelineObject(postId: PostId) {
internalTimelineObjectRepository.deleteByPostId(postId)
}
override suspend fun removeTimelineObject(timelineId: TimelineId, actorId: ActorId) {
internalTimelineObjectRepository.deleteByTimelineIdAndActorId(timelineId, actorId)
}
override suspend fun removeTimelineObject(timelineId: TimelineId) {
internalTimelineObjectRepository.deleteByTimelineId(timelineId)
}
override suspend fun getPostsByTimelineRelationshipList(timelineRelationshipList: List<TimelineRelationship>): List<Post> {
return timelineRelationshipList.flatMap { getActorPost(it.actorId, visibilities(it)) }
}
override suspend fun getPostsByPostId(postIds: List<PostId>): List<Post> {
return postRepository.findAllById(postIds)
}
override suspend fun getTimelineObject(
timelineId: TimelineId,
readTimelineOption: ReadTimelineOption?,
page: Page?
): PaginationList<TimelineObject, PostId> {
return internalTimelineObjectRepository.findByTimelineId(
timelineId,
InternalTimelineObjectOption(
readTimelineOption?.local,
readTimelineOption?.remote,
readTimelineOption?.mediaOnly
),
page
)
}
override suspend fun getActorPost(actorId: ActorId, visibilityList: List<Visibility>): List<Post> {
return postRepository.findByActorIdAndVisibilityInList(
actorId,
visibilityList,
Page.of(limit = defaultTimelineStoreConfig.actorPostsCount)
)
}
override suspend fun getActors(actorIds: List<ActorId>): Map<ActorId, Actor> {
return actorRepository.findAllById(actorIds).associateBy { it.id }
}
override suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail> {
return userDetailRepository.findAllById(userDetailIdList).associateBy { it.id }
}
}

View File

@ -0,0 +1,33 @@
package dev.usbharu.hideout.core.infrastructure.timeline
import dev.usbharu.hideout.core.domain.model.actor.ActorId
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.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.timelineobject.TimelineObject
interface InternalTimelineObjectRepository {
suspend fun save(timelineObject: TimelineObject): TimelineObject
suspend fun saveAll(timelineObjectList: List<TimelineObject>): List<TimelineObject>
suspend fun findByPostId(postId: PostId): List<TimelineObject>
suspend fun deleteByPostId(postId: PostId)
suspend fun deleteByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId)
suspend fun deleteByTimelineId(timelineId: TimelineId)
suspend fun findByTimelineId(
timelineId: TimelineId,
internalTimelineObjectOption: InternalTimelineObjectOption? = null,
page: Page? = null
): PaginationList<TimelineObject, PostId>
}
data class InternalTimelineObjectOption(
val localOnly: Boolean? = null,
val remoteOnly: Boolean? = null,
val mediaOnly: Boolean? = null
)

View File

@ -26,17 +26,15 @@ spring:
default-property-inclusion: always
datasource:
driver-class-name: org.postgresql.Driver
url: "jdbc:postgresql:hideout3"
url: "jdbc:postgresql:hideout"
username: "postgres"
password: ""
password: "password"
data:
mongodb:
auto-index-creation: true
host: localhost
port: 27017
database: hideout
# username: hideoutuser
# password: hideoutpass
servlet:
multipart:
max-file-size: 40MB

View File

@ -44,7 +44,7 @@ create table if not exists actors
created_at timestamp not null,
key_id varchar(1000) not null,
"following" varchar(1000) null,
followers varchar(1000) null,
"followers" varchar(1000) null,
"instance" bigint not null,
locked boolean not null,
following_count int null,
@ -53,9 +53,11 @@ create table if not exists actors
last_post_at timestamp null default null,
last_update_at timestamp not null,
suspend boolean not null,
move_to bigint null default null,
"move_to" bigint null default null,
emojis varchar(3000) not null default '',
deleted boolean not null default false,
"icon" bigint null,
"banner" bigint null,
unique ("name", "domain"),
constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict,
constraint fk_actors_actors__move_to foreign key ("move_to") references actors (id) on delete restrict on update restrict
@ -63,8 +65,8 @@ create table if not exists actors
create table if not exists actor_alsoknownas
(
actor_id bigint not null,
also_known_as bigint not null,
"actor_id" bigint not null,
"also_known_as" bigint not null,
constraint fk_actor_alsoknownas_actors__actor_id foreign key ("actor_id") references actors (id) on delete cascade on update cascade,
constraint fk_actor_alsoknownas_actors__also_known_as foreign key ("also_known_as") references actors (id) on delete cascade on update cascade
);
@ -91,6 +93,13 @@ create table if not exists media
mime_type varchar(255) not null,
description varchar(4000) null
);
alter table actors
add constraint fk_actors_media__icon foreign key ("icon") references media (id) on delete cascade on update cascade;
alter table actors
add constraint fk_actors_media__banner foreign key ("banner") references media (id) on delete cascade on update cascade;
create table if not exists posts
(
id bigint primary key,
@ -170,13 +179,14 @@ create table if not exists relationships
unique (actor_id, target_actor_id)
);
insert into instance (id, name, description, url, icon_url, shared_inbox, software, version, is_blocked, is_muted,
insert into instance (id, "name", description, url, icon_url, shared_inbox, software, version, is_blocked, is_muted,
moderation_note, created_at)
values (0, 'system', '', '', '', null, '', '', false, false, '', current_timestamp);
insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at,
key_id, following, followers, instance, locked, following_count, followers_count, posts_count,
last_post_at, last_update_at, suspend, move_to, emojis)
insert into actors (id, "name", "domain", screen_name, description, inbox, outbox, url, public_key, private_key,
created_at,
key_id, "following", "followers", "instance", locked, following_count, followers_count, posts_count,
last_post_at, last_update_at, suspend, "move_to", emojis)
values (0, '', '', '', '', '', '', '', '', null, current_timestamp, '', null, null, 0, true, null, null, 0, null,
current_timestamp, false, null, '');

View File

@ -2,7 +2,7 @@ package dev.usbharu.hideout.core.domain.model.actor
import dev.usbharu.hideout.core.domain.model.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.shared.Domain
import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService
import kotlinx.coroutines.runBlocking
import java.net.URI

View File

@ -24,7 +24,8 @@ class FilterTest {
actorId = ActorId(1),
password = UserDetailHashedPassword(""),
autoAcceptFolloweeFollowRequest = false,
lastMigration = null
lastMigration = null,
null
)
assertDoesNotThrow {

View File

@ -312,6 +312,7 @@ class PostTest {
Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
overview = null,
content = PostContent.empty,
createdAt = Instant.now(),
@ -327,7 +328,6 @@ class PostTest {
hide = false,
moveTo = null,
actor = actor
)
}
}
@ -339,6 +339,7 @@ class PostTest {
val post = Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
overview = null,
content = PostContent.empty,
createdAt = Instant.now(),
@ -366,6 +367,7 @@ class PostTest {
val post = Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
overview = null,
content = PostContent.empty,
createdAt = Instant.now(),
@ -396,6 +398,7 @@ class PostTest {
Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
overview = null,
content = PostContent.empty,
createdAt = Instant.now(),
@ -425,6 +428,7 @@ class PostTest {
Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
content = PostContent.empty,
createdAt = Instant.now(),
visibility = Visibility.PUBLIC,
@ -447,6 +451,7 @@ class PostTest {
val post = Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
content = PostContent("aaa", "aaa", emojiIds),
createdAt = Instant.now(),
visibility = Visibility.PUBLIC,
@ -472,6 +477,7 @@ class PostTest {
val post = Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
content = PostContent("aaa", "aaa", emojiIds),
createdAt = Instant.now(),
visibility = Visibility.PUBLIC,
@ -510,6 +516,7 @@ class PostTest {
val post = Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
content = PostContent("aaa", "aaa", emojiIds),
createdAt = Instant.now(),
visibility = Visibility.PUBLIC,
@ -536,6 +543,7 @@ class PostTest {
val post = Post.create(
id = PostId(1),
actorId = actor.id,
instanceId = actor.instance,
content = PostContent("aaa", "aaa", emojiIds),
createdAt = Instant.now(),
visibility = Visibility.PUBLIC,

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.core.domain.model.post
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.media.MediaId
import dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService
import kotlinx.coroutines.runBlocking
@ -13,6 +14,7 @@ object TestPostFactory {
fun create(
id: Long = generateId(),
actorId: Long = 1,
instanceId: Long = 1,
overview: String? = null,
content: String = "This is test content",
createdAt: Instant = Instant.now(),
@ -31,20 +33,21 @@ object TestPostFactory {
return Post(
PostId(id),
ActorId(actorId),
instanceId = InstanceId(instanceId),
overview = overview?.let { PostOverview(it) },
content = PostContent(content, content, emptyList()),
createdAt = createdAt,
visibility = visibility,
url = url,
repostId = repostId?.let { PostId(it) },
replyId?.let { PostId(it) },
replyId = replyId?.let { PostId(it) },
sensitive = sensitive,
apId = apId,
deleted = deleted,
mediaIds.map { MediaId(it) },
visibleActors.map { ActorId(it) }.toSet(),
mediaIds = mediaIds.map { MediaId(it) },
visibleActors = visibleActors.map { ActorId(it) }.toSet(),
hide = hide,
moveTo?.let { PostId(it) }
moveTo = moveTo?.let { PostId(it) }
)
}

View File

@ -19,10 +19,18 @@ repositories {
mavenCentral()
}
configurations {
all {
exclude("org.springframework.boot", "spring-boot-starter-logging")
exclude("ch.qos.logback", "logback-classic")
}
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("org.springframework.boot:spring-boot-starter-log4j2")
implementation("dev.usbharu:hideout-core:0.0.1")
@ -75,3 +83,4 @@ sourceSets.main {
"$buildDir/generated/sources/mastodon/src/main/kotlin"
)
}

View File

@ -28,6 +28,7 @@ import dev.usbharu.hideout.mastodon.query.StatusQuery
import dev.usbharu.hideout.mastodon.query.StatusQueryService
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.leftJoin
import org.jetbrains.exposed.sql.selectAll
import org.springframework.stereotype.Repository
import java.net.URI
@ -120,7 +121,7 @@ class StatusQueryServiceImpl : StatusQueryService {
val map = Posts
.leftJoin(PostsMedia)
.leftJoin(Actors)
.leftJoin(Media)
.leftJoin(Media,{PostsMedia.mediaId},{Media.id})
.selectAll()
.where { Posts.id eq id }
.groupBy { it[Posts.id] }