Merge pull request #28 from usbharu/feature/refactor-query

Feature/refactor query
This commit is contained in:
usbharu 2023-08-11 16:09:45 +09:00 committed by GitHub
commit 7a8c3c95ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 1340 additions and 1395 deletions

View File

@ -12,22 +12,19 @@ import dev.usbharu.hideout.domain.model.job.DeliverReactionJob
import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.plugins.* import dev.usbharu.hideout.plugins.*
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.routing.register import dev.usbharu.hideout.routing.register
import dev.usbharu.hideout.service.activitypub.ActivityPubService import dev.usbharu.hideout.service.activitypub.ActivityPubService
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.api.IPostApiService import dev.usbharu.hideout.service.api.IPostApiService
import dev.usbharu.hideout.service.api.IUserApiService import dev.usbharu.hideout.service.api.IUserApiService
import dev.usbharu.hideout.service.api.UserAuthApiService
import dev.usbharu.hideout.service.api.WebFingerApiService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import dev.usbharu.hideout.service.auth.IJwtService import dev.usbharu.hideout.service.core.*
import dev.usbharu.hideout.service.core.IMetaService
import dev.usbharu.hideout.service.core.IServerInitialiseService
import dev.usbharu.hideout.service.core.IdGenerateService
import dev.usbharu.hideout.service.core.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.job.KJobJobQueueParentService import dev.usbharu.hideout.service.job.KJobJobQueueParentService
import dev.usbharu.hideout.service.reaction.IReactionService
import dev.usbharu.hideout.service.user.IUserAuthService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import dev.usbharu.kjob.exposed.ExposedKJob import dev.usbharu.kjob.exposed.ExposedKJob
import io.ktor.client.* import io.ktor.client.*
@ -79,7 +76,7 @@ fun Application.parent() {
level = LogLevel.INFO level = LogLevel.INFO
} }
install(httpSignaturePlugin) { install(httpSignaturePlugin) {
keyMap = KtorKeyMap(get()) keyMap = KtorKeyMap(get(), get())
} }
expectSuccess = true expectSuccess = true
} }
@ -95,6 +92,7 @@ fun Application.parent() {
} }
} }
configureKoin(module, HideoutModule().module) configureKoin(module, HideoutModule().module)
configureStatusPages()
runBlocking { runBlocking {
inject<IServerInitialiseService>().value.init() inject<IServerInitialiseService>().value.init()
} }
@ -103,7 +101,7 @@ fun Application.parent() {
configureStaticRouting() configureStaticRouting()
configureMonitoring() configureMonitoring()
configureSerialization() configureSerialization()
register(inject<IUserService>().value) register(inject<IUserApiService>().value)
configureSecurity( configureSecurity(
inject<JwkProvider>().value, inject<JwkProvider>().value,
@ -116,11 +114,11 @@ fun Application.parent() {
activityPubUserService = inject<ActivityPubUserService>().value, activityPubUserService = inject<ActivityPubUserService>().value,
postService = inject<IPostApiService>().value, postService = inject<IPostApiService>().value,
userApiService = inject<IUserApiService>().value, userApiService = inject<IUserApiService>().value,
reactionService = inject<IReactionService>().value, userQueryService = inject<UserQueryService>().value,
userAuthService = inject<IUserAuthService>().value, followerQueryService = inject<FollowerQueryService>().value,
userRepository = inject<IUserRepository>().value, userAuthApiService = inject<UserAuthApiService>().value,
jwtService = inject<IJwtService>().value, webFingerApiService = inject<WebFingerApiService>().value,
metaService = inject<IMetaService>().value transaction = inject<Transaction>().value
) )
} }

View File

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

View File

@ -1,14 +1,8 @@
package dev.usbharu.hideout.exception package dev.usbharu.hideout.exception
class NotInitException : Exception { class NotInitException : IllegalStateException {
constructor() : super() constructor() : super()
constructor(message: String?) : super(message) constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause) constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause) constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
} }

View File

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

View File

@ -2,7 +2,8 @@ package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.ap.JsonLd import dev.usbharu.hideout.domain.model.ap.JsonLd
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.UserAuthService import dev.usbharu.hideout.service.user.UserAuthService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.client.* import io.ktor.client.*
@ -164,20 +165,22 @@ val httpSignaturePlugin = createClientPlugin("HttpSign", ::HttpSignaturePluginCo
} }
} }
class KtorKeyMap(private val userAuthRepository: IUserRepository) : KeyMap { class KtorKeyMap(private val userQueryService: UserQueryService, private val transaction: Transaction) : KeyMap {
override fun getPublicKey(keyId: String?): PublicKey = runBlocking { override fun getPublicKey(keyId: String?): PublicKey = runBlocking {
val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey") val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey")
.substringAfterLast("/") .substringAfterLast("/")
val publicBytes = Base64.getDecoder().decode( val publicBytes = Base64.getDecoder().decode(
userAuthRepository.findByNameAndDomain( transaction.transaction {
userQueryService.findByNameAndDomain(
username, username,
Config.configData.domain Config.configData.domain
)?.run { ).run {
publicKey publicKey
.replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "")
.replace("\n", "") .replace("\n", "")
} }
}
) )
val x509EncodedKeySpec = X509EncodedKeySpec(publicBytes) val x509EncodedKeySpec = X509EncodedKeySpec(publicBytes)
return@runBlocking KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) return@runBlocking KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec)
@ -187,14 +190,16 @@ class KtorKeyMap(private val userAuthRepository: IUserRepository) : KeyMap {
val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey") val username = (keyId ?: throw IllegalArgumentException("keyId is null")).substringBeforeLast("#pubkey")
.substringAfterLast("/") .substringAfterLast("/")
val publicBytes = Base64.getDecoder().decode( val publicBytes = Base64.getDecoder().decode(
userAuthRepository.findByNameAndDomain( transaction.transaction {
userQueryService.findByNameAndDomain(
username, username,
Config.configData.domain Config.configData.domain
)?.privateKey?.run { ).privateKey?.run {
replace("-----BEGIN PRIVATE KEY-----", "") replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "")
.replace("\n", "") .replace("\n", "")
} }
}
) )
val x509EncodedKeySpec = PKCS8EncodedKeySpec(publicBytes) val x509EncodedKeySpec = PKCS8EncodedKeySpec(publicBytes)
return@runBlocking KeyFactory.getInstance("RSA").generatePrivate(x509EncodedKeySpec) return@runBlocking KeyFactory.getInstance("RSA").generatePrivate(x509EncodedKeySpec)

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.routing.activitypub.inbox import dev.usbharu.hideout.routing.activitypub.inbox
import dev.usbharu.hideout.routing.activitypub.outbox import dev.usbharu.hideout.routing.activitypub.outbox
import dev.usbharu.hideout.routing.activitypub.usersAP import dev.usbharu.hideout.routing.activitypub.usersAP
@ -12,11 +13,10 @@ import dev.usbharu.hideout.service.activitypub.ActivityPubService
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.api.IPostApiService import dev.usbharu.hideout.service.api.IPostApiService
import dev.usbharu.hideout.service.api.IUserApiService import dev.usbharu.hideout.service.api.IUserApiService
import dev.usbharu.hideout.service.api.UserAuthApiService
import dev.usbharu.hideout.service.api.WebFingerApiService
import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService import dev.usbharu.hideout.service.auth.HttpSignatureVerifyService
import dev.usbharu.hideout.service.auth.IJwtService import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.core.IMetaService
import dev.usbharu.hideout.service.reaction.IReactionService
import dev.usbharu.hideout.service.user.IUserAuthService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.* import io.ktor.server.plugins.autohead.*
@ -30,22 +30,22 @@ fun Application.configureRouting(
activityPubUserService: ActivityPubUserService, activityPubUserService: ActivityPubUserService,
postService: IPostApiService, postService: IPostApiService,
userApiService: IUserApiService, userApiService: IUserApiService,
reactionService: IReactionService, userQueryService: UserQueryService,
userAuthService: IUserAuthService, followerQueryService: FollowerQueryService,
userRepository: IUserRepository, userAuthApiService: UserAuthApiService,
jwtService: IJwtService, webFingerApiService: WebFingerApiService,
metaService: IMetaService transaction: Transaction
) { ) {
install(AutoHeadResponse) install(AutoHeadResponse)
routing { routing {
inbox(httpSignatureVerifyService, activityPubService) inbox(httpSignatureVerifyService, activityPubService)
outbox() outbox()
usersAP(activityPubUserService, userService) usersAP(activityPubUserService, userQueryService, followerQueryService, transaction)
webfinger(userService) webfinger(webFingerApiService)
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, reactionService) posts(postService)
users(userService, userApiService) users(userService, userApiService)
auth(userAuthService, userRepository, jwtService) auth(userAuthApiService)
} }
} }
} }

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.exception.InvalidUsernameOrPasswordException
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.* import io.ktor.server.plugins.statuspages.*
@ -10,8 +11,12 @@ fun Application.configureStatusPages() {
exception<IllegalArgumentException> { call, cause -> exception<IllegalArgumentException> { call, cause ->
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest) call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
} }
exception<InvalidUsernameOrPasswordException> { call, _ ->
call.respond(HttpStatusCode.Unauthorized)
}
exception<Throwable> { call, cause -> exception<Throwable> { call, cause ->
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) call.respondText(text = "500: ${cause.stackTraceToString()}", status = HttpStatusCode.InternalServerError)
cause.printStackTrace()
} }
} }
} }

View File

@ -0,0 +1,12 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.User
interface FollowerQueryService {
suspend fun findFollowersById(id: Long): List<User>
suspend fun findFollowersByNameAndDomain(name: String, domain: String): List<User>
suspend fun findFollowingById(id: Long): List<User>
suspend fun findFollowingByNameAndDomain(name: String, domain: String): List<User>
suspend fun appendFollower(user: Long, follower: Long)
suspend fun removeFollower(user: Long, follower: Long)
}

View File

@ -0,0 +1,203 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.UsersFollowers
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.koin.core.annotation.Single
import java.time.Instant
@Single
class FollowerQueryServiceImpl : FollowerQueryService {
override suspend fun findFollowersById(id: Long): List<User> {
val followers = Users.alias("FOLLOWERS")
return Users.innerJoin(
otherTable = UsersFollowers,
onColumn = { Users.id },
otherColumn = { userId }
)
.innerJoin(
otherTable = followers,
onColumn = { UsersFollowers.followerId },
otherColumn = { followers[Users.id] }
)
.slice(
followers[Users.id],
followers[Users.name],
followers[Users.domain],
followers[Users.screenName],
followers[Users.description],
followers[Users.password],
followers[Users.inbox],
followers[Users.outbox],
followers[Users.url],
followers[Users.publicKey],
followers[Users.privateKey],
followers[Users.createdAt]
)
.select { Users.id eq id }
.map {
User(
id = it[followers[Users.id]],
name = it[followers[Users.name]],
domain = it[followers[Users.domain]],
screenName = it[followers[Users.screenName]],
description = it[followers[Users.description]],
password = it[followers[Users.password]],
inbox = it[followers[Users.inbox]],
outbox = it[followers[Users.outbox]],
url = it[followers[Users.url]],
publicKey = it[followers[Users.publicKey]],
privateKey = it[followers[Users.privateKey]],
createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]])
)
}
}
override suspend fun findFollowersByNameAndDomain(name: String, domain: String): List<User> {
val followers = Users.alias("FOLLOWERS")
return Users.innerJoin(
otherTable = UsersFollowers,
onColumn = { Users.id },
otherColumn = { userId }
)
.innerJoin(
otherTable = followers,
onColumn = { UsersFollowers.followerId },
otherColumn = { followers[Users.id] }
)
.slice(
followers[Users.id],
followers[Users.name],
followers[Users.domain],
followers[Users.screenName],
followers[Users.description],
followers[Users.password],
followers[Users.inbox],
followers[Users.outbox],
followers[Users.url],
followers[Users.publicKey],
followers[Users.privateKey],
followers[Users.createdAt]
)
.select { Users.name eq name and (Users.domain eq domain) }
.map {
User(
id = it[followers[Users.id]],
name = it[followers[Users.name]],
domain = it[followers[Users.domain]],
screenName = it[followers[Users.screenName]],
description = it[followers[Users.description]],
password = it[followers[Users.password]],
inbox = it[followers[Users.inbox]],
outbox = it[followers[Users.outbox]],
url = it[followers[Users.url]],
publicKey = it[followers[Users.publicKey]],
privateKey = it[followers[Users.privateKey]],
createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]])
)
}
}
override suspend fun findFollowingById(id: Long): List<User> {
val followers = Users.alias("FOLLOWERS")
return Users.innerJoin(
otherTable = UsersFollowers,
onColumn = { Users.id },
otherColumn = { userId }
)
.innerJoin(
otherTable = followers,
onColumn = { UsersFollowers.followerId },
otherColumn = { followers[Users.id] }
)
.slice(
followers[Users.id],
followers[Users.name],
followers[Users.domain],
followers[Users.screenName],
followers[Users.description],
followers[Users.password],
followers[Users.inbox],
followers[Users.outbox],
followers[Users.url],
followers[Users.publicKey],
followers[Users.privateKey],
followers[Users.createdAt]
)
.select { followers[Users.id] eq id }
.map {
User(
id = it[followers[Users.id]],
name = it[followers[Users.name]],
domain = it[followers[Users.domain]],
screenName = it[followers[Users.screenName]],
description = it[followers[Users.description]],
password = it[followers[Users.password]],
inbox = it[followers[Users.inbox]],
outbox = it[followers[Users.outbox]],
url = it[followers[Users.url]],
publicKey = it[followers[Users.publicKey]],
privateKey = it[followers[Users.privateKey]],
createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]])
)
}
}
override suspend fun findFollowingByNameAndDomain(name: String, domain: String): List<User> {
val followers = Users.alias("FOLLOWERS")
return Users.innerJoin(
otherTable = UsersFollowers,
onColumn = { Users.id },
otherColumn = { userId }
)
.innerJoin(
otherTable = followers,
onColumn = { UsersFollowers.followerId },
otherColumn = { followers[Users.id] }
)
.slice(
followers[Users.id],
followers[Users.name],
followers[Users.domain],
followers[Users.screenName],
followers[Users.description],
followers[Users.password],
followers[Users.inbox],
followers[Users.outbox],
followers[Users.url],
followers[Users.publicKey],
followers[Users.privateKey],
followers[Users.createdAt]
)
.select { followers[Users.name] eq name and (followers[Users.domain] eq domain) }
.map {
User(
id = it[followers[Users.id]],
name = it[followers[Users.name]],
domain = it[followers[Users.domain]],
screenName = it[followers[Users.screenName]],
description = it[followers[Users.description]],
password = it[followers[Users.password]],
inbox = it[followers[Users.inbox]],
outbox = it[followers[Users.outbox]],
url = it[followers[Users.url]],
publicKey = it[followers[Users.publicKey]],
privateKey = it[followers[Users.privateKey]],
createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]])
)
}
}
override suspend fun appendFollower(user: Long, follower: Long) {
UsersFollowers.insert {
it[userId] = user
it[followerId] = follower
}
}
override suspend fun removeFollower(user: Long, follower: Long) {
UsersFollowers.deleteWhere { Users.id eq user and (followerId eq follower) }
}
}

View File

@ -0,0 +1,13 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken
interface JwtRefreshTokenQueryService {
suspend fun findById(id: Long): JwtRefreshToken
suspend fun findByToken(token: String): JwtRefreshToken
suspend fun findByUserId(userId: Long): JwtRefreshToken
suspend fun deleteById(id: Long)
suspend fun deleteByToken(token: String)
suspend fun deleteByUserId(userId: Long)
suspend fun deleteAll()
}

View File

@ -0,0 +1,38 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken
import dev.usbharu.hideout.repository.JwtRefreshTokens
import dev.usbharu.hideout.repository.toJwtRefreshToken
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteAll
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select
import org.koin.core.annotation.Single
@Single
class JwtRefreshTokenQueryServiceImpl : JwtRefreshTokenQueryService {
override suspend fun findById(id: Long): JwtRefreshToken =
JwtRefreshTokens.select { JwtRefreshTokens.id.eq(id) }.single().toJwtRefreshToken()
override suspend fun findByToken(token: String): JwtRefreshToken =
JwtRefreshTokens.select { JwtRefreshTokens.refreshToken.eq(token) }.single().toJwtRefreshToken()
override suspend fun findByUserId(userId: Long): JwtRefreshToken =
JwtRefreshTokens.select { JwtRefreshTokens.userId.eq(userId) }.single().toJwtRefreshToken()
override suspend fun deleteById(id: Long) {
JwtRefreshTokens.deleteWhere { JwtRefreshTokens.id eq id }
}
override suspend fun deleteByToken(token: String) {
JwtRefreshTokens.deleteWhere { refreshToken eq token }
}
override suspend fun deleteByUserId(userId: Long) {
JwtRefreshTokens.deleteWhere { JwtRefreshTokens.userId eq userId }
}
override suspend fun deleteAll() {
JwtRefreshTokens.deleteAll()
}
}

