Merge pull request #33 from usbharu/feature/resource-not-found-exception

Feature/resource not found exception
This commit is contained in:
usbharu 2023-08-15 01:56:31 +09:00 committed by GitHub
commit 5a56c61ee4
20 changed files with 217 additions and 97 deletions

View File

@ -8,6 +8,7 @@ open class Person : Object {
var url: String? = null var url: String? = null
private var icon: Image? = null private var icon: Image? = null
var publicKey: Key? = null var publicKey: Key? = null
var endpoints: Map<String, String> = emptyMap()
protected constructor() : super() protected constructor() : super()
@ -22,7 +23,8 @@ open class Person : Object {
outbox: String?, outbox: String?,
url: String?, url: String?,
icon: Image?, icon: Image?,
publicKey: Key? publicKey: Key?,
endpoints: Map<String, String> = emptyMap()
) : super(add(type, "Person"), name, id = id) { ) : super(add(type, "Person"), name, id = id) {
this.preferredUsername = preferredUsername this.preferredUsername = preferredUsername
this.summary = summary this.summary = summary
@ -31,24 +33,28 @@ open class Person : Object {
this.url = url this.url = url
this.icon = icon this.icon = icon
this.publicKey = publicKey this.publicKey = publicKey
this.endpoints = endpoints
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Person) return false if (other !is Person) return false
if (!super.equals(other)) return false
if (id != other.id) return false
if (preferredUsername != other.preferredUsername) return false if (preferredUsername != other.preferredUsername) return false
if (summary != other.summary) return false if (summary != other.summary) return false
if (inbox != other.inbox) return false if (inbox != other.inbox) return false
if (outbox != other.outbox) return false if (outbox != other.outbox) return false
if (url != other.url) return false if (url != other.url) return false
if (icon != other.icon) return false if (icon != other.icon) return false
return publicKey == other.publicKey if (publicKey != other.publicKey) return false
if (endpoints != other.endpoints) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = id?.hashCode() ?: 0 var result = super.hashCode()
result = 31 * result + (preferredUsername?.hashCode() ?: 0) result = 31 * result + (preferredUsername?.hashCode() ?: 0)
result = 31 * result + (summary?.hashCode() ?: 0) result = 31 * result + (summary?.hashCode() ?: 0)
result = 31 * result + (inbox?.hashCode() ?: 0) result = 31 * result + (inbox?.hashCode() ?: 0)
@ -56,6 +62,7 @@ open class Person : Object {
result = 31 * result + (url?.hashCode() ?: 0) result = 31 * result + (url?.hashCode() ?: 0)
result = 31 * result + (icon?.hashCode() ?: 0) result = 31 * result + (icon?.hashCode() ?: 0)
result = 31 * result + (publicKey?.hashCode() ?: 0) result = 31 * result + (publicKey?.hashCode() ?: 0)
result = 31 * result + endpoints.hashCode()
return result return result
} }
} }

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.exception
open class FailedToGetResourcesException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
}

View File

@ -10,13 +10,14 @@ fun Application.configureStatusPages() {
install(StatusPages) { install(StatusPages) {
exception<IllegalArgumentException> { call, cause -> exception<IllegalArgumentException> { call, cause ->
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest) call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
call.application.log.warn("Bad Request", cause)
} }
exception<InvalidUsernameOrPasswordException> { call, _ -> exception<InvalidUsernameOrPasswordException> { call, _ ->
call.respond(HttpStatusCode.Unauthorized) call.respond(HttpStatusCode.Unauthorized)
} }
exception<Throwable> { call, cause -> exception<Throwable> { call, cause ->
call.respondText(text = "500: ${cause.stackTraceToString()}", status = HttpStatusCode.InternalServerError) call.respondText(text = "500: ${cause.stackTraceToString()}", status = HttpStatusCode.InternalServerError)
cause.printStackTrace() call.application.log.error("Internal Server Error", cause)
} }
} }
} }

View File

@ -9,4 +9,5 @@ interface FollowerQueryService {
suspend fun findFollowingByNameAndDomain(name: String, domain: String): List<User> suspend fun findFollowingByNameAndDomain(name: String, domain: String): List<User>
suspend fun appendFollower(user: Long, follower: Long) suspend fun appendFollower(user: Long, follower: Long)
suspend fun removeFollower(user: Long, follower: Long) suspend fun removeFollower(user: Long, follower: Long)
suspend fun alreadyFollow(userId: Long, followerId: Long): Boolean
} }

