diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt index b078a7ce65d637fb560a592a5b9c8f257ed0b7b1..d392cb32528ab099f2cfbfa4f0865fb16f13f337 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt @@ -17,12 +17,12 @@ import de.justjanne.libquassel.protocol.models.QStringList import de.justjanne.libquassel.protocol.models.types.QtType import de.justjanne.libquassel.protocol.syncables.state.AliasManagerState import de.justjanne.libquassel.protocol.syncables.stubs.AliasManagerStub -import de.justjanne.libquassel.protocol.util.expansion.Expansion import de.justjanne.libquassel.protocol.util.update import de.justjanne.libquassel.protocol.variant.QVariantMap import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.qVariant import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class AliasManager constructor( session: Session @@ -64,120 +64,53 @@ class AliasManager constructor( super.addAlias(name, expansion) } - fun aliases() = state.value.aliases + fun aliases() = state().aliases - fun indexOf(name: String?) = aliases().map(Alias::name).indexOf(name) + fun indexOf(name: String?) = state().indexOf(name) - fun contains(name: String?) = aliases().map(Alias::name).contains(name) + fun contains(name: String?) = state().contains(name) fun processInput( info: BufferInfo, message: String - ) = mutableListOf<Command>().also { - processInput(info, message, it) - } + ) = state().processInput( + info, + session.network(info.networkId)?.state(), + message + ) fun processInput( info: BufferInfo, message: String, previousCommands: MutableList<Command> - ) { - val (command, arguments) = determineMessageCommand(message) - if (command == null) { - // If no command is found, this means the message should be treated as - // pure text. To ensure this won’t be unescaped twice it’s sent with /SAY. - previousCommands.add(Command(info, "/SAY $arguments")) - } else { - val found = aliases().firstOrNull { it.name.equals(command, true) } - if (found != null) { - expand(found.expansion ?: "", info, arguments, previousCommands) - } else { - previousCommands.add(Command(info, message)) - } - } - } + ) = state().processInput( + info, + session.network(info.networkId)?.state(), + message, + previousCommands + ) fun expand( expansion: String, bufferInfo: BufferInfo, arguments: String, previousCommands: MutableList<Command> - ) { - val network = session.network(bufferInfo.networkId) - val params = arguments.split(' ') - previousCommands.add( - Command( - bufferInfo, - expansion.split(';') - .map(String::trimStart) - .map(Expansion.Companion::parse) - .map { - it.map { - when (it) { - is Expansion.Constant -> when (it.field) { - Expansion.ConstantField.CHANNEL -> - bufferInfo.bufferName - Expansion.ConstantField.NICK -> - network?.myNick() - Expansion.ConstantField.NETWORK -> - network?.networkName() - } - is Expansion.Parameter -> when (it.field) { - Expansion.ParameterField.HOSTNAME -> - network?.ircUser(params[it.index])?.host() ?: "*" - Expansion.ParameterField.VERIFIED_IDENT -> - params.getOrNull(it.index)?.let { param -> - network?.ircUser(param)?.verifiedUser() ?: "*" - } - Expansion.ParameterField.IDENT -> - params.getOrNull(it.index)?.let { param -> - network?.ircUser(param)?.user() ?: "*" - } - Expansion.ParameterField.ACCOUNT -> - params.getOrNull(it.index)?.let { param -> - network?.ircUser(param)?.account() ?: "*" - } - null -> params.getOrNull(it.index) ?: it.source - } - is Expansion.ParameterRange -> - params.subList(it.from, it.to ?: params.size) - .joinToString(" ") - is Expansion.Text -> - it.source - } ?: it.source - } - }.joinToString(";") - ) - ) - } + ) = state().expand( + expansion, + bufferInfo, + session.network(bufferInfo.networkId)?.state(), + arguments, + previousCommands + ) + + @Suppress("NOTHING_TO_INLINE") + inline fun state() = flow().value + + @Suppress("NOTHING_TO_INLINE") + inline fun flow(): StateFlow<AliasManagerState> = state - private val state = MutableStateFlow( + @PublishedApi + internal val state = MutableStateFlow( AliasManagerState() ) - - companion object { - private fun determineMessageCommand(message: String) = when { - // Only messages starting with a forward slash are commands - !message.startsWith("/") -> - Pair(null, message) - // If a message starts with //, we consider that an escaped slash - message.startsWith("//") -> - Pair(null, message.substring(1)) - // If the first word of a message contains more than one slash, it is - // usually a regex of format /[a-z][a-z0-9]*/g, or a path of format - // /usr/bin/powerline-go. In that case we also pass it right through - message.startsWith("/") && - message.substringBefore(' ').indexOf('/', 1) != -1 - -> Pair(null, message) - // If the first word is purely a /, we won’t consider it a command either - message.substringBefore(' ') == "/" -> - Pair(null, message) - // Otherwise we treat the first word as a command, and all further words as - // arguments - else -> Pair( - message.trimStart('/').substringBefore(' '), - message.substringAfter(' ') - ) - } - } } diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt index 5acdd7e097bbb37d7f7c9b7c9197a6e45622d014..fbbfd8ad83e4e00ad4957749d149b94c5a8156bb 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt @@ -23,6 +23,7 @@ import de.justjanne.libquassel.protocol.variant.indexed import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.qVariant import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class IrcChannel( name: String, @@ -61,9 +62,9 @@ class IrcChannel( "topic" to qVariant(topic(), QtType.QString), "password" to qVariant(password(), QtType.QString), "encrypted" to qVariant(isEncrypted(), QtType.Bool), - "ChanModes" to qVariant(state.value.channelModes.toVariantMap(), QtType.QVariantMap), + "ChanModes" to qVariant(state().channelModes.toVariantMap(), QtType.QVariantMap), "UserModes" to qVariant( - state.value.userModes.mapValues { (_, value) -> + state().userModes.mapValues { (_, value) -> qVariant(value.joinToString(), QtType.QString) }, QtType.QVariantMap @@ -71,47 +72,22 @@ class IrcChannel( ) } - fun network() = state.value.network - fun name() = state.value.name - fun topic() = state.value.topic - fun password() = state.value.password - fun isEncrypted() = state.value.encrypted - fun ircUsers() = session.network(network())?.let { network -> - state.value.userModes.keys.mapNotNull(network::ircUser) - }.orEmpty() - - fun userCount() = state.value.userModes.size - fun userModes(nick: String) = state.value.userModes[nick] - fun hasMode(mode: Char) = when (session.network(network())?.channelModeType(mode)) { - ChannelModeType.A_CHANMODE -> - state.value.channelModes.a.contains(mode) - ChannelModeType.B_CHANMODE -> - state.value.channelModes.b.contains(mode) - ChannelModeType.C_CHANMODE -> - state.value.channelModes.c.contains(mode) - ChannelModeType.D_CHANMODE -> - state.value.channelModes.d.contains(mode) - else -> - false - } + fun network() = state().network + fun name() = state().name + fun topic() = state().topic + fun password() = state().password + fun isEncrypted() = state().encrypted + fun ircUsers() = state().ircUsers(session.network(network())?.state()) - fun modeValue(mode: Char) = when (session.network(network())?.channelModeType(mode)) { - ChannelModeType.B_CHANMODE -> - state.value.channelModes.b[mode] ?: "" - ChannelModeType.C_CHANMODE -> - state.value.channelModes.c[mode] ?: "" - else -> - "" - } + fun userCount() = state().userModes.size + fun userModes(nick: String) = state().userModes[nick] + fun hasMode(mode: Char) = state().hasMode(session.network(network())?.state(), mode) - fun modeValues(mode: Char) = when (session.network(network())?.channelModeType(mode)) { - ChannelModeType.A_CHANMODE -> - state.value.channelModes.a[mode].orEmpty() - else -> - emptySet() - } + fun modeValue(mode: Char) = state().modeValue(session.network(network())?.state(), mode) - fun channelModeString() = state.value.channelModes.modeString() + fun modeValues(mode: Char) = state().modeValues(session.network(network())?.state(), mode) + + fun channelModeString() = state().channelModeString() override fun setTopic(topic: String) { state.update { @@ -146,7 +122,7 @@ class IrcChannel( private fun joinIrcUsers(map: Map<String, Set<Char>>) { val network = session.network(network()) - val newNicks = map.keys - state.value.userModes.keys + val newNicks = map.keys - state().userModes.keys state.update { copy( userModes = userModes + map.mapValues { (key, value) -> @@ -173,8 +149,8 @@ class IrcChannel( if (partingUser != null) { partingUser.partChannel(name()) - if (network.isMe(partingUser) || state.value.userModes.isEmpty()) { - for (nickname in state.value.userModes.keys.toList()) { + if (network.isMe(partingUser) || state().userModes.isEmpty()) { + for (nickname in state().userModes.keys.toList()) { network.ircUser(nickname)?.partChannel(this) } state.update { @@ -289,7 +265,14 @@ class IrcChannel( super.removeChannelMode(mode, value) } - private val state = MutableStateFlow( + @Suppress("NOTHING_TO_INLINE") + inline fun state() = flow().value + + @Suppress("NOTHING_TO_INLINE") + inline fun flow(): StateFlow<IrcChannelState> = state + + @PublishedApi + internal val state = MutableStateFlow( IrcChannelState( network = network, name = name diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt index 091bc9591ef40622dbe9e7124dbcd070104c870c..c492dc53be450ca32d2290d6b6c3ca7320d5e570 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt @@ -21,6 +21,7 @@ import de.justjanne.libquassel.protocol.variant.indexed import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.qVariant import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.threeten.bp.Instant import org.threeten.bp.temporal.Temporal @@ -34,7 +35,7 @@ class IrcUser( } private fun updateObjectName() { - renameObject("${network().id}/${nick()}") + renameObject(state().identifier()) } override fun fromVariantMap(properties: QVariantMap) = @@ -234,7 +235,7 @@ class IrcUser( } fun joinChannel(channel: IrcChannel, skipChannelJoin: Boolean = false) { - if (state.value.channels.contains(channel.name())) { + if (state().channels.contains(channel.name())) { return } @@ -280,32 +281,36 @@ class IrcUser( super.quit() } - fun network() = state.value.network - fun nick() = state.value.nick - fun user() = state.value.user - fun verifiedUser() = user().let { - if (it.startsWith("~")) null - else it - } - fun host() = state.value.host - fun realName() = state.value.realName - fun account() = state.value.account - fun hostMask() = "${nick()}!${user()}@${host()}" - fun isAway() = state.value.away - fun awayMessage() = state.value.awayMessage - fun server() = state.value.server - fun idleTime() = state.value.idleTime - - fun loginTime() = state.value.loginTime - fun ircOperator() = state.value.ircOperator - fun lastAwayMessageTime() = state.value.lastAwayMessageTime - fun whoisServiceReply() = state.value.whoisServiceReply - fun suserHost() = state.value.suserHost - fun encrypted() = state.value.encrypted - fun userModes() = state.value.userModes - fun channels() = state.value.channels - - private val state = MutableStateFlow( + fun network() = state().network + fun nick() = state().nick + fun user() = state().user + fun verifiedUser() = state().verifiedUser() + fun host() = state().host + fun realName() = state().realName + fun account() = state().account + fun hostMask() = state().hostMask() + fun isAway() = state().away + fun awayMessage() = state().awayMessage + fun server() = state().server + fun idleTime() = state().idleTime + + fun loginTime() = state().loginTime + fun ircOperator() = state().ircOperator + fun lastAwayMessageTime() = state().lastAwayMessageTime + fun whoisServiceReply() = state().whoisServiceReply + fun suserHost() = state().suserHost + fun encrypted() = state().encrypted + fun userModes() = state().userModes + fun channels() = state().channels + + @Suppress("NOTHING_TO_INLINE") + inline fun state() = flow().value + + @Suppress("NOTHING_TO_INLINE") + inline fun flow(): StateFlow<IrcUserState> = state + + @PublishedApi + internal val state = MutableStateFlow( IrcUserState( network = network, nick = HostmaskHelper.nick(hostmask), diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt index a6c3eca5178c9ad72486bcb9cdb6be7276919993..9fdd6fe259aa78efc09c273682f81e1ed279ecbc 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt @@ -24,8 +24,7 @@ import de.justjanne.libquassel.protocol.syncables.state.NetworkState import de.justjanne.libquassel.protocol.syncables.stubs.NetworkStub import de.justjanne.libquassel.protocol.util.indices import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper -import de.justjanne.libquassel.protocol.util.irc.IrcCapability -import de.justjanne.libquassel.protocol.util.irc.IrcCaseMapper +import de.justjanne.libquassel.protocol.util.irc.IrcISupport import de.justjanne.libquassel.protocol.util.transpose import de.justjanne.libquassel.protocol.util.update import de.justjanne.libquassel.protocol.variant.QVariantList @@ -33,16 +32,15 @@ import de.justjanne.libquassel.protocol.variant.QVariantMap import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.qVariant import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import java.nio.ByteBuffer -import java.util.Locale class Network constructor( networkId: NetworkId, session: Session ) : SyncableObject(session, "Network"), NetworkStub { override fun init() { - renameObject("${networkId().id}") - super.init() + renameObject(state().identifier()) } override fun fromVariantMap(properties: QVariantMap) { @@ -233,83 +231,70 @@ class Network constructor( ) ) - fun me() = ircUser(myNick() ?: "") - - fun networkId() = state.value.id - fun networkName() = state.value.networkName - fun isConnected() = state.value.connected - fun connectionState() = state.value.connectionState - fun currentServer() = state.value.currentServer - fun myNick() = state.value.myNick - fun latency() = state.value.latency - fun identity() = state.value.identity - fun nicks() = state.value.ircUsers.keys - fun channels() = state.value.ircChannels.keys - fun caps() = state.value.caps - fun capsEnabled() = state.value.capsEnabled - fun serverList() = state.value.serverList - fun useRandomServer() = state.value.useRandomServer - fun perform() = state.value.perform - fun useAutoIdentify() = state.value.useAutoIdentify - fun autoIdentifyService() = state.value.autoIdentifyService - fun autoIdentifyPassword() = state.value.autoIdentifyPassword - fun useSasl() = state.value.useSasl - fun saslAccount() = state.value.saslAccount - fun saslPassword() = state.value.saslPassword - fun useAutoReconnect() = state.value.useAutoReconnect - fun autoReconnectInterval() = state.value.autoReconnectInterval - fun autoReconnectRetries() = state.value.autoReconnectRetries - fun unlimitedReconnectRetries() = state.value.unlimitedReconnectRetries - fun rejoinChannels() = state.value.rejoinChannels - fun useCustomMessageRate() = state.value.useCustomMessageRate - fun messageRateBurstSize() = state.value.messageRateBurstSize - fun messageRateDelay() = state.value.messageRateDelay - fun unlimitedMessageRate() = state.value.unlimitedMessageRate - fun prefixes() = state.value.prefixes - fun prefixModes() = state.value.prefixModes - fun channelModes() = state.value.channelModes - fun supports() = state.value.supports - fun supports(key: String) = - state.value.supports.containsKey(key.toUpperCase(Locale.ROOT)) - - fun supportValue(key: String) = - state.value.supports[key.toUpperCase(Locale.ROOT)] - - fun capAvailable(capability: String) = - state.value.caps.containsKey(capability.toLowerCase(Locale.ROOT)) - - fun capEnabled(capability: String) = - state.value.capsEnabled.contains(capability.toLowerCase(Locale.ROOT)) - - fun capValue(capability: String) = - state.value.caps[capability.toLowerCase(Locale.ROOT)] ?: "" - - fun skipCaps() = state.value.skipCaps - - fun isSaslSupportLikely(mechanism: String): Boolean { - if (!capAvailable(IrcCapability.SASL)) { - return false - } - val capValue = capValue(IrcCapability.SASL) - return (capValue.isBlank() || capValue.contains(mechanism, ignoreCase = true)) - } - - fun ircUser(nickName: String) = - state.value.ircUsers[caseMapper().toLowerCase(nickName)] - - fun ircUsers() = state.value.ircUsers.values - fun ircUserCount() = state.value.ircUsers.size - fun ircChannel(name: String) = - state.value.ircChannels[caseMapper().toLowerCase(name)] - - fun ircChannels() = state.value.ircChannels.values - fun ircChannelCount() = state.value.ircChannels.size - - fun codecForServer() = state.value.codecForServer - fun codecForEncoding() = state.value.codecForEncoding - fun codecForDecoding() = state.value.codecForDecoding - - fun caseMapper() = IrcCaseMapper[supportValue("CASEMAPPING")] + fun networkId() = state().networkId + fun networkName() = state().networkName + fun isConnected() = state().connected + fun connectionState() = state().connectionState + fun currentServer() = state().currentServer + fun myNick() = state().myNick + fun latency() = state().latency + fun identity() = state().identity + fun nicks() = state().ircUsers.keys + fun channels() = state().ircChannels.keys + fun caps() = state().caps + fun capsEnabled() = state().capsEnabled + + fun serverList() = state().serverList + fun useRandomServer() = state().useRandomServer + fun perform() = state().perform + + fun useAutoIdentify() = state().useAutoIdentify + fun autoIdentifyService() = state().autoIdentifyService + fun autoIdentifyPassword() = state().autoIdentifyPassword + + fun useSasl() = state().useSasl + fun saslAccount() = state().saslAccount + fun saslPassword() = state().saslPassword + + fun useAutoReconnect() = state().useAutoReconnect + fun autoReconnectInterval() = state().autoReconnectInterval + fun autoReconnectRetries() = state().autoReconnectRetries + fun unlimitedReconnectRetries() = state().unlimitedReconnectRetries + fun rejoinChannels() = state().rejoinChannels + + fun useCustomMessageRate() = state().useCustomMessageRate + fun messageRateBurstSize() = state().messageRateBurstSize + fun messageRateDelay() = state().messageRateDelay + fun unlimitedMessageRate() = state().unlimitedMessageRate + + fun prefixes() = state().prefixes + fun prefixModes() = state().prefixModes + fun channelModes() = state().channelModes + + fun supports() = state().supports + fun supports(key: String) = state().supports(key) + fun supportValue(key: String) = state().supportValue(key) + + fun capAvailable(capability: String) = state().capAvailable(capability) + fun capEnabled(capability: String) = state().capEnabled(capability) + fun capValue(capability: String) = state().capValue(capability) + fun skipCaps() = state().skipCaps + + fun isSaslSupportLikely(mechanism: String) = state().isSaslSupportLikely(mechanism) + + fun ircUser(nickName: String) = state().ircUser(nickName) + fun ircUsers() = state().ircUsers.values + fun ircUserCount() = state().ircUsers.size + + fun ircChannel(name: String) = state().ircChannel(name) + fun ircChannels() = state().ircChannels.values + fun ircChannelCount() = state().ircChannels.size + + fun codecForServer() = state().codecForServer + fun codecForEncoding() = state().codecForEncoding + fun codecForDecoding() = state().codecForDecoding + + fun caseMapper() = state().caseMapper() fun networkInfo() = NetworkInfo( networkName = networkName(), @@ -405,15 +390,10 @@ class Network constructor( } } - fun isMe(user: IrcUser): Boolean { - return caseMapper().equalsIgnoreCase(user.nick(), myNick()) - } + fun me() = state().me() + fun isMe(user: IrcUser) = state().isMe(user) - fun channelModeType(mode: Char): ChannelModeType? { - return channelModes().entries.find { - it.value.contains(mode) - }?.key - } + fun channelModeType(mode: Char) = state().channelModeType(mode) override fun addSupport(param: String, value: String) { state.update { @@ -477,7 +457,7 @@ class Network constructor( fun ircUserNickChanged(old: String, new: String) { val oldNick = caseMapper().toLowerCase(old) val newNick = caseMapper().toLowerCase(new) - val user = state.value.ircUsers[oldNick] + val user = state().ircUsers[oldNick] if (user != null) { state.update { copy(ircUsers = ircUsers - oldNick + Pair(newNick, user)) @@ -488,7 +468,7 @@ class Network constructor( private fun determineChannelModeTypes(): Map<ChannelModeType, Set<Char>> { return ChannelModeType.values() .zip( - supportValue("CHANMODES") + supportValue(IrcISupport.CHANMODES) ?.split(',', limit = ChannelModeType.values().size) ?.map(String::toSet) .orEmpty() @@ -500,7 +480,7 @@ class Network constructor( val defaultPrefixes = listOf('~', '&', '@', '%', '+') val defaultPrefixModes = listOf('q', 'a', 'o', 'h', 'v') - val prefix = supportValue("PREFIX") + val prefix = supportValue(IrcISupport.PREFIX) ?: return Pair(defaultPrefixes, defaultPrefixModes) if (prefix.startsWith("(") && prefix.contains(")")) { @@ -743,9 +723,16 @@ class Network constructor( super.setCodecForDecoding(codecForDecoding) } - private val state = MutableStateFlow( + @Suppress("NOTHING_TO_INLINE") + inline fun state() = flow().value + + @Suppress("NOTHING_TO_INLINE") + inline fun flow(): StateFlow<NetworkState> = state + + @PublishedApi + internal val state = MutableStateFlow( NetworkState( - id = networkId + networkId = networkId ) ) } diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/AliasManagerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/AliasManagerState.kt index 89a1d7c8c57e6f5b9168283079f6688eb485bca8..265167750eff9a8f258fa12a9f4616f34bc8e138 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/AliasManagerState.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/AliasManagerState.kt @@ -11,7 +11,124 @@ package de.justjanne.libquassel.protocol.syncables.state import de.justjanne.libquassel.protocol.models.Alias +import de.justjanne.libquassel.protocol.models.BufferInfo +import de.justjanne.libquassel.protocol.models.Command +import de.justjanne.libquassel.protocol.syncables.AliasManager +import de.justjanne.libquassel.protocol.util.expansion.Expansion data class AliasManagerState( val aliases: List<Alias> = emptyList() -) +) { + fun indexOf(name: String?) = aliases.map(Alias::name).indexOf(name) + + fun contains(name: String?) = aliases.map(Alias::name).contains(name) + + fun processInput( + info: BufferInfo, + networkState: NetworkState?, + message: String + ) = mutableListOf<Command>().also { + processInput(info, networkState, message, it) + } + + fun processInput( + info: BufferInfo, + networkState: NetworkState?, + message: String, + previousCommands: MutableList<Command> + ) { + val (command, arguments) = determineMessageCommand(message) + if (command == null) { + // If no command is found, this means the message should be treated as + // pure text. To ensure this won’t be unescaped twice it’s sent with /SAY. + previousCommands.add(Command(info, "/SAY $arguments")) + } else { + val found = aliases.firstOrNull { it.name.equals(command, true) } + if (found != null) { + expand(found.expansion ?: "", info, networkState, arguments, previousCommands) + } else { + previousCommands.add(Command(info, message)) + } + } + } + + fun expand( + expansion: String, + bufferInfo: BufferInfo, + networkState: NetworkState?, + arguments: String, + previousCommands: MutableList<Command> + ) { + val params = arguments.split(' ') + previousCommands.add( + Command( + bufferInfo, + expansion.split(';') + .map(String::trimStart) + .map(Expansion.Companion::parse) + .map { + it.map { + when (it) { + is Expansion.Constant -> when (it.field) { + Expansion.ConstantField.CHANNEL -> + bufferInfo.bufferName + Expansion.ConstantField.NICK -> + networkState?.myNick + Expansion.ConstantField.NETWORK -> + networkState?.networkName + } + is Expansion.Parameter -> when (it.field) { + Expansion.ParameterField.HOSTNAME -> + networkState?.ircUser(params[it.index])?.host() ?: "*" + Expansion.ParameterField.VERIFIED_IDENT -> + params.getOrNull(it.index)?.let { param -> + networkState?.ircUser(param)?.verifiedUser() ?: "*" + } + Expansion.ParameterField.IDENT -> + params.getOrNull(it.index)?.let { param -> + networkState?.ircUser(param)?.user() ?: "*" + } + Expansion.ParameterField.ACCOUNT -> + params.getOrNull(it.index)?.let { param -> + networkState?.ircUser(param)?.account() ?: "*" + } + null -> params.getOrNull(it.index) ?: it.source + } + is Expansion.ParameterRange -> + params.subList(it.from, it.to ?: params.size) + .joinToString(" ") + is Expansion.Text -> + it.source + } ?: it.source + } + }.joinToString(";") + ) + ) + } + + companion object { + private fun determineMessageCommand(message: String) = when { + // Only messages starting with a forward slash are commands + !message.startsWith("/") -> + Pair(null, message) + // If a message starts with //, we consider that an escaped slash + message.startsWith("//") -> + Pair(null, message.substring(1)) + // If the first word of a message contains more than one slash, it is + // usually a regex of format /[a-z][a-z0-9]*/g, or a path of format + // /usr/bin/powerline-go. In that case we also pass it right through + message.startsWith("/") && + message.substringBefore(' ').indexOf('/', 1) != -1 + -> Pair(null, message) + // If the first word is purely a /, we won’t consider it a command either + message.substringBefore(' ') == "/" -> + Pair(null, message) + // Otherwise we treat the first word as a command, and all further words as + // arguments + else -> Pair( + message.trimStart('/').substringBefore(' '), + message.substringAfter(' ') + ) + } + } +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcChannelState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcChannelState.kt index db675e335cfbd6e6895eb398785e7c8d97df163b..06d6c0377fa1a64ba4687e3fa17b70c3d105e147 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcChannelState.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcChannelState.kt @@ -10,6 +10,7 @@ package de.justjanne.libquassel.protocol.syncables.state +import de.justjanne.libquassel.protocol.models.ChannelModeType import de.justjanne.libquassel.protocol.models.ChannelModes import de.justjanne.libquassel.protocol.models.ids.NetworkId @@ -21,4 +22,41 @@ data class IrcChannelState( val encrypted: Boolean = false, val channelModes: ChannelModes = ChannelModes(), val userModes: Map<String, Set<Char>> = emptyMap() -) +) { + fun channelModeString() = channelModes.modeString() + + fun ircUsers(networkState: NetworkState?) = networkState?.let { network -> + userModes.keys.mapNotNull(network::ircUser) + }.orEmpty() + + fun userCount() = userModes.size + fun userModes(nick: String) = userModes[nick] + fun hasMode(networkState: NetworkState?, mode: Char) = when (networkState?.channelModeType(mode)) { + ChannelModeType.A_CHANMODE -> + channelModes.a.contains(mode) + ChannelModeType.B_CHANMODE -> + channelModes.b.contains(mode) + ChannelModeType.C_CHANMODE -> + channelModes.c.contains(mode) + ChannelModeType.D_CHANMODE -> + channelModes.d.contains(mode) + else -> + false + } + + fun modeValue(networkState: NetworkState?, mode: Char) = when (networkState?.channelModeType(mode)) { + ChannelModeType.B_CHANMODE -> + channelModes.b[mode] ?: "" + ChannelModeType.C_CHANMODE -> + channelModes.c[mode] ?: "" + else -> + "" + } + + fun modeValues(networkState: NetworkState?, mode: Char) = when (networkState?.channelModeType(mode)) { + ChannelModeType.A_CHANMODE -> + channelModes.a[mode].orEmpty() + else -> + emptySet() + } +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcUserState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcUserState.kt index 5e6e2ddee24767fb9ec494e43d50d2f3b93a7d6b..408c1a7a337f1310d93b2f9f499e29b60f5600ef 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcUserState.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcUserState.kt @@ -32,4 +32,13 @@ data class IrcUserState( val encrypted: Boolean = false, val channels: Set<String> = emptySet(), val userModes: Set<Char> = emptySet() -) +) { + fun identifier() = "${network.id}/${nick}" + + fun verifiedUser() = user.let { + if (it.startsWith("~")) null + else it + } + + fun hostMask() = "${nick}!${user}@${host}" +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkState.kt index 48311799b6602f730b4beaf31ee033d60f00b362..c2bf06338ffc5ec7563909cee68e8c9827251a8f 100644 --- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkState.kt +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkState.kt @@ -17,9 +17,13 @@ import de.justjanne.libquassel.protocol.models.ids.IdentityId import de.justjanne.libquassel.protocol.models.ids.NetworkId import de.justjanne.libquassel.protocol.syncables.IrcChannel import de.justjanne.libquassel.protocol.syncables.IrcUser +import de.justjanne.libquassel.protocol.util.irc.IrcCapability +import de.justjanne.libquassel.protocol.util.irc.IrcCaseMapper +import de.justjanne.libquassel.protocol.util.irc.IrcISupport +import java.util.Locale data class NetworkState( - val id: NetworkId, + val networkId: NetworkId, val identity: IdentityId = IdentityId(-1), val myNick: String? = "", val latency: Int = 0, @@ -57,4 +61,37 @@ data class NetworkState( val codecForServer: String = "UTF_8", val codecForEncoding: String = "UTF_8", val codecForDecoding: String = "UTF_8" -) +) { + fun identifier() = "${networkId.id}" + + fun caseMapper() = IrcCaseMapper[supportValue(IrcISupport.CASEMAPPING)] + fun supports(key: String) = supports.containsKey(key.toUpperCase(Locale.ROOT)) + fun supportValue(key: String) = supports[key.toUpperCase(Locale.ROOT)] + + fun capAvailable(capability: String) = caps.containsKey(capability.toLowerCase(Locale.ROOT)) + fun capEnabled(capability: String) = capsEnabled.contains(capability.toLowerCase(Locale.ROOT)) + fun capValue(capability: String) = caps[capability.toLowerCase(Locale.ROOT)] ?: "" + + fun isSaslSupportLikely(mechanism: String): Boolean { + if (!capAvailable(IrcCapability.SASL)) { + return false + } + val capValue = capValue(IrcCapability.SASL) + return (capValue.isBlank() || capValue.contains(mechanism, ignoreCase = true)) + } + + fun ircUser(nickName: String) = ircUsers[caseMapper().toLowerCase(nickName)] + fun ircChannel(name: String) = ircChannels[caseMapper().toLowerCase(name)] + + fun me() = myNick?.let(::ircUser) + + fun isMe(user: IrcUser): Boolean { + return caseMapper().equalsIgnoreCase(user.nick(), myNick) + } + + fun channelModeType(mode: Char): ChannelModeType? { + return channelModes.entries.find { + it.value.contains(mode) + }?.key + } +} diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcISupport.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcISupport.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6256324ff401d18c917d6163a8977202b221d00 --- /dev/null +++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcISupport.kt @@ -0,0 +1,477 @@ +/* + * libquassel + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + */ + +package de.justjanne.libquassel.protocol.util.irc + +object IrcISupport { + /** + * Indicates the maximum number of online nicknames a user may have in their accept list. + * + * Format: `ACCEPT=<number>` + * + * Examples: + * - `ACCEPT=20` + */ + const val ACCEPT = "ACCEPT" + + /** + * Indicates the maximum length of an away message. If "number" is not defined, there is no limit. + * + * Format: `AWAYLEN=\[number]` + * + * Examples: + * - `AWAYLEN` + * - `AWAYLEN=8` + */ + const val AWAYLEN = "AWAYLEN" + + /** + * Indicates that the "caller-id" user mode is supported, which rejects messages from unauthorized users. + * "letter" defines the mode character, which is used for this feature. + * If the value is not given, it defaults to the mode "g". + * + * Format: `CALLERID\[=letter]` + * + * Examples: + * - `CALLERID` + * - `CALLERID=g` + */ + const val CALLERID = "CALLERID" + + /** + * Indicates the method that’s used to compare equality of case-insensitive strings (such as nick/channel names). + * Typical values include "ascii" and "rfc1459". + * + * Format: `CASEMAPPING=<string>` + * + * Examples: + * - `CASEMAPPING=rfc1459` + */ + const val CASEMAPPING = "CASEMAPPING" + + /** + * Indicates the maximum number of channels a client may join. + * Though a client shouldn’t assume that other clients are limited to what they receive here. + * If "number" is empty, there is no limit. + * + * Format: `CHANLIMIT=<prefix:[number[,prefix:number[,...]]]> ` + * + * Examples: + * - `CHANLIMIT=#+:25,&:` + * - `CHANLIMIT=&#:50` + */ + const val CHANLIMIT = "CHANLIMIT" + + /** + * Indicates the channel modes available and which types of arguments they do or do not take. + * + * Also see: [MAXLIST] + * + * Format: `CHANMODES=<A>,<B>,<C>,<D>` + * + * Examples: + * - `CHANMODES=b,k,l,imnpst` + * - `CHANMODES=beI,k,l,BCMNORScimnpstz` + */ + const val CHANMODES = "CHANMODES" + + /** + * Specifies the maximum length of a channel name that clients may join. + * + * Format: `CHANNELLEN=<number>` + * + * Examples: + * - `CHANNELLEN=50` + */ + const val CHANNELLEN = "CHANNELLEN" + + /** + * Indicates the types of channels supported on this server. + * These are channel type prefixes as specified in the RFC. + * + * Format: `CHANTYPES=\[string\]` + * + * Examples: + * - `CHANTYPES=&#` + */ + const val CHANTYPES = "CHANTYPES" + + /** + * Indicates that the server supports the "CNOTICE" command. This is an extension to the "NOTICE" command. + * + * Format: `CNOTICE` + * + * Examples: + * - `CNOTICE` + */ + const val CNOTICE = "CNOTICE" + + /** + * Indicates that the server supports the "CNOTICE" command. This is an extension to the "NOTICE" command. + * + * Format: `CPRIVMSG` + * + * Examples: + * - `CPRIVMSG` + */ + const val CPRIVMSG = "CPRIVMSG" + + /** + * Indicates that the server supports the "DEAF" user mode and the given character is used to represent that mode. + * + * Format: `DEAF=<letter>` + * + * Examples: + * - `DEAF=D` + */ + const val DEAF = "DEAF" + + /** + * Indicates that the server supports search extensions to the "LIST" command. + * + * Format: `ELIST=<string>` + * + * Examples: + * - `ELIST=CMNTU` + */ + const val ELIST = "ELIST" + + /** + * Indicates that the server supports filtering extensions to the "SILENCE" command. + * If a value is specified then it contains the supported filter flags. + * + * Format: `ESILENCE=\[flags]` + * + * Examples: + * - `ESILENCE` + * - `ESILENCE=CcdiNnPpTtx` + */ + const val ESILENCE = "ESILENCE" + + /** + * Indicates that the server supports the "ETRACE" command (IRC operators-only), which is similar to "TRACE", but only + * works on nicknames and has a few different options. + * + * Format: `ETRACE` + * + * Examples: + * - `ETRACE` + */ + const val ETRACE = "ETRACE" + + /** + * Indicates that the server supports “ban exemptions”. + * The letter is OPTIONAL and defines the mode character which is used for this. + * When no letter is provided, it defaults to "e". + * + * Format: `EXCEPTS[=letter]` + * + * Examples: + * - `EXCEPTS` + * - `EXCEPTS=e` + */ + const val EXCEPTS = "EXCEPTS" + + /** + * Indicates that the server supports “ban exemptions”. + * The letter is OPTIONAL and defines the mode character which is used for this. + * When no letter is provided, it defaults to "e". + * + * Format: `EXCEPTS[=letter]` + * + * Examples: + * - `EXCEPTS` + * - `EXCEPTS=e` + */ + const val EXTBAN = "EXTBAN" + + /** + * Indicates that the server supports “invite exemptions”. + * The letter is OPTIONAL and defines the mode character, which is used for this. + * When no letter is provided, it defaults to "I". + * + * Format: `INVEX[=letter]` + * + * Examples: + * - `INVEX` + * - `INVEX=I` + */ + const val INVEX = "INVEX" + + /** + * Indicates the maximum length of a channel key. + * + * Format: `KEYLEN=<number>` + * + * Examples: + * - `KEYLEN=23` + */ + const val KEYLEN = "KEYLEN" + + /** + * Indicates the maximum length of a kick message. If "number" is not defined, there is no limit. + * + * Format: `KICKLEN=\[number]` + * + * Examples: + * - `KICKLEN` + * - `KICKLEN=180` + */ + const val KICKLEN = "KICKLEN" + + /** + * Indicates support for the "KNOCK" command, which is used to request an invite to a channel. + * + * Format: `KNOCK` + * + * Examples: + * - `KNOCK` + */ + const val KNOCK = "KNOCK" + + /** + * Indicates how many “variable” modes of "type A" that have been defined in the "CHANMODES" token a client may set in + * total on a channel. The value MUST be specified and is a set of "mode:number" pairs, where "mode" is a number of + * "type A" modes that have been defined in "CHANMODES" and "number" is how many of this mode may be set. + * + * Also see: [CHANMODES] + * + * Format: `MAXLIST=<mode:number[,mode:number[,...]]>` + * + * Examples: + * - `MAXLIST=beI:25` + * - `MAXLIST=b:25,eI:50` + */ + const val MAXLIST = "MAXLIST" + + /** + * Indicates the maximum length of a nickname that a client may use. + * Other clients on the network may have nicknames longer than this. + * + * Format: `MAXNICKLEN=<number>` + * + * Examples: + * - `MAXNICKLEN=9` + * - `MAXNICKLEN=32` + */ + const val MAXNICKLEN = "MAXNICKLEN" + + /** + * Indicates the maximum number of targets for the "PRIVMSG" & "NOTICE" commands. + * + * Also see: [TARGMAX] + * + * Format: `MAXTARGETS=<number>` + * + * Examples: + * - `MAXTARGETS=8` + */ + const val MAXTARGETS = "MAXTARGETS" + + /** + * Indicates the maximum number of keys a user may have in their metadata. + * If "number" is not specified, there is no limit. + * + * Format: `METADATA[=number]` + * + * Examples: + * - `METADATA` + * - `METADATA=30` + */ + const val METADATA = "METADATA" + + /** + * Indicates how many “variable” modes may be set on a channel by a single "MODE" command from a client. A “variable” + * mode is defined as being a "type A/B/C" mode as defined in the "CHANMODES" token. The value is optional and when + * not specified indicates that there is NO limit places on “variable” modes. + * + * Format: `MODES=\[number]` + * + * Examples: + * - `MODES` + * - `MODES=3` + */ + const val MODES = "MODES" + + /** + * Indicates the maximum number of targets a user may have in their monitor list. + * If "number" is not specified, there is no limit. + * + * Also see: [WATCH] + * + * Format: `MONITOR=\[number]` + * + * Examples: + * - `MONITOR` + * - `MONITOR=6` + */ + const val MONITOR = "MONITOR" + + /** + * For INFORMATIONAL PURPOSES ONLY and indicates the name of the IRC network that the client is connected to. + * A client SHOULD NOT use this value to make assumptions about supported features on the server. + * + * Format: `NETWORK=<string>` + * + * Examples: + * - `NETWORK=EFNet` + * - `NETWORK=Rizon` + */ + const val NETWORK = "NETWORK" + + /** + * Indicates the maximum length of a nickname that a client may use. + * Other clients on the network may have nicknames longer than this. + * + * Format: `NICKLEN=<number>` + * + * Examples: + * - `NICKLEN=9` + * - `NICKLEN=32` + */ + const val NICKLEN = "NICKLEN" + + /** + * Indicates the channel membership prefixes available on this server and their order in terms of channel privileges + * they represent, from highest to lowest. If the value is not specified, then NO channel membership prefixes are + * supported by this server. + * + * Format: `PREFIX=\[(modes)prefixes]` + * + * Examples: + * - `PREFIX=` + * - `PREFIX=(ov)@+` + * - `PREFIX=(qaohv)~&@%+` + */ + const val PREFIX = "PREFIX" + + /** + * Indicates that the client may request a "LIST" command from the server without being disconnected due to the large + * amount of data. This token MUST NOT have a value. + * + * Format: `SAFELIST` + * + * Examples: + * - `SAFELIST` + */ + const val SAFELIST = "SAFELIST" + + /** + * Indicates the maximum number of entries a user may have in their silence list. + * The value is OPTIONAL and if not specified indicates that there is no limit. + * + * The "SILENCE" command seems to vary quite a lot between implementations. + * Most clients include client-side filter/ignore commands and servers have the "CALLERID" client mode as alternatives + * to this command. + * + * Format: `SILENCE=\[number]` + * + * Examples: + * - `SILENCE` + * - `SILENCE=15` + */ + const val SILENCE = "SILENCE" + + /** + * Indicates that the server supports a method for the client to send a message via the "NOTICE" command to those + * people on a channel with the specified channel membership prefixes. The value MUST be specified and MUST be a list + * of prefixes as specified in the "PREFIX" token. + * + * Format: `STATUSMSG=<string>` + * + * Examples: + * - `STATUSMSG=@+` + */ + const val STATUSMSG = "STATUSMSG" + + /** + * Certain commands from a client MAY contain multiple targets. + * This token defines the maximum number of targets may be specified on each of these commands. + * The value is OPTIONAL and is a set of "cmd:number" pairs, where "cmd" refers to the specific command and "number" + * refers to the limit for this command. + * + * If the number is not specified for a particular command, then that command does not have a limit on the maximum + * number of targets. If the "TARGMAX" parameter is not advertised or a value is not sent then a client SHOULD + * assume that no commands except the "JOIN" and "PART" commands accept multiple parameters. + * + * Also see: [MAXTARGETS] + * + * Format: `TARGMAX=[cmd:[number][,cmd:[number][,...]]]` + * + * Examples: + * - `TARGMAX=PRIVMSG:3,WHOIS:1,JOIN:` + * - `TARGMAX=` + */ + const val TARGMAX = "TARGMAX" + + /** + * Indicates the maximum length of a topic that a client may set on a channel. + * Channels on the network MAY have topics with longer lengths than this. + * + * Format: `TOPICLEN=<number>` + * + * Examples: + * - `TOPICLEN=120` + */ + const val TOPICLEN = "TOPICLEN" + + /** + * Indicates support for the "USERIP" command, which is used to request the direct IP address of the user with the + * specified nickname. This might be supported by networks that don’t advertise this token. + * + * Format: `USERIP` + * + * Examples: + * - `USERIP` + */ + const val USERIP = "USERIP" + + /** + * Indicates the maximum length of an username in octets. If "number" is not specified, there is no limit. + * + * Format: `USERLEN=\[number]` + * + * Examples: + * - `USERLEN=` + * - `USERLEN=12` + */ + const val USERLEN = "USERLEN" + + /** + * Indicates that the specified list modes may be larger than the value specified in MAXLIST. + * + * Format: `VLIST=<modes>` + * + * Examples: + * - `VLIST=be` + */ + const val VLIST = "VLIST" + + /** + * Indicates the maximum number of nicknames a user may have in their watch list. + * The "MONITOR" command is aimed at being a more consistent alternative to this command. + * + * Format: `WATCH=<number>` + * + * Examples: + * - `WATCH=100` + */ + const val WATCH = "WATCH" + + /** + * Indicates that the server supports extended syntax of the "WHO" command. + * + * Format: `WHOX` + * + * Examples: + * - `WHOX` + */ + const val WHOX = "WHOX" +}