package data /** A configured IRC server/network connection. */ data class ServerConfig( val id: String, // stable key, e.g. "rizon" val label: String, // display name, e.g. "Rizon" val host: String, val port: Int = 6667, val useTls: Boolean = false, val nick: String, val autoJoinChannels: List = emptyList(), val saslUser: String? = null, val saslPass: String? = null, // Raw IRC commands sent right after registration (001), before auto-join channels. // e.g. "PRIVMSG NickServ :IDENTIFY hunter2" or "NS IDENTIFY hunter2". // This is the "on join perform" list — saved to disk and replayed every connect. val onConnectCommands: List = emptyList() ) /** True for channel-style targets ('#', '&', '+', '!' prefixes per the IRC spec). */ fun isChannelName(name: String): Boolean = name.isNotEmpty() && name[0] in "#&+!" enum class MessageKind { CHAT, ACTION, JOIN, PART, QUIT, KICK, MODE, NOTICE, NICK, TOPIC, SYSTEM } // Process-wide, so every IrcMessage ever constructed while the app is running gets a // genuinely unique seq, regardless of which buffer/server it belongs to. private val nextMessageSeq = java.util.concurrent.atomic.AtomicLong(0) data class IrcMessage( val timestampMillis: Long, val kind: MessageKind, val sender: String?, // nick, or null for system/server lines val rawText: String, // still contains mIRC control codes; parsed at render time val target: String? = null, // e.g. for KICK, the nick that got kicked // Monotonically increasing, guaranteed-unique identity for this message - used as the // LazyColumn item key in ChannelScreen (see there). The old key there was // "${timestampMillis}_${sender}_${rawText.hashCode()}" - in a genuinely busy channel, // two messages sharing the same millisecond, sender, and text (not rare for short // repeated lines - "lol", a single emoji, etc.) would produce the exact same key, and // Compose requires unique keys per item. Not persisted to disk - history reloaded on // restart just gets fresh seq values in load order, which is fine since uniqueness only // needs to hold within a single running session, not across restarts. val seq: Long = nextMessageSeq.getAndIncrement() ) /** Buffer key: server id + channel/query name (channel names include '#'). */ data class BufferKey(val serverId: String, val name: String) /** * wIRC main's own buffer: server messages (MOTD, numerics, errors, all whois output) and * services/server notices from every connected server. Server ids are built from the host * with every non-alphanumeric character replaced by '_' (see ServerEditDialog), so one can * never contain '-' - this id can't collide with a real server. Also used as the * window_prefs.json key ("wirc-main__main") for the main window's saved position and size. */ val MAIN_BUFFER_KEY = BufferKey("wirc-main", "main") // Nicks that are network services rather than people. Their PRIVMSGs and NOTICEs go to // wIRC main instead of opening a query window. Anything ending in "serv" is also treated // as a service, which covers the less common ones (MemoServ, HostServ, SaslServ, ...). private val KNOWN_SERVICE_NICKS = setOf("nickserv", "chanserv", "global", "alis", "q", "x") fun isServiceNick(nick: String): Boolean { val lower = nick.lowercase() return lower in KNOWN_SERVICE_NICKS || lower.endsWith("serv") } /** * True when a message's source is a service or the server itself rather than a real user. * A user prefix always has the "nick!user@host" form; a missing prefix or one without '!' * is a server name (e.g. "irc.rizon.io"). */ fun isServiceOrServerPrefix(prefix: String?): Boolean { if (prefix == null || !prefix.contains('!')) return true return isServiceNick(prefix.substringBefore('!')) } data class ChannelUser( val nick: String, val isOp: Boolean = false, val isVoice: Boolean = false ) data class ChannelBuffer( val key: BufferKey, val messages: List = emptyList(), val users: List = emptyList(), val topic: String = "" )