View File

@ -0,0 +1,9 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.Post
interface PostQueryService {
suspend fun findById(id: Long): Post
suspend fun findByUrl(url: String): Post
suspend fun findByApId(string: String): Post
}

View File

@ -0,0 +1,16 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.toPost
import org.jetbrains.exposed.sql.select
import org.koin.core.annotation.Single
@Single
class PostQueryServiceImpl : PostQueryService {
override suspend fun findById(id: Long): Post = Posts.select { Posts.id eq id }.single().toPost()
override suspend fun findByUrl(url: String): Post = Posts.select { Posts.url eq url }.single().toPost()
override suspend fun findByApId(string: String): Post = Posts.select { Posts.apId eq string }.single().toPost()
}

View File

@ -0,0 +1,37 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
@Suppress("LongParameterList")
interface PostResponseQueryService {
suspend fun findById(id: Long, userId: Long?): PostResponse
suspend fun findAll(
since: Long? = null,
until: Long? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<PostResponse>
suspend fun findByUserId(
userId: Long,
since: Long? = null,
until: Long? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId2: Long? = null
): List<PostResponse>
suspend fun findByUserNameAndUserDomain(
name: String,
domain: String,
since: Long? = null,
until: Long? = null,
minId: Long? = null,
maxId: Long? = null,
limit: Int? = null,
userId: Long? = null
): List<PostResponse>
}

View File

@ -0,0 +1,68 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.repository.toUser
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.koin.core.annotation.Single
@Single
class PostResponseQueryServiceImpl : PostResponseQueryService {
override suspend fun findById(id: Long, userId: Long?): PostResponse {
return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { Users.id })
.select { Posts.id eq id }
.single()
.let { PostResponse.from(it.toPost(), it.toUser()) }
}
override suspend fun findAll(
since: Long?,
until: Long?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<PostResponse> {
return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id })
.selectAll()
.map { PostResponse.from(it.toPost(), it.toUser()) }
}
override suspend fun findByUserId(
userId: Long,
since: Long?,
until: Long?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId2: Long?
): List<PostResponse> {
return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id })
.select { Posts.userId eq userId }
.map { PostResponse.from(it.toPost(), it.toUser()) }
}
override suspend fun findByUserNameAndUserDomain(
name: String,
domain: String,
since: Long?,
until: Long?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<PostResponse> {
return Posts
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id })
.select { Users.name eq name and (Users.domain eq domain) }
.map { PostResponse.from(it.toPost(), it.toUser()) }
}
}

View File

@ -0,0 +1,17 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
interface ReactionQueryService {
suspend fun findByPostId(postId: Long, userId: Long? = null): List<Reaction>
@Suppress("FunctionMaxLength")
suspend fun findByPostIdAndUserIdAndEmojiId(postId: Long, userId: Long, emojiId: Long): Reaction
suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean
suspend fun deleteByPostIdAndUserId(postId: Long, userId: Long)
suspend fun findByPostIdWithUsers(postId: Long, userId: Long? = null): List<ReactionResponse>
}

View File

@ -0,0 +1,54 @@
package dev.usbharu.hideout.query
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.entity.Reaction
import dev.usbharu.hideout.repository.Reactions
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toReaction
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.koin.core.annotation.Single
@Single
class ReactionQueryServiceImpl : ReactionQueryService {
override suspend fun findByPostId(postId: Long, userId: Long?): List<Reaction> {
return Reactions.select {
Reactions.postId.eq(postId)
}.map { it.toReaction() }
}
@Suppress("FunctionMaxLength")
override suspend fun findByPostIdAndUserIdAndEmojiId(postId: Long, userId: Long, emojiId: Long): Reaction {
return Reactions
.select {
Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and(
Reactions.emojiId.eq(emojiId)
)
}
.single()
.toReaction()
}
override suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean {
return Reactions.select {
Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and(
Reactions.emojiId.eq(emojiId)
)
}.empty().not()
}
override suspend fun deleteByPostIdAndUserId(postId: Long, userId: Long) {
Reactions.deleteWhere { Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)) }
}
override suspend fun findByPostIdWithUsers(postId: Long, userId: Long?): List<ReactionResponse> {
return Reactions
.leftJoin(Users, onColumn = { Reactions.userId }, otherColumn = { id })
.select { Reactions.postId.eq(postId) }
.groupBy { _: ResultRow -> ReactionResponse("", true, "", listOf()) }
.map { entry: Map.Entry<ReactionResponse, List<ResultRow>> ->
entry.key.copy(accounts = entry.value.map { Account(it[Users.screenName], "", it[Users.url]) })
}
}
}

View File

@ -0,0 +1,13 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.User
interface UserQueryService {
suspend fun findAll(limit: Int, offset: Long): List<User>
suspend fun findById(id: Long): User
suspend fun findByName(name: String): List<User>
suspend fun findByNameAndDomain(name: String, domain: String): User
suspend fun findByUrl(url: String): User
suspend fun findByIds(ids: List<Long>): List<User>
suspend fun existByNameAndDomain(name: String, domain: String): Boolean
}

View File

@ -0,0 +1,30 @@
package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toUser
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.koin.core.annotation.Single
@Single
class UserQueryServiceImpl : UserQueryService {
override suspend fun findAll(limit: Int, offset: Long): List<User> =
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 findByName(name: String): List<User> = Users.select { Users.name eq name }.map { it.toUser() }
override suspend fun findByNameAndDomain(name: String, domain: String): User =
Users.select { Users.name eq name and (Users.domain eq domain) }.single().toUser()
override suspend fun findByUrl(url: String): User = Users.select { Users.url eq url }.single().toUser()
override suspend fun findByIds(ids: List<Long>): List<User> =
Users.select { Users.id inList ids }.map { it.toUser() }
override suspend fun existByNameAndDomain(name: String, domain: String): Boolean =
Users.select { Users.name eq name and (Users.domain eq domain) }.empty().not()
}

View File

@ -8,13 +8,6 @@ interface IJwtRefreshTokenRepository {
suspend fun save(token: JwtRefreshToken) suspend fun save(token: JwtRefreshToken)
suspend fun findById(id: Long): JwtRefreshToken? suspend fun findById(id: Long): JwtRefreshToken?
suspend fun findByToken(token: String): JwtRefreshToken?
suspend fun findByUserId(userId: Long): JwtRefreshToken?
suspend fun delete(token: JwtRefreshToken) suspend fun delete(token: JwtRefreshToken)
suspend fun deleteById(id: Long)
suspend fun deleteByToken(token: String)
suspend fun deleteByUserId(userId: Long)
suspend fun deleteAll()
} }

View File

@ -1,44 +1,11 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import java.time.Instant
@Suppress("LongParameterList") @Suppress("LongParameterList")
interface IPostRepository { interface IPostRepository {
suspend fun generateId(): Long suspend fun generateId(): Long
suspend fun save(post: Post): Post suspend fun save(post: Post): Post
suspend fun findOneById(id: Long, userId: Long? = null): Post?
suspend fun findByUrl(url: String): Post?
suspend fun delete(id: Long) suspend fun delete(id: Long)
suspend fun findAll( suspend fun findById(id: Long): Post
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post>
suspend fun findByUserNameAndDomain(
username: String,
s: String,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post>
suspend fun findByUserId(
idOrNull: Long,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post>
suspend fun findByApId(id: String): Post?
} }

View File

@ -8,35 +8,10 @@ interface IUserRepository {
suspend fun findById(id: Long): User? suspend fun findById(id: Long): User?
suspend fun findByIds(ids: List<Long>): List<User>
suspend fun findByName(name: String): List<User>
suspend fun findByNameAndDomain(name: String, domain: String): User?
suspend fun findByDomain(domain: String): List<User>
suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User>
suspend fun findByUrl(url: String): User?
suspend fun findByUrls(urls: List<String>): List<User>
@Deprecated("", ReplaceWith("save(userEntity)"))
suspend fun update(userEntity: User) = save(userEntity)
suspend fun delete(id: Long) suspend fun delete(id: Long)
suspend fun findAll(): List<User>
suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long = 0): List<User>
suspend fun createFollower(id: Long, follower: Long)
suspend fun deleteFollower(id: Long, follower: Long)
suspend fun findFollowersById(id: Long): List<User>
suspend fun addFollowRequest(id: Long, follower: Long)
suspend fun deleteFollowRequest(id: Long, follower: Long) suspend fun deleteFollowRequest(id: Long, follower: Long)
suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean
suspend fun nextId(): Long suspend fun nextId(): Long

View File

@ -2,10 +2,8 @@ package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken
import dev.usbharu.hideout.service.core.IdGenerateService import dev.usbharu.hideout.service.core.IdGenerateService
import kotlinx.coroutines.Dispatchers
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.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.time.Instant import java.time.Instant
@ -24,14 +22,9 @@ class JwtRefreshTokenRepositoryImpl(
} }
} }
@Suppress("InjectDispatcher")
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun generateId(): Long = idGenerateService.generateId() override suspend fun generateId(): Long = idGenerateService.generateId()
override suspend fun save(token: JwtRefreshToken) { override suspend fun save(token: JwtRefreshToken) {
query {
if (JwtRefreshTokens.select { JwtRefreshTokens.id.eq(token.id) }.empty()) { if (JwtRefreshTokens.select { JwtRefreshTokens.id.eq(token.id) }.empty()) {
JwtRefreshTokens.insert { JwtRefreshTokens.insert {
it[id] = token.id it[id] = token.id
@ -49,55 +42,13 @@ class JwtRefreshTokenRepositoryImpl(
} }
} }
} }
}
override suspend fun findById(id: Long): JwtRefreshToken? { override suspend fun findById(id: Long): JwtRefreshToken? =
return query {
JwtRefreshTokens.select { JwtRefreshTokens.id.eq(id) }.singleOrNull()?.toJwtRefreshToken() JwtRefreshTokens.select { JwtRefreshTokens.id.eq(id) }.singleOrNull()?.toJwtRefreshToken()
}
}
override suspend fun findByToken(token: String): JwtRefreshToken? {
return query {
JwtRefreshTokens.select { JwtRefreshTokens.refreshToken.eq(token) }.singleOrNull()?.toJwtRefreshToken()
}
}
override suspend fun findByUserId(userId: Long): JwtRefreshToken? {
return query {
JwtRefreshTokens.select { JwtRefreshTokens.userId.eq(userId) }.singleOrNull()?.toJwtRefreshToken()
}
}
override suspend fun delete(token: JwtRefreshToken) { override suspend fun delete(token: JwtRefreshToken) {
return query {
JwtRefreshTokens.deleteWhere { id eq token.id } JwtRefreshTokens.deleteWhere { id eq token.id }
} }
}
override suspend fun deleteById(id: Long) {
return query {
JwtRefreshTokens.deleteWhere { JwtRefreshTokens.id eq id }
}
}
override suspend fun deleteByToken(token: String) {
return query {
JwtRefreshTokens.deleteWhere { refreshToken eq token }
}
}
override suspend fun deleteByUserId(userId: Long) {
return query {
JwtRefreshTokens.deleteWhere { JwtRefreshTokens.userId eq userId }
}
}
override suspend fun deleteAll() {
return query {
JwtRefreshTokens.deleteAll()
}
}
} }
fun ResultRow.toJwtRefreshToken(): JwtRefreshToken { fun ResultRow.toJwtRefreshToken(): JwtRefreshToken {

View File

@ -1,9 +1,7 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Jwt import dev.usbharu.hideout.domain.model.hideout.entity.Jwt
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.util.* import java.util.*
@ -18,12 +16,7 @@ class MetaRepositoryImpl(private val database: Database) : IMetaRepository {
} }
} }
@Suppress("InjectDispatcher")
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun save(meta: dev.usbharu.hideout.domain.model.hideout.entity.Meta) { override suspend fun save(meta: dev.usbharu.hideout.domain.model.hideout.entity.Meta) {
return query {
if (Meta.select { Meta.id eq 1 }.empty()) { if (Meta.select { Meta.id eq 1 }.empty()) {
Meta.insert { Meta.insert {
it[id] = 1 it[id] = 1
@ -41,18 +34,15 @@ class MetaRepositoryImpl(private val database: Database) : IMetaRepository {
} }
} }
} }
}
override suspend fun get(): dev.usbharu.hideout.domain.model.hideout.entity.Meta? { override suspend fun get(): dev.usbharu.hideout.domain.model.hideout.entity.Meta? {
return query { return Meta.select { Meta.id eq 1 }.singleOrNull()?.let {
Meta.select { Meta.id eq 1 }.singleOrNull()?.let {
dev.usbharu.hideout.domain.model.hideout.entity.Meta( dev.usbharu.hideout.domain.model.hideout.entity.Meta(
it[Meta.version], it[Meta.version],
Jwt(UUID.fromString(it[Meta.kid]), it[Meta.jwtPrivateKey], it[Meta.jwtPublicKey]) Jwt(UUID.fromString(it[Meta.kid]), it[Meta.jwtPrivateKey], it[Meta.jwtPublicKey])
) )
} }
} }
}
} }
object Meta : Table("meta_info") { object Meta : Table("meta_info") {

View File

@ -3,13 +3,10 @@ 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.service.core.IdGenerateService import dev.usbharu.hideout.service.core.IdGenerateService
import kotlinx.coroutines.Dispatchers
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.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.time.Instant
@Single @Single
class PostRepositoryImpl(database: Database, private val idGenerateService: IdGenerateService) : IPostRepository { class PostRepositoryImpl(database: Database, private val idGenerateService: IdGenerateService) : IPostRepository {
@ -23,12 +20,7 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
override suspend fun generateId(): Long = idGenerateService.generateId() override suspend fun generateId(): Long = idGenerateService.generateId()
@Suppress("InjectDispatcher")
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun save(post: Post): Post { override suspend fun save(post: Post): Post {
return query {
val singleOrNull = Posts.select { Posts.id eq post.id }.singleOrNull() val singleOrNull = Posts.select { Posts.id eq post.id }.singleOrNull()
if (singleOrNull == null) { if (singleOrNull == null) {
Posts.insert { Posts.insert {
@ -58,71 +50,14 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
it[apId] = post.apId it[apId] = post.apId
} }
} }
return@query post return post
}
} }
override suspend fun findOneById(id: Long, userId: Long?): Post? { override suspend fun findById(id: Long): Post = Posts.select { Posts.id eq id }.single().toPost()
return query {
Posts.select { Posts.id eq id }.singleOrNull()?.toPost()
}
}
override suspend fun findByUrl(url: String): Post? {
return query {
Posts.select { Posts.url eq url }.singleOrNull()?.toPost()
}
}
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
return query {
Posts.deleteWhere { Posts.id eq id } Posts.deleteWhere { Posts.id eq id }
} }
}
override suspend fun findAll(
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
return query {
Posts.select { Posts.visibility eq Visibility.PUBLIC.ordinal }.map { it.toPost() }
}
}
override suspend fun findByUserNameAndDomain(
username: String,
s: String,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
TODO("Not yet implemented")
}
override suspend fun findByUserId(
idOrNull: Long,
since: Instant?,
until: Instant?,
minId: Long?,
maxId: Long?,
limit: Int?,
userId: Long?
): List<Post> {
TODO("Not yet implemented")
}
override suspend fun findByApId(id: String): Post? {
return query {
Posts.select { Posts.apId eq id }.singleOrNull()?.toPost()
}
}
} }
object Posts : Table() { object Posts : Table() {

View File

@ -5,11 +5,5 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
interface ReactionRepository { interface ReactionRepository {
suspend fun generateId(): Long suspend fun generateId(): Long
suspend fun save(reaction: Reaction): Reaction suspend fun save(reaction: Reaction): Reaction
suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean
suspend fun findByPostId(postId: Long): List<Reaction>
suspend fun delete(reaction: Reaction): Reaction suspend fun delete(reaction: Reaction): Reaction
suspend fun deleteById(id: Long)
suspend fun deleteByPostId(postId: Long)
suspend fun deleteByUserId(userId: Long)
suspend fun deleteByPostIdAndUserId(postId: Long, userId: Long)
} }

View File

@ -2,11 +2,9 @@ package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.service.core.IdGenerateService import dev.usbharu.hideout.service.core.IdGenerateService
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.dao.id.LongIdTable
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.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@ -23,14 +21,9 @@ class ReactionRepositoryImpl(
} }
} }
@Suppress("InjectDispatcher")
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun generateId(): Long = idGenerateService.generateId() override suspend fun generateId(): Long = idGenerateService.generateId()
override suspend fun save(reaction: Reaction): Reaction { override suspend fun save(reaction: Reaction): Reaction {
query {
if (Reactions.select { Reactions.id eq reaction.id }.empty()) { if (Reactions.select { Reactions.id eq reaction.id }.empty()) {
Reactions.insert { Reactions.insert {
it[id] = reaction.id it[id] = reaction.id
@ -45,63 +38,18 @@ class ReactionRepositoryImpl(
it[userId] = reaction.userId it[userId] = reaction.userId
} }
} }
}
return reaction return reaction
} }
override suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean {
return query {
Reactions.select {
Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and(
Reactions.emojiId.eq(emojiId)
)
}.empty().not()
}
}
override suspend fun findByPostId(postId: Long): List<Reaction> {
return query {
Reactions.select {
Reactions.postId.eq(postId)
}.map { it.toReaction() }
}
}
override suspend fun delete(reaction: Reaction): Reaction { override suspend fun delete(reaction: Reaction): Reaction {
query {
Reactions.deleteWhere { Reactions.deleteWhere {
id.eq(reaction.id) id.eq(reaction.id)
.and(postId.eq(reaction.postId)) .and(postId.eq(reaction.postId))
.and(userId.eq(reaction.postId)) .and(userId.eq(reaction.postId))
.and(emojiId.eq(reaction.emojiId)) .and(emojiId.eq(reaction.emojiId))
} }
}
return reaction return reaction
} }
override suspend fun deleteById(id: Long) {
query {
Reactions.deleteWhere { Reactions.id.eq(id) }
}
}
override suspend fun deleteByPostId(postId: Long) {
query {
Reactions.deleteWhere { Reactions.postId.eq(postId) }
}
}
override suspend fun deleteByUserId(userId: Long) {
query {
Reactions.deleteWhere { Reactions.userId.eq(userId) }
}
}
override suspend fun deleteByPostIdAndUserId(postId: Long, userId: Long) {
query {
Reactions.deleteWhere { Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)) }
}
}
} }
fun ResultRow.toReaction(): Reaction { fun ResultRow.toReaction(): Reaction {

View File

@ -2,11 +2,9 @@ package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.service.core.IdGenerateService import dev.usbharu.hideout.service.core.IdGenerateService
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.dao.id.LongIdTable
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.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.time.Instant import java.time.Instant
@ -25,12 +23,7 @@ class UserRepository(private val database: Database, private val idGenerateServi
} }
} }
@Suppress("InjectDispatcher")
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun save(user: User): User { override suspend fun save(user: User): User {
return query {
val singleOrNull = Users.select { Users.id eq user.id }.singleOrNull() val singleOrNull = Users.select { Users.id eq user.id }.singleOrNull()
if (singleOrNull == null) { if (singleOrNull == null) {
Users.insert { Users.insert {
@ -62,171 +55,27 @@ class UserRepository(private val database: Database, private val idGenerateServi
it[privateKey] = user.privateKey it[privateKey] = user.privateKey
} }
} }
return@query user return user
}
}
override suspend fun createFollower(id: Long, follower: Long) {
return query {
UsersFollowers.insert {
it[userId] = id
it[followerId] = follower
}
}
} }
override suspend fun findById(id: Long): User? { override suspend fun findById(id: Long): User? {
return query { return Users.select { Users.id eq id }.map {
Users.select { Users.id eq id }.map {
it.toUser() it.toUser()
}.singleOrNull() }.singleOrNull()
} }
}
override suspend fun findByIds(ids: List<Long>): List<User> {
return query {
Users.select { Users.id inList ids }.map {
it.toUser()
}
}
}
override suspend fun findByName(name: String): List<User> {
return query {
Users.select { Users.name eq name }.map {
it.toUser()
}
}
}
override suspend fun findByNameAndDomain(name: String, domain: String): User? {
return query {
Users.select { Users.name eq name and (Users.domain eq domain) }.singleOrNull()?.toUser()
}
}
override suspend fun findByDomain(domain: String): List<User> {
return query {
Users.select { Users.domain eq domain }.map {
it.toUser()
}
}
}
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> {
return query {
val selectAll = Users.selectAll()
names.forEach { (name, domain) ->
selectAll.orWhere { Users.name eq name and (Users.domain eq domain) }
}
selectAll.map { it.toUser() }
}
}
override suspend fun findByUrl(url: String): User? {
return query {
Users.select { Users.url eq url }.singleOrNull()?.toUser()
}
}
override suspend fun findByUrls(urls: List<String>): List<User> {
return query {
Users.select { Users.url inList urls }.map { it.toUser() }
}
}
override suspend fun findFollowersById(id: Long): List<User> {
return query {
val followers = Users.alias("FOLLOWERS")
Users.innerJoin(
otherTable = UsersFollowers,
onColumn = { Users.id },
otherColumn = { userId }
)
.innerJoin(
otherTable = followers,
onColumn = { UsersFollowers.followerId },
otherColumn = { followers[Users.id] }
)
.slice(
followers.get(Users.id),
followers.get(Users.name),
followers.get(Users.domain),
followers.get(Users.screenName),
followers.get(Users.description),
followers.get(Users.password),
followers.get(Users.inbox),
followers.get(Users.outbox),
followers.get(Users.url),
followers.get(Users.publicKey),
followers.get(Users.privateKey),
followers.get(Users.createdAt)
)
.select { Users.id eq id }
.map {
User(
id = it[followers[Users.id]],
name = it[followers[Users.name]],
domain = it[followers[Users.domain]],
screenName = it[followers[Users.screenName]],
description = it[followers[Users.description]],
password = it[followers[Users.password]],
inbox = it[followers[Users.inbox]],
outbox = it[followers[Users.outbox]],
url = it[followers[Users.url]],
publicKey = it[followers[Users.publicKey]],
privateKey = it[followers[Users.privateKey]],
createdAt = Instant.ofEpochMilli(it[followers[Users.createdAt]])
)
}
}
}
override suspend fun addFollowRequest(id: Long, follower: Long) {
query {
FollowRequests.insert {
it[userId] = id
it[followerId] = follower
}
}
}
override suspend fun deleteFollowRequest(id: Long, follower: Long) { override suspend fun deleteFollowRequest(id: Long, follower: Long) {
query {
FollowRequests.deleteWhere { userId.eq(id) and followerId.eq(follower) } FollowRequests.deleteWhere { userId.eq(id) and followerId.eq(follower) }
} }
}
override suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean { override suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean {
return query { return FollowRequests.select { (FollowRequests.userId eq id) and (FollowRequests.followerId eq follower) }
FollowRequests.select { (FollowRequests.userId eq id) and (FollowRequests.followerId eq follower) }
.singleOrNull() != null .singleOrNull() != null
} }
}
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
query {
Users.deleteWhere { Users.id.eq(id) } Users.deleteWhere { Users.id.eq(id) }
} }
}
override suspend fun deleteFollower(id: Long, follower: Long) {
query {
UsersFollowers.deleteWhere { (userId eq id).and(followerId eq follower) }
}
}
override suspend fun findAll(): List<User> {
return query {
Users.selectAll().map { it.toUser() }
}
}
override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<User> {
return query {
Users.selectAll().limit(limit, offset).map { it.toUser() }
}
}
override suspend fun nextId(): Long = idGenerateService.generateId() override suspend fun nextId(): Long = idGenerateService.generateId()
} }