View File

@ -198,6 +198,12 @@ class FollowerQueryServiceImpl : FollowerQueryService {
} }
override suspend fun removeFollower(user: Long, follower: Long) { override suspend fun removeFollower(user: Long, follower: Long) {
UsersFollowers.deleteWhere { Users.id eq user and (followerId eq follower) } UsersFollowers.deleteWhere { userId eq user and (followerId eq follower) }
}
override suspend fun alreadyFollow(userId: Long, followerId: Long): Boolean {
return UsersFollowers.select { UsersFollowers.userId eq userId or (UsersFollowers.followerId eq followerId) }
.empty()
.not()
} }
} }

View File

@ -1,8 +1,10 @@
package dev.usbharu.hideout.query package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.JwtRefreshTokens import dev.usbharu.hideout.repository.JwtRefreshTokens
import dev.usbharu.hideout.repository.toJwtRefreshToken import dev.usbharu.hideout.repository.toJwtRefreshToken
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteAll import org.jetbrains.exposed.sql.deleteAll
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
@ -12,13 +14,19 @@ import org.koin.core.annotation.Single
@Single @Single
class JwtRefreshTokenQueryServiceImpl : JwtRefreshTokenQueryService { class JwtRefreshTokenQueryServiceImpl : JwtRefreshTokenQueryService {
override suspend fun findById(id: Long): JwtRefreshToken = override suspend fun findById(id: Long): JwtRefreshToken =
JwtRefreshTokens.select { JwtRefreshTokens.id.eq(id) }.single().toJwtRefreshToken() JwtRefreshTokens.select { JwtRefreshTokens.id.eq(id) }
.singleOr { FailedToGetResourcesException("id: $id is a duplicate or does not exist.", it) }
.toJwtRefreshToken()
override suspend fun findByToken(token: String): JwtRefreshToken = override suspend fun findByToken(token: String): JwtRefreshToken =
JwtRefreshTokens.select { JwtRefreshTokens.refreshToken.eq(token) }.single().toJwtRefreshToken() JwtRefreshTokens.select { JwtRefreshTokens.refreshToken.eq(token) }
.singleOr { FailedToGetResourcesException("token: $token is a duplicate or does not exist.", it) }
.toJwtRefreshToken()
override suspend fun findByUserId(userId: Long): JwtRefreshToken = override suspend fun findByUserId(userId: Long): JwtRefreshToken =
JwtRefreshTokens.select { JwtRefreshTokens.userId.eq(userId) }.single().toJwtRefreshToken() JwtRefreshTokens.select { JwtRefreshTokens.userId.eq(userId) }
.singleOr { FailedToGetResourcesException("userId: $userId is a duplicate or does not exist.", it) }
.toJwtRefreshToken()
override suspend fun deleteById(id: Long) { override suspend fun deleteById(id: Long) {
JwtRefreshTokens.deleteWhere { JwtRefreshTokens.id eq id } JwtRefreshTokens.deleteWhere { JwtRefreshTokens.id eq id }

View File

@ -1,16 +1,22 @@
package dev.usbharu.hideout.query package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Posts import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@Single @Single
class PostQueryServiceImpl : PostQueryService { class PostQueryServiceImpl : PostQueryService {
override suspend fun findById(id: Long): Post = Posts.select { Posts.id eq id }.single().toPost() override suspend fun findById(id: Long): Post =
Posts.select { Posts.id eq id }
.singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }.toPost()
override suspend fun findByUrl(url: String): Post = Posts.select { Posts.url eq url }.single().toPost() override suspend fun findByUrl(url: String): Post = Posts.select { Posts.url eq url }
.singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }.toPost()
override suspend fun findByApId(string: String): Post = Posts.select { Posts.apId eq string }.single().toPost() override suspend fun findByApId(string: String): Post = Posts.select { Posts.apId eq string }
.singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) }.toPost()
} }

View File

@ -1,10 +1,12 @@
package dev.usbharu.hideout.query package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Posts import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.Users import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.repository.toUser import dev.usbharu.hideout.repository.toUser
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.innerJoin import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@ -17,7 +19,7 @@ class PostResponseQueryServiceImpl : PostResponseQueryService {
return Posts return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { Users.id }) .innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { Users.id })
.select { Posts.id eq id } .select { Posts.id eq id }
.single() .singleOr { FailedToGetResourcesException("id: $id,userId: $userId is a duplicate or does not exist.", it) }
.let { PostResponse.from(it.toPost(), it.toUser()) } .let { PostResponse.from(it.toPost(), it.toUser()) }
} }

