Merge pull request #82 from usbharu/feature/logging

Feature/logging
This commit is contained in:
usbharu 2023-10-12 02:29:11 +09:00 committed by GitHub
commit 42036dd8b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 239 additions and 65 deletions

View File

@ -113,10 +113,6 @@ dependencies {
compileOnly("io.swagger.core.v3:swagger-annotations:2.2.6") compileOnly("io.swagger.core.v3:swagger-annotations:2.2.6")
implementation("io.swagger.core.v3:swagger-models:2.2.6") implementation("io.swagger.core.v3:swagger-models:2.2.6")
implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version") implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version")
implementation("org.jetbrains.exposed:spring-transaction:$exposed_version")
implementation("org.springframework.data:spring-data-commons")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure") testImplementation("org.springframework.boot:spring-boot-test-autoconfigure")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
@ -149,7 +145,7 @@ dependencies {
implementation("org.drewcarlson:kjob-mongo:0.6.0") implementation("org.drewcarlson:kjob-mongo:0.6.0")
testImplementation("org.slf4j:slf4j-simple:2.0.7") testImplementation("org.slf4j:slf4j-simple:2.0.7")
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.22.0") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.1")
} }
detekt { detekt {

View File

@ -3,9 +3,11 @@ package dev.usbharu.hideout
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching
@SpringBootApplication @SpringBootApplication
@ConfigurationPropertiesScan @ConfigurationPropertiesScan
@EnableCaching
class SpringApplication class SpringApplication
@Suppress("SpreadOperator") @Suppress("SpreadOperator")

View File

@ -6,6 +6,7 @@ import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.core.Transaction
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.cio.* import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.logging.* import io.ktor.client.plugins.logging.*
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@ -20,7 +21,9 @@ class HttpClientConfig {
} }
install(Logging) { install(Logging) {
logger = Logger.DEFAULT logger = Logger.DEFAULT
level = LogLevel.ALL level = LogLevel.INFO
}
install(HttpCache) {
} }
expectSuccess = true expectSuccess = true
} }

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.config package dev.usbharu.hideout.config
import com.fasterxml.jackson.annotation.JsonInclude
import com.nimbusds.jose.jwk.JWKSet import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.ImmutableJWKSet import com.nimbusds.jose.jwk.source.ImmutableJWKSet
@ -8,12 +9,16 @@ import com.nimbusds.jose.proc.SecurityContext
import dev.usbharu.hideout.domain.model.UserDetailsImpl import dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.util.RsaUtil import dev.usbharu.hideout.util.RsaUtil
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
import org.springframework.boot.autoconfigure.security.servlet.PathRequest import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.core.annotation.Order import org.springframework.core.annotation.Order
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.security.config.Customizer import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
@ -155,6 +160,21 @@ class SecurityConfig {
} }
} }
} }
@Bean
@Primary
fun jackson2ObjectMapperBuilderCustomizer(): Jackson2ObjectMapperBuilderCustomizer {
return Jackson2ObjectMapperBuilderCustomizer {
it.serializationInclusion(JsonInclude.Include.ALWAYS).serializers()
}
}
@Bean
fun mappingJackson2HttpMessageConverter(): MappingJackson2HttpMessageConverter {
val builder = Jackson2ObjectMapperBuilder()
.serializationInclusion(JsonInclude.Include.NON_NULL)
return MappingJackson2HttpMessageConverter(builder.build())
}
} }
@ConfigurationProperties("hideout.security.jwt") @ConfigurationProperties("hideout.security.jwt")

View File

@ -2,6 +2,7 @@ package dev.usbharu.hideout.controller
import dev.usbharu.hideout.service.ap.APService import dev.usbharu.hideout.service.ap.APService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
@ -11,7 +12,12 @@ import org.springframework.web.bind.annotation.RestController
class InboxControllerImpl(private val apService: APService) : InboxController { class InboxControllerImpl(private val apService: APService) : InboxController {
override fun inbox(@RequestBody string: String): ResponseEntity<Unit> = runBlocking { override fun inbox(@RequestBody string: String): ResponseEntity<Unit> = runBlocking {
val parseActivity = apService.parseActivity(string) val parseActivity = apService.parseActivity(string)
LOGGER.info("INBOX Processing Activity Type: {}", parseActivity)
apService.processActivity(string, parseActivity) apService.processActivity(string, parseActivity)
ResponseEntity(HttpStatus.ACCEPTED) ResponseEntity(HttpStatus.ACCEPTED)
} }
companion object {
val LOGGER = LoggerFactory.getLogger(InboxControllerImpl::class.java)
}
} }