View File

@ -1,7 +1,6 @@
package dev.usbharu.hideout.routing package dev.usbharu.hideout.routing
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto import dev.usbharu.hideout.service.api.IUserApiService
import dev.usbharu.hideout.service.user.IUserService
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
@ -9,7 +8,7 @@ import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
fun Application.register(userService: IUserService) { fun Application.register(userApiService: IUserApiService) {
routing { routing {
get("/register") { get("/register") {
val principal = call.principal<UserIdPrincipal>() val principal = call.principal<UserIdPrincipal>()
@ -37,10 +36,7 @@ fun Application.register(userService: IUserService) {
val parameters = call.receiveParameters() val parameters = call.receiveParameters()
val password = parameters["password"] ?: return@post call.respondRedirect("/register") val password = parameters["password"] ?: return@post call.respondRedirect("/register")
val username = parameters["username"] ?: return@post call.respondRedirect("/register") val username = parameters["username"] ?: return@post call.respondRedirect("/register")
if (userService.usernameAlreadyUse(username)) { userApiService.createUser(username, password)
return@post call.respondRedirect("/register")
}
userService.createLocalUser(UserCreateDto(username, username, "", password))
call.respondRedirect("/users/$username") call.respondRedirect("/users/$username")
} }
} }

View File

@ -1,9 +1,12 @@
package dev.usbharu.hideout.routing.activitypub package dev.usbharu.hideout.routing.activitypub
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.exception.ParameterNotExistException import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.plugins.respondAp import dev.usbharu.hideout.plugins.respondAp
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import dev.usbharu.hideout.util.HttpUtil.JsonLd import dev.usbharu.hideout.util.HttpUtil.JsonLd
import io.ktor.http.* import io.ktor.http.*
@ -12,7 +15,12 @@ import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
fun Routing.usersAP(activityPubUserService: ActivityPubUserService, userService: IUserService) { fun Routing.usersAP(
activityPubUserService: ActivityPubUserService,
userQueryService: UserQueryService,
followerQueryService: FollowerQueryService,
transaction: Transaction
) {
route("/users/{name}") { route("/users/{name}") {
createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle { createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle {
call.application.log.debug("Signature: ${call.request.header("Signature")}") call.application.log.debug("Signature: ${call.request.header("Signature")}")
@ -26,10 +34,15 @@ fun Routing.usersAP(activityPubUserService: ActivityPubUserService, userService:
) )
} }
get { get {
val userEntity = userService.findByNameLocalUser( // TODO: 暫定処置なので治す
call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.") transaction.transaction {
val userEntity = userQueryService.findByNameAndDomain(
call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='name') does not exist."),
Config.configData.domain
) )
call.respondText(userEntity.toString() + "\n" + userService.findFollowersById(userEntity.id)) call.respondText(userEntity.toString() + "\n" + followerQueryService.findFollowersById(userEntity.id))
}
} }
} }
} }

View File

