Merge pull request #684 from usbharu/nodeinfo

Nodeinfoを実装
This commit is contained in:
usbharu 2025-02-25 11:44:48 +09:00 committed by GitHub
commit f49bec3252
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 235 additions and 10 deletions

View File

@ -0,0 +1,16 @@
package dev.usbharu.hideout.activitypub.application.nodeinfo
@Suppress("ClassName")
data class Nodeinfo2_0(
val version: String = "2,0",
val software: Map<String, String>,
val protocol: List<String>,
val usage: NodeinfoUsage,
val openRegistration: Boolean,
val metadata: Map<String, Any>,
)
data class NodeinfoUsage(
val users: Map<String, Long>,
val localPosts: Long,
)

View File

@ -0,0 +1,52 @@
package dev.usbharu.hideout.activitypub.application.nodeinfo
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.info.BuildProperties
import org.springframework.stereotype.Service
@Service/*
* 今後ユーザーエージェントに応じてレスポンスを変更する可能性があるためApplicationServiceとして作成する commandは現時点ではUnitだが今後変化する可能性あり
*/
class NodeinfoApplicationService(
@Autowired(required = false) private val buildInfo: BuildProperties?,
private val applicationConfig: ApplicationConfig,
transaction: Transaction,
) : AbstractApplicationService<NodeinfoRequest, Nodeinfo2_0>(
transaction,
logger
) {
override suspend fun internalExecute(command: NodeinfoRequest, principal: Principal): Nodeinfo2_0 {
return when (command.version) {
"2.0", "2.1" -> Nodeinfo2_0(
version = command.version,
software = mapOf(
"name" to "hideout",
"version" to (buildInfo?.version ?: "UNKNOWN")
),
protocol = listOf("activitypub"),
NodeinfoUsage(
users = mapOf(
"total" to 0,
"activeMonth" to 0,
"activeHalfyear" to 0
),
localPosts = 0
),
openRegistration = applicationConfig.private.not(),
metadata = mapOf()
)
else -> throw IllegalArgumentException("Invalid command version ${command.version}")
}
}
companion object {
private val logger: Logger = LoggerFactory.getLogger(NodeinfoApplicationService::class.java)
}
}

View File

@ -0,0 +1,3 @@
package dev.usbharu.hideout.activitypub.application.nodeinfo
data class NodeinfoRequest(val version: String)

View File

