mirror of https://github.com/usbharu/Hideout.git
wip
This commit is contained in:
parent
992cc18c62
commit
0463ad6b69
|
@ -15,7 +15,7 @@ data class ActorDetail(
|
|||
val iconUrl: URI?,
|
||||
val bannerURL: URI?,
|
||||
val followingCount: Int?,
|
||||
val followersCount: Int?
|
||||
val followersCount: Int?,
|
||||
) {
|
||||
companion object {
|
||||
fun of(actor: Actor, iconUrl: URI?, bannerURL: URI?): ActorDetail {
|
||||
|
@ -31,7 +31,7 @@ data class ActorDetail(
|
|||
iconUrl = iconUrl,
|
||||
bannerURL = bannerURL,
|
||||
followingCount = actor.followingCount?.relationshipCount,
|
||||
followersCount = actor.followersCount?.relationshipCount
|
||||
followersCount = actor.followersCount?.relationshipCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package dev.usbharu.hideout.core.application.timeline
|
||||
|
||||
import dev.usbharu.hideout.core.domain.model.support.page.Page
|
||||
|
||||
data class GetUserTimeline(val id: Long, val page: Page)
|
|
@ -0,0 +1,49 @@
|
|||
package dev.usbharu.hideout.core.application.timeline
|
||||
|
||||
import dev.usbharu.hideout.core.application.post.PostDetail
|
||||
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
|
||||
import dev.usbharu.hideout.core.application.shared.Transaction
|
||||
import dev.usbharu.hideout.core.domain.model.actor.ActorId
|
||||
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.PaginationList
|
||||
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
|
||||
import dev.usbharu.hideout.core.query.usertimeline.UserTimelineQueryService
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class GetUserTimelineApplicationService(
|
||||
private val userTimelineQueryService: UserTimelineQueryService,
|
||||
private val postRepository: PostRepository,
|
||||
transaction: Transaction
|
||||
) :
|
||||
AbstractApplicationService<GetUserTimeline, PaginationList<PostDetail, PostId>>(transaction, logger) {
|
||||
override suspend fun internalExecute(
|
||||
command: GetUserTimeline,
|
||||
principal: Principal
|
||||
): PaginationList<PostDetail, PostId> {
|
||||
val postList = postRepository.findByActorIdAndVisibilityInList(
|
||||
ActorId(command.id),
|
||||
listOf(Visibility.PUBLIC, Visibility.UNLISTED, Visibility.FOLLOWERS),
|
||||
command.page
|
||||
)
|
||||
|
||||
val postIdList =
|
||||
postList.mapNotNull { it.repostId } + postList.mapNotNull { it.replyId } + postList.map { it.id }
|
||||
|
||||
val postDetailMap = userTimelineQueryService.findByIdAll(postIdList, principal).associateBy { it.id }
|
||||
|
||||
return PaginationList(postList.mapNotNull {
|
||||
postDetailMap[it.id.id]?.copy(
|
||||
repost = postDetailMap[it.repostId?.id],
|
||||
reply = postDetailMap[it.replyId?.id]
|
||||
)
|
||||
}, postList.next, postList.prev)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(GetUserTimelineApplicationService::class.java)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package dev.usbharu.hideout.core.infrastructure.exposedquery
|
||||
|
||||
import dev.usbharu.hideout.core.application.post.ActorDetail
|
||||
import dev.usbharu.hideout.core.application.post.MediaDetail
|
||||
import dev.usbharu.hideout.core.application.post.PostDetail
|
||||
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.principal.Principal
|
||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
|
||||
import dev.usbharu.hideout.core.query.usertimeline.UserTimelineQueryService
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.net.URI
|
||||
|
||||
@Repository
|
||||
class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractRepository() {
|
||||
|
||||
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, onColumn = { Posts.actorId }, otherColumn = { actorId })
|
||||
.leftJoin(
|
||||
relationshipsAlias,
|
||||
onColumn = { Posts.actorId },
|
||||
otherColumn = { relationshipsAlias[Relationships.targetActorId] }
|
||||
)
|
||||
.select(Posts.columns)
|
||||
.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))) or
|
||||
(Posts.actorId eq principal.actorId.id)
|
||||
}
|
||||
.alias("authorized_table")
|
||||
}
|
||||
|
||||
override suspend fun findByIdAll(idList: List<PostId>, principal: Principal): List<PostDetail> {
|
||||
val authorizedQuery = authorizedQuery(principal)
|
||||
|
||||
val iconMedia = Media.alias("ICON_MEDIA")
|
||||
|
||||
return authorizedQuery
|
||||
.leftJoin(PostsVisibleActors, { authorizedQuery[Posts.id] }, { PostsVisibleActors.postId })
|
||||
.leftJoin(Actors, { authorizedQuery[Posts.actorId] }, { Actors.id })
|
||||
.leftJoin(iconMedia, { Actors.icon }, { iconMedia[Media.id] })
|
||||
.leftJoin(PostsMedia, { authorizedQuery[Posts.id] }, { PostsMedia.postId })
|
||||
.leftJoin(Media, { PostsMedia.mediaId }, { Media.id })
|
||||
.selectAll()
|
||||
.where { authorizedQuery[Posts.id] inList idList.map { it.id } }
|
||||
.groupBy { it[authorizedQuery[Posts.id]] }
|
||||
.map { it.value }
|
||||
.map {
|
||||
toPostDetail(it.first(), authorizedQuery, iconMedia).copy(
|
||||
mediaDetailList = it.mapNotNull { resultRow ->
|
||||
resultRow.toMediaOrNull()?.let { it1 -> MediaDetail.of(it1) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toPostDetail(it: ResultRow, authorizedQuery: QueryAlias, iconMedia: Alias<Media>): PostDetail {
|
||||
return PostDetail(
|
||||
it[authorizedQuery[Posts.id]],
|
||||
ActorDetail(
|
||||
actorId = it[authorizedQuery[Posts.actorId]],
|
||||
instanceId = it[Actors.instance],
|
||||
name = it[Actors.name],
|
||||
domain = it[Actors.domain],
|
||||
screenName = it[Actors.screenName],
|
||||
url = URI.create(it[Actors.url]),
|
||||
locked = it[Actors.locked],
|
||||
icon = it.getOrNull(iconMedia[Media.url])?.let { URI.create(it) }
|
||||
),
|
||||
overview = it[authorizedQuery[Posts.overview]],
|
||||
text = it[authorizedQuery[Posts.text]],
|
||||
content = it[authorizedQuery[Posts.content]],
|
||||
createdAt = it[authorizedQuery[Posts.createdAt]],
|
||||
visibility = Visibility.valueOf(it[authorizedQuery[Posts.visibility]]),
|
||||
pureRepost = false,
|
||||
url = URI.create(it[authorizedQuery[Posts.url]]),
|
||||
apId = URI.create(it[authorizedQuery[Posts.apId]]),
|
||||
repost = null,
|
||||
reply = null,
|
||||
sensitive = it[authorizedQuery[Posts.sensitive]],
|
||||
deleted = it[authorizedQuery[Posts.deleted]],
|
||||
mediaDetailList = emptyList(),
|
||||
moveTo = null
|
||||
)
|
||||
}
|
||||
|
||||
override val logger: Logger
|
||||
get() = Companion.logger
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(ExposedUserTimelineQueryService::class.java)
|
||||
}
|
||||
}
|
|
@ -211,17 +211,38 @@ class ExposedPostRepository(
|
|||
visibilityList: List<Visibility>,
|
||||
of: Page?
|
||||
): PaginationList<Post, PostId> {
|
||||
val postList = query {
|
||||
val query = Posts
|
||||
.selectAll()
|
||||
.where {
|
||||
Posts.actorId eq actorId.id and (visibility inList visibilityList.map { it.name })
|
||||
}
|
||||
|
||||
if (of?.minId != null) {
|
||||
query.orderBy(Posts.createdAt, SortOrder.ASC)
|
||||
of.minId?.let { query.andWhere { Posts.id greater it } }
|
||||
of.maxId?.let { query.andWhere { Posts.id less it } }
|
||||
} else {
|
||||
query.orderBy(Posts.createdAt, SortOrder.DESC)
|
||||
of?.sinceId?.let { query.andWhere { Posts.id greater it } }
|
||||
of?.maxId?.let { query.andWhere { Posts.id less it } }
|
||||
}
|
||||
|
||||
of?.limit?.let { query.limit(it) }
|
||||
|
||||
query.let(postQueryMapper::map)
|
||||
}
|
||||
|
||||
val posts = if (of?.minId != null) {
|
||||
postList.reversed()
|
||||
} else {
|
||||
postList
|
||||
}
|
||||
|
||||
return PaginationList(
|
||||
query {
|
||||
Posts
|
||||
.selectAll()
|
||||
.where {
|
||||
Posts.actorId eq actorId.id and (visibility inList visibilityList.map { it.name })
|
||||
}
|
||||
.let(postQueryMapper::map)
|
||||
},
|
||||
null,
|
||||
null
|
||||
posts,
|
||||
posts.lastOrNull()?.id,
|
||||
posts.firstOrNull()?.id
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -108,6 +108,23 @@ fun ResultRow.toMedia(): EntityMedia {
|
|||
)
|
||||
}
|
||||
|
||||
fun ResultRow.toMediaOrNull(): EntityMedia? {
|
||||
val fileType = FileType.valueOf(this.getOrNull(Media.type) ?: return null)
|
||||
val mimeType = this.getOrNull(Media.mimeType) ?: return null
|
||||
return EntityMedia(
|
||||
id = MediaId(this.getOrNull(Media.id) ?: return null),
|
||||
name = MediaName(this.getOrNull(Media.name) ?: return null),
|
||||
url = URI.create(this.getOrNull(Media.url) ?: return null),
|
||||
remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) },
|
||||
thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) },
|
||||
type = FileType.valueOf(this[Media.type]),
|
||||
blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) },
|
||||
mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType),
|
||||
description = this[Media.description]?.let { MediaDescription(it) },
|
||||
actorId = ActorId(this[Media.actorId])
|
||||
)
|
||||
}
|
||||
|
||||
object Media : Table("media") {
|
||||
val id = long("id")
|
||||
val name = varchar("name", 255)
|
||||
|
|
|
@ -3,28 +3,48 @@ package dev.usbharu.hideout.core.interfaces.web.user
|
|||
import dev.usbharu.hideout.core.application.actor.GetActorDetail
|
||||
import dev.usbharu.hideout.core.application.actor.GetActorDetailApplicationService
|
||||
import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
|
||||
import dev.usbharu.hideout.core.application.timeline.GetUserTimeline
|
||||
import dev.usbharu.hideout.core.application.timeline.GetUserTimelineApplicationService
|
||||
import dev.usbharu.hideout.core.domain.model.support.acct.Acct
|
||||
import dev.usbharu.hideout.core.domain.model.support.page.Page
|
||||
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
|
||||
import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
|
||||
@Controller
|
||||
class UserController(
|
||||
private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService,
|
||||
private val getUserDetailApplicationService: GetActorDetailApplicationService,
|
||||
private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder,
|
||||
private val getUserTimelineApplicationService: GetUserTimelineApplicationService
|
||||
) {
|
||||
@GetMapping("/users/{name}")
|
||||
suspend fun userById(@PathVariable name: String, model: Model): String {
|
||||
suspend fun userById(
|
||||
@PathVariable name: String,
|
||||
@RequestParam minId: Long?,
|
||||
@RequestParam maxId: Long?,
|
||||
@RequestParam sinceId: Long?,
|
||||
model: Model
|
||||
): String {
|
||||
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
|
||||
|
||||
model.addAttribute("instance", getLocalInstanceApplicationService.execute(Unit, Anonymous))
|
||||
val actorDetail = getUserDetailApplicationService.execute(GetActorDetail(Acct.of(name)), principal)
|
||||
model.addAttribute(
|
||||
"user",
|
||||
getUserDetailApplicationService.execute(GetActorDetail(Acct.of(name)), principal)
|
||||
actorDetail
|
||||
)
|
||||
model.addAttribute(
|
||||
"userTimeline", getUserTimelineApplicationService.execute(
|
||||
GetUserTimeline(
|
||||
actorDetail.id,
|
||||
Page.of(maxId, sinceId, minId, 20)
|
||||
), principal
|
||||
)
|
||||
)
|
||||
return "userById"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package dev.usbharu.hideout.core.query.usertimeline
|
||||
|
||||
import dev.usbharu.hideout.core.application.post.PostDetail
|
||||
import dev.usbharu.hideout.core.domain.model.post.PostId
|
||||
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
|
||||
|
||||
interface UserTimelineQueryService {
|
||||
/**
|
||||
* replyやrepost等はnullになります
|
||||
*/
|
||||
suspend fun findByIdAll(idList: List<PostId>, principal: Principal): List<PostDetail>
|
||||
}
|
|
@ -22,4 +22,7 @@ post-form.new-posts-form-label=\u4ECA\u306A\u306B\u3057\u3066\u308B?
|
|||
post-form.new-posts-submit=\u6295\u7A3F\u3059\u308B
|
||||
post.repost=\u30EA\u30DD\u30B9\u30C8
|
||||
post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8
|
||||
user-by-id.followersCount={0} \u30D5\u30A9\u30ED\u30EF\u30FC
|
||||
user-by-id.followingCount={0} \u30D5\u30A9\u30ED\u30FC\u4E2D
|
||||
user-by-id.postsCount={0} \u6295\u7A3F
|
||||
user-by-id.title={0} \u3055\u3093 - {1}
|
|
@ -20,4 +20,7 @@ post-form.new-posts-cw-title=Add content warning
|
|||
post-form.new-posts-form-label=What's on your mind?
|
||||
post-form.new-posts-submit=Submit!
|
||||
post.repost=Repost
|
||||
post.repost-by=Repost by {0}
|
||||
post.repost-by=Repost by {0}
|
||||
user-by-id.followersCount={0} Followers
|
||||
user-by-id.followingCount={0} Following
|
||||
user-by-id.postsCount={0} Posts
|
|
@ -22,4 +22,7 @@ post-form.new-posts-form-label=\u4ECA\u306A\u306B\u3057\u3066\u308B?
|
|||
post-form.new-posts-submit=\u6295\u7A3F\u3059\u308B
|
||||
post.repost=\u30EA\u30DD\u30B9\u30C8
|
||||
post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8
|
||||
user-by-id.followersCount={0} \u30D5\u30A9\u30ED\u30EF\u30FC
|
||||
user-by-id.followingCount={0} \u30D5\u30A9\u30ED\u30FC\u4E2D
|
||||
user-by-id.postsCount={0} \u6295\u7A3F
|
||||
user-by-id.title={0} \u3055\u3093 - {1}
|
|
@ -7,8 +7,31 @@
|
|||
<body>
|
||||
<noscript>
|
||||
<div>
|
||||
<img th:src="user.icon">
|
||||
<img height="150px" th:src="${user.iconUrl}" width="150px">
|
||||
<img height="150px" th:src="${user.bannerURL}" width="600px">
|
||||
</div>
|
||||
<div>
|
||||
<th:block th:if="${user.locked}">
|
||||
<h2 th:text="${user.screenName} + '(private)'"></h2>
|
||||
</th:block>
|
||||
|
||||
<th:block th:if="!${user.locked}">
|
||||
<h2 th:text="${user.screenName}"></h2>
|
||||
</th:block>
|
||||
<p th:text="${user.name} + '@' + ${user.host}"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p th:text="${user.description}"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p th:if="user.postsCount != null" th:text="#{user-by-id.postsCount(${user.postsCount})}">0 Posts</p>
|
||||
<p th:if="user.followingCount != null" th:text="#{user-by-id.followingCount(${user.followingCount})}">0
|
||||
Following</p>
|
||||
<p th:if="user.followersCount != null" th:text="#{user-by-id.followersCount(${user.followersCount})}">0
|
||||
Followers</p>
|
||||
</div>
|
||||
<div th:replace="~{fragments-timeline :: simple-timline(${userTimeline},'/users/'+${user.name}+'@'+${user.host})}"></div>
|
||||
|
||||
</noscript>
|
||||
</body>
|
||||
</html>
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
package dev.usbharu.hideout.mastodon.infrastructure.exposedquery
|
||||
|
||||
import dev.usbharu.hideout.core.domain.model.actor.ActorId
|
||||
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
|
||||
import dev.usbharu.hideout.core.domain.model.media.*
|
||||
import dev.usbharu.hideout.core.domain.model.media.FileType
|
||||
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
||||
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
|
||||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
|
||||
|
@ -30,7 +29,6 @@ import dev.usbharu.hideout.mastodon.query.StatusQuery
|
|||
import dev.usbharu.hideout.mastodon.query.StatusQueryService
|
||||
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
|
||||
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.CustomEmoji as MastodonEmoji
|
||||
|
||||
|
@ -274,40 +272,6 @@ private fun toStatus(it: ResultRow, queryAlias: QueryAlias, inReplyToAlias: Alia
|
|||
editedAt = null
|
||||
)
|
||||
|
||||
fun ResultRow.toMedia(): EntityMedia {
|
||||
val fileType = FileType.valueOf(this[Media.type])
|
||||
val mimeType = this[Media.mimeType]
|
||||
return EntityMedia(
|
||||
id = MediaId(this[Media.id]),
|
||||
name = MediaName(this[Media.name]),
|
||||
url = URI.create(this[Media.url]),
|
||||
remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) },
|
||||
thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) },
|
||||
type = fileType,
|
||||
blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) },
|
||||
mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType),
|
||||
description = this[Media.description]?.let { MediaDescription(it) },
|
||||
actorId = ActorId(this[Media.actorId])
|
||||
)
|
||||
}
|
||||
|
||||
fun ResultRow.toMediaOrNull(): EntityMedia? {
|
||||
val fileType = FileType.valueOf(this.getOrNull(Media.type) ?: return null)
|
||||
val mimeType = this.getOrNull(Media.mimeType) ?: return null
|
||||
return EntityMedia(
|
||||
id = MediaId(this.getOrNull(Media.id) ?: return null),
|
||||
name = MediaName(this.getOrNull(Media.name) ?: return null),
|
||||
url = URI.create(this.getOrNull(Media.url) ?: return null),
|
||||
remoteUrl = this[Media.remoteUrl]?.let { URI.create(it) },
|
||||
thumbnailUrl = this[Media.thumbnailUrl]?.let { URI.create(it) },
|
||||
type = FileType.valueOf(this[Media.type]),
|
||||
blurHash = this[Media.blurhash]?.let { MediaBlurHash(it) },
|
||||
mimeType = MimeType(mimeType.substringBefore("/"), mimeType.substringAfter("/"), fileType),
|
||||
description = this[Media.description]?.let { MediaDescription(it) },
|
||||
actorId = ActorId(this[Media.actorId])
|
||||
)
|
||||
}
|
||||
|
||||
fun EntityMedia.toMediaAttachments(): MediaAttachment = MediaAttachment(
|
||||
id = id.id.toString(),
|
||||
type = when (type) {
|
||||
|
|
Loading…
Reference in New Issue