✨ Sync chat from SN
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "2.1.20-Beta2"
|
kotlin("jvm") version "2.1.20-Beta2"
|
||||||
kotlin("plugin.serialization") version "2.1.10"
|
kotlin("plugin.serialization") version "2.1.10"
|
||||||
id("com.github.johnrengelman.shadow") version "8.1.1"
|
id("com.github.johnrengelman.shadow") version "8.1.1" // add shadow plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.solsynth"
|
group = "dev.solsynth"
|
||||||
@@ -23,10 +23,16 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT")
|
compileOnly("org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT")
|
||||||
compileOnly("com.github.MilkBowl:VaultAPI:1.7")
|
compileOnly("com.github.MilkBowl:VaultAPI:1.7")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
// These will be packaged into the shadow JAR
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
|
||||||
|
// Test dependencies
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
val targetJavaVersion = 21
|
val targetJavaVersion = 21
|
||||||
@@ -34,11 +40,21 @@ kotlin {
|
|||||||
jvmToolchain(targetJavaVersion)
|
jvmToolchain(targetJavaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.build {
|
// Configure the shadowJar task
|
||||||
dependsOn("shadowJar")
|
tasks {
|
||||||
|
shadowJar {
|
||||||
|
archiveClassifier.set("") // so that the shadow JAR replaces the “normal” JAR
|
||||||
|
mergeServiceFiles()
|
||||||
|
// Optionally relocate packages to avoid conflicts with other plugins
|
||||||
|
// e.g. relocate("kotlin", "dev.solsynth.shadow.kotlin")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.processResources {
|
// Make “build” produce the shadow jar
|
||||||
|
build {
|
||||||
|
dependsOn(shadowJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
val props = mapOf("version" to version)
|
val props = mapOf("version" to version)
|
||||||
inputs.properties(props)
|
inputs.properties(props)
|
||||||
filteringCharset = "UTF-8"
|
filteringCharset = "UTF-8"
|
||||||
@@ -46,3 +62,4 @@ tasks.processResources {
|
|||||||
expand(props)
|
expand(props)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@@ -2,15 +2,71 @@ package dev.solsynth.snConnect
|
|||||||
|
|
||||||
import dev.solsynth.snConnect.commands.SnCommand
|
import dev.solsynth.snConnect.commands.SnCommand
|
||||||
import dev.solsynth.snConnect.commands.SnCommandCompleter
|
import dev.solsynth.snConnect.commands.SnCommandCompleter
|
||||||
|
import dev.solsynth.snConnect.models.*
|
||||||
import dev.solsynth.snConnect.services.SnService
|
import dev.solsynth.snConnect.services.SnService
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.md_5.bungee.api.ChatColor
|
||||||
|
import net.md_5.bungee.api.chat.BaseComponent
|
||||||
|
import net.md_5.bungee.api.chat.ClickEvent
|
||||||
|
import net.md_5.bungee.api.chat.ComponentBuilder
|
||||||
|
import net.md_5.bungee.api.chat.HoverEvent
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent
|
||||||
import net.milkbowl.vault.economy.Economy
|
import net.milkbowl.vault.economy.Economy
|
||||||
import org.bukkit.Bukkit
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.Bukkit.getOnlinePlayers
|
||||||
|
import org.bukkit.Color
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
|
|
||||||
class SolarNetworkConnect : JavaPlugin() {
|
class SolarNetworkConnect : JavaPlugin() {
|
||||||
private var economy: Economy? = null
|
private var economy: Economy? = null
|
||||||
private var sn: SnService? = null
|
private var sn: SnService? = null
|
||||||
|
private var syncChatRooms: List<String> = emptyList()
|
||||||
|
|
||||||
|
private fun handleWebSocketPacket(packet: WebSocketPacket) {
|
||||||
|
logger.info("Received WebSocket packet: type=${packet.type}")
|
||||||
|
if (packet.type.startsWith("messages") && packet.data != null) {
|
||||||
|
try {
|
||||||
|
when (packet.type) {
|
||||||
|
"messages.new" -> {
|
||||||
|
val message = SnChatMessage.fromJson(packet.data)
|
||||||
|
if (syncChatRooms.isEmpty() || syncChatRooms.contains(message.chatRoomId)) {
|
||||||
|
val roomName = message.chatRoom.name ?: "DM"
|
||||||
|
val senderName = message.sender.account.nick
|
||||||
|
val profileUrl = "https://solian.app/@${message.sender.account.name}"
|
||||||
|
val componentBuilder = ComponentBuilder()
|
||||||
|
.append("☀").color(ChatColor.YELLOW)
|
||||||
|
.event(HoverEvent(HoverEvent.Action.SHOW_TEXT, arrayOf(TextComponent("Solar Network"))))
|
||||||
|
.append(" ")
|
||||||
|
.append(roomName).color(ChatColor.GOLD)
|
||||||
|
.append(" ").color(ChatColor.YELLOW)
|
||||||
|
.append(senderName).color(ChatColor.YELLOW)
|
||||||
|
.event(ClickEvent(ClickEvent.Action.OPEN_URL, profileUrl))
|
||||||
|
.append(" » ").color(ChatColor.GRAY)
|
||||||
|
if (message.content != null && message.content.isNotBlank()) {
|
||||||
|
componentBuilder.append(message.content).color(ChatColor.WHITE)
|
||||||
|
}
|
||||||
|
if (message.attachments.isNotEmpty()) {
|
||||||
|
for (attachment in message.attachments) {
|
||||||
|
val fileName = attachment.name.takeIf { it.isNotBlank() } ?: "attachment"
|
||||||
|
val url = "https://solian.app/files/${attachment.id}"
|
||||||
|
componentBuilder.append(" <$fileName>").color(ChatColor.BLUE)
|
||||||
|
.event(ClickEvent(ClickEvent.Action.OPEN_URL, url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val component = componentBuilder.create()
|
||||||
|
for (player in getOnlinePlayers()) {
|
||||||
|
player.spigot().sendMessage(*component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warning("Failed to parse chat message: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEnable() {
|
override fun onEnable() {
|
||||||
logger.info(String.format("Enabling Version %s", description.version));
|
logger.info(String.format("Enabling Version %s", description.version));
|
||||||
@@ -45,7 +101,15 @@ class SolarNetworkConnect : JavaPlugin() {
|
|||||||
val baseUrl = config.getString("sn.endpoint") ?: return false;
|
val baseUrl = config.getString("sn.endpoint") ?: return false;
|
||||||
val clientId = config.getString("sn.client_id") ?: return false;
|
val clientId = config.getString("sn.client_id") ?: return false;
|
||||||
val clientSecret = config.getString("sn.client_secret") ?: return false;
|
val clientSecret = config.getString("sn.client_secret") ?: return false;
|
||||||
sn = SnService(baseUrl, clientId, clientSecret);
|
val botApiKey = config.getString("sn.bot_secret");
|
||||||
|
val syncRooms = config.getStringList("chat.sync_chat_rooms")
|
||||||
|
syncChatRooms = syncRooms
|
||||||
|
sn = SnService(baseUrl, clientId, clientSecret, botApiKey);
|
||||||
|
GlobalScope.launch {
|
||||||
|
sn!!.connectWebSocketAsFlow().collect { packet ->
|
||||||
|
handleWebSocketPacket(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
39
src/main/kotlin/dev/solsynth/snConnect/WebSocketTester.kt
Normal file
39
src/main/kotlin/dev/solsynth/snConnect/WebSocketTester.kt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package dev.solsynth.snConnect
|
||||||
|
|
||||||
|
import dev.solsynth.snConnect.services.SnService
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
|
class WebSocketTester {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
if (args.size < 4) {
|
||||||
|
println("Usage: WebSocketTester <baseUrl> <clientId> <clientSecret> <botApiKey>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseUrl = args[0]
|
||||||
|
val clientId = args[1]
|
||||||
|
val clientSecret = args[2]
|
||||||
|
val botApiKey = if (args[3] == "null") null else args[3]
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
val service = SnService(baseUrl, clientId, clientSecret, botApiKey)
|
||||||
|
println("Starting WebSocket test for $baseUrl")
|
||||||
|
|
||||||
|
val result = withTimeoutOrNull(30000L) { // 30 seconds timeout
|
||||||
|
service.connectWebSocketAsFlow().collect { packet ->
|
||||||
|
println("Received packet: type=${packet.type}, endpoint=${packet.endpoint}, data=${packet.data}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
println("WebSocket test timed out")
|
||||||
|
} else {
|
||||||
|
println("WebSocket test completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
src/main/kotlin/dev/solsynth/snConnect/models/SnAccount.kt
Normal file
37
src/main/kotlin/dev/solsynth/snConnect/models/SnAccount.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import dev.solsynth.snConnect.models.SnAccountProfile
|
||||||
|
import dev.solsynth.snConnect.models.SnContactMethod
|
||||||
|
import dev.solsynth.snConnect.models.SnWalletSubscriptionRef
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class SnAccount(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val nick: String,
|
||||||
|
val language: String,
|
||||||
|
val region: String = "",
|
||||||
|
@SerialName("is_superuser") val isSuperuser: Boolean,
|
||||||
|
@SerialName("automated_id") val automatedId: String?,
|
||||||
|
val profile: SnAccountProfile,
|
||||||
|
@SerialName("perk_subscription") val perkSubscription: SnWalletSubscriptionRef?,
|
||||||
|
val contacts: List<SnContactMethod> = emptyList(),
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromJson(jsonElement: JsonElement): SnAccount {
|
||||||
|
return json.decodeFromJsonElement<SnAccount>(jsonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,44 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class SnAccountProfile(
|
||||||
|
val id: String,
|
||||||
|
@SerialName("first_name") val firstName: String = "",
|
||||||
|
@SerialName("middle_name") val middleName: String = "",
|
||||||
|
@SerialName("last_name") val lastName: String = "",
|
||||||
|
val bio: String = "",
|
||||||
|
val gender: String = "",
|
||||||
|
val pronouns: String = "",
|
||||||
|
val location: String = "",
|
||||||
|
@SerialName("time_zone") val timeZone: String = "",
|
||||||
|
val birthday: String?,
|
||||||
|
@SerialName("last_seen_at") val lastSeenAt: String?,
|
||||||
|
val experience: Int,
|
||||||
|
val level: Int,
|
||||||
|
@SerialName("social_credits") val socialCredits: Double = 100.0,
|
||||||
|
@SerialName("social_credits_level") val socialCreditsLevel: Int = 0,
|
||||||
|
@SerialName("leveling_progress") val levelingProgress: Double,
|
||||||
|
val picture: SnCloudFile?,
|
||||||
|
val background: SnCloudFile?,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromJson(jsonElement: JsonElement): SnAccountProfile {
|
||||||
|
return json.decodeFromJsonElement<SnAccountProfile>(jsonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class SnAccountStatus(
|
||||||
|
val id: String,
|
||||||
|
val attitude: Int,
|
||||||
|
@SerialName("is_online") val isOnline: Boolean,
|
||||||
|
@SerialName("is_invisible") val isInvisible: Boolean,
|
||||||
|
@SerialName("is_not_disturb") val isNotDisturb: Boolean,
|
||||||
|
@SerialName("is_customized") val isCustomized: Boolean,
|
||||||
|
val label: String = "",
|
||||||
|
@SerialName("cleared_at") val clearedAt: String?,
|
||||||
|
@SerialName("account_id") val accountId: String,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromJson(jsonElement: JsonElement): SnAccountStatus {
|
||||||
|
return json.decodeFromJsonElement<SnAccountStatus>(jsonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import SnAccount
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class SnChatMember(
|
||||||
|
val id: String,
|
||||||
|
@SerialName("chat_room_id") val chatRoomId: String,
|
||||||
|
// val chatRoom: SnChatRoom?, // Placeholder
|
||||||
|
@SerialName("account_id") val accountId: String,
|
||||||
|
val account: SnAccount, // Placeholder
|
||||||
|
val nick: String?,
|
||||||
|
val role: Int,
|
||||||
|
val notify: Int,
|
||||||
|
@SerialName("joined_at") val joinedAt: String?,
|
||||||
|
@SerialName("break_until") val breakUntil: String? = null,
|
||||||
|
@SerialName("timeout_until") val timeoutUntil: String? = null,
|
||||||
|
@SerialName("is_bot") val isBot: Boolean,
|
||||||
|
// val status: SnAccountStatus?, // Placeholder
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
)
|
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class SnChatMessage(
|
||||||
|
val id: String,
|
||||||
|
val type: String = "text",
|
||||||
|
val content: String?,
|
||||||
|
val nonce: String?,
|
||||||
|
@SerialName("members_mentioned") val membersMentioned: List<String>? = emptyList(),
|
||||||
|
@SerialName("edited_at") val editedAt: String?,
|
||||||
|
val attachments: List<SnCloudFile> = emptyList(),
|
||||||
|
// val reactions: List<SnChatReaction> = emptyList(), // Placeholder
|
||||||
|
@SerialName("replied_message_id") val repliedMessageId: String?,
|
||||||
|
@SerialName("forwarded_message_id") val forwardedMessageId: String?,
|
||||||
|
@SerialName("sender_id") val senderId: String,
|
||||||
|
val sender: SnChatMember,
|
||||||
|
@SerialName("chat_room") val chatRoom: SnChatRoom,
|
||||||
|
@SerialName("chat_room_id") val chatRoomId: String,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromJson(jsonElement: JsonElement): SnChatMessage {
|
||||||
|
return json.decodeFromJsonElement<SnChatMessage>(jsonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class SnChatReaction(
|
||||||
|
val id: String,
|
||||||
|
@SerialName("message_id") val messageId: String,
|
||||||
|
@SerialName("sender_id") val senderId: String,
|
||||||
|
// val sender: SnChatMember, // Placeholder
|
||||||
|
val symbol: String,
|
||||||
|
val attitude: Int,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
)
|
26
src/main/kotlin/dev/solsynth/snConnect/models/SnChatRoom.kt
Normal file
26
src/main/kotlin/dev/solsynth/snConnect/models/SnChatRoom.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class SnChatRoom(
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val description: String?,
|
||||||
|
val type: Int,
|
||||||
|
@SerialName("is_public") val isPublic: Boolean = false,
|
||||||
|
@SerialName("is_community") val isCommunity: Boolean = false,
|
||||||
|
// val picture: SnCloudFile?, // Placeholder
|
||||||
|
// val background: SnCloudFile?, // Placeholder
|
||||||
|
val realmId: String? = null,
|
||||||
|
// val realm: SnRealm?, // Placeholder
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
// val members: List<SnChatMember>?, // Placeholder
|
||||||
|
)
|
22
src/main/kotlin/dev/solsynth/snConnect/models/SnCloudFile.kt
Normal file
22
src/main/kotlin/dev/solsynth/snConnect/models/SnCloudFile.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SnCloudFile(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val size: Long,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromJson(jsonElement: JsonElement): SnCloudFile {
|
||||||
|
return json.decodeFromJsonElement<SnCloudFile>(jsonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class SnContactMethod(
|
||||||
|
val id: String,
|
||||||
|
val type: Int,
|
||||||
|
@SerialName("verified_at") val verifiedAt: String?,
|
||||||
|
@SerialName("is_primary") val isPrimary: Boolean,
|
||||||
|
@SerialName("is_public") val isPublic: Boolean,
|
||||||
|
val content: String,
|
||||||
|
@SerialName("account_id") val accountId: String,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromJson(jsonElement: JsonElement): SnContactMethod {
|
||||||
|
return json.decodeFromJsonElement<SnContactMethod>(jsonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SnWalletSubscriptionRef(
|
||||||
|
val id: String,
|
||||||
|
val identifier: String,
|
||||||
|
@SerialName("account_id") val accountId: String,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
|
@SerialName("deleted_at") val deletedAt: String?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromJson(jsonElement: JsonElement): SnWalletSubscriptionRef {
|
||||||
|
return json.decodeFromJsonElement<SnWalletSubscriptionRef>(jsonElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.solsynth.snConnect.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
@Serializable
|
||||||
|
data class WebSocketPacket(
|
||||||
|
val type: String,
|
||||||
|
@SerialName("data") val data: JsonElement?,
|
||||||
|
val endpoint: String?,
|
||||||
|
@SerialName("error_message") val errorMessage: String?
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromJson(jsonString: String): WebSocketPacket? {
|
||||||
|
return try {
|
||||||
|
json.decodeFromString(jsonString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +1,73 @@
|
|||||||
package dev.solsynth.snConnect.services
|
package dev.solsynth.snConnect.services
|
||||||
|
|
||||||
|
import dev.solsynth.snConnect.models.WebSocketPacket
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.WebSocket
|
||||||
|
import okhttp3.WebSocketListener
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.ByteString
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
class SnService(private val baseUrl: String, val clientId: String, val clientSecret: String) {
|
class SnService(private val baseUrl: String, val clientId: String, val clientSecret: String, private val botApiKey: String?) {
|
||||||
val client = OkHttpClient.Builder().build();
|
val client = OkHttpClient.Builder().build();
|
||||||
|
private val logger = Logger.getLogger(SnService::class.java.name)
|
||||||
|
|
||||||
fun getUrl(service: String, segment: String): String {
|
fun getUrl(service: String, segment: String): String {
|
||||||
return "$baseUrl/cgi/$service$segment"
|
return "$baseUrl/$service$segment"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getWsBaseUrl(): String {
|
||||||
|
return baseUrl.replaceFirst("http", "ws")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connectWebSocket(listener: WebSocketListener): WebSocket {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("${getWsBaseUrl()}/ws")
|
||||||
|
.apply {
|
||||||
|
botApiKey?.let { header("Authorization", "Bearer $it") }
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return client.newWebSocket(request, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connectWebSocketAsFlow(): Flow<WebSocketPacket> = callbackFlow {
|
||||||
|
val url = "${getWsBaseUrl()}/ws";
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.apply {
|
||||||
|
botApiKey?.let { header("Authorization", "Bearer $it") }
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
val websocket = client.newWebSocket(request, object : WebSocketListener() {
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
logger.info("WebSocket connection opened to $url")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
WebSocketPacket.fromJson(text)?.let { trySend(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
|
||||||
|
val text = bytes.string(Charsets.UTF_8)
|
||||||
|
WebSocketPacket.fromJson(text)?.let { trySend(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
|
logger.severe("WebSocket connection failed: ${t.message}, response: ${response?.code}")
|
||||||
|
close(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
logger.info("WebSocket connection closed: code=$code, reason=$reason")
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
awaitClose {
|
||||||
|
websocket.close(1000, "Flow closed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
37
src/test/kotlin/dev/solsynth/snConnect/WebSocketTest.kt
Normal file
37
src/test/kotlin/dev/solsynth/snConnect/WebSocketTest.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package dev.solsynth.snConnect
|
||||||
|
|
||||||
|
import dev.solsynth.snConnect.services.SnService
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
class WebSocketTest {
|
||||||
|
private val logger = Logger.getLogger(WebSocketTest::class.java.name)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWebSocketConnection() = runBlocking {
|
||||||
|
val service = SnService(
|
||||||
|
baseUrl = "http://localhost:8080", // Replace with actual test server URL
|
||||||
|
clientId = "testClientId",
|
||||||
|
clientSecret = "testClientSecret",
|
||||||
|
botApiKey = "testBotApiKey"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Collect from the flow for a limited time to avoid hanging on failure
|
||||||
|
val packets = withTimeoutOrNull(10000L) { // 10 seconds timeout
|
||||||
|
val collectedPackets = mutableListOf<String>()
|
||||||
|
service.connectWebSocketAsFlow().collect { packet ->
|
||||||
|
collectedPackets.add("Received packet: type=${packet.type}, endpoint=${packet.endpoint}")
|
||||||
|
logger.info(collectedPackets.last())
|
||||||
|
// In a real test, you might check packet contents or behavior
|
||||||
|
}
|
||||||
|
collectedPackets
|
||||||
|
}
|
||||||
|
|
||||||
|
// For this test, we're mainly checking that the flow can be created and the setup doesn't crash
|
||||||
|
// In real scenarios, you'd need a WebSocket server running at the specified URL
|
||||||
|
logger.info("WebSocket test finished. Packets received: ${packets?.size ?: 0}")
|
||||||
|
assert(packets != null) // Flow started without immediate error
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user