View File

@ -3,9 +3,11 @@ package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.Account import dev.usbharu.hideout.domain.model.hideout.dto.Account
import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Reactions import dev.usbharu.hideout.repository.Reactions
import dev.usbharu.hideout.repository.Users import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toReaction import dev.usbharu.hideout.repository.toReaction
import dev.usbharu.hideout.util.singleOr
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.koin.core.annotation.Single import org.koin.core.annotation.Single
@ -26,7 +28,12 @@ class ReactionQueryServiceImpl : ReactionQueryService {
Reactions.emojiId.eq(emojiId) Reactions.emojiId.eq(emojiId)
) )
} }
.single() .singleOr {
FailedToGetResourcesException(
"postId: $postId,userId: $userId,emojiId: $emojiId is duplicate or does not exist.",
it
)
}
.toReaction() .toReaction()
} }

View File

@ -1,8 +1,10 @@
package dev.usbharu.hideout.query package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Users import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toUser import dev.usbharu.hideout.repository.toUser
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
@ -13,14 +15,22 @@ class UserQueryServiceImpl : UserQueryService {
override suspend fun findAll(limit: Int, offset: Long): List<User> = override suspend fun findAll(limit: Int, offset: Long): List<User> =
Users.selectAll().limit(limit, offset).map { it.toUser() } Users.selectAll().limit(limit, offset).map { it.toUser() }
override suspend fun findById(id: Long): User = Users.select { Users.id eq id }.single().toUser() override suspend fun findById(id: Long): User = Users.select { Users.id eq id }
.singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }.toUser()
override suspend fun findByName(name: String): List<User> = Users.select { Users.name eq name }.map { it.toUser() } override suspend fun findByName(name: String): List<User> = Users.select { Users.name eq name }.map { it.toUser() }
override suspend fun findByNameAndDomain(name: String, domain: String): User = override suspend fun findByNameAndDomain(name: String, domain: String): User =
Users.select { Users.name eq name and (Users.domain eq domain) }.single().toUser() Users
.select { Users.name eq name and (Users.domain eq domain) }
.singleOr {
FailedToGetResourcesException("name: $name,domain: $domain is duplicate or does not exist.", it)
}
.toUser()
override suspend fun findByUrl(url: String): User = Users.select { Users.url eq url }.single().toUser() override suspend fun findByUrl(url: String): User = Users.select { Users.url eq url }
.singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }
.toUser()
override suspend fun findByIds(ids: List<Long>): List<User> = override suspend fun findByIds(ids: List<Long>): List<User> =
Users.select { Users.id inList ids }.map { it.toUser() } Users.select { Users.id inList ids }.map { it.toUser() }

View File

@ -2,6 +2,7 @@ package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.service.core.IdGenerateService import dev.usbharu.hideout.service.core.IdGenerateService
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
@ -53,7 +54,8 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
return post return post
} }
override suspend fun findById(id: Long): Post = Posts.select { Posts.id eq id }.single().toPost() override suspend fun findById(id: Long): Post = Posts.select { Posts.id eq id }.singleOrNull()?.toPost()
?: throw FailedToGetResourcesException("id: $id was not found.")
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
Posts.deleteWhere { Posts.id eq id } Posts.deleteWhere { Posts.id eq id }

View File

