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

@ -67,3 +67,4 @@ tasks.register("run") {
springBoot { springBoot {
mainClass = "dev.usbharu.hideout.SpringApplicationKt" 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.owl.broker)
implementation(libs.bundles.spring.boot.oauth2) implementation(libs.bundles.spring.boot.oauth2)
implementation(libs.bundles.spring.boot.data.mongodb) 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-actuator")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")

View File

@ -63,6 +63,9 @@ class RegisterLocalActorApplicationService(
id = UserDetailId(idGenerateService.generateId()), id = UserDetailId(idGenerateService.generateId()),
actorId = actor.id, actorId = actor.id,
password = userDetailDomainService.hashPassword(command.password), password = userDetailDomainService.hashPassword(command.password),
autoAcceptFolloweeFollowRequest = false,
lastMigration = null,
homeTimelineId = null
) )
userDetailRepository.save(userDetail) userDetailRepository.save(userDetail)
return actor.url 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 { override suspend fun internalExecute(command: RegisterLocalPost, executor: CommandExecutor): Long {
val actorId = ( val actorId = (
userDetailRepository.findById(command.userDetailId) userDetailRepository.findById(command.userDetailId)
?: throw IllegalStateException("actor not found") ?: throw IllegalStateException("actor not found")
).actorId ).actorId
val actor = actorRepository.findById(actorId)!! val actor = actorRepository.findById(actorId)!!

View File

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

View File

@ -23,3 +23,8 @@ interface CommandExecutor {
interface UserDetailGettableCommandExecutor : CommandExecutor { interface UserDetailGettableCommandExecutor : CommandExecutor {
val userDetailId: Long 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 import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class ActorDomainEventFactory(private val actor: Actor) { class ActorDomainEventFactory(private val actor: Actor) {
fun createEvent(actorEvent: ActorEvent): DomainEvent { fun createEvent(actorEvent: ActorEvent): DomainEvent<ActorEventBody> {
return DomainEvent.create( return DomainEvent.create(
actorEvent.eventName, actorEvent.eventName,
ActorEventBody(actor), 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 import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class ActorInstanceRelationshipDomainEventFactory(private val actorInstanceRelationship: ActorInstanceRelationship) { class ActorInstanceRelationshipDomainEventFactory(private val actorInstanceRelationship: ActorInstanceRelationship) {
fun createEvent(actorInstanceRelationshipEvent: ActorInstanceRelationshipEvent): DomainEvent { fun createEvent(
actorInstanceRelationshipEvent: ActorInstanceRelationshipEvent
): DomainEvent<ActorInstanceRelationshipEventBody> {
return DomainEvent.create( return DomainEvent.create(
actorInstanceRelationshipEvent.eventName, actorInstanceRelationshipEvent.eventName,
ActorInstanceRelationshipEventBody(actorInstanceRelationship) 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 import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class InstanceEventFactory(private val instance: Instance) { class InstanceEventFactory(private val instance: Instance) {
fun createEvent(event: InstanceEvent): DomainEvent { fun createEvent(event: InstanceEvent): DomainEvent<InstanceEventBody> {
return DomainEvent.create( return DomainEvent.create(
event.eventName, event.eventName,
InstanceEventBody(instance) 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 import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class PostDomainEventFactory(private val post: Post, private val actor: Actor? = null) { 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( return DomainEvent.create(
postEvent.eventName, postEvent.eventName,
PostEventBody(post, actor) 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) { enum class PostEvent(val eventName: String) {
DELETE("PostDelete"), 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 import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class RelationshipEventFactory(private val relationship: Relationship) { class RelationshipEventFactory(private val relationship: Relationship) {
fun createEvent(relationshipEvent: RelationshipEvent): DomainEvent = fun createEvent(relationshipEvent: RelationshipEvent): DomainEvent<RelationshipEventBody> =
DomainEvent.create(relationshipEvent.eventName, RelationshipEventBody(relationship)) 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) { enum class RelationshipEvent(val eventName: String) {
FOLLOW("RelationshipFollow"), 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.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId 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.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 dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable
import java.net.URI import java.net.URI
import java.time.Instant import java.time.Instant

View File

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

View File

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

View File

@ -21,4 +21,5 @@ interface ActorRepository {
suspend fun delete(actor: Actor) suspend fun delete(actor: Actor)
suspend fun findById(id: ActorId): Actor? suspend fun findById(id: ActorId): Actor?
suspend fun findByNameAndDomain(name: String, domain: String): 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, LOCAL,
MODERATOR, MODERATOR,
ADMINISTRATOR, ADMINISTRATOR,
REMOTE; REMOTE
} }

View File

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

View File

@ -17,7 +17,7 @@
package dev.usbharu.hideout.core.domain.model.emoji package dev.usbharu.hideout.core.domain.model.emoji
import dev.usbharu.hideout.core.domain.model.instance.InstanceId 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.net.URI
import java.time.Instant import java.time.Instant

View File

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

View File

@ -1,9 +1,13 @@
package dev.usbharu.hideout.core.domain.model.filter package dev.usbharu.hideout.core.domain.model.filter
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
interface FilterRepository { interface FilterRepository {
suspend fun save(filter: Filter): Filter suspend fun save(filter: Filter): Filter
suspend fun delete(filter: Filter) suspend fun delete(filter: Filter)
suspend fun findByFilterKeywordId(filterKeywordId: FilterKeywordId): Filter? suspend fun findByFilterKeywordId(filterKeywordId: FilterKeywordId): Filter?
suspend fun findByFilterId(filterId: FilterId): 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 { override fun toString(): String {
return "Media(" + return "Media(" +
"id=$id, " + "id=$id, " +
"name=$name, " + "name=$name, " +
"url=$url, " + "url=$url, " +
"remoteUrl=$remoteUrl, " + "remoteUrl=$remoteUrl, " +
"thumbnailUrl=$thumbnailUrl, " + "thumbnailUrl=$thumbnailUrl, " +
"type=$type, " + "type=$type, " +
"mimeType=$mimeType, " + "mimeType=$mimeType, " +
"blurHash=$blurHash, " + "blurHash=$blurHash, " +
"description=$description" + "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.ActorId
import dev.usbharu.hideout.core.domain.model.actor.Role 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.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.media.MediaId
import dev.usbharu.hideout.core.domain.model.post.Post.Companion.Action.* import dev.usbharu.hideout.core.domain.model.post.Post.Companion.Action.*
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventStorable
@ -32,6 +33,7 @@ import java.time.Instant
class Post( class Post(
val id: PostId, val id: PostId,
actorId: ActorId, actorId: ActorId,
val instanceId: InstanceId,
overview: PostOverview?, overview: PostOverview?,
content: PostContent, content: PostContent,
val createdAt: Instant, val createdAt: Instant,
@ -227,6 +229,7 @@ class Post(
return Post( return Post(
id = id, id = id,
actorId = actorId, actorId = actorId,
instanceId = instanceId,
overview = overview, overview = overview,
content = PostContent(this.content.text, this.content.content, emojis), content = PostContent(this.content.text, this.content.content, emojis),
createdAt = createdAt, 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 { companion object {
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun create( fun create(
id: PostId, id: PostId,
actorId: ActorId, actorId: ActorId,
instanceId: InstanceId,
overview: PostOverview? = null, overview: PostOverview? = null,
content: PostContent, content: PostContent,
createdAt: Instant, createdAt: Instant,
@ -277,6 +304,7 @@ class Post(
val post = Post( val post = Post(
id = id, id = id,
actorId = actorId, actorId = actorId,
instanceId = instanceId,
overview = overview, overview = overview,
content = content, content = content,
createdAt = createdAt, createdAt = createdAt,

View File

@ -17,11 +17,19 @@
package dev.usbharu.hideout.core.domain.model.post package dev.usbharu.hideout.core.domain.model.post
import dev.usbharu.hideout.core.domain.model.actor.ActorId 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 { interface PostRepository {
suspend fun save(post: Post): Post suspend fun save(post: Post): Post
suspend fun saveAll(posts: List<Post>): List<Post> suspend fun saveAll(posts: List<Post>): List<Post>
suspend fun findById(id: PostId): 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 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 save(relationship: Relationship): Relationship
suspend fun delete(relationship: Relationship) suspend fun delete(relationship: Relationship)
suspend fun findByActorIdAndTargetId(actorId: ActorId, targetId: ActorId): 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. * limitations under the License.
*/ */
package dev.usbharu.hideout.core.domain.model.shared package dev.usbharu.hideout.core.domain.model.support.domain
@JvmInline @JvmInline
value class Domain(val domain: String) { 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 package dev.usbharu.hideout.core.domain.model.userdetails
import dev.usbharu.hideout.core.domain.model.actor.ActorId import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import java.time.Instant import java.time.Instant
class UserDetail private constructor( class UserDetail private constructor(
@ -25,6 +26,7 @@ class UserDetail private constructor(
var password: UserDetailHashedPassword, var password: UserDetailHashedPassword,
var autoAcceptFolloweeFollowRequest: Boolean, var autoAcceptFolloweeFollowRequest: Boolean,
var lastMigration: Instant? = null, var lastMigration: Instant? = null,
val homeTimelineId: TimelineId?
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -45,13 +47,15 @@ class UserDetail private constructor(
password: UserDetailHashedPassword, password: UserDetailHashedPassword,
autoAcceptFolloweeFollowRequest: Boolean = false, autoAcceptFolloweeFollowRequest: Boolean = false,
lastMigration: Instant? = null, lastMigration: Instant? = null,
homeTimelineId: TimelineId? = null
): UserDetail { ): UserDetail {
return UserDetail( return UserDetail(
id, id,
actorId, actorId,
password, password,
autoAcceptFolloweeFollowRequest, autoAcceptFolloweeFollowRequest,
lastMigration lastMigration,
homeTimelineId
) )
} }
} }

View File

@ -21,4 +21,5 @@ interface UserDetailRepository {
suspend fun delete(userDetail: UserDetail) suspend fun delete(userDetail: UserDetail)
suspend fun findByActorId(actorId: Long): UserDetail? suspend fun findByActorId(actorId: Long): UserDetail?
suspend fun findById(id: 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 body ドメインイベントのボディ
* @property collectable trueで同じドメインイベント名でをまとめる * @property collectable trueで同じドメインイベント名でをまとめる
*/ */
data class DomainEvent( data class DomainEvent<out T : DomainEventBody>(
val id: String, val id: String,
val name: String, val name: String,
val occurredOn: Instant, val occurredOn: Instant,
val body: DomainEventBody, val body: T,
val collectable: Boolean = false val collectable: Boolean = false
) { ) {
companion object { companion object {
fun create(name: String, body: DomainEventBody, collectable: Boolean = false): DomainEvent = fun <T : DomainEventBody> create(name: String, body: T, collectable: Boolean = false): DomainEvent<T> =
DomainEvent(UUID.randomUUID().toString(), name, Instant.now(), body, collectable) DomainEvent<T>(UUID.randomUUID().toString(), name, Instant.now(), body, collectable)
fun reconstruct( fun <T : DomainEventBody> reconstruct(
id: String, id: String,
name: String, name: String,
occurredOn: Instant, occurredOn: Instant,
body: DomainEventBody, body: T,
collectable: Boolean 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 package dev.usbharu.hideout.core.domain.shared.domainevent
@Suppress("UnnecessaryAbstractClass") @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 fun toMap(): Map<String, Any?> = map
} }

View File

@ -1,5 +1,5 @@
package dev.usbharu.hideout.core.domain.shared.domainevent package dev.usbharu.hideout.core.domain.shared.domainevent
interface DomainEventPublisher { 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") @Suppress("UnnecessaryAbstractClass")
abstract class DomainEventStorable { 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) domainEvents.add(domainEvent)
} }
fun clearDomainEvents() = domainEvents.clear() 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.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId 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.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 dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.springframework.stereotype.Component import org.springframework.stereotype.Component

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.* 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.domainevent.DomainEventPublisher
import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository
import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper 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 { companion object {
private val logger = LoggerFactory.getLogger(ExposedActorRepository::class.java) 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.FilterId
import dev.usbharu.hideout.core.domain.model.filter.FilterKeywordId 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.filter.FilterRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
@ -72,6 +73,10 @@ class ExposedFilterRepository(private val filterQueryMapper: QueryMapper<Filter>
return filterQueryMapper.map(where).firstOrNull() 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 { companion object {
private val logger = LoggerFactory.getLogger(ExposedFilterRepository::class.java) 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.actor.ActorId
import dev.usbharu.hideout.core.domain.model.post.* 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.domainevent.DomainEventPublisher
import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository import dev.usbharu.hideout.core.domain.shared.repository.DomainEventPublishableRepository
import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper import dev.usbharu.hideout.core.infrastructure.exposed.QueryMapper
@ -160,15 +162,30 @@ class ExposedPostRepository(
.first() .first()
} }
override suspend fun findByActorId(id: ActorId): List<Post> = query { override suspend fun findAllById(ids: List<PostId>): List<Post> {
Posts return query {
.selectAll() Posts
.where { .selectAll()
actorId eq id.id .where {
} Posts.id inList ids.map { it.id }
.let(postQueryMapper::map) }
.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) { override suspend fun delete(post: Post) {
query { query {
Posts.deleteWhere { Posts.deleteWhere {
@ -178,6 +195,25 @@ class ExposedPostRepository(
update(post) 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 { companion object {
private val logger = LoggerFactory.getLogger(ExposedPostRepository::class.java) private val logger = LoggerFactory.getLogger(ExposedPostRepository::class.java)
} }
@ -186,6 +222,7 @@ class ExposedPostRepository(
object Posts : Table("posts") { object Posts : Table("posts") {
val id = long("id") val id = long("id")
val actorId = long("actor_id").references(Actors.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 overview = varchar("overview", PostOverview.LENGTH).nullable()
val content = varchar("content", PostContent.CONTENT_LENGTH) val content = varchar("content", PostContent.CONTENT_LENGTH)
val text = varchar("text", PostContent.TEXT_LENGTH) val text = varchar("text", PostContent.TEXT_LENGTH)

View File

@ -17,6 +17,7 @@
package dev.usbharu.hideout.core.infrastructure.exposedrepository package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId 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.Relationship
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventPublisher
@ -66,11 +67,45 @@ class ExposedRelationshipRepository(override val domainEventPublisher: DomainEve
}.singleOrNull()?.toRelationships() }.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 { companion object {
private val logger = LoggerFactory.getLogger(ExposedRelationshipRepository::class.java) 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( fun ResultRow.toRelationships(): Relationship = Relationship(
actorId = ActorId(this[Relationships.actorId]), actorId = ActorId(this[Relationships.actorId]),
targetActorId = ActorId(this[Relationships.targetActorId]), 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 package dev.usbharu.hideout.core.infrastructure.exposedrepository
import dev.usbharu.hideout.core.domain.model.actor.ActorId 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.UserDetail
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailHashedPassword 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.UserDetailId
@ -64,13 +65,7 @@ class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() {
.selectAll().where { UserDetails.actorId eq actorId } .selectAll().where { UserDetails.actorId eq actorId }
.singleOrNull() .singleOrNull()
?.let { ?.let {
UserDetail.create( userDetail(it)
UserDetailId(it[UserDetails.id]),
ActorId(it[UserDetails.actorId]),
UserDetailHashedPassword(it[UserDetails.password]),
it[UserDetails.autoAcceptFolloweeFollowRequest],
it[UserDetails.lastMigration]
)
} }
} }
@ -79,16 +74,30 @@ class UserDetailRepositoryImpl : UserDetailRepository, AbstractRepository() {
.selectAll().where { UserDetails.id eq id } .selectAll().where { UserDetails.id eq id }
.singleOrNull() .singleOrNull()
?.let { ?.let {
UserDetail.create( userDetail(it)
UserDetailId(it[UserDetails.id]),
ActorId(it[UserDetails.actorId]),
UserDetailHashedPassword(it[UserDetails.password]),
it[UserDetails.autoAcceptFolloweeFollowRequest],
it[UserDetails.lastMigration]
)
} }
} }
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 { companion object {
private val logger = LoggerFactory.getLogger(UserDetailRepositoryImpl::class.java) private val logger = LoggerFactory.getLogger(UserDetailRepositoryImpl::class.java)
} }
@ -100,5 +109,6 @@ object UserDetails : Table("user_details") {
val password = varchar("password", 255) val password = varchar("password", 255)
val autoAcceptFolloweeFollowRequest = bool("auto_accept_followee_follow_request") val autoAcceptFolloweeFollowRequest = bool("auto_accept_followee_follow_request")
val lastMigration = timestamp("last_migration").nullable() val lastMigration = timestamp("last_migration").nullable()
val homeTimelineId = long("home_timeline_id").references(Timelines.id).nullable()
override val primaryKey: PrimaryKey = PrimaryKey(id) 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.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.* import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.instance.InstanceId 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 dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.net.URI import java.net.URI

View File

@ -52,6 +52,7 @@ class PostFactoryImpl(
return Post.create( return Post.create(
id = PostId(id), id = PostId(id),
actorId = actor.id, actorId = actor.id,
instanceId = actor.instance,
overview = overview, overview = overview,
content = postContentFactoryImpl.create(content), content = postContentFactoryImpl.create(content),
createdAt = Instant.now(), 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 @Component
class SpringFrameworkDomainEventPublisher(private val applicationEventPublisher: ApplicationEventPublisher) : class SpringFrameworkDomainEventPublisher(private val applicationEventPublisher: ApplicationEventPublisher) :
DomainEventPublisher { DomainEventPublisher {
override suspend fun publishEvent(domainEvent: DomainEvent) { override suspend fun publishEvent(domainEvent: DomainEvent<*>) {
applicationEventPublisher.publishEvent(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 { override fun toString(): String {
return "HideoutUserDetails(" + return "HideoutUserDetails(" +
"password='$password', " + "password='$password', " +
"username='$username', " + "username='$username', " +
"userDetailsId=$userDetailsId, " + "userDetailsId=$userDetailsId, " +
"authorities=$authorities" + "authorities=$authorities" +
")" ")"
} }
companion object { 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 default-property-inclusion: always
datasource: datasource:
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
url: "jdbc:postgresql:hideout3" url: "jdbc:postgresql:hideout"
username: "postgres" username: "postgres"
password: "" password: "password"
data: data:
mongodb: mongodb:
auto-index-creation: true auto-index-creation: true
host: localhost host: localhost
port: 27017 port: 27017
database: hideout database: hideout
# username: hideoutuser
# password: hideoutpass
servlet: servlet:
multipart: multipart:
max-file-size: 40MB max-file-size: 40MB

View File

@ -44,7 +44,7 @@ create table if not exists actors
created_at timestamp not null, created_at timestamp not null,
key_id varchar(1000) not null, key_id varchar(1000) not null,
"following" varchar(1000) null, "following" varchar(1000) null,
followers varchar(1000) null, "followers" varchar(1000) null,
"instance" bigint not null, "instance" bigint not null,
locked boolean not null, locked boolean not null,
following_count int null, following_count int null,
@ -53,9 +53,11 @@ create table if not exists actors
last_post_at timestamp null default null, last_post_at timestamp null default null,
last_update_at timestamp not null, last_update_at timestamp not null,
suspend boolean not null, suspend boolean not null,
move_to bigint null default null, "move_to" bigint null default null,
emojis varchar(3000) not null default '', emojis varchar(3000) not null default '',
deleted boolean not null default false, deleted boolean not null default false,
"icon" bigint null,
"banner" bigint null,
unique ("name", "domain"), unique ("name", "domain"),
constraint fk_actors_instance__id foreign key ("instance") references instance (id) on delete restrict on update restrict, 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 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 create table if not exists actor_alsoknownas
( (
actor_id bigint not null, "actor_id" bigint not null,
also_known_as 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__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 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, mime_type varchar(255) not null,
description varchar(4000) 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 create table if not exists posts
( (
id bigint primary key, id bigint primary key,
@ -170,13 +179,14 @@ create table if not exists relationships
unique (actor_id, target_actor_id) 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) moderation_note, created_at)
values (0, 'system', '', '', '', null, '', '', false, false, '', current_timestamp); 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, insert into actors (id, "name", "domain", screen_name, description, inbox, outbox, url, public_key, private_key,
key_id, following, followers, instance, locked, following_count, followers_count, posts_count, created_at,
last_post_at, last_update_at, suspend, move_to, emojis) 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, values (0, '', '', '', '', '', '', '', '', null, current_timestamp, '', null, null, 0, true, null, null, 0, null,
current_timestamp, false, 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.emoji.EmojiId
import dev.usbharu.hideout.core.domain.model.instance.InstanceId 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 dev.usbharu.hideout.core.infrastructure.other.TwitterSnowflakeIdGenerateService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.net.URI import java.net.URI

View File

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

View File

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

View File

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

View File

@ -19,10 +19,18 @@ repositories {
mavenCentral() mavenCentral()
} }
configurations {
all {
exclude("org.springframework.boot", "spring-boot-starter-logging")
exclude("ch.qos.logback", "logback-classic")
}
}
dependencies { dependencies {
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("org.springframework.boot:spring-boot-starter-log4j2")
implementation("dev.usbharu:hideout-core:0.0.1") implementation("dev.usbharu:hideout-core:0.0.1")
@ -75,3 +83,4 @@ sourceSets.main {
"$buildDir/generated/sources/mastodon/src/main/kotlin" "$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 dev.usbharu.hideout.mastodon.query.StatusQueryService
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.leftJoin
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.net.URI import java.net.URI
@ -120,7 +121,7 @@ class StatusQueryServiceImpl : StatusQueryService {
val map = Posts val map = Posts
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
.leftJoin(Actors) .leftJoin(Actors)
.leftJoin(Media) .leftJoin(Media,{PostsMedia.mediaId},{Media.id})
.selectAll() .selectAll()
.where { Posts.id eq id } .where { Posts.id eq id }
.groupBy { it[Posts.id] } .groupBy { it[Posts.id] }