diff --git a/core/src/main/kotlin/dev/usbharu/multim/MultiM.kt b/core/src/main/kotlin/dev/usbharu/multim/MultiM.kt new file mode 100644 index 0000000..824be05 --- /dev/null +++ b/core/src/main/kotlin/dev/usbharu/multim/MultiM.kt @@ -0,0 +1,27 @@ +package dev.usbharu.multim + +import dev.usbharu.multim.api.NodeinfoApi +import dev.usbharu.multim.factory.MultiMApis +import dev.usbharu.multim.factory.PlatformApiFactory +import io.ktor.client.* +import io.ktor.client.engine.cio.* + +object MultiM { + + + + // todo 認証等を何とかするために、返却するのはAPIをクライアントではなくAPIクライアントのビルダーにする。 + /** + * URLにアクセスして実装を推測し、自動でAPIクライアントを作成します。. + * URLからの推測にはwell-known/nodeinfoを使用しています。 + * + * @param url 使用するURLの末尾にスラッシュを付けてください。 + * @param factory 使用するファクトリークラスのインスタンス。 + * @return 作成されたAPIクライアント + */ + suspend fun createClient(url:String, factory: PlatformApiFactory):MultiMApis{ + val httpClient = HttpClient(CIO) + val nodeinfo = NodeinfoApi(httpClient).nodeinfo(url) + return factory.factory(nodeInfo = nodeinfo, httpClient, "", url) + } +} diff --git a/core/src/main/kotlin/dev/usbharu/multim/api/ApiClient.kt b/core/src/main/kotlin/dev/usbharu/multim/api/ApiClient.kt index 85e668c..515ddd2 100644 --- a/core/src/main/kotlin/dev/usbharu/multim/api/ApiClient.kt +++ b/core/src/main/kotlin/dev/usbharu/multim/api/ApiClient.kt @@ -19,7 +19,6 @@ abstract class ApiClient(var baseUrl: String, val client: HttpClient) { return post.body() } - @OptIn(InternalAPI::class) suspend inline fun post( content: T, path: String, diff --git a/core/src/main/kotlin/dev/usbharu/multim/api/NodeinfoApi.kt b/core/src/main/kotlin/dev/usbharu/multim/api/NodeinfoApi.kt new file mode 100644 index 0000000..eab4fbe --- /dev/null +++ b/core/src/main/kotlin/dev/usbharu/multim/api/NodeinfoApi.kt @@ -0,0 +1,29 @@ +package dev.usbharu.multim.api + +import dev.usbharu.multim.model.wellknown.NodeinfoList +import dev.usbharu.multim.model.wellknown.nodeinfo.NodeInfo +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* + +// todo well-knwonだけでパッケージ作ったほうがいいかも +class NodeinfoApi(val httpClient: HttpClient) { + suspend fun wellKnownNodeinfo(url:String): NodeinfoList { + return httpClient.get("$url.well-known/nodeinfo").body() + } + + fun nodeinfoLink(nodeinfoList: NodeinfoList): NodeinfoList.NodeinfoLink { + return nodeinfoList.links.sortedBy { it.rel.substringAfterLast("/", "0").toFloat() }.first() + } + + + // todo 強制で2.0のが返ってくるのでバージョンを識別する + suspend fun nodeinfo(nodeinfoLink: NodeinfoList.NodeinfoLink): NodeInfo { + + return httpClient.get(nodeinfoLink.href).body() + } + + suspend fun nodeinfo(url:String):NodeInfo{ + return nodeinfo(nodeinfoLink(wellKnownNodeinfo(url))) + } +} diff --git a/core/src/main/kotlin/dev/usbharu/multim/factory/PlatformApiFactory.kt b/core/src/main/kotlin/dev/usbharu/multim/factory/PlatformApiFactory.kt index aa1e99a..1d05bf6 100644 --- a/core/src/main/kotlin/dev/usbharu/multim/factory/PlatformApiFactory.kt +++ b/core/src/main/kotlin/dev/usbharu/multim/factory/PlatformApiFactory.kt @@ -1,7 +1,7 @@ package dev.usbharu.multim.factory import dev.usbharu.multim.api.PlatformApis -import dev.usbharu.multim.model.nodeinfo.NodeInfo +import dev.usbharu.multim.model.wellknown.nodeinfo.NodeInfo import io.ktor.client.* interface PlatformApiFactory { diff --git a/core/src/main/kotlin/dev/usbharu/multim/model/nodeinfo/NodeInfo.kt b/core/src/main/kotlin/dev/usbharu/multim/model/nodeinfo/NodeInfo.kt deleted file mode 100644 index 1a692e0..0000000 --- a/core/src/main/kotlin/dev/usbharu/multim/model/nodeinfo/NodeInfo.kt +++ /dev/null @@ -1,187 +0,0 @@ -package dev.usbharu.multim.model.nodeinfo - -import kotlinx.serialization.SerialName - -@kotlinx.serialization.Serializable - -data class NodeInfo( - val version: String, - val software: Software, - val protocols: List, - val services: Services, - val openRegistrations: Boolean, - val usage: Usage -) { - @kotlinx.serialization.Serializable - data class Software( - val name: String, - val version: String - ) - - @kotlinx.serialization.Serializable - enum class Protocol { - @SerialName("activitypub") - ACTIVITYPUB, - - @SerialName("buddycloud") - BUDDYCLOUD, - - @SerialName("dfrn") - DFRN, - - @SerialName("diaspora") - DIASPORA, - - @SerialName("libertree") - LIBERTREE, - - @SerialName("ostatus") - OSTATUS, - - @SerialName("pumpio") - PUMPIO, - - @SerialName("tent") - TENT, - - @SerialName("xmpp") - XMPP, - - @SerialName("zot") - ZOT - } - - @kotlinx.serialization.Serializable - data class Services( - val inbound: List, - val outBound: List - ) { - @kotlinx.serialization.Serializable - enum class Inbound { - @SerialName("atom1.0") - ATOM1, - - @SerialName("gnusocial") - GNUSOCIAL, - - @SerialName("imap") - IMAP, - - @SerialName("pnut") - PNUT, - - @SerialName("pop3") - POP3, - - @SerialName("pumpio") - PUMPIO, - - @SerialName("rss2.0") - RSS2, - - @SerialName("twitter") - TWITTER, - } - - @kotlinx.serialization.Serializable - enum class Outbound { - @SerialName("atom1.0") - ATOM1, - - @SerialName("blogger") - BLOGGER, - - @SerialName("buddycloud") - BUDDYCLOUD, - - @SerialName("diaspora") - DIASPORA, - - @SerialName("dreamwidth") - DREAMWIDTH, - - @SerialName("drupal") - DRUPAL, - - @SerialName("facebook") - FACEBOOK, - - @SerialName("friendica") - FRIENDICA, - - @SerialName("gnusocial") - GNUSOCIAL, - - @SerialName("google") - GOOGLE, - - @SerialName("insanejournal") - INSANEJOURNAL, - - @SerialName("libertree") - LIBERTREE, - - @SerialName("linkedin") - LINKEDIN, - - @SerialName("livejournal") - LIVEJOURNAL, - - @SerialName("mediagoblin") - MEDIAGOBLIN, - - @SerialName("myspace") - MYSPACE, - - @SerialName("pinterest") - PINTEREST, - - @SerialName("pnut") - PNUT, - - @SerialName("posterous") - POSTEROUS, - - @SerialName("pumpio") - PUMPIO, - - @SerialName("redmatrix") - REDMATRIX, - - @SerialName("rss2.0") - RSS2, - - @SerialName("smtp") - SMTP, - - @SerialName("tent") - TENT, - - @SerialName("tumblr") - TUMBLR, - - @SerialName("twitter") - TWITTER, - - @SerialName("wordpress") - WORDPRESS, - - @SerialName("xmpp") - XMPP - } - } - - @kotlinx.serialization.Serializable - data class Usage( - val users: Users, - val localPosts: Int, - val localComments: Int - ) { - @kotlinx.serialization.Serializable - data class Users( - val total: Int, - val activeHalfyear: Int? = null, - val activeMonth: Int? = null - ) - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/usbharu/multim/model/wellknown/NodeinfoList.kt b/core/src/main/kotlin/dev/usbharu/multim/model/wellknown/NodeinfoList.kt new file mode 100644 index 0000000..1a23046 --- /dev/null +++ b/core/src/main/kotlin/dev/usbharu/multim/model/wellknown/NodeinfoList.kt @@ -0,0 +1,10 @@ +package dev.usbharu.multim.model.wellknown + +import kotlinx.serialization.Serializable + +@Serializable +data class NodeinfoList(val links: List) { + + @Serializable + data class NodeinfoLink(val href: String, val rel: String) +} diff --git a/core/src/main/kotlin/dev/usbharu/multim/model/wellknown/nodeinfo/NodeInfo.kt b/core/src/main/kotlin/dev/usbharu/multim/model/wellknown/nodeinfo/NodeInfo.kt new file mode 100644 index 0000000..8c0ac4c --- /dev/null +++ b/core/src/main/kotlin/dev/usbharu/multim/model/wellknown/nodeinfo/NodeInfo.kt @@ -0,0 +1,394 @@ +package dev.usbharu.multim.model.wellknown.nodeinfo + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +@Serializable(with = NodeinfoSerializer::class) +sealed class NodeInfo + +@Serializer(forClass = NodeInfo::class) +object NodeinfoSerializer : JsonContentPolymorphicSerializer(NodeInfo::class){ + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + when (element.jsonObject["version"]?.jsonPrimitive?.content) { + "2.0" -> V2_0.serializer() + "2.1" -> V2_1.serializer() + else -> TODO() + } +} + + +@Serializable +// todo seald classにして基底クラス同じにする。 +// あとついでにversion分ける +data class V2_0( + val version: String, + val software: Software, + val protocols: List, + val services: Services, + val openRegistrations: Boolean, + val usage: Usage +) : NodeInfo() { + @Serializable + data class Software( + val name: String, + val version: String + ) + + @Serializable + enum class Protocol { + @SerialName("activitypub") + ACTIVITYPUB, + + @SerialName("buddycloud") + BUDDYCLOUD, + + @SerialName("dfrn") + DFRN, + + @SerialName("diaspora") + DIASPORA, + + @SerialName("libertree") + LIBERTREE, + + @SerialName("ostatus") + OSTATUS, + + @SerialName("pumpio") + PUMPIO, + + @SerialName("tent") + TENT, + + @SerialName("xmpp") + XMPP, + + @SerialName("zot") + ZOT + } + + @Serializable + data class Services( + val inbound: List, + val outBound: List + ) { + @Serializable + enum class Inbound { + @SerialName("atom1.0") + ATOM1, + + @SerialName("gnusocial") + GNUSOCIAL, + + @SerialName("imap") + IMAP, + + @SerialName("pnut") + PNUT, + + @SerialName("pop3") + POP3, + + @SerialName("pumpio") + PUMPIO, + + @SerialName("rss2.0") + RSS2, + + @SerialName("twitter") + TWITTER, + } + + @Serializable + enum class Outbound { + @SerialName("atom1.0") + ATOM1, + + @SerialName("blogger") + BLOGGER, + + @SerialName("buddycloud") + BUDDYCLOUD, + + @SerialName("diaspora") + DIASPORA, + + @SerialName("dreamwidth") + DREAMWIDTH, + + @SerialName("drupal") + DRUPAL, + + @SerialName("facebook") + FACEBOOK, + + @SerialName("friendica") + FRIENDICA, + + @SerialName("gnusocial") + GNUSOCIAL, + + @SerialName("google") + GOOGLE, + + @SerialName("insanejournal") + INSANEJOURNAL, + + @SerialName("libertree") + LIBERTREE, + + @SerialName("linkedin") + LINKEDIN, + + @SerialName("livejournal") + LIVEJOURNAL, + + @SerialName("mediagoblin") + MEDIAGOBLIN, + + @SerialName("myspace") + MYSPACE, + + @SerialName("pinterest") + PINTEREST, + + @SerialName("pnut") + PNUT, + + @SerialName("posterous") + POSTEROUS, + + @SerialName("pumpio") + PUMPIO, + + @SerialName("redmatrix") + REDMATRIX, + + @SerialName("rss2.0") + RSS2, + + @SerialName("smtp") + SMTP, + + @SerialName("tent") + TENT, + + @SerialName("tumblr") + TUMBLR, + + @SerialName("twitter") + TWITTER, + + @SerialName("wordpress") + WORDPRESS, + + @SerialName("xmpp") + XMPP + } + } + + @Serializable + data class Usage( + val users: Users, + val localPosts: Int, + val localComments: Int + ) { + @Serializable + data class Users( + val total: Int, + val activeHalfyear: Int? = null, + val activeMonth: Int? = null + ) + } +} + +@Serializable +data class V2_1( + val version: String, + val software: Software, + val protocols: List, + val service: Service, + val openRegistrations: Boolean, + val usage: Usage +) : NodeInfo() { + + @Serializable + data class Software( + val name: String, + val version: String, + val repository: String? = null, + val homepage: String? = null + ) + + @Serializable + enum class Protocol { + @SerialName("activitypub") + ACTIVITYPUB, + + @SerialName("buddycloud") + BUDDYCLOUD, + + @SerialName("dfrn") + DFRN, + + @SerialName("diaspora") + DIASPORA, + + @SerialName("libertree") + LIBERTREE, + + @SerialName("ostatus") + OSTATUS, + + @SerialName("pumpio") + PUMPIO, + + @SerialName("tent") + TENT, + + @SerialName("xmpp") + XMPP, + + @SerialName("zot") + ZOT + } + + @Serializable + data class Service( + val inbound: List, + val outbound: List + ) { + enum class Inbound { + @SerialName("atom1.0") + ATOM1, + + @SerialName("gnusocial") + GNUSOCIAL, + + @SerialName("imap") + IMAP, + + @SerialName("pnut") + PNUT, + + @SerialName("pop3") + POP3, + + @SerialName("pumpio") + PUMPIO, + + @SerialName("rss2.0") + RSS2, + + @SerialName("twitter") + TWITTER + } + + enum class Outbound { + @SerialName("atom1.0") + ATOM1, + + @SerialName("blogger") + BLOGGER, + + @SerialName("buddycloud") + BUDDYCLOUD, + + @SerialName("diaspora") + DIASPORA, + + @SerialName("dreamwidth") + DREAMWIDTH, + + @SerialName("drupal") + DRUPAL, + + @SerialName("facebook") + FACEBOOK, + + @SerialName("friendica") + FRIENDICA, + + @SerialName("gnusocial") + GNUSOCIAL, + + @SerialName("google") + GOOGLE, + + @SerialName("insanejournal") + INSANEJOURNAL, + + @SerialName("libertree") + LIBERTREE, + + @SerialName("linkedin") + LINKEDIN, + + @SerialName("livejournal") + LIVEJOURNAL, + + @SerialName("mediagoblin") + MEDIAGOBLIN, + + @SerialName("myspace") + MYSPACE, + + @SerialName("pinterest") + PINTEREST, + + @SerialName("pnut") + PNUT, + + @SerialName("posterous") + POSTEROUS, + + @SerialName("pumpio") + PUMPIO, + + @SerialName("redmatrix") + REDMATRIX, + + @SerialName("rss2.0") + RSS2, + + @SerialName("smtp") + SMTP, + + @SerialName("tent") + TENT, + + @SerialName("tumblr") + TUMBLR, + + @SerialName("twitter") + TWITTER, + + @SerialName("wordpress") + WORDPRESS, + + @SerialName("xmpp") + XMPP + } + } + + @Serializable + data class Usage( + val users: Users, + val localPosts: Int? = null, + val localComments: Int? = null + ) { + @Serializable + data class Users( + val total: Int? = null, + val activeHalfyear: Int? = null, + val activeMonth: Int? = null + ) + } + +} diff --git a/impl/misskey/src/main/kotlin/dev/usbharu/multim/misskey/factory/DefaultPlatformApiFactory.kt b/impl/misskey/src/main/kotlin/dev/usbharu/multim/misskey/factory/DefaultPlatformApiFactory.kt deleted file mode 100644 index 5c4819c..0000000 --- a/impl/misskey/src/main/kotlin/dev/usbharu/multim/misskey/factory/DefaultPlatformApiFactory.kt +++ /dev/null @@ -1,45 +0,0 @@ -package dev.usbharu.multim.misskey.factory - -import dev.usbharu.multim.api.PlatformApis -import dev.usbharu.multim.factory.MultiMApis -import dev.usbharu.multim.factory.PlatformApiFactory -import dev.usbharu.multim.misskey.v12.api.MisskeyApis -import dev.usbharu.multim.misskey.v12.common.api.MisskeyAccountApi -import dev.usbharu.multim.misskey.v12.common.api.MisskeyApiClient -import dev.usbharu.multim.misskey.v12.common.api.MisskeyStatusApi -import dev.usbharu.multim.model.nodeinfo.NodeInfo -import io.ktor.client.* - - -//todo factoryはbuilderを返すだけにする。 -// ↑認証方式が違うAPIに対応できないため -open class DefaultPlatformApiFactory : PlatformApiFactory { - override fun factory( - nodeInfo: NodeInfo, - httpClient: HttpClient, - token: String, - baseUrl: String, - build: MultiMApis.(PlatformApis) -> Unit - ): MultiMApis { - if (nodeInfo.software.name == "misskey") { - return misskey(nodeInfo, httpClient, token, baseUrl, build) - } - TODO() - } - - protected fun misskey( - nodeInfo: NodeInfo, - httpClient: HttpClient, - token: String, - baseUrl: String, - build: (MultiMApis.(PlatformApis) -> Unit)? - ): MisskeyMultiMApis { - if (nodeInfo.software.version.startsWith("12.")) { - val misskeyApis = MisskeyApis(MisskeyApiClient(token, baseUrl, httpClient)) - val misskeyMultiMApis = - MisskeyMultiMApis(MisskeyStatusApi(misskeyApis), MisskeyAccountApi(misskeyApis)) - return misskeyMultiMApis.apply { build?.invoke(this, misskeyApis) } - } - TODO() - } -} diff --git a/impl/misskey/src/main/kotlin/dev/usbharu/multim/misskey/factory/MisskeyMultiMApis.kt b/impl/misskey/src/main/kotlin/dev/usbharu/multim/misskey/factory/MisskeyMultiMApis.kt deleted file mode 100644 index c3b4abd..0000000 --- a/impl/misskey/src/main/kotlin/dev/usbharu/multim/misskey/factory/MisskeyMultiMApis.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.usbharu.multim.misskey.factory - -import dev.usbharu.multim.factory.MultiMApis -import dev.usbharu.multim.misskey.v12.common.api.MisskeyAccountApi -import dev.usbharu.multim.misskey.v12.common.api.MisskeyStatusApi - -class MisskeyMultiMApis(statusApi: MisskeyStatusApi, accountApi: MisskeyAccountApi) : - MultiMApis(statusApi, accountApi) diff --git a/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/api/ApTest.kt b/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/api/ApTest.kt index c1064fa..4f8e56b 100644 --- a/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/api/ApTest.kt +++ b/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/api/ApTest.kt @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) +@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalCoroutinesApi::class) package dev.usbharu.multim.v12.api diff --git a/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/api/NotesTest.kt b/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/api/NotesTest.kt index e426b81..8253452 100644 --- a/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/api/NotesTest.kt +++ b/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/api/NotesTest.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + package dev.usbharu.multim.v12.api import MisskeyTestUtil.createFakeNote diff --git a/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/common/api/MisskeyStatusApiTest.kt b/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/common/api/MisskeyStatusApiTest.kt index cf1d69b..c2585b5 100644 --- a/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/common/api/MisskeyStatusApiTest.kt +++ b/impl/misskey/src/test/kotlin/dev/usbharu/multim/v12/common/api/MisskeyStatusApiTest.kt @@ -5,6 +5,7 @@ import dev.usbharu.multim.misskey.v12.api.MisskeyApis import dev.usbharu.multim.misskey.v12.common.MisskeyStatusId import dev.usbharu.multim.misskey.v12.common.api.MisskeyApiClient import dev.usbharu.multim.misskey.v12.common.api.MisskeyStatusApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString @@ -27,6 +28,7 @@ class MisskeyStatusApiTest { ) ) + @Suppress("JSON_FORMAT_REDUNDANT") @Test fun serializationTest() { @@ -39,6 +41,7 @@ class MisskeyStatusApiTest { }.decodeFromString("""{"a": "aaaa","b": "bbbb"}""") } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun findByIdTest() = runTest { val findById = misskeyStatusApi.findById(MisskeyStatusId("9a65528e5z", "https://lcalhost"))