✨ Sync chat from SN
This commit is contained in:
		@@ -1,7 +1,7 @@
 | 
			
		||||
plugins {
 | 
			
		||||
    kotlin("jvm") version "2.1.20-Beta2"
 | 
			
		||||
    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"
 | 
			
		||||
@@ -23,10 +23,16 @@ repositories {
 | 
			
		||||
dependencies {
 | 
			
		||||
    compileOnly("org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT")
 | 
			
		||||
    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")
 | 
			
		||||
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
 | 
			
		||||
 | 
			
		||||
    // These will be packaged into the shadow JAR
 | 
			
		||||
    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
 | 
			
		||||
@@ -34,15 +40,26 @@ kotlin {
 | 
			
		||||
    jvmToolchain(targetJavaVersion)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.build {
 | 
			
		||||
    dependsOn("shadowJar")
 | 
			
		||||
}
 | 
			
		||||
// Configure the shadowJar task
 | 
			
		||||
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 {
 | 
			
		||||
    val props = mapOf("version" to version)
 | 
			
		||||
    inputs.properties(props)
 | 
			
		||||
    filteringCharset = "UTF-8"
 | 
			
		||||
    filesMatching("plugin.yml") {
 | 
			
		||||
        expand(props)
 | 
			
		||||
    // Make “build” produce the shadow jar
 | 
			
		||||
    build {
 | 
			
		||||
        dependsOn(shadowJar)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    processResources {
 | 
			
		||||
        val props = mapOf("version" to version)
 | 
			
		||||
        inputs.properties(props)
 | 
			
		||||
        filteringCharset = "UTF-8"
 | 
			
		||||
        filesMatching("plugin.yml") {
 | 
			
		||||
            expand(props)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,15 +2,71 @@ package dev.solsynth.snConnect
 | 
			
		||||
 | 
			
		||||
import dev.solsynth.snConnect.commands.SnCommand
 | 
			
		||||
import dev.solsynth.snConnect.commands.SnCommandCompleter
 | 
			
		||||
import dev.solsynth.snConnect.models.*
 | 
			
		||||
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 org.bukkit.Bukkit
 | 
			
		||||
import org.bukkit.Bukkit.getOnlinePlayers
 | 
			
		||||
import org.bukkit.Color
 | 
			
		||||
import org.bukkit.plugin.java.JavaPlugin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SolarNetworkConnect : JavaPlugin() {
 | 
			
		||||
    private var economy: Economy? = 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() {
 | 
			
		||||
        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 clientId = config.getString("sn.client_id") ?: 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
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.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();
 | 
			
		||||
    private val logger = Logger.getLogger(SnService::class.java.name)
 | 
			
		||||
 | 
			
		||||
    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