@ -1,14 +1,9 @@
package dev.usbharu.hideout.routing.api.internal.v1 package dev.usbharu.hideout.routing.api.internal.v1
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.plugins.TOKEN_AUTH import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.service.api.UserAuthApiService
import dev.usbharu.hideout.service.auth.IJwtService
import dev.usbharu.hideout.service.user.IUserAuthService
import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.* import io.ktor.server.auth.jwt.*
@ -16,27 +11,15 @@ import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
fun Route.auth( fun Route.auth(userAuthApiService: UserAuthApiService) {
userAuthService: IUserAuthService,
userRepository: IUserRepository,
jwtService: IJwtService
) {
post("/login") { post("/login") {
val loginUser = call.receive<UserLogin>() val loginUser = call.receive<UserLogin>()
val check = userAuthService.verifyAccount(loginUser.username, loginUser.password) return@post call.respond(userAuthApiService.login(loginUser.username, loginUser.password))
if (check.not()) {
return@post call.respond(HttpStatusCode.Unauthorized)
}
val user = userRepository.findByNameAndDomain(loginUser.username, Config.configData.domain)
?: throw UserNotFoundException("${loginUser.username} was not found.")
return@post call.respond(jwtService.createToken(user))
} }
post("/refresh-token") { post("/refresh-token") {
val refreshToken = call.receive<RefreshToken>() val refreshToken = call.receive<RefreshToken>()
return@post call.respond(jwtService.refreshToken(refreshToken)) return@post call.respond(userAuthApiService.refreshToken(refreshToken))
} }
authenticate(TOKEN_AUTH) { authenticate(TOKEN_AUTH) {
get("/auth-check") { get("/auth-check") {

View File

@ -5,7 +5,6 @@ import dev.usbharu.hideout.domain.model.hideout.form.Reaction
import dev.usbharu.hideout.exception.ParameterNotExistException import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.plugins.TOKEN_AUTH import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.service.api.IPostApiService import dev.usbharu.hideout.service.api.IPostApiService
import dev.usbharu.hideout.service.reaction.IReactionService
import dev.usbharu.hideout.util.InstantParseUtil import dev.usbharu.hideout.util.InstantParseUtil
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
@ -16,7 +15,7 @@ import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@Suppress("LongMethod") @Suppress("LongMethod")
fun Route.posts(postApiService: IPostApiService, reactionService: IReactionService) { fun Route.posts(postApiService: IPostApiService) {
route("/posts") { route("/posts") {
authenticate(TOKEN_AUTH) { authenticate(TOKEN_AUTH) {
post { post {
@ -36,7 +35,7 @@ fun Route.posts(postApiService: IPostApiService, reactionService: IReactionServi
call.parameters["id"]?.toLong() call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.") ?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
) )
call.respond(reactionService.findByPostIdForUser(postId, userId)) call.respond(postApiService.getReactionByPostId(postId, userId))
} }
post { post {
val jwtPrincipal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal") val jwtPrincipal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
@ -45,11 +44,11 @@ fun Route.posts(postApiService: IPostApiService, reactionService: IReactionServi
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.") ?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
val reaction = try { val reaction = try {
call.receive<Reaction>() call.receive<Reaction>()
} catch (e: ContentTransformationException) { } catch (_: ContentTransformationException) {
Reaction(null) Reaction(null)
} }
reactionService.sendReaction(reaction.reaction ?: "", userId, postId) postApiService.appendReaction(reaction.reaction ?: "", userId, postId)
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
delete { delete {
@ -57,7 +56,7 @@ fun Route.posts(postApiService: IPostApiService, reactionService: IReactionServi
val userId = jwtPrincipal.payload.getClaim("uid").asLong() val userId = jwtPrincipal.payload.getClaim("uid").asLong()
val postId = call.parameters["id"]?.toLong() val postId = call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.") ?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
reactionService.removeReaction(userId, postId) postApiService.removeReaction(userId, postId)
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
} }

View File

@ -16,7 +16,7 @@ import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@Suppress("LongMethod") @Suppress("LongMethod", "CognitiveComplexMethod")
fun Route.users(userService: IUserService, userApiService: IUserApiService) { fun Route.users(userService: IUserService, userApiService: IUserApiService) {
route("/users") { route("/users") {
get { get {

View File

@ -4,14 +4,14 @@ import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.wellknown.WebFinger import dev.usbharu.hideout.domain.model.wellknown.WebFinger
import dev.usbharu.hideout.exception.IllegalParameterException import dev.usbharu.hideout.exception.IllegalParameterException
import dev.usbharu.hideout.exception.ParameterNotExistException import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.api.WebFingerApiService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
fun Routing.webfinger(userService: IUserService) { fun Routing.webfinger(webFingerApiService: WebFingerApiService) {
route("/.well-known/webfinger") { route("/.well-known/webfinger") {
get { get {
val acct = call.request.queryParameters["resource"]?.decodeURLPart() val acct = call.request.queryParameters["resource"]?.decodeURLPart()
@ -25,7 +25,7 @@ fun Routing.webfinger(userService: IUserService) {
.substringAfter("acct:") .substringAfter("acct:")
.trimStart('@') .trimStart('@')
val userEntity = userService.findByNameLocalUser(accountName) val userEntity = webFingerApiService.findByNameAndDomain(accountName, Config.configData.domain)
val webFinger = WebFinger( val webFinger = WebFinger(
subject = acct, subject = acct,

View File

@ -5,12 +5,16 @@ 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.UserQueryService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import io.ktor.http.* import io.ktor.http.*
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@Single @Single
class ActivityPubAcceptServiceImpl(private val userService: IUserService) : ActivityPubAcceptService { class ActivityPubAcceptServiceImpl(
private val userService: IUserService,
private val userQueryService: UserQueryService
) : ActivityPubAcceptService {
override suspend fun receiveAccept(accept: Accept): ActivityPubResponse { override suspend fun receiveAccept(accept: Accept): ActivityPubResponse {
val value = accept.`object` ?: throw IllegalActivityPubObjectException("object is null") val value = accept.`object` ?: throw IllegalActivityPubObjectException("object is null")
if (value.type.contains("Follow").not()) { if (value.type.contains("Follow").not()) {
@ -20,8 +24,8 @@ class ActivityPubAcceptServiceImpl(private val userService: IUserService) : Acti
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 = userService.findByUrl(userUrl) val user = userQueryService.findByUrl(userUrl)
val follower = userService.findByUrl(followerUrl) val follower = userQueryService.findByUrl(followerUrl)
userService.follow(user.id, follower.id) userService.follow(user.id, follower.id)
return ActivityPubStringResponse(HttpStatusCode.OK, "accepted") return ActivityPubStringResponse(HttpStatusCode.OK, "accepted")
} }

View File

@ -5,12 +5,14 @@ import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
import dev.usbharu.hideout.domain.model.ap.Create import dev.usbharu.hideout.domain.model.ap.Create
import dev.usbharu.hideout.domain.model.ap.Note import dev.usbharu.hideout.domain.model.ap.Note
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.service.core.Transaction
import io.ktor.http.* import io.ktor.http.*
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@Single @Single
class ActivityPubCreateServiceImpl( class ActivityPubCreateServiceImpl(
private val activityPubNoteService: ActivityPubNoteService private val activityPubNoteService: ActivityPubNoteService,
private val transaction: Transaction
) : ActivityPubCreateService { ) : ActivityPubCreateService {
override suspend fun receiveCreate(create: Create): ActivityPubResponse { override suspend fun receiveCreate(create: Create): ActivityPubResponse {
val value = create.`object` ?: throw IllegalActivityPubObjectException("object is null") val value = create.`object` ?: throw IllegalActivityPubObjectException("object is null")
@ -18,8 +20,10 @@ class ActivityPubCreateServiceImpl(
throw IllegalActivityPubObjectException("object is not Note") throw IllegalActivityPubObjectException("object is not Note")
} }
return transaction.transaction {
val note = value as Note val note = value as Note
activityPubNoteService.fetchNote(note) activityPubNoteService.fetchNote(note)
return ActivityPubStringResponse(HttpStatusCode.OK, "Created") ActivityPubStringResponse(HttpStatusCode.OK, "Created")
}
} }
} }

View File

@ -3,11 +3,11 @@ package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.domain.model.ActivityPubResponse import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ActivityPubStringResponse import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
import dev.usbharu.hideout.domain.model.ap.Like import dev.usbharu.hideout.domain.model.ap.Like
import dev.usbharu.hideout.exception.PostNotFoundException
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.repository.IPostRepository import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.reaction.IReactionService import dev.usbharu.hideout.service.reaction.IReactionService
import dev.usbharu.hideout.service.user.IUserService
import io.ktor.http.* import io.ktor.http.*
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@ -15,26 +15,28 @@ import org.koin.core.annotation.Single
class ActivityPubLikeServiceImpl( class ActivityPubLikeServiceImpl(
private val reactionService: IReactionService, private val reactionService: IReactionService,
private val activityPubUserService: ActivityPubUserService, private val activityPubUserService: ActivityPubUserService,
private val userService: IUserService, private val activityPubNoteService: ActivityPubNoteService,
private val postService: IPostRepository, private val userQueryService: UserQueryService,
private val activityPubNoteService: ActivityPubNoteService private val postQueryService: PostQueryService,
private val transaction: Transaction
) : ActivityPubLikeService { ) : ActivityPubLikeService {
override suspend fun receiveLike(like: Like): ActivityPubResponse { override suspend fun receiveLike(like: Like): ActivityPubResponse {
val actor = like.actor ?: throw IllegalActivityPubObjectException("actor is null") val actor = like.actor ?: throw IllegalActivityPubObjectException("actor is null")
val content = like.content ?: throw IllegalActivityPubObjectException("content is null") val content = like.content ?: throw IllegalActivityPubObjectException("content is null")
like.`object` ?: throw IllegalActivityPubObjectException("object is null") like.`object` ?: throw IllegalActivityPubObjectException("object is null")
transaction.transaction {
val person = activityPubUserService.fetchPerson(actor) val person = activityPubUserService.fetchPerson(actor)
activityPubNoteService.fetchNote(like.`object`!!) activityPubNoteService.fetchNote(like.`object`!!)
val user = userService.findByUrl( val user = userQueryService.findByUrl(
person.url person.url
?: throw IllegalActivityPubObjectException("actor is not found") ?: throw IllegalActivityPubObjectException("actor is not found")
) )
val post = postService.findByUrl(like.`object`!!) val post = postQueryService.findByUrl(like.`object`!!)
?: throw PostNotFoundException("${like.`object`} was not found")
reactionService.receiveReaction(content, actor.substringAfter("://").substringBefore("/"), user.id, post.id) reactionService.receiveReaction(content, actor.substringAfter("://").substringBefore("/"), user.id, post.id)
}
return ActivityPubStringResponse(HttpStatusCode.OK, "") return ActivityPubStringResponse(HttpStatusCode.OK, "")
} }
} }

View File

@ -10,9 +10,11 @@ import dev.usbharu.hideout.domain.model.job.DeliverPostJob
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
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.IPostRepository import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.user.IUserService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import kjob.core.job.JobProps import kjob.core.job.JobProps
@ -24,16 +26,18 @@ import java.time.Instant
class ActivityPubNoteServiceImpl( class ActivityPubNoteServiceImpl(
private val httpClient: HttpClient, private val httpClient: HttpClient,
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
private val userService: IUserService,
private val postRepository: IPostRepository, private val postRepository: IPostRepository,
private val activityPubUserService: ActivityPubUserService private val activityPubUserService: ActivityPubUserService,
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val postQueryService: PostQueryService
) : ActivityPubNoteService { ) : ActivityPubNoteService {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
override suspend fun createNote(post: Post) { override suspend fun createNote(post: Post) {
val followers = userService.findFollowersById(post.userId) val followers = followerQueryService.findFollowersById(post.userId)
val userEntity = userService.findById(post.userId) val userEntity = userQueryService.findById(post.userId)
val note = Config.configData.objectMapper.writeValueAsString(post) val note = Config.configData.objectMapper.writeValueAsString(post)
followers.forEach { followerEntity -> followers.forEach { followerEntity ->
jobQueueParentService.schedule(DeliverPostJob) { jobQueueParentService.schedule(DeliverPostJob) {
@ -70,7 +74,7 @@ class ActivityPubNoteServiceImpl(
} }
override suspend fun fetchNote(url: String, targetActor: String?): Note { override suspend fun fetchNote(url: String, targetActor: String?): Note {
val post = postRepository.findByUrl(url) val post = postQueryService.findByUrl(url)
if (post != null) { if (post != null) {
return postToNote(post) return postToNote(post)
} }
@ -83,8 +87,8 @@ class ActivityPubNoteServiceImpl(
} }
private suspend fun postToNote(post: Post): Note { private suspend fun postToNote(post: Post): Note {
val user = userService.findById(post.userId) val user = userQueryService.findById(post.userId)
val reply = post.replyId?.let { postRepository.findOneById(it) } val reply = post.replyId?.let { postQueryService.findById(it) }
return Note( return Note(
name = "Post", name = "Post",
id = post.apId, id = post.apId,
@ -98,21 +102,28 @@ class ActivityPubNoteServiceImpl(
) )
} }
private suspend fun ActivityPubNoteServiceImpl.note( private suspend fun note(
note: Note, note: Note,
targetActor: String?, targetActor: String?,
url: String url: String
): Note { ): Note {
val findByApId = postRepository.findByApId(url) val findByApId = try {
if (findByApId != null) { postQueryService.findByApId(url)
} catch (_: NoSuchElementException) {
return internalNote(note, targetActor, url)
} catch (_: IllegalArgumentException) {
return internalNote(note, targetActor, url)
}
return postToNote(findByApId) return postToNote(findByApId)
} }
private suspend fun internalNote(note: Note, targetActor: String?, url: String): Note {
val person = activityPubUserService.fetchPerson( val person = activityPubUserService.fetchPerson(
note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"),
targetActor targetActor
) )
val user = val user =
userService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null")) 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)) {
@ -127,7 +138,7 @@ class ActivityPubNoteServiceImpl(
val reply = note.inReplyTo?.let { val reply = note.inReplyTo?.let {
fetchNote(it, targetActor) fetchNote(it, targetActor)
postRepository.findByUrl(it) postQueryService.findByUrl(it)
} }
postRepository.save( postRepository.save(

View File

@ -7,11 +7,12 @@ import dev.usbharu.hideout.domain.model.ap.Undo
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.domain.model.job.DeliverReactionJob import dev.usbharu.hideout.domain.model.job.DeliverReactionJob
import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob
import dev.usbharu.hideout.exception.PostNotFoundException
import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.plugins.postAp
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.IPostRepository import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.user.IUserService
import io.ktor.client.* import io.ktor.client.*
import kjob.core.job.JobProps import kjob.core.job.JobProps
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@ -19,16 +20,18 @@ import java.time.Instant
@Single @Single
class ActivityPubReactionServiceImpl( class ActivityPubReactionServiceImpl(
private val userService: IUserService,
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
private val iPostRepository: IPostRepository, private val iPostRepository: IPostRepository,
private val httpClient: HttpClient private val httpClient: HttpClient,
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val postQueryService: PostQueryService
) : ActivityPubReactionService { ) : ActivityPubReactionService {
override suspend fun reaction(like: Reaction) { override suspend fun reaction(like: Reaction) {
val followers = userService.findFollowersById(like.userId) val followers = followerQueryService.findFollowersById(like.userId)
val user = userService.findById(like.userId) val user = userQueryService.findById(like.userId)
val post = val post =
iPostRepository.findOneById(like.postId) ?: throw PostNotFoundException("${like.postId} was not found.") postQueryService.findById(like.postId)
followers.forEach { follower -> followers.forEach { follower ->
jobQueueParentService.schedule(DeliverReactionJob) { jobQueueParentService.schedule(DeliverReactionJob) {
props[it.actor] = user.url props[it.actor] = user.url
@ -41,10 +44,10 @@ class ActivityPubReactionServiceImpl(
} }
override suspend fun removeReaction(like: Reaction) { override suspend fun removeReaction(like: Reaction) {
val followers = userService.findFollowersById(like.userId) val followers = followerQueryService.findFollowersById(like.userId)
val user = userService.findById(like.userId) val user = userQueryService.findById(like.userId)
val post = val post =
iPostRepository.findOneById(like.postId) ?: throw PostNotFoundException("${like.postId} was not found.") postQueryService.findById(like.postId)
followers.forEach { follower -> followers.forEach { follower ->
jobQueueParentService.schedule(DeliverRemoveReactionJob) { jobQueueParentService.schedule(DeliverRemoveReactionJob) {
props[it.actor] = user.url props[it.actor] = user.url

View File

@ -8,6 +8,8 @@ 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.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.plugins.postAp
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import io.ktor.client.* import io.ktor.client.*
@ -20,7 +22,9 @@ class ActivityPubReceiveFollowServiceImpl(
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
private val activityPubUserService: ActivityPubUserService, private val activityPubUserService: ActivityPubUserService,
private val userService: IUserService, private val userService: IUserService,
private val httpClient: HttpClient private val httpClient: HttpClient,
private val userQueryService: UserQueryService,
private val transaction: Transaction
) : ActivityPubReceiveFollowService { ) : ActivityPubReceiveFollowService {
override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { override suspend fun receiveFollow(follow: Follow): ActivityPubResponse {
// TODO: Verify HTTP Signature // TODO: Verify HTTP Signature
@ -33,6 +37,7 @@ class ActivityPubReceiveFollowServiceImpl(
} }
override suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) { override suspend fun receiveFollowJob(props: JobProps<ReceiveFollowJob>) {
transaction.transaction {
val actor = props[ReceiveFollowJob.actor] val actor = props[ReceiveFollowJob.actor]
val targetActor = props[ReceiveFollowJob.targetActor] val targetActor = props[ReceiveFollowJob.targetActor]
val person = activityPubUserService.fetchPerson(actor, targetActor) val person = activityPubUserService.fetchPerson(actor, targetActor)
@ -46,9 +51,12 @@ class ActivityPubReceiveFollowServiceImpl(
actor = targetActor actor = targetActor
) )
) )
val users =
userService.findByUrls(listOf(targetActor, follow.actor ?: throw IllegalArgumentException("actor is null")))
userService.followRequest(users.first { it.url == targetActor }.id, users.first { it.url == follow.actor }.id) val targetEntity = userQueryService.findByUrl(targetActor)
val followActorEntity =
userQueryService.findByUrl(follow.actor ?: throw java.lang.IllegalArgumentException("Actor is null"))
userService.followRequest(targetEntity.id, followActorEntity.id)
}
} }
} }

View File

@ -4,6 +4,8 @@ import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ActivityPubStringResponse import dev.usbharu.hideout.domain.model.ActivityPubStringResponse
import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.ap.Undo import dev.usbharu.hideout.domain.model.ap.Undo
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import io.ktor.http.* import io.ktor.http.*
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@ -12,7 +14,9 @@ import org.koin.core.annotation.Single
@Suppress("UnsafeCallOnNullableType") @Suppress("UnsafeCallOnNullableType")
class ActivityPubUndoServiceImpl( class ActivityPubUndoServiceImpl(
private val userService: IUserService, private val userService: IUserService,
private val activityPubUserService: ActivityPubUserService private val activityPubUserService: ActivityPubUserService,
private val userQueryService: UserQueryService,
private val transaction: Transaction
) : ActivityPubUndoService { ) : ActivityPubUndoService {
override suspend fun receiveUndo(undo: Undo): ActivityPubResponse { override suspend fun receiveUndo(undo: Undo): ActivityPubResponse {
if (undo.actor == null) { if (undo.actor == null) {
@ -31,12 +35,13 @@ class ActivityPubUndoServiceImpl(
if (follow.`object` == null) { if (follow.`object` == null) {
return ActivityPubStringResponse(HttpStatusCode.BadRequest, "object.object is null") return ActivityPubStringResponse(HttpStatusCode.BadRequest, "object.object is null")
} }
transaction.transaction {
activityPubUserService.fetchPerson(undo.actor!!, follow.`object`) activityPubUserService.fetchPerson(undo.actor!!, follow.`object`)
val follower = userService.findByUrl(undo.actor!!) val follower = userQueryService.findByUrl(undo.actor!!)
val target = userService.findByUrl(follow.`object`!!) val target = userQueryService.findByUrl(follow.`object`!!)
userService.unfollow(target.id, follower.id) userService.unfollow(target.id, follower.id)
} }
}
else -> {} else -> {}
} }

View File

@ -6,9 +6,10 @@ 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.exception.UserNotFoundException
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.service.core.Transaction
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import io.ktor.client.* import io.ktor.client.*
@ -20,13 +21,17 @@ import org.koin.core.annotation.Single
@Single @Single
class ActivityPubUserServiceImpl( class ActivityPubUserServiceImpl(
private val userService: IUserService, private val userService: IUserService,
private val httpClient: HttpClient private val httpClient: HttpClient,
private val userQueryService: UserQueryService,
private val transaction: Transaction
) : ) :
ActivityPubUserService { ActivityPubUserService {
override suspend fun getPersonByName(name: String): Person { override suspend fun getPersonByName(name: String): Person {
val userEntity = transaction.transaction {
userQueryService.findByNameAndDomain(name, Config.configData.domain)
}
// TODO: JOINで書き直し // TODO: JOINで書き直し
val userEntity = userService.findByNameLocalUser(name)
val userUrl = "${Config.configData.url}/users/$name" val userUrl = "${Config.configData.url}/users/$name"
return Person( return Person(
type = emptyList(), type = emptyList(),
@ -55,7 +60,7 @@ class ActivityPubUserServiceImpl(
override suspend fun fetchPerson(url: String, targetActor: String?): Person { override suspend fun fetchPerson(url: String, targetActor: String?): Person {
return try { return try {
val userEntity = userService.findByUrl(url) val userEntity = userQueryService.findByUrl(url)
return Person( return Person(
type = emptyList(), type = emptyList(),
name = userEntity.name, name = userEntity.name,
@ -79,7 +84,7 @@ class ActivityPubUserServiceImpl(
publicKeyPem = userEntity.publicKey publicKeyPem = userEntity.publicKey
) )
) )
} catch (ignore: UserNotFoundException) { } catch (ignore: NoSuchElementException) {
val httpResponse = if (targetActor != null) { val httpResponse = if (targetActor != null) {
httpClient.getAp(url, "$targetActor#pubkey") httpClient.getAp(url, "$targetActor#pubkey")
} else { } else {

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.service.api package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
import java.time.Instant import java.time.Instant
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -25,4 +26,8 @@ interface IPostApiService {
limit: Int? = null, limit: Int? = null,
userId: Long? = null userId: Long? = null
): List<PostResponse> ): List<PostResponse>
suspend fun getReactionByPostId(postId: Long, userId: Long? = null): List<ReactionResponse>
suspend fun appendReaction(reaction: String, userId: Long, postId: Long)
suspend fun removeReaction(userId: Long, postId: Long)
} }

View File

@ -12,8 +12,6 @@ interface IUserApiService {
suspend fun findByAcct(acct: Acct): UserResponse suspend fun findByAcct(acct: Acct): UserResponse
suspend fun findByAccts(accts: List<Acct>): List<UserResponse>
suspend fun findFollowers(userId: Long): List<UserResponse> suspend fun findFollowers(userId: Long): List<UserResponse>
suspend fun findFollowings(userId: Long): List<UserResponse> suspend fun findFollowings(userId: Long): List<UserResponse>
@ -21,4 +19,6 @@ interface IUserApiService {
suspend fun findFollowersByAcct(acct: Acct): List<UserResponse> suspend fun findFollowersByAcct(acct: Acct): List<UserResponse>
suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse>
suspend fun createUser(username: String, password: String): UserResponse
} }

View File

@ -3,15 +3,14 @@ package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
import dev.usbharu.hideout.repository.* import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
import dev.usbharu.hideout.query.PostResponseQueryService
import dev.usbharu.hideout.query.ReactionQueryService
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.post.IPostService import dev.usbharu.hideout.service.post.IPostService
import dev.usbharu.hideout.service.reaction.IReactionService
import dev.usbharu.hideout.util.AcctUtil import dev.usbharu.hideout.util.AcctUtil
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.time.Instant import java.time.Instant
import dev.usbharu.hideout.domain.model.hideout.form.Post as FormPost import dev.usbharu.hideout.domain.model.hideout.form.Post as FormPost
@ -19,10 +18,14 @@ import dev.usbharu.hideout.domain.model.hideout.form.Post as FormPost
@Single @Single
class PostApiServiceImpl( class PostApiServiceImpl(
private val postService: IPostService, private val postService: IPostService,
private val postRepository: IPostRepository, private val userRepository: IUserRepository,
private val userRepository: IUserRepository private val postResponseQueryService: PostResponseQueryService,
private val reactionQueryService: ReactionQueryService,
private val reactionService: IReactionService,
private val transaction: Transaction
) : IPostApiService { ) : IPostApiService {
override suspend fun createPost(postForm: FormPost, userId: Long): PostResponse { override suspend fun createPost(postForm: FormPost, userId: Long): PostResponse {
return transaction.transaction {
val createdPost = postService.createLocal( val createdPost = postService.createLocal(
PostCreateDto( PostCreateDto(
text = postForm.text, text = postForm.text,
@ -34,20 +37,11 @@ class PostApiServiceImpl(
) )
) )
val creator = userRepository.findById(userId) val creator = userRepository.findById(userId)
return PostResponse.from(createdPost, creator!!) PostResponse.from(createdPost, creator!!)
}
} }
@Suppress("InjectDispatcher") override suspend fun getById(id: Long, userId: Long?): PostResponse = postResponseQueryService.findById(id, userId)
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun getById(id: Long, userId: Long?): PostResponse {
val query = query {
Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { Users.id }).select { Posts.id eq id }
.single()
}
return PostResponse.from(query.toPost(), query.toUser())
}
override suspend fun getAll( override suspend fun getAll(
since: Instant?, since: Instant?,
@ -56,11 +50,15 @@ class PostApiServiceImpl(
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<PostResponse> { ): List<PostResponse> = transaction.transaction {
return query { postResponseQueryService.findAll(
Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }).selectAll() since = since?.toEpochMilli(),
.map { PostResponse.from(it.toPost(), it.toUser()) } until = until?.toEpochMilli(),
} minId = minId,
maxId = maxId,
limit = limit,
userId = userId
)
} }
override suspend fun getByUser( override suspend fun getByUser(
@ -75,18 +73,24 @@ class PostApiServiceImpl(
val idOrNull = nameOrId.toLongOrNull() val idOrNull = nameOrId.toLongOrNull()
return if (idOrNull == null) { return if (idOrNull == null) {
val acct = AcctUtil.parse(nameOrId) val acct = AcctUtil.parse(nameOrId)
query { postResponseQueryService.findByUserNameAndUserDomain(acct.username, acct.domain ?: Config.configData.domain)
Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }).select {
Users.name.eq(acct.username)
.and(Users.domain eq (acct.domain ?: Config.configData.domain))
}.map { PostResponse.from(it.toPost(), it.toUser()) }
}
} else { } else {
query { postResponseQueryService.findByUserId(idOrNull)
Posts.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }).select {
Posts.userId eq idOrNull
}.map { PostResponse.from(it.toPost(), it.toUser()) }
} }
} }
override suspend fun getReactionByPostId(postId: Long, userId: Long?): List<ReactionResponse> =
transaction.transaction { reactionQueryService.findByPostIdWithUsers(postId, userId) }
override suspend fun appendReaction(reaction: String, userId: Long, postId: Long) {
transaction.transaction {
reactionService.sendReaction(reaction, userId, postId)
}
}
override suspend fun removeReaction(userId: Long, postId: Long) {
transaction.transaction {
reactionService.removeReaction(userId, postId)
}
} }
} }

View File

@ -2,37 +2,54 @@ package dev.usbharu.hideout.service.api
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.Acct
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
import dev.usbharu.hideout.exception.UsernameAlreadyExistException
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import kotlin.math.min
@Single @Single
class UserApiServiceImpl(private val userService: IUserService) : IUserApiService { class UserApiServiceImpl(
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService,
private val userService: IUserService,
private val transaction: Transaction
) : IUserApiService {
override suspend fun findAll(limit: Int?, offset: Long): List<UserResponse> = override suspend fun findAll(limit: Int?, offset: Long): List<UserResponse> =
userService.findAll(limit, 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(userService.findById(id)) override suspend fun findById(id: Long): UserResponse = UserResponse.from(userQueryService.findById(id))
override suspend fun findByIds(ids: List<Long>): List<UserResponse> = override suspend fun findByIds(ids: List<Long>): List<UserResponse> =
userService.findByIds(ids).map { UserResponse.from(it) } userQueryService.findByIds(ids).map { UserResponse.from(it) }
override suspend fun findByAcct(acct: Acct): UserResponse = override suspend fun findByAcct(acct: Acct): UserResponse =
UserResponse.from(userService.findByNameAndDomain(acct.username, acct.domain)) UserResponse.from(userQueryService.findByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain))
override suspend fun findByAccts(accts: List<Acct>): List<UserResponse> {
return userService.findByNameAndDomains(accts.map { it.username to (it.domain ?: Config.configData.domain) })
.map { UserResponse.from(it) }
}
override suspend fun findFollowers(userId: Long): List<UserResponse> = override suspend fun findFollowers(userId: Long): List<UserResponse> =
userService.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> =
userService.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> =
userService.findFollowersByNameAndDomain(acct.username, acct.domain).map { UserResponse.from(it) } followerQueryService.findFollowersByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)
.map { UserResponse.from(it) }
override suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> = override suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> =
userService.findFollowingByNameAndDomain(acct.username, acct.domain).map { UserResponse.from(it) } followerQueryService.findFollowingByNameAndDomain(acct.username, acct.domain ?: Config.configData.domain)
.map { UserResponse.from(it) }
override suspend fun createUser(username: String, password: String): UserResponse {
return transaction.transaction {
if (userQueryService.existByNameAndDomain(username, Config.configData.domain)) {
throw UsernameAlreadyExistException()
}
UserResponse.from(userService.createLocalUser(UserCreateDto(username, username, "", password)))
}
}
} }

View File

@ -0,0 +1,9 @@
package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
interface UserAuthApiService {
suspend fun login(username: String, password: String): JwtToken
suspend fun refreshToken(refreshToken: RefreshToken): JwtToken
}

View File

@ -0,0 +1,35 @@
package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.exception.InvalidUsernameOrPasswordException
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.auth.IJwtService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.UserAuthService
import org.koin.core.annotation.Single
@Single
class UserAuthApiServiceImpl(
private val userAuthService: UserAuthService,
private val userQueryService: UserQueryService,
private val jwtService: IJwtService,
private val transaction: Transaction
) : UserAuthApiService {
override suspend fun login(username: String, password: String): JwtToken {
return transaction.transaction {
if (userAuthService.verifyAccount(username, password).not()) {
throw InvalidUsernameOrPasswordException()
}
val user = userQueryService.findByNameAndDomain(username, Config.configData.domain)
jwtService.createToken(user)
}
}
override suspend fun refreshToken(refreshToken: RefreshToken): JwtToken {
return transaction.transaction {
jwtService.refreshToken(refreshToken)
}
}
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.domain.model.hideout.entity.User
interface WebFingerApiService {
suspend fun findByNameAndDomain(name: String, domain: String): User
}

View File

@ -0,0 +1,16 @@
package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import org.koin.core.annotation.Single
@Single
class WebFingerApiServiceImpl(private val transaction: Transaction, private val userQueryService: UserQueryService) :
WebFingerApiService {
override suspend fun findByNameAndDomain(name: String, domain: String): User {
return transaction.transaction {
userQueryService.findByNameAndDomain(name, domain)
}
}
}

View File

@ -1,15 +1,19 @@
package dev.usbharu.hideout.service.auth package dev.usbharu.hideout.service.auth
import dev.usbharu.hideout.plugins.KtorKeyMap import dev.usbharu.hideout.plugins.KtorKeyMap
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import io.ktor.http.* import io.ktor.http.*
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import tech.barbero.http.message.signing.SignatureHeaderVerifier import tech.barbero.http.message.signing.SignatureHeaderVerifier
@Single @Single
class HttpSignatureVerifyServiceImpl(private val userAuthService: IUserRepository) : HttpSignatureVerifyService { class HttpSignatureVerifyServiceImpl(
private val userQueryService: UserQueryService,
private val transaction: Transaction
) : HttpSignatureVerifyService {
override fun verify(headers: Headers): Boolean { override fun verify(headers: Headers): Boolean {
val build = SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userAuthService)).build() val build = SignatureHeaderVerifier.builder().keyMap(KtorKeyMap(userQueryService, transaction)).build()
return true return true
// build.verify(object : HttpMessage { // build.verify(object : HttpMessage {
// override fun headerValues(name: String?): MutableList<String> { // override fun headerValues(name: String?): MutableList<String> {

View File

@ -8,13 +8,12 @@ import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken
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.RefreshToken import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.exception.InvalidRefreshTokenException import dev.usbharu.hideout.exception.InvalidRefreshTokenException
import dev.usbharu.hideout.query.JwtRefreshTokenQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.IJwtRefreshTokenRepository import dev.usbharu.hideout.repository.IJwtRefreshTokenRepository
import dev.usbharu.hideout.service.core.IMetaService import dev.usbharu.hideout.service.core.IMetaService
import dev.usbharu.hideout.service.user.IUserService
import dev.usbharu.hideout.util.RsaUtil import dev.usbharu.hideout.util.RsaUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@ -25,26 +24,19 @@ import java.util.*
class JwtServiceImpl( class JwtServiceImpl(
private val metaService: IMetaService, private val metaService: IMetaService,
private val refreshTokenRepository: IJwtRefreshTokenRepository, private val refreshTokenRepository: IJwtRefreshTokenRepository,
private val userService: IUserService private val userQueryService: UserQueryService,
private val refreshTokenQueryService: JwtRefreshTokenQueryService
) : IJwtService { ) : IJwtService {
private val privateKey by lazy { private val privateKey = runBlocking {
CoroutineScope(Dispatchers.IO).async {
RsaUtil.decodeRsaPrivateKey(metaService.getJwtMeta().privateKey) RsaUtil.decodeRsaPrivateKey(metaService.getJwtMeta().privateKey)
} }
}
private val publicKey by lazy { private val publicKey = runBlocking {
CoroutineScope(Dispatchers.IO).async {
RsaUtil.decodeRsaPublicKey(metaService.getJwtMeta().publicKey) RsaUtil.decodeRsaPublicKey(metaService.getJwtMeta().publicKey)
} }
}
private val keyId by lazy { private val keyId = runBlocking { metaService.getJwtMeta().kid }
CoroutineScope(Dispatchers.IO).async {
metaService.getJwtMeta().kid
}
}
@Suppress("MagicNumber") @Suppress("MagicNumber")
override suspend fun createToken(user: User): JwtToken { override suspend fun createToken(user: User): JwtToken {
@ -52,10 +44,10 @@ class JwtServiceImpl(
val token = JWT.create() val token = JWT.create()
.withAudience("${Config.configData.url}/users/${user.name}") .withAudience("${Config.configData.url}/users/${user.name}")
.withIssuer(Config.configData.url) .withIssuer(Config.configData.url)
.withKeyId(keyId.await().toString()) .withKeyId(keyId.toString())
.withClaim("uid", user.id) .withClaim("uid", user.id)
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) .withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
.sign(Algorithm.RSA256(publicKey.await(), privateKey.await())) .sign(Algorithm.RSA256(publicKey, privateKey))
val jwtRefreshToken = JwtRefreshToken( val jwtRefreshToken = JwtRefreshToken(
id = refreshTokenRepository.generateId(), id = refreshTokenRepository.generateId(),
@ -69,10 +61,13 @@ class JwtServiceImpl(
} }
override suspend fun refreshToken(refreshToken: RefreshToken): JwtToken { override suspend fun refreshToken(refreshToken: RefreshToken): JwtToken {
val token = refreshTokenRepository.findByToken(refreshToken.refreshToken) val token = try {
?: throw InvalidRefreshTokenException("Invalid Refresh Token") refreshTokenQueryService.findByToken(refreshToken.refreshToken)
} catch (_: NoSuchElementException) {
throw InvalidRefreshTokenException("Invalid Refresh Token")
}
val user = userService.findById(token.userId) val user = userQueryService.findById(token.userId)
val now = Instant.now() val now = Instant.now()
if (token.createdAt.isAfter(now)) { if (token.createdAt.isAfter(now)) {
@ -87,14 +82,14 @@ class JwtServiceImpl(
} }
override suspend fun revokeToken(refreshToken: RefreshToken) { override suspend fun revokeToken(refreshToken: RefreshToken) {
refreshTokenRepository.deleteByToken(refreshToken.refreshToken) refreshTokenQueryService.deleteByToken(refreshToken.refreshToken)
} }
override suspend fun revokeToken(user: User) { override suspend fun revokeToken(user: User) {
refreshTokenRepository.deleteByUserId(user.id) refreshTokenQueryService.deleteByUserId(user.id)
} }
override suspend fun revokeAll() { override suspend fun revokeAll() {
refreshTokenRepository.deleteAll() refreshTokenQueryService.deleteAll()
} }
} }

View File

@ -0,0 +1,13 @@
package dev.usbharu.hideout.service.core
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.koin.core.annotation.Single
@Single
class ExposedTransaction : Transaction {
override suspend fun <T> transaction(block: suspend () -> T): T {
return newSuspendedTransaction {
block()
}
}
}

View File

@ -7,10 +7,12 @@ import dev.usbharu.hideout.repository.IMetaRepository
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@Single @Single
class MetaServiceImpl(private val metaRepository: IMetaRepository) : IMetaService { class MetaServiceImpl(private val metaRepository: IMetaRepository, private val transaction: Transaction) :
override suspend fun getMeta(): Meta = metaRepository.get() ?: throw NotInitException("Meta is null") IMetaService {
override suspend fun getMeta(): Meta =
transaction.transaction { metaRepository.get() ?: throw NotInitException("Meta is null") }
override suspend fun updateMeta(meta: Meta) { override suspend fun updateMeta(meta: Meta) = transaction.transaction {
metaRepository.save(meta) metaRepository.save(meta)
} }

View File

@ -11,18 +11,23 @@ import java.security.KeyPairGenerator
import java.util.* import java.util.*
@Single @Single
class ServerInitialiseServiceImpl(private val metaRepository: IMetaRepository) : IServerInitialiseService { class ServerInitialiseServiceImpl(
private val metaRepository: IMetaRepository,
private val transaction: Transaction
) :
IServerInitialiseService {
val logger: Logger = LoggerFactory.getLogger(ServerInitialiseServiceImpl::class.java) val logger: Logger = LoggerFactory.getLogger(ServerInitialiseServiceImpl::class.java)
override suspend fun init() { override suspend fun init() {
transaction.transaction {
val savedMeta = metaRepository.get() val savedMeta = metaRepository.get()
val implementationVersion = ServerUtil.getImplementationVersion() val implementationVersion = ServerUtil.getImplementationVersion()
if (wasInitialised(savedMeta).not()) { if (wasInitialised(savedMeta).not()) {
logger.info("Start Initialise") logger.info("Start Initialise")
initialise(implementationVersion) initialise(implementationVersion)
logger.info("Finish Initialise") logger.info("Finish Initialise")
return return@transaction
} }
if (isVersionChanged(requireNotNull(savedMeta))) { if (isVersionChanged(requireNotNull(savedMeta))) {
@ -30,6 +35,7 @@ class ServerInitialiseServiceImpl(private val metaRepository: IMetaRepository) :
updateVersion(savedMeta, implementationVersion) updateVersion(savedMeta, implementationVersion)
} }
} }
}
private fun wasInitialised(meta: Meta?): Boolean { private fun wasInitialised(meta: Meta?): Boolean {
logger.debug("Initialise checking...") logger.debug("Initialise checking...")

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.service.core
interface Transaction {
suspend fun <T> transaction(block: suspend () -> T): T
}

View File

@ -1,10 +1,7 @@
package dev.usbharu.hideout.service.reaction package dev.usbharu.hideout.service.reaction
import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse
interface IReactionService { interface IReactionService {
suspend fun receiveReaction(name: String, domain: String, userId: Long, postId: Long) suspend fun receiveReaction(name: String, domain: String, userId: Long, postId: Long)
suspend fun sendReaction(name: String, userId: Long, postId: Long) suspend fun sendReaction(name: String, userId: Long, postId: Long)
suspend fun removeReaction(userId: Long, postId: Long) suspend fun removeReaction(userId: Long, postId: Long)
suspend fun findByPostIdForUser(postId: Long, userId: Long): List<ReactionResponse>
} }

View File

@ -1,25 +1,19 @@
package dev.usbharu.hideout.service.reaction package dev.usbharu.hideout.service.reaction
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.entity.Reaction import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.query.ReactionQueryService
import dev.usbharu.hideout.repository.ReactionRepository import dev.usbharu.hideout.repository.ReactionRepository
import dev.usbharu.hideout.repository.Reactions
import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.service.activitypub.ActivityPubReactionService import dev.usbharu.hideout.service.activitypub.ActivityPubReactionService
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.leftJoin
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@Single @Single
class ReactionServiceImpl( class ReactionServiceImpl(
private val reactionRepository: ReactionRepository, private val reactionRepository: ReactionRepository,
private val activityPubReactionService: ActivityPubReactionService private val activityPubReactionService: ActivityPubReactionService,
private val reactionQueryService: ReactionQueryService
) : IReactionService { ) : IReactionService {
override suspend fun receiveReaction(name: String, domain: String, userId: Long, postId: Long) { override suspend fun receiveReaction(name: String, domain: String, userId: Long, postId: Long) {
if (reactionRepository.reactionAlreadyExist(postId, userId, 0).not()) { if (reactionQueryService.reactionAlreadyExist(postId, userId, 0).not()) {
reactionRepository.save( reactionRepository.save(
Reaction(reactionRepository.generateId(), 0, postId, userId) Reaction(reactionRepository.generateId(), 0, postId, userId)
) )
@ -27,9 +21,9 @@ class ReactionServiceImpl(
} }
override suspend fun sendReaction(name: String, userId: Long, postId: Long) { override suspend fun sendReaction(name: String, userId: Long, postId: Long) {
if (reactionRepository.reactionAlreadyExist(postId, userId, 0)) { if (reactionQueryService.reactionAlreadyExist(postId, userId, 0)) {
// delete // delete
reactionRepository.deleteByPostIdAndUserId(postId, userId) reactionQueryService.deleteByPostIdAndUserId(postId, userId)
} else { } else {
val reaction = Reaction(reactionRepository.generateId(), 0, postId, userId) val reaction = Reaction(reactionRepository.generateId(), 0, postId, userId)
reactionRepository.save(reaction) reactionRepository.save(reaction)
@ -38,18 +32,6 @@ class ReactionServiceImpl(
} }
override suspend fun removeReaction(userId: Long, postId: Long) { override suspend fun removeReaction(userId: Long, postId: Long) {
reactionRepository.deleteByPostIdAndUserId(postId, userId) reactionQueryService.deleteByPostIdAndUserId(postId, userId)
}
override suspend fun findByPostIdForUser(postId: Long, userId: Long): List<ReactionResponse> {
return newSuspendedTransaction {
Reactions
.leftJoin(Users, onColumn = { Reactions.userId }, otherColumn = { id })
.select { Reactions.postId.eq(postId) }
.groupBy { resultRow: ResultRow -> ReactionResponse("", true, "", listOf()) }
.map { entry: Map.Entry<ReactionResponse, List<ResultRow>> ->
entry.key.copy(accounts = entry.value.map { Account(it[Users.screenName], "", it[Users.url]) })
}
}
} }
} }

View File

@ -6,23 +6,6 @@ import dev.usbharu.hideout.domain.model.hideout.entity.User
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
interface IUserService { interface IUserService {
suspend fun findAll(limit: Int? = 100, offset: Long? = 0): List<User>
suspend fun findById(id: Long): User
suspend fun findByIds(ids: List<Long>): List<User>
suspend fun findByName(name: String): List<User>
suspend fun findByNameLocalUser(name: String): User
suspend fun findByNameAndDomain(name: String, domain: String? = null): User
suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User>
suspend fun findByUrl(url: String): User
suspend fun findByUrls(urls: List<String>): List<User>
suspend fun usernameAlreadyUse(username: String): Boolean suspend fun usernameAlreadyUse(username: String): Boolean
@ -30,14 +13,6 @@ interface IUserService {
suspend fun createRemoteUser(user: RemoteUserCreateDto): User suspend fun createRemoteUser(user: RemoteUserCreateDto): User
suspend fun findFollowersById(id: Long): List<User>
suspend fun findFollowersByNameAndDomain(name: String, domain: String?): List<User>
suspend fun findFollowingById(id: Long): List<User>
suspend fun findFollowingByNameAndDomain(name: String, domain: String?): List<User>
/** /**
* フォローリクエストを送信する * フォローリクエストを送信する
* *

View File

@ -1,7 +1,7 @@
package dev.usbharu.hideout.service.user package dev.usbharu.hideout.service.user
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.query.UserQueryService
import io.ktor.util.* import io.ktor.util.*
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.security.* import java.security.*
@ -9,7 +9,7 @@ import java.util.*
@Single @Single
class UserAuthService( class UserAuthService(
val userRepository: IUserRepository val userQueryService: UserQueryService
) : IUserAuthService { ) : IUserAuthService {
override fun hash(password: String): String { override fun hash(password: String): String {
@ -18,13 +18,12 @@ class UserAuthService(
} }
override suspend fun usernameAlreadyUse(username: String): Boolean { override suspend fun usernameAlreadyUse(username: String): Boolean {
userRepository.findByName(username) userQueryService.findByName(username)
return true return true
} }
override suspend fun verifyAccount(username: String, password: String): Boolean { override suspend fun verifyAccount(username: String, password: String): Boolean {
val userEntity = userRepository.findByNameAndDomain(username, Config.configData.domain) val userEntity = userQueryService.findByNameAndDomain(username, Config.configData.domain)
?: return false
return userEntity.password == hash(password) return userEntity.password == hash(password)
} }

View File

@ -6,55 +6,25 @@ import dev.usbharu.hideout.domain.model.hideout.dto.SendFollowDto
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.activitypub.ActivityPubSendFollowService import dev.usbharu.hideout.service.activitypub.ActivityPubSendFollowService
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import java.lang.Integer.min
import java.time.Instant import java.time.Instant
@Single @Single
class UserService( class UserService(
private val userRepository: IUserRepository, private val userRepository: IUserRepository,
private val userAuthService: IUserAuthService, private val userAuthService: IUserAuthService,
private val activityPubSendFollowService: ActivityPubSendFollowService private val activityPubSendFollowService: ActivityPubSendFollowService,
private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService
) : ) :
IUserService { IUserService {
private val maxLimit = 100
override suspend fun findAll(limit: Int?, offset: Long?): List<User> {
return userRepository.findAllByLimitAndByOffset(
min(limit ?: maxLimit, maxLimit),
offset ?: 0
)
}
override suspend fun findById(id: Long): User =
userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.")
override suspend fun findByIds(ids: List<Long>): List<User> = userRepository.findByIds(ids)
override suspend fun findByName(name: String): List<User> = userRepository.findByName(name)
override suspend fun findByNameLocalUser(name: String): User {
return userRepository.findByNameAndDomain(name, Config.configData.domain)
?: throw UserNotFoundException("$name was not found.")
}
override suspend fun findByNameAndDomain(name: String, domain: String?): User {
return userRepository.findByNameAndDomain(name, domain ?: Config.configData.domain)
?: throw UserNotFoundException("$name was not found.")
}
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> =
userRepository.findByNameAndDomains(names)
override suspend fun findByUrl(url: String): User =
userRepository.findByUrl(url) ?: throw UserNotFoundException("$url was not found.")
override suspend fun findByUrls(urls: List<String>): List<User> = userRepository.findByUrls(urls)
override suspend fun usernameAlreadyUse(username: String): Boolean { override suspend fun usernameAlreadyUse(username: String): Boolean {
val findByNameAndDomain = userRepository.findByNameAndDomain(username, Config.configData.domain) val findByNameAndDomain = userQueryService.findByNameAndDomain(username, Config.configData.domain)
return findByNameAndDomain != null return findByNameAndDomain != null
} }
@ -96,19 +66,6 @@ class UserService(
return userRepository.save(userEntity) return userRepository.save(userEntity)
} }
override suspend fun findFollowersById(id: Long): List<User> = userRepository.findFollowersById(id)
override suspend fun findFollowersByNameAndDomain(name: String, domain: String?): List<User> {
TODO("Not yet implemented")
}
override suspend fun findFollowingById(id: Long): List<User> {
TODO("Not yet implemented")
}
override suspend fun findFollowingByNameAndDomain(name: String, domain: String?): List<User> {
TODO("Not yet implemented")
}
// TODO APのフォロー処理を作る // TODO APのフォロー処理を作る
override suspend fun followRequest(id: Long, followerId: Long): Boolean { override suspend fun followRequest(id: Long, followerId: Long): Boolean {
val user = userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.") val user = userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.")
@ -127,14 +84,14 @@ class UserService(
} }
override suspend fun follow(id: Long, followerId: Long) { override suspend fun follow(id: Long, followerId: Long) {
userRepository.createFollower(id, followerId) followerQueryService.appendFollower(id, followerId)
if (userRepository.findFollowRequestsById(id, followerId)) { if (userRepository.findFollowRequestsById(id, followerId)) {
userRepository.deleteFollowRequest(id, followerId) userRepository.deleteFollowRequest(id, followerId)
} }
} }
override suspend fun unfollow(id: Long, followerId: Long): Boolean { override suspend fun unfollow(id: Long, followerId: Long): Boolean {
userRepository.deleteFollower(id, followerId) followerQueryService.removeFollower(id, followerId)
return false return false
} }
} }

View File

@ -202,6 +202,7 @@ class ExposedJobRepository(
} }
} }
@Suppress("CyclomaticComplexMethod")
private fun String?.parseJsonMap(): Map<String, Any> { private fun String?.parseJsonMap(): Map<String, Any> {
this ?: return emptyMap() this ?: return emptyMap()
return json.parseToJsonElement(this).jsonObject.mapValues { (_, el) -> return json.parseToJsonElement(this).jsonObject.mapValues { (_, el) ->
@ -232,6 +233,7 @@ class ExposedJobRepository(
} }
} }
@Suppress("CyclomaticComplexMethod")
private fun Map<String, Any>.stringify(): String? { private fun Map<String, Any>.stringify(): String? {
if (isEmpty()) { if (isEmpty()) {
return null return null

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="TRACE"> <root level="INFO">
<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

@ -2,41 +2,29 @@ package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.domain.model.ap.JsonLd import dev.usbharu.hideout.domain.model.ap.JsonLd
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.user.toPem import dev.usbharu.hideout.service.user.toPem
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.mock.* import io.ktor.client.engine.mock.*
import io.ktor.client.plugins.logging.* import io.ktor.client.plugins.logging.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
import utils.TestTransaction
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.time.Instant import java.time.Instant
class ActivityPubKtTest { class ActivityPubKtTest {
@Test @Test
fun HttpSignTest(): Unit = runBlocking { fun HttpSignTest() {
val ktorKeyMap = KtorKeyMap(object : IUserRepository { val userQueryService = mock<UserQueryService> {
override suspend fun save(user: User): User { onBlocking { findByNameAndDomain(any(), any()) } doAnswer {
TODO("Not yet implemented")
}
override suspend fun findById(id: Long): User? {
TODO("Not yet implemented")
}
override suspend fun findByIds(ids: List<Long>): List<User> {
TODO("Not yet implemented")
}
override suspend fun findByName(name: String): List<User> {
TODO()
}
override suspend fun findByNameAndDomain(name: String, domain: String): User {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA") val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024) keyPairGenerator.initialize(1024)
val generateKeyPair = keyPairGenerator.generateKeyPair() val generateKeyPair = keyPairGenerator.generateKeyPair()
return User( User(
1, 1,
"test", "test",
"localhost", "localhost",
@ -51,63 +39,9 @@ class ActivityPubKtTest {
Instant.now() Instant.now()
) )
} }
override suspend fun findByDomain(domain: String): List<User> {
TODO("Not yet implemented")
} }
runBlocking {
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> { val ktorKeyMap = KtorKeyMap(userQueryService, TestTransaction)
TODO("Not yet implemented")
}
override suspend fun findByUrl(url: String): User? {
TODO("Not yet implemented")
}
override suspend fun findByUrls(urls: List<String>): List<User> {
TODO("Not yet implemented")
}
override suspend fun delete(id: Long) {
TODO("Not yet implemented")
}
override suspend fun findAll(): List<User> {
TODO("Not yet implemented")
}
override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<User> {
TODO("Not yet implemented")
}
override suspend fun createFollower(id: Long, follower: Long) {
TODO("Not yet implemented")
}
override suspend fun deleteFollower(id: Long, follower: Long) {
TODO("Not yet implemented")
}
override suspend fun findFollowersById(id: Long): List<User> {
TODO("Not yet implemented")
}
override suspend fun addFollowRequest(id: Long, follower: Long) {
TODO("Not yet implemented")
}
override suspend fun deleteFollowRequest(id: Long, follower: Long) {
TODO("Not yet implemented")
}
override suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean {
TODO("Not yet implemented")
}
override suspend fun nextId(): Long {
TODO("Not yet implemented")
}
})
val httpClient = HttpClient( val httpClient = HttpClient(
MockEngine { httpRequestData -> MockEngine { httpRequestData ->
@ -125,4 +59,5 @@ class ActivityPubKtTest {
httpClient.postAp("https://localhost", "test", JsonLd(emptyList())) httpClient.postAp("https://localhost", "test", JsonLd(emptyList()))
} }
}
} }

View File

@ -1,9 +1,13 @@
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.user.toPem import dev.usbharu.hideout.service.user.toPem
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
import utils.TestTransaction
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.time.Instant import java.time.Instant
@ -11,28 +15,12 @@ class KtorKeyMapTest {
@Test @Test
fun getPrivateKey() { fun getPrivateKey() {
val ktorKeyMap = KtorKeyMap(object : IUserRepository { val userQueryService = mock<UserQueryService> {
override suspend fun save(user: User): User { onBlocking { findByNameAndDomain(any(), any()) } doAnswer {
TODO("Not yet implemented")
}
override suspend fun findById(id: Long): User? {
TODO("Not yet implemented")
}
override suspend fun findByIds(ids: List<Long>): List<User> {
TODO("Not yet implemented")
}
override suspend fun findByName(name: String): List<User> {
TODO()
}
override suspend fun findByNameAndDomain(name: String, domain: String): User {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA") val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(1024) keyPairGenerator.initialize(1024)
val generateKeyPair = keyPairGenerator.generateKeyPair() val generateKeyPair = keyPairGenerator.generateKeyPair()
return User( User(
1, 1,
"test", "test",
"localhost", "localhost",
@ -47,63 +35,8 @@ class KtorKeyMapTest {
createdAt = Instant.now() createdAt = Instant.now()
) )
} }
override suspend fun findByDomain(domain: String): List<User> {
TODO("Not yet implemented")
} }
val ktorKeyMap = KtorKeyMap(userQueryService, TestTransaction)
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> {
TODO("Not yet implemented")
}
override suspend fun findByUrl(url: String): User? {
TODO("Not yet implemented")
}
override suspend fun findByUrls(urls: List<String>): List<User> {
TODO("Not yet implemented")
}
override suspend fun delete(id: Long) {
TODO("Not yet implemented")
}
override suspend fun findAll(): List<User> {
TODO("Not yet implemented")
}
override suspend fun findAllByLimitAndByOffset(limit: Int, offset: Long): List<User> {
TODO("Not yet implemented")
}
override suspend fun createFollower(id: Long, follower: Long) {
TODO("Not yet implemented")
}
override suspend fun deleteFollower(id: Long, follower: Long) {
TODO("Not yet implemented")
}
override suspend fun findFollowersById(id: Long): List<User> {
TODO("Not yet implemented")
}
override suspend fun addFollowRequest(id: Long, follower: Long) {
TODO("Not yet implemented")
}
override suspend fun deleteFollowRequest(id: Long, follower: Long) {
TODO("Not yet implemented")
}
override suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean {
TODO("Not yet implemented")
}
override suspend fun nextId(): Long {
TODO("Not yet implemented")
}
})
ktorKeyMap.getPrivateKey("test") ktorKeyMap.getPrivateKey("test")
} }

View File

@ -14,8 +14,10 @@ import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
import dev.usbharu.hideout.exception.InvalidRefreshTokenException import dev.usbharu.hideout.exception.InvalidRefreshTokenException
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.exception.InvalidUsernameOrPasswordException
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.routing.api.internal.v1.auth import dev.usbharu.hideout.routing.api.internal.v1.auth
import dev.usbharu.hideout.service.api.UserAuthApiService
import dev.usbharu.hideout.service.auth.IJwtService import dev.usbharu.hideout.service.auth.IJwtService
import dev.usbharu.hideout.service.core.IMetaService import dev.usbharu.hideout.service.core.IMetaService
import dev.usbharu.hideout.service.user.IUserAuthService import dev.usbharu.hideout.service.user.IUserAuthService
@ -45,11 +47,12 @@ class SecurityKtTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
val userAuthService = mock<IUserAuthService> { val jwtToken = JwtToken("Token", "RefreshToken")
onBlocking { verifyAccount(eq("testUser"), eq("password")) } doReturn true val userAuthService = mock<UserAuthApiService> {
onBlocking { login(eq("testUser"), eq("password")) } doReturn jwtToken
} }
val metaService = mock<IMetaService>() val metaService = mock<IMetaService>()
val userRepository = mock<IUserRepository> { val userQueryService = mock<UserQueryService> {
onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User( onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User(
id = 1L, id = 1L,
name = "testUser", name = "testUser",
@ -65,16 +68,12 @@ class SecurityKtTest {
createdAt = Instant.now() createdAt = Instant.now()
) )
} }
val jwtToken = JwtToken("Token", "RefreshToken")
val jwtService = mock<IJwtService> {
onBlocking { createToken(any()) } doReturn jwtToken
}
val jwkProvider = mock<JwkProvider>() val jwkProvider = mock<JwkProvider>()
application { application {
configureSerialization() configureSerialization()
configureSecurity(jwkProvider, metaService) configureSecurity(jwkProvider, metaService)
routing { routing {
auth(userAuthService, userRepository, jwtService) auth(userAuthService)
} }
} }
@ -88,23 +87,28 @@ class SecurityKtTest {
} }
@Test @Test
fun `login 存在しないユーザーのログインに失敗する`() = testApplication { fun `login 存在しないユーザーのログインに失敗する`() {
testApplication {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
val userAuthService = mock<IUserAuthService> { mock<IUserAuthService> {
onBlocking { verifyAccount(anyString(), anyString()) }.doReturn(false) onBlocking { verifyAccount(anyString(), anyString()) }.doReturn(false)
} }
val metaService = mock<IMetaService>() val metaService = mock<IMetaService>()
val userRepository = mock<IUserRepository>() mock<UserQueryService>()
val jwtService = mock<IJwtService>() mock<IJwtService>()
val jwkProvider = mock<JwkProvider>() val jwkProvider = mock<JwkProvider>()
val userAuthApiService = mock<UserAuthApiService> {
onBlocking { login(anyString(), anyString()) } doThrow InvalidUsernameOrPasswordException()
}
application { application {
configureStatusPages()
configureSerialization() configureSerialization()
configureSecurity(jwkProvider, metaService) configureSecurity(jwkProvider, metaService)
routing { routing {
auth(userAuthService, userRepository, jwtService) auth(userAuthApiService)
} }
} }
client.post("/login") { client.post("/login") {
@ -114,6 +118,7 @@ class SecurityKtTest {
assertEquals(HttpStatusCode.Unauthorized, call.response.status) assertEquals(HttpStatusCode.Unauthorized, call.response.status)
} }
} }
}
@Test @Test
fun `login 不正なパスワードのログインに失敗する`() = testApplication { fun `login 不正なパスワードのログインに失敗する`() = testApplication {
@ -121,18 +126,17 @@ class SecurityKtTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper()) Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
val userAuthService = mock<IUserAuthService> {
onBlocking { verifyAccount(anyString(), eq("InvalidPassword")) } doReturn false
}
val metaService = mock<IMetaService>() val metaService = mock<IMetaService>()
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
val jwkProvider = mock<JwkProvider>() val jwkProvider = mock<JwkProvider>()
val userAuthApiService = mock<UserAuthApiService> {
onBlocking { login(anyString(), eq("InvalidPassword")) } doThrow InvalidUsernameOrPasswordException()
}
application { application {
configureStatusPages()
configureSerialization() configureSerialization()
configureSecurity(jwkProvider, metaService) configureSecurity(jwkProvider, metaService)
routing { routing {
auth(userAuthService, userRepository, jwtService) auth(userAuthApiService)
} }
} }
client.post("/login") { client.post("/login") {
@ -153,7 +157,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
client.get("/auth-check").apply { client.get("/auth-check").apply {
@ -171,7 +175,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
client.get("/auth-check") { client.get("/auth-check") {
@ -191,7 +195,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
client.get("/auth-check") { client.get("/auth-check") {
@ -212,7 +216,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
client.get("/auth-check") { client.get("/auth-check") {
@ -271,7 +275,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(jwkProvider, metaService) configureSecurity(jwkProvider, metaService)
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
@ -332,7 +336,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(jwkProvider, metaService) configureSecurity(jwkProvider, metaService)
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
client.get("/auth-check") { client.get("/auth-check") {
@ -391,7 +395,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(jwkProvider, metaService) configureSecurity(jwkProvider, metaService)
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
client.get("/auth-check") { client.get("/auth-check") {
@ -450,7 +454,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(jwkProvider, metaService) configureSecurity(jwkProvider, metaService)
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
client.get("/auth-check") { client.get("/auth-check") {
@ -508,7 +512,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(jwkProvider, metaService) configureSecurity(jwkProvider, metaService)
routing { routing {
auth(mock(), mock(), mock()) auth(mock())
} }
} }
client.get("/auth-check") { client.get("/auth-check") {
@ -524,14 +528,14 @@ class SecurityKtTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val jwtService = mock<IJwtService> { val jwtService = mock<UserAuthApiService> {
onBlocking { refreshToken(any()) }.doReturn(JwtToken("token", "refreshToken2")) onBlocking { refreshToken(any()) }.doReturn(JwtToken("token", "refreshToken2"))
} }
application { application {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
auth(mock(), mock(), jwtService) auth(jwtService)
} }
} }
client.post("/refresh-token") { client.post("/refresh-token") {
@ -548,7 +552,7 @@ class SecurityKtTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val jwtService = mock<IJwtService> { val jwtService = mock<UserAuthApiService> {
onBlocking { refreshToken(any()) } doThrow InvalidRefreshTokenException("Invalid Refresh Token") onBlocking { refreshToken(any()) } doThrow InvalidRefreshTokenException("Invalid Refresh Token")
} }
application { application {
@ -556,7 +560,7 @@ class SecurityKtTest {
configureSerialization() configureSerialization()
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
auth(mock(), mock(), jwtService) auth(jwtService)
} }
} }
client.post("/refresh-token") { client.post("/refresh-token") {

View File

@ -10,6 +10,8 @@ import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
@ -37,6 +39,7 @@ class JwtRefreshTokenRepositoryImplTest {
transaction(db) { transaction(db) {
SchemaUtils.drop(JwtRefreshTokens) SchemaUtils.drop(JwtRefreshTokens)
} }
TransactionManager.closeAndUnregister(db)
} }
@Test @Test
@ -53,10 +56,12 @@ class JwtRefreshTokenRepositoryImplTest {
val expiresAt = now.plus(10, ChronoUnit.MINUTES) val expiresAt = now.plus(10, ChronoUnit.MINUTES)
val expect = JwtRefreshToken(1L, 2L, "refreshToken", now, expiresAt) val expect = JwtRefreshToken(1L, 2L, "refreshToken", now, expiresAt)
newSuspendedTransaction {
repository.save(expect) repository.save(expect)
val actual = repository.findById(1L) val actual = repository.findById(1L)
assertEquals(expect, actual) assertEquals(expect, actual)
} }
}
@Test @Test
fun `save 存在する場合はupdateする`() = runTest { fun `save 存在する場合はupdateする`() = runTest {
@ -68,7 +73,7 @@ class JwtRefreshTokenRepositoryImplTest {
} }
} }
) )
transaction { newSuspendedTransaction {
JwtRefreshTokens.insert { JwtRefreshTokens.insert {
it[id] = 1L it[id] = 1L
it[userId] = 2L it[userId] = 2L
@ -76,8 +81,6 @@ class JwtRefreshTokenRepositoryImplTest {
it[createdAt] = Instant.now().toEpochMilli() it[createdAt] = Instant.now().toEpochMilli()
it[expiresAt] = Instant.now().plus(10, ChronoUnit.MINUTES).toEpochMilli() it[expiresAt] = Instant.now().plus(10, ChronoUnit.MINUTES).toEpochMilli()
} }
}
repository.save( repository.save(
JwtRefreshToken( JwtRefreshToken(
id = 1L, id = 1L,
@ -87,6 +90,8 @@ class JwtRefreshTokenRepositoryImplTest {
expiresAt = Instant.now().plus(10, ChronoUnit.MINUTES) expiresAt = Instant.now().plus(10, ChronoUnit.MINUTES)
) )
) )
}
transaction { transaction {
val toJwtRefreshToken = JwtRefreshTokens.select { JwtRefreshTokens.id.eq(1L) }.single().toJwtRefreshToken() val toJwtRefreshToken = JwtRefreshTokens.select { JwtRefreshTokens.id.eq(1L) }.single().toJwtRefreshToken()
assertEquals("refreshToken2", toJwtRefreshToken.refreshToken) assertEquals("refreshToken2", toJwtRefreshToken.refreshToken)

View File

@ -1,151 +0,0 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.service.core.IdGenerateService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertIterableEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
class UserRepositoryTest {
lateinit var db: Database
@BeforeEach
fun beforeEach() {
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
transaction(db) {
SchemaUtils.create(Users)
SchemaUtils.create(UsersFollowers)
SchemaUtils.create(FollowRequests)
}
}
@AfterEach
fun tearDown() {
transaction(db) {
SchemaUtils.drop(UsersFollowers)
SchemaUtils.drop(FollowRequests)
SchemaUtils.drop(Users)
}
}
@Test
fun `findFollowersById フォロワー一覧を取得`() = runTest {
val userRepository = UserRepository(
db,
object : IdGenerateService {
override suspend fun generateId(): Long {
TODO("Not yet implemented")
}
}
)
val user = userRepository.save(
User(
id = 0L,
name = "test",
domain = "example.com",
screenName = "testUser",
description = "This user is test user.",
password = "https://example.com/inbox",
inbox = "",
outbox = "https://example.com/outbox",
url = "https://example.com",
publicKey = "",
createdAt = Instant.now(Clock.tickMillis(ZoneId.systemDefault()))
)
)
val follower = userRepository.save(
User(
id = 1L,
name = "follower",
domain = "follower.example.com",
screenName = "followerUser",
description = "This user is follower user.",
password = "",
inbox = "https://follower.example.com/inbox",
outbox = "https://follower.example.com/outbox",
url = "https://follower.example.com",
publicKey = "",
createdAt = Instant.now(Clock.tickMillis(ZoneId.systemDefault()))
)
)
val follower2 = userRepository.save(
User(
id = 3L,
name = "follower2",
domain = "follower2.example.com",
screenName = "followerUser2",
description = "This user is follower user 2.",
password = "",
inbox = "https://follower2.example.com/inbox",
outbox = "https://follower2.example.com/outbox",
url = "https://follower2.example.com",
publicKey = "",
createdAt = Instant.now(Clock.tickMillis(ZoneId.systemDefault()))
)
)
userRepository.createFollower(user.id, follower.id)
userRepository.createFollower(user.id, follower2.id)
assertIterableEquals(listOf(follower, follower2), userRepository.findFollowersById(user.id))
}
@Test
fun `createFollower フォロワー追加`() = runTest {
val userRepository = UserRepository(
db,
object : IdGenerateService {
override suspend fun generateId(): Long {
TODO("Not yet implemented")
}
}
)
val user = userRepository.save(
User(
0L,
"test",
"example.com",
"testUser",
"This user is test user.",
"https://example.com/inbox",
"",
"https://example.com/outbox",
"https://example.com",
publicKey = "",
createdAt = Instant.now()
)
)
val follower = userRepository.save(
User(
1L,
"follower",
"follower.example.com",
"followerUser",
"This user is follower user.",
"",
"https://follower.example.com/inbox",
"https://follower.example.com/outbox",
"https://follower.example.com",
publicKey = "",
createdAt = Instant.now()
)
)
userRepository.createFollower(user.id, follower.id)
transaction {
val followerIds =
UsersFollowers.select { UsersFollowers.userId eq user.id }.map { it[UsersFollowers.followerId] }
assertIterableEquals(listOf(follower.id), followerIds)
}
}
}

View File

@ -11,8 +11,8 @@ 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.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.plugins.configureSerialization import dev.usbharu.hideout.plugins.configureSerialization
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.user.IUserService
import dev.usbharu.hideout.util.HttpUtil.Activity import dev.usbharu.hideout.util.HttpUtil.Activity
import dev.usbharu.hideout.util.HttpUtil.JsonLd import dev.usbharu.hideout.util.HttpUtil.JsonLd
import io.ktor.client.request.* import io.ktor.client.request.*
@ -26,6 +26,7 @@ import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq import org.mockito.kotlin.eq
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import utils.TestTransaction
import java.time.Instant import java.time.Instant
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -62,8 +63,6 @@ class UsersAPTest {
) )
person.context = listOf("https://www.w3.org/ns/activitystreams") person.context = listOf("https://www.w3.org/ns/activitystreams")
val userService = mock<IUserService> {}
val activityPubUserService = mock<ActivityPubUserService> { val activityPubUserService = mock<ActivityPubUserService> {
onBlocking { getPersonByName(anyString()) } doReturn person onBlocking { getPersonByName(anyString()) } doReturn person
} }
@ -71,7 +70,7 @@ class UsersAPTest {
application { application {
configureSerialization() configureSerialization()
routing { routing {
usersAP(activityPubUserService, userService) usersAP(activityPubUserService, mock(), mock(), TestTransaction)
} }
} }
client.get("/users/test") { client.get("/users/test") {
@ -122,8 +121,6 @@ class UsersAPTest {
) )
person.context = listOf("https://www.w3.org/ns/activitystreams") person.context = listOf("https://www.w3.org/ns/activitystreams")
val userService = mock<IUserService> {}
val activityPubUserService = mock<ActivityPubUserService> { val activityPubUserService = mock<ActivityPubUserService> {
onBlocking { getPersonByName(anyString()) } doReturn person onBlocking { getPersonByName(anyString()) } doReturn person
} }
@ -131,7 +128,7 @@ class UsersAPTest {
application { application {
configureSerialization() configureSerialization()
routing { routing {
usersAP(activityPubUserService, userService) usersAP(activityPubUserService, mock(), mock(), TestTransaction)
} }
} }
client.get("/users/test") { client.get("/users/test") {
@ -174,8 +171,8 @@ class UsersAPTest {
environment { environment {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val userService = mock<IUserService> { val userService = mock<UserQueryService> {
onBlocking { findByNameLocalUser(eq("test")) } doReturn User( onBlocking { findByNameAndDomain(eq("test"), anyString()) } doReturn User(
1L, 1L,
"test", "test",
"example.com", "example.com",
@ -192,7 +189,7 @@ class UsersAPTest {
} }
application { application {
routing { routing {
usersAP(mock(), userService) usersAP(mock(), userService, mock(), TestTransaction)
} }
} }
client.get("/users/test") { client.get("/users/test") {

View File

@ -6,7 +6,6 @@ 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.hideout.dto.PostResponse import dev.usbharu.hideout.domain.model.hideout.dto.PostResponse
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.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.plugins.TOKEN_AUTH import dev.usbharu.hideout.plugins.TOKEN_AUTH
import dev.usbharu.hideout.plugins.configureSecurity import dev.usbharu.hideout.plugins.configureSecurity
@ -78,7 +77,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -159,7 +158,7 @@ class PostsTest {
configureSerialization() configureSerialization()
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -200,7 +199,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -251,7 +250,7 @@ class PostsTest {
} }
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -308,7 +307,7 @@ class PostsTest {
} }
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
configureSerialization() configureSerialization()
@ -379,7 +378,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -440,7 +439,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -501,7 +500,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -562,7 +561,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -602,7 +601,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -642,7 +641,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -682,7 +681,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }
@ -722,7 +721,7 @@ class PostsTest {
configureSecurity(mock(), mock()) configureSecurity(mock(), mock())
routing { routing {
route("/api/internal/v1") { route("/api/internal/v1") {
posts(postService, mock()) posts(postService)
} }
} }
} }

View File

@ -9,8 +9,9 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Post
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.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.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.user.IUserService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.mock.* import io.ktor.client.engine.mock.*
import kjob.core.job.JobProps import kjob.core.job.JobProps
@ -55,7 +56,7 @@ class ActivityPubNoteServiceImplTest {
createdAt = Instant.now() createdAt = Instant.now()
) )
) )
val userService = mock<IUserService> { val userQueryService = mock<UserQueryService> {
onBlocking { findById(eq(1L)) } doReturn User( onBlocking { findById(eq(1L)) } doReturn User(
1L, 1L,
"test", "test",
@ -69,11 +70,21 @@ class ActivityPubNoteServiceImplTest {
publicKey = "", publicKey = "",
createdAt = Instant.now() createdAt = Instant.now()
) )
}
val followerQueryService = mock<FollowerQueryService> {
onBlocking { findFollowersById(eq(1L)) } doReturn followers onBlocking { findFollowersById(eq(1L)) } doReturn followers
} }
val jobQueueParentService = mock<JobQueueParentService>() val jobQueueParentService = mock<JobQueueParentService>()
val activityPubNoteService = val activityPubNoteService =
ActivityPubNoteServiceImpl(mock(), jobQueueParentService, userService, mock(), mock()) ActivityPubNoteServiceImpl(
mock(),
jobQueueParentService,
mock(),
mock(),
userQueryService,
followerQueryService,
mock()
)
val postEntity = Post( val postEntity = Post(
1L, 1L,
1L, 1L,
@ -96,7 +107,15 @@ class ActivityPubNoteServiceImplTest {
respondOk() respondOk()
} }
) )
val activityPubNoteService = ActivityPubNoteServiceImpl(httpClient, mock(), mock(), mock(), mock()) val activityPubNoteService = ActivityPubNoteServiceImpl(
httpClient,
mock(),
mock(),
mock(),
mock(),
mock(),
mock()
)
activityPubNoteService.createNoteJob( activityPubNoteService.createNoteJob(
JobProps( JobProps(
data = mapOf<String, Any>( data = mapOf<String, Any>(

View File

@ -9,6 +9,7 @@ import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.ap.* import dev.usbharu.hideout.domain.model.ap.*
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import io.ktor.client.* import io.ktor.client.*
@ -23,6 +24,7 @@ import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.JsonObjectMapper import utils.JsonObjectMapper
import utils.TestTransaction
import java.time.Instant import java.time.Instant
class ActivityPubReceiveFollowServiceImplTest { class ActivityPubReceiveFollowServiceImplTest {
@ -32,7 +34,7 @@ class ActivityPubReceiveFollowServiceImplTest {
onBlocking { schedule(eq(ReceiveFollowJob), any()) } doReturn Unit onBlocking { schedule(eq(ReceiveFollowJob), any()) } doReturn Unit
} }
val activityPubFollowService = val activityPubFollowService =
ActivityPubReceiveFollowServiceImpl(jobQueueParentService, mock(), mock(), mock()) ActivityPubReceiveFollowServiceImpl(jobQueueParentService, mock(), mock(), mock(), mock(), TestTransaction)
activityPubFollowService.receiveFollow( activityPubFollowService.receiveFollow(
Follow( Follow(
emptyList(), emptyList(),
@ -97,8 +99,8 @@ class ActivityPubReceiveFollowServiceImplTest {
val activityPubUserService = mock<ActivityPubUserService> { val activityPubUserService = mock<ActivityPubUserService> {
onBlocking { fetchPerson(anyString(), any()) } doReturn person onBlocking { fetchPerson(anyString(), any()) } doReturn person
} }
val userService = mock<IUserService> { val userQueryService = mock<UserQueryService> {
onBlocking { findByUrls(any()) } doReturn listOf( onBlocking { findByUrl(eq("https://example.com")) } doReturn
User( User(
id = 1L, id = 1L,
name = "test", name = "test",
@ -110,7 +112,8 @@ class ActivityPubReceiveFollowServiceImplTest {
url = "https://example.com", url = "https://example.com",
publicKey = "", publicKey = "",
createdAt = Instant.now() createdAt = Instant.now()
), )
onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn
User( User(
id = 2L, id = 2L,
name = "follower", name = "follower",
@ -123,7 +126,9 @@ class ActivityPubReceiveFollowServiceImplTest {
publicKey = "", publicKey = "",
createdAt = Instant.now() createdAt = Instant.now()
) )
) }
val userService = mock<IUserService> {
onBlocking { followRequest(any(), any()) } doReturn false onBlocking { followRequest(any(), any()) } doReturn false
} }
val activityPubFollowService = val activityPubFollowService =
@ -156,7 +161,9 @@ class ActivityPubReceiveFollowServiceImplTest {
) )
respondOk() respondOk()
} }
) ),
userQueryService,
TestTransaction
) )
activityPubFollowService.receiveFollowJob( activityPubFollowService.receiveFollowJob(
JobProps( JobProps(

View File

@ -12,15 +12,17 @@ import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken
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.RefreshToken import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.exception.InvalidRefreshTokenException import dev.usbharu.hideout.exception.InvalidRefreshTokenException
import dev.usbharu.hideout.query.JwtRefreshTokenQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.IJwtRefreshTokenRepository import dev.usbharu.hideout.repository.IJwtRefreshTokenRepository
import dev.usbharu.hideout.service.core.IMetaService import dev.usbharu.hideout.service.core.IMetaService
import dev.usbharu.hideout.service.user.IUserService
import dev.usbharu.hideout.util.Base64Util import dev.usbharu.hideout.util.Base64Util
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPrivateKey
@ -50,7 +52,7 @@ class JwtServiceImplTest {
val refreshTokenRepository = mock<IJwtRefreshTokenRepository> { val refreshTokenRepository = mock<IJwtRefreshTokenRepository> {
onBlocking { generateId() } doReturn 1L onBlocking { generateId() } doReturn 1L
} }
val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, mock()) val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, mock(), mock())
val token = jwtService.createToken( val token = jwtService.createToken(
User( User(
id = 1L, id = 1L,
@ -93,6 +95,10 @@ class JwtServiceImplTest {
val generateKeyPair = keyPairGenerator.generateKeyPair() val generateKeyPair = keyPairGenerator.generateKeyPair()
val refreshTokenRepository = mock<IJwtRefreshTokenRepository> { val refreshTokenRepository = mock<IJwtRefreshTokenRepository> {
onBlocking { generateId() } doReturn 2L
}
val jwtRefreshTokenQueryService = mock<JwtRefreshTokenQueryService> {
onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken( onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken(
id = 1L, id = 1L,
userId = 1L, userId = 1L,
@ -100,9 +106,8 @@ class JwtServiceImplTest {
createdAt = Instant.now().minus(60, ChronoUnit.MINUTES), createdAt = Instant.now().minus(60, ChronoUnit.MINUTES),
expiresAt = Instant.now().plus(14, ChronoUnit.DAYS).minus(60, ChronoUnit.MINUTES) expiresAt = Instant.now().plus(14, ChronoUnit.DAYS).minus(60, ChronoUnit.MINUTES)
) )
onBlocking { generateId() } doReturn 2L
} }
val userService = mock<IUserService> { val userService = mock<UserQueryService> {
onBlocking { findById(1L) } doReturn User( onBlocking { findById(1L) } doReturn User(
id = 1L, id = 1L,
name = "test", name = "test",
@ -125,7 +130,7 @@ class JwtServiceImplTest {
Base64Util.encode(generateKeyPair.public.encoded) Base64Util.encode(generateKeyPair.public.encoded)
) )
} }
val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, userService) val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, userService, jwtRefreshTokenQueryService)
val refreshToken = jwtService.refreshToken(RefreshToken("refreshToken")) val refreshToken = jwtService.refreshToken(RefreshToken("refreshToken"))
assertNotEquals("", refreshToken.token) assertNotEquals("", refreshToken.token)
assertNotEquals("", refreshToken.refreshToken) assertNotEquals("", refreshToken.refreshToken)
@ -147,16 +152,28 @@ class JwtServiceImplTest {
@Test @Test
fun `refreshToken 無効なリフレッシュトークンは失敗する`() = runTest { fun `refreshToken 無効なリフレッシュトークンは失敗する`() = runTest {
val refreshTokenRepository = mock<IJwtRefreshTokenRepository> { val refreshTokenRepository = mock<JwtRefreshTokenQueryService> {
onBlocking { findByToken("InvalidRefreshToken") } doReturn null onBlocking { findByToken("InvalidRefreshToken") } doThrow NoSuchElementException()
} }
val jwtService = JwtServiceImpl(mock(), refreshTokenRepository, mock()) val kid = UUID.randomUUID()
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val generateKeyPair = keyPairGenerator.generateKeyPair()
val metaService = mock<IMetaService> {
onBlocking { getJwtMeta() } doReturn Jwt(
kid,
Base64Util.encode(generateKeyPair.private.encoded),
Base64Util.encode(generateKeyPair.public.encoded)
)
}
val jwtService = JwtServiceImpl(metaService, mock(), mock(), refreshTokenRepository)
assertThrows<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("InvalidRefreshToken")) } assertThrows<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("InvalidRefreshToken")) }
} }
@Test @Test
fun `refreshToken 未来に作成されたリフレッシュトークンは失敗する`() = runTest { fun `refreshToken 未来に作成されたリフレッシュトークンは失敗する`() = runTest {
val refreshTokenRepository = mock<IJwtRefreshTokenRepository> { val refreshTokenRepository = mock<JwtRefreshTokenQueryService> {
onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken( onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken(
id = 1L, id = 1L,
userId = 1L, userId = 1L,
@ -165,13 +182,25 @@ class JwtServiceImplTest {
expiresAt = Instant.now().plus(10, ChronoUnit.MINUTES).plus(14, ChronoUnit.DAYS) expiresAt = Instant.now().plus(10, ChronoUnit.MINUTES).plus(14, ChronoUnit.DAYS)
) )
} }
val jwtService = JwtServiceImpl(mock(), refreshTokenRepository, mock()) val kid = UUID.randomUUID()
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val generateKeyPair = keyPairGenerator.generateKeyPair()
val metaService = mock<IMetaService> {
onBlocking { getJwtMeta() } doReturn Jwt(
kid,
Base64Util.encode(generateKeyPair.private.encoded),
Base64Util.encode(generateKeyPair.public.encoded)
)
}
val jwtService = JwtServiceImpl(metaService, mock(), mock(), refreshTokenRepository)
assertThrows<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("refreshToken")) } assertThrows<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("refreshToken")) }
} }
@Test @Test
fun `refreshToken 期限切れのリフレッシュトークンでは失敗する`() = runTest { fun `refreshToken 期限切れのリフレッシュトークンでは失敗する`() = runTest {
val refreshTokenRepository = mock<IJwtRefreshTokenRepository> { val refreshTokenRepository = mock<JwtRefreshTokenQueryService> {
onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken( onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken(
id = 1L, id = 1L,
userId = 1L, userId = 1L,
@ -180,7 +209,19 @@ class JwtServiceImplTest {
expiresAt = Instant.now().minus(16, ChronoUnit.DAYS) expiresAt = Instant.now().minus(16, ChronoUnit.DAYS)
) )
} }
val jwtService = JwtServiceImpl(mock(), refreshTokenRepository, mock()) val kid = UUID.randomUUID()
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048)
val generateKeyPair = keyPairGenerator.generateKeyPair()
val metaService = mock<IMetaService> {
onBlocking { getJwtMeta() } doReturn Jwt(
kid,
Base64Util.encode(generateKeyPair.private.encoded),
Base64Util.encode(generateKeyPair.public.encoded)
)
}
val jwtService = JwtServiceImpl(metaService, mock(), mock(), refreshTokenRepository)
assertThrows<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("refreshToken")) } assertThrows<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("refreshToken")) }
} }
} }

View File

@ -11,6 +11,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.TestTransaction
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -21,7 +22,7 @@ class MetaServiceImplTest {
val metaRepository = mock<IMetaRepository> { val metaRepository = mock<IMetaRepository> {
onBlocking { get() } doReturn meta onBlocking { get() } doReturn meta
} }
val metaService = MetaServiceImpl(metaRepository) val metaService = MetaServiceImpl(metaRepository, TestTransaction)
val actual = metaService.getMeta() val actual = metaService.getMeta()
assertEquals(meta, actual) assertEquals(meta, actual)
} }
@ -31,7 +32,7 @@ class MetaServiceImplTest {
val metaRepository = mock<IMetaRepository> { val metaRepository = mock<IMetaRepository> {
onBlocking { get() } doReturn null onBlocking { get() } doReturn null
} }
val metaService = MetaServiceImpl(metaRepository) val metaService = MetaServiceImpl(metaRepository, TestTransaction)
assertThrows<NotInitException> { metaService.getMeta() } assertThrows<NotInitException> { metaService.getMeta() }
} }
@ -41,7 +42,7 @@ class MetaServiceImplTest {
val metaRepository = mock<IMetaRepository> { val metaRepository = mock<IMetaRepository> {
onBlocking { save(any()) } doReturn Unit onBlocking { save(any()) } doReturn Unit
} }
val metaServiceImpl = MetaServiceImpl(metaRepository) val metaServiceImpl = MetaServiceImpl(metaRepository, TestTransaction)
metaServiceImpl.updateMeta(meta) metaServiceImpl.updateMeta(meta)
argumentCaptor<Meta> { argumentCaptor<Meta> {
verify(metaRepository).save(capture()) verify(metaRepository).save(capture())
@ -55,7 +56,7 @@ class MetaServiceImplTest {
val metaRepository = mock<IMetaRepository> { val metaRepository = mock<IMetaRepository> {
onBlocking { get() } doReturn meta onBlocking { get() } doReturn meta
} }
val metaService = MetaServiceImpl(metaRepository) val metaService = MetaServiceImpl(metaRepository, TestTransaction)
val actual = metaService.getJwtMeta() val actual = metaService.getJwtMeta()
assertEquals(meta.jwt, actual) assertEquals(meta.jwt, actual)
} }
@ -65,7 +66,7 @@ class MetaServiceImplTest {
val metaRepository = mock<IMetaRepository> { val metaRepository = mock<IMetaRepository> {
onBlocking { get() } doReturn null onBlocking { get() } doReturn null
} }
val metaService = MetaServiceImpl(metaRepository) val metaService = MetaServiceImpl(metaRepository, TestTransaction)
assertThrows<NotInitException> { metaService.getJwtMeta() } assertThrows<NotInitException> { metaService.getJwtMeta() }
} }
} }

View File

@ -10,6 +10,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.TestTransaction
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -20,7 +21,7 @@ class ServerInitialiseServiceImplTest {
onBlocking { get() } doReturn null onBlocking { get() } doReturn null
onBlocking { save(any()) } doReturn Unit onBlocking { save(any()) } doReturn Unit
} }
val serverInitialiseServiceImpl = ServerInitialiseServiceImpl(metaRepository) val serverInitialiseServiceImpl = ServerInitialiseServiceImpl(metaRepository, TestTransaction)
serverInitialiseServiceImpl.init() serverInitialiseServiceImpl.init()
verify(metaRepository, times(1)).save(any()) verify(metaRepository, times(1)).save(any())
@ -32,7 +33,7 @@ class ServerInitialiseServiceImplTest {
val metaRepository = mock<IMetaRepository> { val metaRepository = mock<IMetaRepository> {
onBlocking { get() } doReturn meta onBlocking { get() } doReturn meta
} }
val serverInitialiseServiceImpl = ServerInitialiseServiceImpl(metaRepository) val serverInitialiseServiceImpl = ServerInitialiseServiceImpl(metaRepository, TestTransaction)
serverInitialiseServiceImpl.init() serverInitialiseServiceImpl.init()
verify(metaRepository, times(0)).save(any()) verify(metaRepository, times(0)).save(any())
} }
@ -45,7 +46,7 @@ class ServerInitialiseServiceImplTest {
onBlocking { save(any()) } doReturn Unit onBlocking { save(any()) } doReturn Unit
} }
val serverInitialiseServiceImpl = ServerInitialiseServiceImpl(metaRepository) val serverInitialiseServiceImpl = ServerInitialiseServiceImpl(metaRepository, TestTransaction)
serverInitialiseServiceImpl.init() serverInitialiseServiceImpl.init()
verify(metaRepository, times(1)).save(any()) verify(metaRepository, times(1)).save(any())
argumentCaptor<Meta> { argumentCaptor<Meta> {

View File

@ -28,7 +28,7 @@ class UserServiceTest {
onBlocking { hash(anyString()) } doReturn "hashedPassword" onBlocking { hash(anyString()) } doReturn "hashedPassword"
onBlocking { generateKeyPair() } doReturn generateKeyPair onBlocking { generateKeyPair() } doReturn generateKeyPair
} }
val userService = UserService(userRepository, userAuthService, mock()) val userService = UserService(userRepository, userAuthService, mock(), mock(), mock())
userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test")) userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test"))
verify(userRepository, times(1)).save(any()) verify(userRepository, times(1)).save(any())
argumentCaptor<dev.usbharu.hideout.domain.model.hideout.entity.User> { argumentCaptor<dev.usbharu.hideout.domain.model.hideout.entity.User> {
@ -54,7 +54,7 @@ class UserServiceTest {
val userRepository = mock<IUserRepository> { val userRepository = mock<IUserRepository> {
onBlocking { nextId() } doReturn 113345L onBlocking { nextId() } doReturn 113345L
} }
val userService = UserService(userRepository, mock(), mock()) val userService = UserService(userRepository, mock(), mock(), mock(), mock())
val user = RemoteUserCreateDto( val user = RemoteUserCreateDto(
"test", "test",
"example.com", "example.com",

View File

@ -1,28 +0,0 @@
package utils
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.junit.jupiter.api.extension.*
class DBResetInterceptor : BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
private lateinit var transactionAll: Transaction
private lateinit var transactionEach: Transaction
override fun beforeAll(context: ExtensionContext?) {
transactionAll = TransactionManager.manager.newTransaction()
}
override fun afterAll(context: ExtensionContext?) {
transactionAll.rollback()
transactionAll.close()
}
override fun beforeEach(context: ExtensionContext?) {
transactionEach = TransactionManager.manager.newTransaction(outerTransaction = transactionAll)
}
override fun afterEach(context: ExtensionContext?) {
transactionEach.rollback()
transactionEach.close()
}
}

View File

@ -1,18 +0,0 @@
package utils
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.DatabaseConfig
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(DBResetInterceptor::class)
abstract class DatabaseTestBase {
companion object {
init {
Database.connect(
"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
driver = "org.h2.Driver",
databaseConfig = DatabaseConfig { useNestedTransactions = true }
)
}
}
}

View File

@ -0,0 +1,7 @@
package utils
import dev.usbharu.hideout.service.core.Transaction
object TestTransaction : Transaction {
override suspend fun <T> transaction(block: suspend () -> T): T = block()
}