@ -24,6 +24,7 @@ class ActivityPubSecurityConfig {
)
authorizeHttpRequests {
authorize(GET, "/.well-known/**", permitAll)
authorize(GET, "/nodeinfo/**", permitAll)
authorize(GET, "/error", permitAll)
authorize(POST, "/inbox", permitAll)
authorize(POST, "/users/{username}/inbox", permitAll)

View File

@ -19,12 +19,8 @@ class HostmetaController(private val linkList: List<Link> = emptyList()) {
@Order(2)
@GetMapping("/host-meta", produces = ["application/json"])
fun hostmetaJson(): XRD {
return XRD(linkList)
}
fun hostmetaJson(): XRD = XRD(linkList)
@GetMapping("/host-meta.json", produces = ["application/json"])
fun hostmetaJson2(): XRD {
return XRD(linkList)
}
fun hostmetaJson2(): XRD = XRD(linkList)
}

View File

@ -0,0 +1,35 @@
package dev.usbharu.hideout.activitypub.interfaces.wellknown
import dev.usbharu.hideout.activitypub.application.nodeinfo.Nodeinfo2_0
import dev.usbharu.hideout.activitypub.application.nodeinfo.NodeinfoApplicationService
import dev.usbharu.hideout.activitypub.application.nodeinfo.NodeinfoRequest
import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class NodeinfoController(
private val applicationConfig: ApplicationConfig,
private val nodeinfoApplicationService: NodeinfoApplicationService,
) {
@GetMapping("/.well-known/nodeinfo", produces = ["application/json"])
fun nodeinfo(): XRD = XRD(
listOf(
Link(
"http://nodeinfo.diaspora.software/ns/schema/2.1",
href = applicationConfig.url.resolve("/nodeinfo/2.1").toString()
),
Link(
"http://nodeinfo.diaspora.software/ns/schema/2.0",
href = applicationConfig.url.resolve("/nodeinfo/2.0").toString()
)
)
)
@GetMapping("/nodeinfo/2.0", produces = ["application/json"])
suspend fun nodeinfo2_0(): Nodeinfo2_0 = nodeinfoApplicationService.execute(NodeinfoRequest("2.0"), Anonymous)
@GetMapping("/nodeinfo/2.1", produces = ["application/json"])
suspend fun nodeinfo2_1(): Nodeinfo2_0 = nodeinfoApplicationService.execute(NodeinfoRequest("2.1"), Anonymous)
}

View File

@ -24,8 +24,9 @@ data class XRD(
data class Link(
@JacksonXmlProperty(localName = "rel", isAttribute = true) val rel: String,
@JsonInclude(JsonInclude.Include.NON_NULL)
@JacksonXmlProperty(localName = "template", isAttribute = true) val template: String?,
@JacksonXmlProperty(localName = "type", isAttribute = true) val type: String,
@JacksonXmlProperty(localName = "template", isAttribute = true) val template: String? = null,
@JsonInclude(JsonInclude.Include.NON_NULL)
@JacksonXmlProperty(localName = "href", isAttribute = true) val href: String?,
@JacksonXmlProperty(localName = "type", isAttribute = true) val type: String? = null,
@JsonInclude(JsonInclude.Include.NON_NULL)
@JacksonXmlProperty(localName = "href", isAttribute = true) val href: String? = null,
)

View File

@ -0,0 +1,52 @@
package dev.usbharu.hideout.activitypub.application.webfinger
import dev.usbharu.hideout.activitypub.application.nodeinfo.NodeinfoApplicationService
import dev.usbharu.hideout.activitypub.application.nodeinfo.NodeinfoRequest
import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Spy
import org.mockito.junit.jupiter.MockitoExtension
import org.springframework.boot.info.BuildProperties
import util.TestTransaction
import java.net.URI
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@ExtendWith(MockitoExtension::class)
class NodeinfoApplicationServiceTest {
@InjectMocks
lateinit var nodeinfoApplicationService: NodeinfoApplicationService
@Spy
val buildInfo: BuildProperties = BuildProperties(Properties())
@Spy
val applicationConfig = ApplicationConfig(URI.create("https://example.com"))
@Spy
val transaction = TestTransaction
@Test
fun nodeinfo2_0() = runTest {
val execute = nodeinfoApplicationService.execute(NodeinfoRequest("2.0"), Anonymous)
assertFalse(execute.openRegistration)
assertEquals("2.0", execute.version)
assertEquals("hideout", execute.software["name"])
}
@Test
fun nodeinfo2_1() = runTest {
val execute = nodeinfoApplicationService.execute(NodeinfoRequest("2.1"), Anonymous)
assertFalse(execute.openRegistration)
assertEquals("2.1", execute.version)
assertEquals("hideout", execute.software["name"])
}
}

View File

@ -0,0 +1,69 @@
package wellknown
import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
class NodeinfoControllerTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
}
@Test
fun nodeinfo() {
mockMvc.get("/.well-known/nodeinfo")
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
}
@Test
fun nodeinfo2_0() {
mockMvc.get("/nodeinfo/2.0")
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
}
@Test
fun nodeinfo2_1() {
mockMvc.get("/nodeinfo/2.1")
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -90,7 +90,7 @@ class SecurityConfig {
authorize("/error", permitAll)
authorize("/auth/sign_in", permitAll)
authorize(GET, "/.well-known/**", permitAll)
authorize(GET, "/nodeinfo/2.0", permitAll)
authorize(GET, "/nodeinfo/**", permitAll)
authorize(GET, "/auth/sign_up", hasRole("ANONYMOUS"))
authorize(POST, "/auth/sign_up", permitAll)