View File

@ -15,7 +15,6 @@ import org.springframework.web.bind.annotation.RequestParam
@Controller @Controller
class MastodonAppsApiController(private val appApiService: AppApiService) : AppApi { class MastodonAppsApiController(private val appApiService: AppApiService) : AppApi {
override fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity<Application> = runBlocking { override fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity<Application> = runBlocking {
println(appsRequest)
ResponseEntity( ResponseEntity(
appApiService.createApp(appsRequest), appApiService.createApp(appsRequest),
HttpStatus.OK HttpStatus.OK

View File

@ -13,7 +13,9 @@ import org.springframework.stereotype.Controller
@Controller @Controller
class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi { class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi {
override fun apiV1StatusesPost(devUsbharuHideoutDomainModelMastodonStatusesRequest: StatusesRequest): ResponseEntity<Status> { override fun apiV1StatusesPost(
devUsbharuHideoutDomainModelMastodonStatusesRequest: StatusesRequest
): ResponseEntity<Status> {
return runBlocking { return runBlocking {
val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt

View File

@ -51,7 +51,6 @@ class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UserDetailsImpl { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UserDetailsImpl {
val mapper = p.codec as ObjectMapper val mapper = p.codec as ObjectMapper
val jsonNode: JsonNode = mapper.readTree(p) val jsonNode: JsonNode = mapper.readTree(p)
println(jsonNode)
val authorities: Set<GrantedAuthority> = mapper.convertValue( val authorities: Set<GrantedAuthority> = mapper.convertValue(
jsonNode["authorities"], jsonNode["authorities"],
SIMPLE_GRANTED_AUTHORITY_SET SIMPLE_GRANTED_AUTHORITY_SET

View File

@ -54,7 +54,6 @@ open class Object : JsonLd {
class TypeSerializer : JsonSerializer<List<String>>() { class TypeSerializer : JsonSerializer<List<String>>() {
override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider?) { override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider?) {
println(value)
if (value?.size == 1) { if (value?.size == 1) {
gen?.writeString(value[0]) gen?.writeString(value[0])
} else { } else {

View File

@ -34,7 +34,6 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
return when (activityType) { return when (activityType) {
ExtendedActivityVocabulary.Follow -> { ExtendedActivityVocabulary.Follow -> {
val readValue = p.codec.treeToValue(treeNode, Follow::class.java) val readValue = p.codec.treeToValue(treeNode, Follow::class.java)
println(readValue)
readValue readValue
} }

View File

@ -1,7 +1,9 @@
package dev.usbharu.hideout.domain.model.hideout.entity package dev.usbharu.hideout.domain.model.hideout.entity
import org.springframework.data.annotation.Id import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
@Document
data class Timeline( data class Timeline(
@Id @Id
val id: Long, val id: Long,

View File

@ -72,6 +72,6 @@ class StatusesRequest {
`public`, `public`,
unlisted, unlisted,
private, private,
direct; direct
} }
} }

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.exception.ap
import dev.usbharu.hideout.exception.FailedToGetResourcesException
class FailedToGetActivityPubResourceException : FailedToGetResourcesException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
}

View File

@ -67,11 +67,9 @@ val httpSignaturePlugin: ClientPlugin<HttpSignaturePluginConfig> = createClientP
request.header("Date", format.format(Date())) request.header("Date", format.format(Date()))
request.header("Host", request.url.host) request.header("Host", request.url.host)
println(request.bodyType)
println(request.bodyType?.type)
if (request.bodyType?.type == String::class) { if (request.bodyType?.type == String::class) {
println(body as String) body as String
println("Digest !!")
// UserAuthService.sha256.reset() // UserAuthService.sha256.reset()
val digest = val digest =
Base64.getEncoder().encodeToString(UserAuthServiceImpl.sha256.digest(body.toByteArray(Charsets.UTF_8))) Base64.getEncoder().encodeToString(UserAuthServiceImpl.sha256.digest(body.toByteArray(Charsets.UTF_8)))

View File

@ -6,25 +6,24 @@ import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.PostsMedia import dev.usbharu.hideout.repository.PostsMedia
import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.repository.toPost
import dev.usbharu.hideout.util.singleOr import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
class PostQueryServiceImpl : PostQueryService { class PostQueryServiceImpl : PostQueryService {
override suspend fun findById(id: Long): Post = override suspend fun findById(id: Long): Post =
Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) Posts.leftJoin(PostsMedia)
.select { Posts.id eq id } .select { Posts.id eq id }
.singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }.toPost() .singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }.toPost()
override suspend fun findByUrl(url: String): Post = override suspend fun findByUrl(url: String): Post =
Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) Posts.leftJoin(PostsMedia)
.select { Posts.url eq url } .select { Posts.url eq url }
.toPost() .toPost()
.singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) } .singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }
override suspend fun findByApId(string: String): Post = override suspend fun findByApId(string: String): Post =
Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) Posts.leftJoin(PostsMedia)
.select { Posts.apId eq string } .select { Posts.apId eq string }
.toPost() .toPost()
.singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) } .singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) }

View File

@ -50,16 +50,16 @@ class StatusQueryServiceImpl : StatusQueryService {
@Suppress("FunctionMaxLength") @Suppress("FunctionMaxLength")
private suspend fun findByPostIdsWithMediaAttachments(ids: List<Long>): List<Status> { private suspend fun findByPostIdsWithMediaAttachments(ids: List<Long>): List<Status> {
val pairs = Posts val pairs = Posts
.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) .leftJoin(PostsMedia)
.innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }) .leftJoin(Users)
.innerJoin(Media, onColumn = { PostsMedia.mediaId }, otherColumn = { id }) .leftJoin(Media)
.select { Posts.id inList ids } .select { Posts.id inList ids }
.groupBy { it[Posts.id] } .groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { .map {
toStatus(it.first()).copy( toStatus(it.first()).copy(
mediaAttachments = it.map { mediaAttachments = it.mapNotNull {
it.toMedia().let { it.toMediaOrNull()?.let {
MediaAttachment( MediaAttachment(
id = it.id.toString(), id = it.id.toString(),
type = when (it.type) { type = when (it.type) {
@ -132,7 +132,7 @@ private fun toStatus(it: ResultRow) = Status(
favouritesCount = 0, favouritesCount = 0,
repliesCount = 0, repliesCount = 0,
url = it[Posts.apId], url = it[Posts.apId],
inReplyToId = it[Posts.replyId].toString(), inReplyToId = it[Posts.replyId]?.toString(),
inReplyToAccountId = null, inReplyToAccountId = null,
language = null, language = null,
text = it[Posts.text], text = it[Posts.text],

View File

@ -69,6 +69,18 @@ fun ResultRow.toMedia(): EntityMedia {
) )
} }
fun ResultRow.toMediaOrNull(): EntityMedia? {
return EntityMedia(
id = this.getOrNull(Media.id) ?: return null,
name = this.getOrNull(Media.name) ?: return null,
url = this.getOrNull(Media.url) ?: return null,
remoteUrl = this[Media.remoteUrl],
thumbnailUrl = this[Media.thumbnailUrl],
type = FileType.values().first { it.ordinal == this.getOrNull(Media.type) },
blurHash = this[Media.blurhash],
)
}
object Media : Table("media") { object Media : Table("media") {
val id = long("id") val id = long("id")
val name = varchar("name", 255) val name = varchar("name", 255)

View File

@ -54,6 +54,11 @@ class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : Pos
it[apId] = post.apId it[apId] = post.apId
} }
} }
assert(Posts.select { Posts.id eq post.id }.singleOrNull() != null) {
"Faild to insert"
}
return post return post
} }
@ -109,5 +114,5 @@ fun ResultRow.toPost(): Post {
fun Query.toPost(): List<Post> { fun Query.toPost(): List<Post> {
return this.groupBy { it[Posts.id] } return this.groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { it.first().toPost().copy(mediaIds = it.map { it[PostsMedia.mediaId] }) } .map { it.first().toPost().copy(mediaIds = it.mapNotNull { it.getOrNull(PostsMedia.mediaId) }) }
} }

View File

@ -14,8 +14,8 @@ class UserRepositoryImpl(private val idGenerateService: IdGenerateService) :
UserRepository { UserRepository {
override suspend fun save(user: User): User { override suspend fun save(user: User): User {
val singleOrNull = Users.select { Users.id eq user.id }.singleOrNull() val singleOrNull = Users.select { Users.id eq user.id or (Users.url eq user.url) }.empty()
if (singleOrNull == null) { if (singleOrNull) {
Users.insert { Users.insert {
it[id] = user.id it[id] = user.id
it[name] = user.name it[name] = user.name

View File

@ -10,6 +10,7 @@ import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.user.UserService import dev.usbharu.hideout.service.user.UserService
import io.ktor.http.* import io.ktor.http.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
interface APAcceptService { interface APAcceptService {
@ -25,21 +26,32 @@ class APAcceptServiceImpl(
) : APAcceptService { ) : APAcceptService {
override suspend fun receiveAccept(accept: Accept): ActivityPubResponse { override suspend fun receiveAccept(accept: Accept): ActivityPubResponse {
return transaction.transaction { return transaction.transaction {
LOGGER.debug("START Follow")
LOGGER.trace("{}", accept)
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()) {
LOGGER.warn("FAILED Activity type is not 'Follow'")
throw IllegalActivityPubObjectException("Invalid type ${value.type}") throw IllegalActivityPubObjectException("Invalid type ${value.type}")
} }
val follow = value as Follow val follow = value as Follow
val userUrl = follow.`object` ?: throw IllegalActivityPubObjectException("object is null") val userUrl = follow.`object` ?: throw IllegalActivityPubObjectException("object is null")
val followerUrl = follow.actor ?: throw IllegalActivityPubObjectException("actor is null") val followerUrl = follow.actor ?: throw IllegalActivityPubObjectException("actor is null")
val user = userQueryService.findByUrl(userUrl) val user = userQueryService.findByUrl(userUrl)
val follower = userQueryService.findByUrl(followerUrl) val follower = userQueryService.findByUrl(followerUrl)
if (followerQueryService.alreadyFollow(user.id, follower.id)) { if (followerQueryService.alreadyFollow(user.id, follower.id)) {
LOGGER.debug("END User already follow from ${follower.url} to ${user.url}")
return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "accepted") return@transaction ActivityPubStringResponse(HttpStatusCode.OK, "accepted")
} }
userService.follow(user.id, follower.id) userService.follow(user.id, follower.id)
LOGGER.debug("SUCCESS Follow from ${follower.url} to ${user.url}.")
ActivityPubStringResponse(HttpStatusCode.OK, "accepted") ActivityPubStringResponse(HttpStatusCode.OK, "accepted")
} }
} }
companion object {
private val LOGGER = LoggerFactory.getLogger(APAcceptServiceImpl::class.java)
}
} }

View File

@ -7,6 +7,7 @@ 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 dev.usbharu.hideout.service.core.Transaction
import io.ktor.http.* import io.ktor.http.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
interface APCreateService { interface APCreateService {
@ -19,15 +20,24 @@ class APCreateServiceImpl(
private val transaction: Transaction private val transaction: Transaction
) : APCreateService { ) : APCreateService {
override suspend fun receiveCreate(create: Create): ActivityPubResponse { override suspend fun receiveCreate(create: Create): ActivityPubResponse {
LOGGER.debug("START Create new remote note.")
LOGGER.trace("{}", create)
val value = create.`object` ?: throw IllegalActivityPubObjectException("object is null") val value = create.`object` ?: throw IllegalActivityPubObjectException("object is null")
if (value.type.contains("Note").not()) { if (value.type.contains("Note").not()) {
LOGGER.warn("FAILED Object type is not 'Note'")
throw IllegalActivityPubObjectException("object is not Note") throw IllegalActivityPubObjectException("object is not Note")
} }
return transaction.transaction { return transaction.transaction {
val note = value as Note val note = value as Note
apNoteService.fetchNote(note) apNoteService.fetchNote(note)
LOGGER.debug("SUCCESS Create new remote note. ${note.id} by ${note.attributedTo}")
ActivityPubStringResponse(HttpStatusCode.OK, "Created") ActivityPubStringResponse(HttpStatusCode.OK, "Created")
} }
} }
companion object {
private val LOGGER = LoggerFactory.getLogger(APCreateServiceImpl::class.java)
}
} }

View File

@ -3,11 +3,13 @@ package dev.usbharu.hideout.service.ap
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.ap.FailedToGetActivityPubResourceException
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.reaction.ReactionService import dev.usbharu.hideout.service.reaction.ReactionService
import io.ktor.http.* import io.ktor.http.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
interface APLikeService { interface APLikeService {
@ -23,14 +25,27 @@ class APLikeServiceImpl(
private val transaction: Transaction private val transaction: Transaction
) : APLikeService { ) : APLikeService {
override suspend fun receiveLike(like: Like): ActivityPubResponse { override suspend fun receiveLike(like: Like): ActivityPubResponse {
LOGGER.debug("START Add Like")
LOGGER.trace("{}", like)
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(java.sql.Connection.TRANSACTION_SERIALIZABLE) { transaction.transaction {
LOGGER.trace("FETCH Liked Person $actor")
val person = apUserService.fetchPersonWithEntity(actor) val person = apUserService.fetchPersonWithEntity(actor)
apNoteService.fetchNote(like.`object` ?: return@transaction) LOGGER.trace("{}", person.second)
LOGGER.trace("FETCH Liked Note ${like.`object`}")
try {
apNoteService.fetchNoteAsync(like.`object` ?: return@transaction).await()
} catch (e: FailedToGetActivityPubResourceException) {
LOGGER.debug("FAILED Failed to Get ${like.`object`}")
LOGGER.trace("", e)
return@transaction
}
val post = postQueryService.findByUrl(like.`object` ?: return@transaction) val post = postQueryService.findByUrl(like.`object` ?: return@transaction)
LOGGER.trace("{}", post)
reactionService.receiveReaction( reactionService.receiveReaction(
content, content,
@ -38,7 +53,12 @@ class APLikeServiceImpl(
person.second.id, person.second.id,
post.id post.id
) )
LOGGER.debug("SUCCESS Add Like($content) from ${person.second.url} to ${post.url}")
} }
return ActivityPubStringResponse(HttpStatusCode.OK, "") return ActivityPubStringResponse(HttpStatusCode.OK, "")
} }
companion object {
private val LOGGER = LoggerFactory.getLogger(APLikeServiceImpl::class.java)
}
} }

View File

@ -10,6 +10,7 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.exception.ap.FailedToGetActivityPubResourceException
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
@ -22,10 +23,17 @@ import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.post.PostCreateInterceptor import dev.usbharu.hideout.service.post.PostCreateInterceptor
import dev.usbharu.hideout.service.post.PostService import dev.usbharu.hideout.service.post.PostService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import kjob.core.job.JobProps import kjob.core.job.JobProps
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant import java.time.Instant
@ -34,6 +42,15 @@ interface APNoteService {
suspend fun createNote(post: Post) suspend fun createNote(post: Post)
suspend fun createNoteJob(props: JobProps<DeliverPostJob>) suspend fun createNoteJob(props: JobProps<DeliverPostJob>)
@Cacheable("fetchNote")
fun fetchNoteAsync(url: String, targetActor: String? = null): Deferred<Note> {
return CoroutineScope(Dispatchers.IO).async {
newSuspendedTransaction {
fetchNote(url, targetActor)
}
}
}
suspend fun fetchNote(url: String, targetActor: String? = null): Note suspend fun fetchNote(url: String, targetActor: String? = null): Note
suspend fun fetchNote(note: Note, targetActor: String? = null): Note suspend fun fetchNote(note: Note, targetActor: String? = null): Note
} }
@ -62,7 +79,13 @@ class APNoteServiceImpl(
private val logger = LoggerFactory.getLogger(APNoteServiceImpl::class.java) private val logger = LoggerFactory.getLogger(APNoteServiceImpl::class.java)
override suspend fun createNote(post: Post) { override suspend fun createNote(post: Post) {
logger.info("CREATE Create Local Note ${post.url}")
logger.debug("START Create Local Note ${post.url}")
logger.trace("{}", post)
val followers = followerQueryService.findFollowersById(post.userId) val followers = followerQueryService.findFollowersById(post.userId)
logger.debug("DELIVER Deliver Note Create ${followers.size} accounts.")
val userEntity = userQueryService.findById(post.userId) val userEntity = userQueryService.findById(post.userId)
val note = objectMapper.writeValueAsString(post) val note = objectMapper.writeValueAsString(post)
val mediaList = objectMapper.writeValueAsString(mediaQueryService.findByPostId(post.id)) val mediaList = objectMapper.writeValueAsString(mediaQueryService.findByPostId(post.id))
@ -74,6 +97,8 @@ class APNoteServiceImpl(
props[DeliverPostJob.media] = mediaList props[DeliverPostJob.media] = mediaList
} }
} }
logger.debug("SUCCESS Create Local Note ${post.url}")
} }
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) { override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
@ -109,18 +134,32 @@ class APNoteServiceImpl(
} }
override suspend fun fetchNote(url: String, targetActor: String?): Note { override suspend fun fetchNote(url: String, targetActor: String?): Note {
logger.debug("START Fetch Note url: {}", url)
try { try {
val post = postQueryService.findByUrl(url) val post = postQueryService.findByUrl(url)
logger.debug("SUCCESS Found in local url: {}", url)
return postToNote(post) return postToNote(post)
} catch (_: FailedToGetResourcesException) { } catch (_: FailedToGetResourcesException) {
} }
val response = httpClient.getAp( logger.info("AP GET url: {}", url)
url, val response = try {
targetActor?.let { "$targetActor#pubkey" } httpClient.getAp(
) url,
targetActor?.let { "$targetActor#pubkey" }
)
} catch (e: ClientRequestException) {
logger.warn(
"FAILED Failed to retrieve ActivityPub resource. HTTP Status Code: {} url: {}",
e.response.status,
url
)
throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e)
}
val note = objectMapper.readValue<Note>(response.bodyAsText()) val note = objectMapper.readValue<Note>(response.bodyAsText())
return note(note, targetActor, url) val savedNote = saveIfMissing(note, targetActor, url)
logger.debug("SUCCESS Fetch Note url: {}", url)
return savedNote
} }
private suspend fun postToNote(post: Post): Note { private suspend fun postToNote(post: Post): Note {
@ -139,7 +178,7 @@ class APNoteServiceImpl(
) )
} }
private suspend fun note( private suspend fun saveIfMissing(
note: Note, note: Note,
targetActor: String?, targetActor: String?,
url: String url: String
@ -152,12 +191,12 @@ class APNoteServiceImpl(
val findByApId = try { val findByApId = try {
postQueryService.findByApId(note.id!!) postQueryService.findByApId(note.id!!)
} catch (_: FailedToGetResourcesException) { } catch (_: FailedToGetResourcesException) {
return internalNote(note, targetActor, url) return saveNote(note, targetActor, url)
} }
return postToNote(findByApId) return postToNote(findByApId)
} }
private suspend fun internalNote(note: Note, targetActor: String?, url: String): Note { private suspend fun saveNote(note: Note, targetActor: String?, url: String): Note {
val person = apUserService.fetchPersonWithEntity( val person = apUserService.fetchPersonWithEntity(
note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"),
targetActor targetActor
@ -197,7 +236,7 @@ class APNoteServiceImpl(
} }
override suspend fun fetchNote(note: Note, targetActor: String?): Note = override suspend fun fetchNote(note: Note, targetActor: String?): Note =
note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null")) saveIfMissing(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null"))
override suspend fun run(post: Post) { override suspend fun run(post: Post) {
createNote(post) createNote(post)

View File

@ -203,16 +203,12 @@ class APServiceImpl(
@Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration") @Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration")
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse { override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse {
logger.debug("proccess activity: {}", type) logger.debug("process activity: {}", type)
return when (type) { return when (type) {
ActivityType.Accept -> apAcceptService.receiveAccept(objectMapper.readValue(json)) ActivityType.Accept -> apAcceptService.receiveAccept(objectMapper.readValue(json))
ActivityType.Follow -> apReceiveFollowService.receiveFollow( ActivityType.Follow ->
objectMapper.readValue( apReceiveFollowService
json, .receiveFollow(objectMapper.readValue(json, Follow::class.java))
Follow::class.java
)
)
ActivityType.Create -> apCreateService.receiveCreate(objectMapper.readValue(json)) ActivityType.Create -> apCreateService.receiveCreate(objectMapper.readValue(json))
ActivityType.Like -> apLikeService.receiveLike(objectMapper.readValue(json)) ActivityType.Like -> apLikeService.receiveLike(objectMapper.readValue(json))
ActivityType.Undo -> apUndoService.receiveUndo(objectMapper.readValue(json)) ActivityType.Undo -> apUndoService.receiveUndo(objectMapper.readValue(json))
@ -226,8 +222,6 @@ class APServiceImpl(
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) { override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
logger.debug("processActivity: ${hideoutJob.name}") logger.debug("processActivity: ${hideoutJob.name}")
// println(apReceiveFollowService::class.java)
// apReceiveFollowService.receiveFollowJob(job.props as JobProps<ReceiveFollowJob>)
when (hideoutJob) { when (hideoutJob) {
is ReceiveFollowJob -> { is ReceiveFollowJob -> {
apReceiveFollowService.receiveFollowJob( apReceiveFollowService.receiveFollowJob(

View File

@ -39,7 +39,6 @@ class StatsesApiServiceImpl(
statusesRequest: dev.usbharu.hideout.domain.model.mastodon.StatusesRequest, statusesRequest: dev.usbharu.hideout.domain.model.mastodon.StatusesRequest,
userId: Long userId: Long
): Status = transaction.transaction { ): Status = transaction.transaction {
println("Post status media ids " + statusesRequest.media_ids)
val visibility = when (statusesRequest.visibility) { val visibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Visibility.PUBLIC StatusesRequest.Visibility.public -> Visibility.PUBLIC
StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED

View File

@ -6,7 +6,7 @@ import org.springframework.stereotype.Service
@Service @Service
class ExposedTransaction : Transaction { class ExposedTransaction : Transaction {
override suspend fun <T> transaction(block: suspend () -> T): T { override suspend fun <T> transaction(block: suspend () -> T): T {
return newSuspendedTransaction(transactionIsolation = java.sql.Connection.TRANSACTION_SERIALIZABLE) { return newSuspendedTransaction {
block() block()
} }
} }

View File

@ -0,0 +1,26 @@
package dev.usbharu.hideout.service.core
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import org.slf4j.MDC
import org.springframework.stereotype.Service
import java.util.*
@Service
class MdcXrequestIdFilter : Filter {
override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain) {
val uuid = UUID.randomUUID()
try {
MDC.put(KEY, uuid.toString())
chain.doFilter(request, response)
} finally {
MDC.remove(KEY)
}
}
companion object {
private const val KEY = "x-request-id"
}
}

View File

@ -3,8 +3,10 @@ package dev.usbharu.hideout.service.post
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.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.repository.PostRepository import dev.usbharu.hideout.repository.PostRepository
import dev.usbharu.hideout.repository.UserRepository import dev.usbharu.hideout.repository.UserRepository
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -13,7 +15,8 @@ import java.util.*
class PostServiceImpl( class PostServiceImpl(
private val postRepository: PostRepository, private val postRepository: PostRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val timelineService: TimelineService private val timelineService: TimelineService,
private val postQueryService: PostQueryService
) : PostService { ) : PostService {
private val interceptors = Collections.synchronizedList(mutableListOf<PostCreateInterceptor>()) private val interceptors = Collections.synchronizedList(mutableListOf<PostCreateInterceptor>())
@ -30,8 +33,13 @@ class PostServiceImpl(
} }
private suspend fun internalCreate(post: Post, isLocal: Boolean): Post { private suspend fun internalCreate(post: Post, isLocal: Boolean): Post {
timelineService.publishTimeline(post, isLocal) val save = try {
return postRepository.save(post) postRepository.save(post)
} catch (_: ExposedSQLException) {
postQueryService.findByApId(post.apId)
}
timelineService.publishTimeline(save, isLocal)
return save
} }
private suspend fun internalCreate(post: PostCreateDto, isLocal: Boolean): Post { private suspend fun internalCreate(post: PostCreateDto, isLocal: Boolean): Post {

View File

@ -4,6 +4,8 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.query.ReactionQueryService import dev.usbharu.hideout.query.ReactionQueryService
import dev.usbharu.hideout.repository.ReactionRepository import dev.usbharu.hideout.repository.ReactionRepository
import dev.usbharu.hideout.service.ap.APReactionService import dev.usbharu.hideout.service.ap.APReactionService
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
@ -14,9 +16,14 @@ class ReactionServiceImpl(
) : ReactionService { ) : ReactionService {
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 (reactionQueryService.reactionAlreadyExist(postId, userId, 0).not()) { if (reactionQueryService.reactionAlreadyExist(postId, userId, 0).not()) {
reactionRepository.save( try {
Reaction(reactionRepository.generateId(), 0, postId, userId) reactionRepository.save(
) Reaction(reactionRepository.generateId(), 0, postId, userId)
)
} catch (e: ExposedSQLException) {
LOGGER.warn("FAILED Failure to persist reaction information.")
LOGGER.debug("FAILED", e)
}
} }
} }
@ -34,4 +41,8 @@ class ReactionServiceImpl(
override suspend fun removeReaction(userId: Long, postId: Long) { override suspend fun removeReaction(userId: Long, postId: Long) {
reactionQueryService.deleteByPostIdAndUserId(postId, userId) reactionQueryService.deleteByPostIdAndUserId(postId, userId)
} }
companion object {
val LOGGER = LoggerFactory.getLogger(ReactionServiceImpl::class.java)
}
} }

View File

@ -10,6 +10,7 @@ import dev.usbharu.hideout.query.FollowerQueryService
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.repository.UserRepository import dev.usbharu.hideout.repository.UserRepository
import dev.usbharu.hideout.service.ap.APSendFollowService import dev.usbharu.hideout.service.ap.APSendFollowService
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant import java.time.Instant
@ -64,7 +65,11 @@ class UserServiceImpl(
publicKey = user.publicKey, publicKey = user.publicKey,
createdAt = Instant.now() createdAt = Instant.now()
) )
return userRepository.save(userEntity) return try {
userRepository.save(userEntity)
} catch (_: ExposedSQLException) {
userQueryService.findByUrl(user.url)
}
} }
// TODO APのフォロー処理を作る // TODO APのフォロー処理を作る

View File

@ -20,7 +20,6 @@ object HttpUtil {
subType: String, subType: String,
parameter: String parameter: String
): Boolean { ): Boolean {
println("$contentType/$subType $parameter")
if (contentType != "application") { if (contentType != "application") {
return false return false
} }

View File

@ -49,7 +49,7 @@ class ExposedLockRepository(
val lock = Lock(id, now) val lock = Lock(id, now)
query { query {
if (locks.select(locks.id eq id).limit(1) if (locks.select(locks.id eq id).limit(1)
.map { Lock(it[locks.id].value, Instant.ofEpochMilli(it[locks.expiresAt])) }.isEmpty() .map { Lock(it[locks.id].value, Instant.ofEpochMilli(it[locks.expiresAt])) }.isEmpty()
) { ) {
locks.insert { locks.insert {
it[locks.id] = id it[locks.id] = id

View File

@ -16,6 +16,7 @@ spring:
jackson: jackson:
serialization: serialization:
WRITE_DATES_AS_TIMESTAMPS: false WRITE_DATES_AS_TIMESTAMPS: false
default-property-inclusion: always
datasource: datasource:
driver-class-name: org.h2.Driver driver-class-name: org.h2.Driver
url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL" url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL"

View File

@ -1,10 +1,10 @@
<configuration> <configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<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 [%X{x-request-id}] %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="DEBUG"> <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"/>
@ -12,6 +12,5 @@
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/> <logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
<logger name="Exposed" level="INFO"/> <logger name="Exposed" level="INFO"/>
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/> <logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
<logger name="org.springframework.security" level="DEBUG"/> <logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="INFO"/>
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG"/>
</configuration> </configuration>