diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationService.kt index 6d990ee2..0d67740d 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationService.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/actor/GetUserDetailApplicationService.kt @@ -17,12 +17,11 @@ package dev.usbharu.hideout.core.application.actor import dev.usbharu.hideout.core.application.exception.InternalServerException -import dev.usbharu.hideout.core.application.shared.AbstractApplicationService +import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository -import dev.usbharu.hideout.core.domain.model.support.principal.Principal -import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId +import dev.usbharu.hideout.core.domain.model.support.principal.FromApi import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @@ -34,10 +33,10 @@ class GetUserDetailApplicationService( private val customEmojiRepository: CustomEmojiRepository, transaction: Transaction, ) : - AbstractApplicationService(transaction, Companion.logger) { - override suspend fun internalExecute(command: GetUserDetail, principal: Principal): UserDetail { - val userDetail = userDetailRepository.findById(UserDetailId(command.id)) - ?: throw IllegalArgumentException("User ${command.id} does not exist") + LocalUserAbstractApplicationService(transaction, Companion.logger) { + override suspend fun internalExecute(command: Unit, principal: FromApi): UserDetail { + val userDetail = userDetailRepository.findById(principal.userDetailId) + ?: throw IllegalArgumentException("User ${principal.userDetailId} does not exist") val actor = actorRepository.findById(userDetail.actorId) ?: throw InternalServerException("Actor ${userDetail.actorId} not found") @@ -47,6 +46,6 @@ class GetUserDetailApplicationService( } companion object { - val logger = LoggerFactory.getLogger(GetUserDetailApplicationService::class.java) + private val logger = LoggerFactory.getLogger(GetUserDetailApplicationService::class.java) } } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationService.kt index cc4ddef8..1bcd4b26 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationService.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/application/shared/LocalUserAbstractApplicationService.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.core.application.shared +import dev.usbharu.hideout.core.application.exception.PermissionDeniedException import dev.usbharu.hideout.core.domain.model.support.principal.FromApi import dev.usbharu.hideout.core.domain.model.support.principal.Principal import org.slf4j.Logger @@ -7,7 +8,9 @@ import org.slf4j.Logger abstract class LocalUserAbstractApplicationService(transaction: Transaction, logger: Logger) : AbstractApplicationService(transaction, logger) { override suspend fun internalExecute(command: T, principal: Principal): R { - require(principal is FromApi) + if (principal !is FromApi) { + throw PermissionDeniedException() + } return internalExecute(command, principal) } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt index e9b09eba..e0bb4f46 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/ExposedPostRepository.kt @@ -30,6 +30,7 @@ import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.createdAt import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.deleted import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.hide import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.id +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.instanceId import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.moveTo import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.overview import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.replyId @@ -61,6 +62,7 @@ class ExposedPostRepository( Posts.upsert { it[id] = post.id.id it[actorId] = post.actorId.id + it[instanceId] = post.instanceId.instanceId it[overview] = post.overview?.overview it[content] = post.content.content it[text] = post.content.text @@ -106,6 +108,7 @@ class ExposedPostRepository( Posts.batchUpsert(posts, id) { this[id] = it.id.id this[actorId] = it.actorId.id + this[instanceId] = it.instanceId.instanceId this[overview] = it.overview?.overview this[content] = it.content.content this[text] = it.content.text diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SpringSecurityOauth2PrincipalContextHolder.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SpringSecurityOauth2PrincipalContextHolder.kt index 68341349..ebf56530 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SpringSecurityOauth2PrincipalContextHolder.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/oauth2/SpringSecurityOauth2PrincipalContextHolder.kt @@ -1,7 +1,10 @@ package dev.usbharu.hideout.core.infrastructure.springframework.oauth2 +import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.domain.model.support.acct.Acct +import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous import dev.usbharu.hideout.core.domain.model.support.principal.FromApi +import dev.usbharu.hideout.core.domain.model.support.principal.Principal import dev.usbharu.hideout.core.domain.model.support.principal.PrincipalContextHolder import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId import dev.usbharu.hideout.core.query.principal.PrincipalQueryService @@ -10,18 +13,24 @@ import org.springframework.security.oauth2.jwt.Jwt import org.springframework.stereotype.Component @Component -class SpringSecurityOauth2PrincipalContextHolder(private val principalQueryService: PrincipalQueryService) : +class SpringSecurityOauth2PrincipalContextHolder( + private val principalQueryService: PrincipalQueryService, + private val transaction: Transaction +) : PrincipalContextHolder { - override suspend fun getPrincipal(): FromApi { - val principal = SecurityContextHolder.getContext().authentication?.principal as Jwt + override suspend fun getPrincipal(): Principal { + val principal = + SecurityContextHolder.getContext().authentication?.principal as? Jwt ?: return Anonymous - val id = principal.getClaim("uid").toLong() - val userDetail = principalQueryService.findByUserDetailId(UserDetailId(id)) + return transaction.transaction { + val id = principal.getClaim("uid").toLong() + val userDetail = principalQueryService.findByUserDetailId(UserDetailId(id)) - return FromApi( - userDetail.actorId, - userDetail.userDetailId, - Acct(userDetail.username, userDetail.host) - ) + return@transaction FromApi( + userDetail.actorId, + userDetail.userDetailId, + Acct(userDetail.username, userDetail.host) + ) + } } } \ No newline at end of file diff --git a/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql b/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql index d6f0c0f4..c42a3f7f 100644 --- a/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql +++ b/hideout-core/src/main/resources/db/migration/V1__Init_DB.sql @@ -71,6 +71,14 @@ create table if not exists actor_alsoknownas constraint fk_actor_alsoknownas_actors__also_known_as foreign key ("also_known_as") references actors (id) on delete cascade on update cascade ); +create table timelines +( + id bigint primary key, + user_detail_id bigint not null, + name varchar(255) not null, + visibility varchar(100) not null, + is_system boolean not null default false +); create table if not exists user_details ( id bigserial primary key, @@ -78,9 +86,14 @@ create table if not exists user_details password varchar(255) not null, auto_accept_followee_follow_request boolean not null, last_migration timestamp null default null, - constraint fk_user_details_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict + home_timeline_id bigint null default null, + constraint fk_user_details_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict, + constraint fk_user_details_timelines_id__id foreign key (home_timeline_id) references timelines (id) on delete cascade on update cascade ); +alter table timelines + add constraint fk_timelines_user_details__user_detail_id foreign key ("user_detail_id") references user_details (id) on delete cascade on update cascade; + create table if not exists media ( id bigint primary key, @@ -104,6 +117,7 @@ create table if not exists posts ( id bigint primary key, actor_id bigint not null, + instance_id bigint not null, overview varchar(100) null, content varchar(5000) not null, text varchar(3000) not null, @@ -118,6 +132,8 @@ create table if not exists posts hide boolean default false not null, move_to bigint default null null ); +alter table posts + add constraint fk_posts_instance_id__id foreign key (instance_id) references instance (id) on delete cascade on update cascade; alter table posts add constraint fk_posts_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict; alter table posts diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt index 2d3a4bc7..4fdc95c1 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/ExposedStatusQueryService.kt @@ -27,10 +27,7 @@ import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status.Visibility.* import dev.usbharu.hideout.mastodon.query.StatusQuery import dev.usbharu.hideout.mastodon.query.StatusQueryService -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.andWhere -import org.jetbrains.exposed.sql.leftJoin -import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.* import org.springframework.stereotype.Repository import java.net.URI import dev.usbharu.hideout.core.domain.model.media.Media as EntityMedia @@ -39,6 +36,33 @@ import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.CustomEmoji a @Suppress("IncompleteDestructuring") @Repository class StatusQueryServiceImpl : StatusQueryService { + + protected fun authorizedQuery(principal: Principal? = null): QueryAlias { + if (principal == null) { + return Posts + .selectAll() + .where { + Posts.visibility eq Visibility.PUBLIC.name or (Posts.visibility eq Visibility.UNLISTED.name) + } + .alias("authorized_table") + } + + val relationshipsAlias = Relationships.alias("inverse_relationships") + + return Posts + .leftJoin(PostsVisibleActors) + .leftJoin(Relationships, otherColumn = { actorId }) + .leftJoin(relationshipsAlias, otherColumn = { relationshipsAlias[Relationships.actorId] }) + .selectAll() + .where { + Posts.visibility eq Visibility.PUBLIC.name or + (Posts.visibility eq Visibility.UNLISTED.name) or + (Posts.visibility eq Visibility.DIRECT.name and (PostsVisibleActors.actorId eq principal.actorId.id)) or + (Posts.visibility eq Visibility.FOLLOWERS.name and (Relationships.blocking eq false and (relationshipsAlias[Relationships.following] eq true))) + } + .alias("authorized_table") + } + override suspend fun findByPostIds(ids: List): List = findByPostIdsWithMedia(ids) override suspend fun findByPostIdsWithMediaIds(statusQueries: List): List { @@ -50,10 +74,12 @@ class StatusQueryServiceImpl : StatusQueryService { val emojiIdSet = mutableSetOf() emojiIdSet.addAll(statusQueries.flatMap { it.emojiIds }) + val qa = authorizedQuery() + val postMap = Posts .leftJoin(Actors) .selectAll().where { Posts.id inList postIdSet } - .associate { it[Posts.id] to toStatus(it) } + .associate { it[Posts.id] to toStatus(it, qa) } val mediaMap = Media.selectAll().where { Media.id inList mediaIdSet } .associate { it[Media.id] to it.toMedia().toMediaAttachments() @@ -82,7 +108,8 @@ class StatusQueryServiceImpl : StatusQueryService { tagged: String?, includeFollowers: Boolean, ): List { - val query = Posts + val qa = authorizedQuery() + val query = qa .leftJoin(PostsMedia) .leftJoin(Actors) .leftJoin(Media) @@ -107,7 +134,7 @@ class StatusQueryServiceImpl : StatusQueryService { .groupBy { it[Posts.id] } .map { it.value } .map { - toStatus(it.first()).copy( + toStatus(it.first(), qa).copy( mediaAttachments = it.mapNotNull { resultRow -> resultRow.toMediaOrNull()?.toMediaAttachments() } @@ -119,21 +146,22 @@ class StatusQueryServiceImpl : StatusQueryService { } override suspend fun findByPostId(id: Long, principal: Principal?): Status? { - val map = Posts - .leftJoin(PostsMedia) - .leftJoin(Actors) - .leftJoin(Media,{PostsMedia.mediaId},{Media.id}) + val aq = authorizedQuery(principal) + val map = aq + .leftJoin(PostsMedia, { aq[Posts.id] }, { PostsMedia.postId }) + .leftJoin(Actors, { aq[Posts.actorId] }, { Actors.id }) + .leftJoin(Media, { PostsMedia.mediaId }, { Media.id }) .selectAll() - .where { Posts.id eq id } - .groupBy { it[Posts.id] } + .where { aq[Posts.id] eq id } + .groupBy { it[aq[Posts.id]] } .map { it.value } .map { - toStatus(it.first()).copy( + toStatus(it.first(), aq).copy( mediaAttachments = it.mapNotNull { resultRow -> resultRow.toMediaOrNull()?.toMediaAttachments() }, emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() } - ) to it.first()[Posts.repostId] + ) to it.first()[aq[Posts.repostId]] } return resolveReplyAndRepost(map).singleOrNull() } @@ -160,6 +188,7 @@ class StatusQueryServiceImpl : StatusQueryService { } private suspend fun findByPostIdsWithMedia(ids: List): List { + val qa = authorizedQuery() val pairs = Posts .leftJoin(PostsMedia) .leftJoin(PostsEmojis) @@ -170,7 +199,7 @@ class StatusQueryServiceImpl : StatusQueryService { .groupBy { it[Posts.id] } .map { it.value } .map { - toStatus(it.first()).copy( + toStatus(it.first(), qa).copy( mediaAttachments = it.mapNotNull { resultRow -> resultRow.toMediaOrNull()?.toMediaAttachments() }, @@ -189,10 +218,10 @@ private fun CustomEmoji.toMastodonEmoji(): MastodonEmoji = MastodonEmoji( category = this.category.orEmpty() ) -private fun toStatus(it: ResultRow) = Status( - id = it[Posts.id].toString(), - uri = it[Posts.apId], - createdAt = it[Posts.createdAt].toString(), +private fun toStatus(it: ResultRow, queryAlias: QueryAlias) = Status( + id = it[queryAlias[Posts.id]].toString(), + uri = it[queryAlias[Posts.apId]], + createdAt = it[queryAlias[Posts.createdAt]].toString(), account = Account( id = it[Actors.id].toString(), username = it[Actors.name], @@ -220,15 +249,15 @@ private fun toStatus(it: ResultRow) = Status( suspended = false, limited = false ), - content = it[Posts.text], - visibility = when (Visibility.valueOf(it[Posts.visibility])) { + content = it[queryAlias[Posts.text]], + visibility = when (Visibility.valueOf(it[queryAlias[Posts.visibility]])) { Visibility.PUBLIC -> public Visibility.UNLISTED -> unlisted Visibility.FOLLOWERS -> private Visibility.DIRECT -> direct }, - sensitive = it[Posts.sensitive], - spoilerText = it[Posts.overview].orEmpty(), + sensitive = it[queryAlias[Posts.sensitive]], + spoilerText = it[queryAlias[Posts.overview]].orEmpty(), mediaAttachments = emptyList(), mentions = emptyList(), tags = emptyList(), @@ -236,11 +265,11 @@ private fun toStatus(it: ResultRow) = Status( reblogsCount = 0, favouritesCount = 0, repliesCount = 0, - url = it[Posts.apId], - inReplyToId = it[Posts.replyId]?.toString(), + url = it[queryAlias[Posts.apId]], + inReplyToId = it[queryAlias[Posts.replyId]]?.toString(), inReplyToAccountId = null, language = null, - text = it[Posts.text], + text = it[queryAlias[Posts.text]], editedAt = null ) diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAccountApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAccountApi.kt index 4a3dd110..a860cded 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAccountApi.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringAccountApi.kt @@ -16,7 +16,6 @@ package dev.usbharu.hideout.mastodon.interfaces.api -import dev.usbharu.hideout.core.application.actor.GetUserDetail import dev.usbharu.hideout.core.application.actor.GetUserDetailApplicationService import dev.usbharu.hideout.core.application.relationship.acceptfollowrequest.AcceptFollowRequest import dev.usbharu.hideout.core.application.relationship.acceptfollowrequest.UserAcceptFollowRequestApplicationService @@ -160,7 +159,7 @@ class SpringAccountApi( override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity { val principal = principalContextHolder.getPrincipal() val localActor = - getUserDetailApplicationService.execute(GetUserDetail(principal.userDetailId.id), principal) + getUserDetailApplicationService.execute(Unit, principal) return ResponseEntity.ok( CredentialAccount( diff --git a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt index 0fe04e58..6c2f468a 100644 --- a/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt +++ b/hideout-mastodon/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/SpringStatusApi.kt @@ -54,6 +54,7 @@ class SpringStatusApi( override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity { + val principal = principalContextHolder.getPrincipal() val execute = registerLocalPostApplicationService.execute( RegisterLocalPost( content = statusesRequest.status.orEmpty(), @@ -69,12 +70,12 @@ class SpringStatusApi( replyId = statusesRequest.inReplyToId?.toLong(), sensitive = statusesRequest.sensitive == true, mediaIds = statusesRequest.mediaIds.orEmpty().map { it.toLong() } - ), principalContextHolder.getPrincipal() + ), principal ) val status = - getStatusApplicationService.execute(GetStatus(execute.toString()), principalContextHolder.getPrincipal()) + getStatusApplicationService.execute(GetStatus(execute.toString()), principal) return ResponseEntity.ok( status )