diff --git a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/Nodeinfo2_0.kt b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/Nodeinfo2_0.kt new file mode 100644 index 00000000..281099e0 --- /dev/null +++ b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/Nodeinfo2_0.kt @@ -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, +) diff --git a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/NodeinfoApplicationService.kt b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/NodeinfoApplicationService.kt new file mode 100644 index 00000000..e057084f --- /dev/null +++ b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/NodeinfoApplicationService.kt @@ -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) + } +} diff --git a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/NodeinfoRequest.kt b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/NodeinfoRequest.kt new file mode 100644 index 00000000..7176f3d8 --- /dev/null +++ b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/application/nodeinfo/NodeinfoRequest.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.activitypub.application.nodeinfo + +data class NodeinfoRequest(val version: String) diff --git a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/config/ActivityPubSecurityConfig.kt b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/config/ActivityPubSecurityConfig.kt index b445a086..9dfbbe4c 100644 --- a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/config/ActivityPubSecurityConfig.kt +++ b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/config/ActivityPubSecurityConfig.kt @@ -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) diff --git a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/HostmetaController.kt b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/HostmetaController.kt index 7b11c9cb..81641d1a 100644 --- a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/HostmetaController.kt +++ b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/HostmetaController.kt @@ -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) } diff --git a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/NodeinfoController.kt b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/NodeinfoController.kt new file mode 100644 index 00000000..4fc95339 --- /dev/null +++ b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/NodeinfoController.kt @@ -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) +} diff --git a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/XRD.kt b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/XRD.kt index e6617c3f..02a9792d 100644 --- a/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/XRD.kt +++ b/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/wellknown/XRD.kt @@ -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, ) diff --git a/hideout/hideout-activitypub/src/test/kotlin/dev/usbharu/hideout/activitypub/application/webfinger/NodeinfoApplicationServiceTest.kt b/hideout/hideout-activitypub/src/test/kotlin/dev/usbharu/hideout/activitypub/application/webfinger/NodeinfoApplicationServiceTest.kt new file mode 100644 index 00000000..8888de4f --- /dev/null +++ b/hideout/hideout-activitypub/src/test/kotlin/dev/usbharu/hideout/activitypub/application/webfinger/NodeinfoApplicationServiceTest.kt @@ -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"]) + } +} \ No newline at end of file diff --git a/hideout/hideout-activitypub/src/test/kotlin/wellknown/NodeinfoControllerTest.kt b/hideout/hideout-activitypub/src/test/kotlin/wellknown/NodeinfoControllerTest.kt new file mode 100644 index 00000000..4c440e57 --- /dev/null +++ b/hideout/hideout-activitypub/src/test/kotlin/wellknown/NodeinfoControllerTest.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/hideout/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt b/hideout/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt index 9a1ea2da..97d62803 100644 --- a/hideout/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt +++ b/hideout/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/config/SecurityConfig.kt @@ -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)