@ -28,10 +28,7 @@ fun Routing.usersAP(
val name = val name =
call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.") call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.")
val person = apUserService.getPersonByName(name) val person = apUserService.getPersonByName(name)
return@handle call.respondAp( return@handle call.respondAp(person, HttpStatusCode.OK)
person,
HttpStatusCode.OK
)
} }
get { get {
// TODO: 暫定処置なので治す // TODO: 暫定処置なので治す
@ -41,7 +38,13 @@ fun Routing.usersAP(
?: throw ParameterNotExistException("Parameter(name='name') does not exist."), ?: throw ParameterNotExistException("Parameter(name='name') does not exist."),
Config.configData.domain Config.configData.domain
) )
call.respondText(userEntity.toString() + "\n" + followerQueryService.findFollowersById(userEntity.id)) val personByName = apUserService.getPersonByName(userEntity.name)
call.respondText(
userEntity.toString() + "\n" + followerQueryService.findFollowersById(userEntity.id) +
"\n" + Config.configData.objectMapper.writeValueAsString(
personByName
)
)
} }
} }
} }
@ -51,9 +54,7 @@ class ContentTypeRouteSelector(private vararg val contentType: ContentType) : Ro
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation { override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
context.call.application.log.debug("Accept: ${context.call.request.accept()}") context.call.application.log.debug("Accept: ${context.call.request.accept()}")
val requestContentType = context.call.request.accept() ?: return RouteSelectorEvaluation.FailedParameter val requestContentType = context.call.request.accept() ?: return RouteSelectorEvaluation.FailedParameter
return if (requestContentType.split(",") return if (requestContentType.split(",").any { contentType.any { contentType -> contentType.match(it) } }) {
.any { contentType.any { contentType -> contentType.match(it) } }
) {
RouteSelectorEvaluation.Constant RouteSelectorEvaluation.Constant
} else { } else {
RouteSelectorEvaluation.FailedParameter RouteSelectorEvaluation.FailedParameter

View File

@ -42,11 +42,11 @@ fun Route.users(userService: UserService, userApiService: UserApiService) {
authenticate(TOKEN_AUTH, optional = true) { authenticate(TOKEN_AUTH, optional = true) {
get { get {
val userParameter = ( val userParameter = (
call.parameters["name"] call.parameters["name"]
?: throw ParameterNotExistException( ?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist." "Parameter(name='userName@domain') does not exist."
)
) )
)
if (userParameter.toLongOrNull() != null) { if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findById(userParameter.toLong())) return@get call.respond(userApiService.findById(userParameter.toLong()))
} else { } else {
@ -72,19 +72,16 @@ fun Route.users(userService: UserService, userApiService: UserApiService) {
?: throw IllegalStateException("no principal") ?: throw IllegalStateException("no principal")
val userParameter = call.parameters["name"] val userParameter = call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
if (userParameter.toLongOrNull() != null) { if (if (userParameter.toLongOrNull() != null) {
if (userService.followRequest(userParameter.toLong(), userId)) { userApiService.follow(userParameter.toLong(), userId)
return@post call.respond(HttpStatusCode.OK)
} else { } else {
return@post call.respond(HttpStatusCode.Accepted) val parse = AcctUtil.parse(userParameter)
userApiService.follow(parse, userId)
} }
} ) {
val acct = AcctUtil.parse(userParameter) call.respond(HttpStatusCode.OK)
val targetUser = userApiService.findByAcct(acct)
if (userService.followRequest(targetUser.id.toLong(), userId)) {
return@post call.respond(HttpStatusCode.OK)
} else { } else {
return@post call.respond(HttpStatusCode.Accepted) call.respond(HttpStatusCode.Accepted)
} }
} }
} }
@ -92,11 +89,11 @@ fun Route.users(userService: UserService, userApiService: UserApiService) {
route("/following") { route("/following") {
get { get {
val userParameter = ( val userParameter = (
call.parameters["name"] call.parameters["name"]
?: throw ParameterNotExistException( ?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist." "Parameter(name='userName@domain') does not exist."
)
) )
)
if (userParameter.toLongOrNull() != null) { if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findFollowings(userParameter.toLong())) return@get call.respond(userApiService.findFollowings(userParameter.toLong()))
} }

View File

@ -5,7 +5,9 @@ import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
import dev.usbharu.hideout.domain.model.ap.Accept import dev.usbharu.hideout.domain.model.ap.Accept
import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.UserService import dev.usbharu.hideout.service.user.UserService
import io.ktor.http.* import io.ktor.http.*
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@ -17,20 +19,27 @@ interface APAcceptService {
@Single @Single
class APAcceptServiceImpl( class APAcceptServiceImpl(
private val userService: UserService, private val userService: UserService,
private val userQueryService: UserQueryService private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val transaction: Transaction
) : APAcceptService { ) : APAcceptService {
override suspend fun receiveAccept(accept: Accept): ActivityPubResponse { override suspend fun receiveAccept(accept: Accept): ActivityPubResponse {
val value = accept.`object` ?: throw IllegalActivityPubObjectException("object is null") return transaction.transaction {
if (value.type.contains("Follow").not()) { val value = accept.`object` ?: throw IllegalActivityPubObjectException("object is null")
throw IllegalActivityPubObjectException("Invalid type ${value.type}") if (value.type.contains("Follow").not()) {
} throw IllegalActivityPubObjectException("Invalid type ${value.type}")
}
val follow = value as Follow val follow = value as Follow
val userUrl = follow.`object` ?: throw IllegalActivityPubObjectException("object is null") val userUrl = follow.`object` ?: throw IllegalActivityPubObjectException("object is null")
val followerUrl = follow.actor ?: throw IllegalActivityPubObjectException("actor is null") val followerUrl = follow.actor ?: throw IllegalActivityPubObjectException("actor is null")
val user = userQueryService.findByUrl(userUrl) val user = userQueryService.findByUrl(userUrl)
val follower = userQueryService.findByUrl(followerUrl) val follower = userQueryService.findByUrl(followerUrl)
userService.follow(user.id, follower.id) if (followerQueryService.alreadyFollow(user.id, follower.id)) {
return ActivityPubStringResponse(HttpStatusCode.OK, "accepted") return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "accepted")
}
userService.follow(user.id, follower.id)
ActivityPubStringResponse(HttpStatusCode.OK, "accepted")
}
} }
} }

View File

@ -7,6 +7,7 @@ import dev.usbharu.hideout.domain.model.ap.Note
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.plugins.getAp import dev.usbharu.hideout.plugins.getAp
import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.plugins.postAp
@ -83,11 +84,10 @@ class APNoteServiceImpl(
} }
override suspend fun fetchNote(url: String, targetActor: String?): Note { override suspend fun fetchNote(url: String, targetActor: String?): Note {
val post = postQueryService.findByUrl(url)
try { try {
val post = postQueryService.findByUrl(url)
return postToNote(post) return postToNote(post)
} catch (_: NoSuchElementException) { } catch (_: FailedToGetResourcesException) {
} catch (_: IllegalArgumentException) {
} }
val response = httpClient.getAp( val response = httpClient.getAp(
@ -119,23 +119,23 @@ class APNoteServiceImpl(
targetActor: String?, targetActor: String?,
url: String url: String
): Note { ): Note {
val findByApId = try { if (note.id == null) {
postQueryService.findByApId(url)
} catch (_: NoSuchElementException) {
return internalNote(note, targetActor, url) return internalNote(note, targetActor, url)
} catch (_: IllegalArgumentException) { }
val findByApId = try {
postQueryService.findByApId(note.id!!)
} catch (_: FailedToGetResourcesException) {
return internalNote(note, targetActor, url) return internalNote(note, targetActor, url)
} }
return postToNote(findByApId) return postToNote(findByApId)
} }
private suspend fun internalNote(note: Note, targetActor: String?, url: String): Note { private suspend fun internalNote(note: Note, targetActor: String?, url: String): Note {
val person = apUserService.fetchPerson( val person = apUserService.fetchPersonWithEntity(
note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"),
targetActor targetActor
) )
val user =
userQueryService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null"))
val visibility = val visibility =
if (note.to.contains(public) && note.cc.contains(public)) { if (note.to.contains(public) && note.cc.contains(public)) {
@ -156,7 +156,7 @@ class APNoteServiceImpl(
postRepository.save( postRepository.save(
Post( Post(
id = postRepository.generateId(), id = postRepository.generateId(),
userId = user.id, userId = person.second.id,
overview = null, overview = null,
text = note.content.orEmpty(), text = note.content.orEmpty(),
createdAt = Instant.parse(note.published).toEpochMilli(), createdAt = Instant.parse(note.published).toEpochMilli(),

View File

@ -6,6 +6,8 @@ import dev.usbharu.hideout.domain.model.ap.Image
import dev.usbharu.hideout.domain.model.ap.Key import dev.usbharu.hideout.domain.model.ap.Key
import dev.usbharu.hideout.domain.model.ap.Person import dev.usbharu.hideout.domain.model.ap.Person
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.plugins.getAp import dev.usbharu.hideout.plugins.getAp
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
@ -29,6 +31,8 @@ interface APUserService {
* @return * @return
*/ */
suspend fun fetchPerson(url: String, targetActor: String? = null): Person suspend fun fetchPerson(url: String, targetActor: String? = null): Person
suspend fun fetchPersonWithEntity(url: String, targetActor: String? = null): Pair<Person, User>
} }
@Single @Single
@ -67,11 +71,15 @@ class APUserServiceImpl(
id = "$userUrl#pubkey", id = "$userUrl#pubkey",
owner = userUrl, owner = userUrl,
publicKeyPem = userEntity.publicKey publicKeyPem = userEntity.publicKey
) ),
endpoints = mapOf("sharedInbox" to "${Config.configData.url}/inbox")
) )
} }
override suspend fun fetchPerson(url: String, targetActor: String?): Person { override suspend fun fetchPerson(url: String, targetActor: String?): Person =
fetchPersonWithEntity(url, targetActor).first
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> {
return try { return try {
val userEntity = userQueryService.findByUrl(url) val userEntity = userQueryService.findByUrl(url)
return Person( return Person(
@ -95,9 +103,10 @@ class APUserServiceImpl(
id = "$url#pubkey", id = "$url#pubkey",
owner = url, owner = url,
publicKeyPem = userEntity.publicKey publicKeyPem = userEntity.publicKey
) ),
) endpoints = mapOf("sharedInbox" to "${Config.configData.url}/inbox")
} catch (ignore: NoSuchElementException) { ) to userEntity
} catch (ignore: FailedToGetResourcesException) {
val httpResponse = if (targetActor != null) { val httpResponse = if (targetActor != null) {
httpClient.getAp(url, "$targetActor#pubkey") httpClient.getAp(url, "$targetActor#pubkey")
} else { } else {
@ -107,7 +116,7 @@ class APUserServiceImpl(
} }
val person = Config.configData.objectMapper.readValue<Person>(httpResponse.bodyAsText()) val person = Config.configData.objectMapper.readValue<Person>(httpResponse.bodyAsText())
userService.createRemoteUser( person to userService.createRemoteUser(
RemoteUserCreateDto( RemoteUserCreateDto(
name = person.preferredUsername name = person.preferredUsername
?: throw IllegalActivityPubObjectException("preferredUsername is null"), ?: throw IllegalActivityPubObjectException("preferredUsername is null"),
@ -122,7 +131,6 @@ class APUserServiceImpl(
?: throw IllegalActivityPubObjectException("publicKey is null"), ?: throw IllegalActivityPubObjectException("publicKey is null"),
) )
) )
person
} }
} }
} }

View File

@ -12,6 +12,7 @@ import dev.usbharu.hideout.service.user.UserService
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import kotlin.math.min import kotlin.math.min
@Suppress("TooManyFunctions")
interface UserApiService { interface UserApiService {
suspend fun findAll(limit: Int? = 100, offset: Long = 0): List<UserResponse> suspend fun findAll(limit: Int? = 100, offset: Long = 0): List<UserResponse>
@ -30,6 +31,9 @@ interface UserApiService {
suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse>
suspend fun createUser(username: String, password: String): UserResponse suspend fun createUser(username: String, password: String): UserResponse
suspend fun follow(targetId: Long, sourceId: Long): Boolean
suspend fun follow(targetAcct: Acct, sourceId: Long): Boolean
} }
@Single @Single
@ -39,30 +43,47 @@ class UserApiServiceImpl(
private val userService: UserService, private val userService: UserService,
private val transaction: Transaction private val transaction: Transaction
) : UserApiService { ) : UserApiService {
override suspend fun findAll(limit: Int?, offset: Long): List<UserResponse> = override suspend fun findAll(limit: Int?, offset: Long): List<UserResponse> = transaction.transaction {
userQueryService.findAll(min(limit ?: 100, 100), offset).map { UserResponse.from(it) } userQueryService.findAll(min(limit ?: 100, 100), offset).map { UserResponse.from(it) }
}
override suspend fun findById(id: Long): UserResponse = UserResponse.from(userQueryService.findById(id)) override suspend fun findById(id: Long): UserResponse =
transaction.transaction { UserResponse.from(userQueryService.findById(id)) }
override suspend fun findByIds(ids: List<Long>): List<UserResponse> = override suspend fun findByIds(ids: List<Long>): List<UserResponse> {
userQueryService.findByIds(ids).map { UserResponse.from(it) } return transaction.transaction {
userQueryService.findByIds(ids).map { UserResponse.from(it) }
}
}
override suspend fun findByAcct(acct: Acct): UserResponse = override suspend fun findByAcct(acct: Acct): UserResponse {
UserResponse.from(userQueryService.findByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)) return transaction.transaction {
UserResponse.from(
userQueryService.findByNameAndDomain(
acct.username,
acct.domain ?: Config.configData.domain
)
)
}
}
override suspend fun findFollowers(userId: Long): List<UserResponse> = override suspend fun findFollowers(userId: Long): List<UserResponse> = transaction.transaction {
followerQueryService.findFollowersById(userId).map { UserResponse.from(it) } followerQueryService.findFollowersById(userId).map { UserResponse.from(it) }
}
override suspend fun findFollowings(userId: Long): List<UserResponse> = override suspend fun findFollowings(userId: Long): List<UserResponse> = transaction.transaction {
followerQueryService.findFollowingById(userId).map { UserResponse.from(it) } followerQueryService.findFollowingById(userId).map { UserResponse.from(it) }
}
override suspend fun findFollowersByAcct(acct: Acct): List<UserResponse> = override suspend fun findFollowersByAcct(acct: Acct): List<UserResponse> = transaction.transaction {
followerQueryService.findFollowersByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain) followerQueryService.findFollowersByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)
.map { UserResponse.from(it) } .map { UserResponse.from(it) }
}
override suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> = override suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> = transaction.transaction {
followerQueryService.findFollowingByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain) followerQueryService.findFollowingByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)
.map { UserResponse.from(it) } .map { UserResponse.from(it) }
}
override suspend fun createUser(username: String, password: String): UserResponse { override suspend fun createUser(username: String, password: String): UserResponse {
return transaction.transaction { return transaction.transaction {
@ -72,4 +93,22 @@ class UserApiServiceImpl(
UserResponse.from(userService.createLocalUser(UserCreateDto(username, username, "", password))) UserResponse.from(userService.createLocalUser(UserCreateDto(username, username, "", password)))
} }
} }
override suspend fun follow(targetId: Long, sourceId: Long): Boolean {
return transaction.transaction {
userService.followRequest(targetId, sourceId)
}
}
override suspend fun follow(targetAcct: Acct, sourceId: Long): Boolean {
return transaction.transaction {
userService.followRequest(
userQueryService.findByNameAndDomain(
targetAcct.username,
targetAcct.domain ?: Config.configData.domain
).id,
sourceId
)
}
}
} }

View File

@ -0,0 +1,13 @@
package dev.usbharu.hideout.util
class CollectionUtil
fun <T> Iterable<T>.singleOr(block: (e: RuntimeException) -> Throwable): T {
return try {
this.single()
} catch (e: NoSuchElementException) {
throw block(e)
} catch (e: IllegalArgumentException) {
throw block(e)
}
}

View File

@ -4,7 +4,7 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="INFO"> <root level="TRACE">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>

View File

@ -4,6 +4,7 @@ import com.auth0.jwt.interfaces.Claim
import com.auth0.jwt.interfaces.Payload import com.auth0.jwt.interfaces.Payload
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.Acct
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.form.UserCreate import dev.usbharu.hideout.domain.model.hideout.form.UserCreate
@ -433,9 +434,7 @@ class UsersTest {
"https://example.com/test", "https://example.com/test",
Instant.now().toEpochMilli() Instant.now().toEpochMilli()
) )
} onBlocking { follow(any<Acct>(), eq(1234)) } doReturn true
val userService = mock<UserService> {
onBlocking { followRequest(eq(1235), eq(1234)) } doReturn true
} }
application { application {
configureSerialization() configureSerialization()
@ -448,7 +447,7 @@ class UsersTest {
} }
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(userService, userApiService) users(mock(), userApiService)
} }
} }
} }
@ -483,9 +482,7 @@ class UsersTest {
"https://example.com/test", "https://example.com/test",
Instant.now().toEpochMilli() Instant.now().toEpochMilli()
) )
} onBlocking { follow(any<Acct>(), eq(1234)) } doReturn false
val userService = mock<UserService> {
onBlocking { followRequest(eq(1235), eq(1234)) } doReturn false
} }
application { application {
configureSerialization() configureSerialization()
@ -498,7 +495,7 @@ class UsersTest {
} }
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(userService, userApiService) users(mock(), userApiService)
} }
} }
} }
@ -533,9 +530,7 @@ class UsersTest {
"https://example.com/test", "https://example.com/test",
Instant.now().toEpochMilli() Instant.now().toEpochMilli()
) )
} onBlocking { follow(eq(1235), eq(1234)) } doReturn false
val userService = mock<UserService> {
onBlocking { followRequest(eq(1235), eq(1234)) } doReturn false
} }
application { application {
configureSerialization() configureSerialization()
@ -548,7 +543,7 @@ class UsersTest {
} }
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
users(userService, userApiService) users(mock(), userApiService)
} }
} }
} }