Skip to content
Snippets Groups Projects
Verified Commit d48850f8 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Move as much functionality as possible into the state holders

parent 45d0bce1
Branches
Tags
No related merge requests found
Showing
with 855 additions and 269 deletions
...@@ -17,12 +17,12 @@ import de.justjanne.libquassel.protocol.models.QStringList ...@@ -17,12 +17,12 @@ import de.justjanne.libquassel.protocol.models.QStringList
import de.justjanne.libquassel.protocol.models.types.QtType import de.justjanne.libquassel.protocol.models.types.QtType
import de.justjanne.libquassel.protocol.syncables.state.AliasManagerState import de.justjanne.libquassel.protocol.syncables.state.AliasManagerState
import de.justjanne.libquassel.protocol.syncables.stubs.AliasManagerStub 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.util.update
import de.justjanne.libquassel.protocol.variant.QVariantMap import de.justjanne.libquassel.protocol.variant.QVariantMap
import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.into
import de.justjanne.libquassel.protocol.variant.qVariant import de.justjanne.libquassel.protocol.variant.qVariant
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class AliasManager constructor( class AliasManager constructor(
session: Session session: Session
...@@ -64,120 +64,53 @@ class AliasManager constructor( ...@@ -64,120 +64,53 @@ class AliasManager constructor(
super.addAlias(name, expansion) 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( fun processInput(
info: BufferInfo, info: BufferInfo,
message: String message: String
) = mutableListOf<Command>().also { ) = state().processInput(
processInput(info, message, it) info,
} session.network(info.networkId)?.state(),
message
)
fun processInput( fun processInput(
info: BufferInfo, info: BufferInfo,
message: String, message: String,
previousCommands: MutableList<Command> previousCommands: MutableList<Command>
) { ) = state().processInput(
val (command, arguments) = determineMessageCommand(message) info,
if (command == null) { session.network(info.networkId)?.state(),
// If no command is found, this means the message should be treated as message,
// pure text. To ensure this won’t be unescaped twice it’s sent with /SAY. previousCommands
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))
}
}
}
fun expand( fun expand(
expansion: String, expansion: String,
bufferInfo: BufferInfo, bufferInfo: BufferInfo,
arguments: String, arguments: String,
previousCommands: MutableList<Command> previousCommands: MutableList<Command>
) { ) = state().expand(
val network = session.network(bufferInfo.networkId) expansion,
val params = arguments.split(' ')
previousCommands.add(
Command(
bufferInfo, bufferInfo,
expansion.split(';') session.network(bufferInfo.networkId)?.state(),
.map(String::trimStart) arguments,
.map(Expansion.Companion::parse) previousCommands
.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(";")
)
) )
}
private val state = MutableStateFlow( @Suppress("NOTHING_TO_INLINE")
AliasManagerState() inline fun state() = flow().value
)
@Suppress("NOTHING_TO_INLINE")
inline fun flow(): StateFlow<AliasManagerState> = state
companion object { @PublishedApi
private fun determineMessageCommand(message: String) = when { internal val state = MutableStateFlow(
// Only messages starting with a forward slash are commands AliasManagerState()
!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(' ')
) )
} }
}
}
...@@ -23,6 +23,7 @@ import de.justjanne.libquassel.protocol.variant.indexed ...@@ -23,6 +23,7 @@ import de.justjanne.libquassel.protocol.variant.indexed
import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.into
import de.justjanne.libquassel.protocol.variant.qVariant import de.justjanne.libquassel.protocol.variant.qVariant
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class IrcChannel( class IrcChannel(
name: String, name: String,
...@@ -61,9 +62,9 @@ class IrcChannel( ...@@ -61,9 +62,9 @@ class IrcChannel(
"topic" to qVariant(topic(), QtType.QString), "topic" to qVariant(topic(), QtType.QString),
"password" to qVariant(password(), QtType.QString), "password" to qVariant(password(), QtType.QString),
"encrypted" to qVariant(isEncrypted(), QtType.Bool), "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( "UserModes" to qVariant(
state.value.userModes.mapValues { (_, value) -> state().userModes.mapValues { (_, value) ->
qVariant(value.joinToString(), QtType.QString) qVariant(value.joinToString(), QtType.QString)
}, },
QtType.QVariantMap QtType.QVariantMap
...@@ -71,47 +72,22 @@ class IrcChannel( ...@@ -71,47 +72,22 @@ class IrcChannel(
) )
} }
fun network() = state.value.network fun network() = state().network
fun name() = state.value.name fun name() = state().name
fun topic() = state.value.topic fun topic() = state().topic
fun password() = state.value.password fun password() = state().password
fun isEncrypted() = state.value.encrypted fun isEncrypted() = state().encrypted
fun ircUsers() = session.network(network())?.let { network -> fun ircUsers() = state().ircUsers(session.network(network())?.state())
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 modeValue(mode: Char) = when (session.network(network())?.channelModeType(mode)) { fun userCount() = state().userModes.size
ChannelModeType.B_CHANMODE -> fun userModes(nick: String) = state().userModes[nick]
state.value.channelModes.b[mode] ?: "" fun hasMode(mode: Char) = state().hasMode(session.network(network())?.state(), mode)
ChannelModeType.C_CHANMODE ->
state.value.channelModes.c[mode] ?: ""
else ->
""
}
fun modeValues(mode: Char) = when (session.network(network())?.channelModeType(mode)) { fun modeValue(mode: Char) = state().modeValue(session.network(network())?.state(), mode)
ChannelModeType.A_CHANMODE ->
state.value.channelModes.a[mode].orEmpty()
else ->
emptySet()
}
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) { override fun setTopic(topic: String) {
state.update { state.update {
...@@ -146,7 +122,7 @@ class IrcChannel( ...@@ -146,7 +122,7 @@ class IrcChannel(
private fun joinIrcUsers(map: Map<String, Set<Char>>) { private fun joinIrcUsers(map: Map<String, Set<Char>>) {
val network = session.network(network()) val network = session.network(network())
val newNicks = map.keys - state.value.userModes.keys val newNicks = map.keys - state().userModes.keys
state.update { state.update {
copy( copy(
userModes = userModes + map.mapValues { (key, value) -> userModes = userModes + map.mapValues { (key, value) ->
...@@ -173,8 +149,8 @@ class IrcChannel( ...@@ -173,8 +149,8 @@ class IrcChannel(
if (partingUser != null) { if (partingUser != null) {
partingUser.partChannel(name()) partingUser.partChannel(name())
if (network.isMe(partingUser) || state.value.userModes.isEmpty()) { if (network.isMe(partingUser) || state().userModes.isEmpty()) {
for (nickname in state.value.userModes.keys.toList()) { for (nickname in state().userModes.keys.toList()) {
network.ircUser(nickname)?.partChannel(this) network.ircUser(nickname)?.partChannel(this)
} }
state.update { state.update {
...@@ -289,7 +265,14 @@ class IrcChannel( ...@@ -289,7 +265,14 @@ class IrcChannel(
super.removeChannelMode(mode, value) 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( IrcChannelState(
network = network, network = network,
name = name name = name
......
...@@ -21,6 +21,7 @@ import de.justjanne.libquassel.protocol.variant.indexed ...@@ -21,6 +21,7 @@ import de.justjanne.libquassel.protocol.variant.indexed
import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.into
import de.justjanne.libquassel.protocol.variant.qVariant import de.justjanne.libquassel.protocol.variant.qVariant
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.threeten.bp.Instant import org.threeten.bp.Instant
import org.threeten.bp.temporal.Temporal import org.threeten.bp.temporal.Temporal
...@@ -34,7 +35,7 @@ class IrcUser( ...@@ -34,7 +35,7 @@ class IrcUser(
} }
private fun updateObjectName() { private fun updateObjectName() {
renameObject("${network().id}/${nick()}") renameObject(state().identifier())
} }
override fun fromVariantMap(properties: QVariantMap) = override fun fromVariantMap(properties: QVariantMap) =
...@@ -234,7 +235,7 @@ class IrcUser( ...@@ -234,7 +235,7 @@ class IrcUser(
} }
fun joinChannel(channel: IrcChannel, skipChannelJoin: Boolean = false) { fun joinChannel(channel: IrcChannel, skipChannelJoin: Boolean = false) {
if (state.value.channels.contains(channel.name())) { if (state().channels.contains(channel.name())) {
return return
} }
...@@ -280,32 +281,36 @@ class IrcUser( ...@@ -280,32 +281,36 @@ class IrcUser(
super.quit() super.quit()
} }
fun network() = state.value.network fun network() = state().network
fun nick() = state.value.nick fun nick() = state().nick
fun user() = state.value.user fun user() = state().user
fun verifiedUser() = user().let { fun verifiedUser() = state().verifiedUser()
if (it.startsWith("~")) null fun host() = state().host
else it fun realName() = state().realName
} fun account() = state().account
fun host() = state.value.host fun hostMask() = state().hostMask()
fun realName() = state.value.realName fun isAway() = state().away
fun account() = state.value.account fun awayMessage() = state().awayMessage
fun hostMask() = "${nick()}!${user()}@${host()}" fun server() = state().server
fun isAway() = state.value.away fun idleTime() = state().idleTime
fun awayMessage() = state.value.awayMessage
fun server() = state.value.server fun loginTime() = state().loginTime
fun idleTime() = state.value.idleTime fun ircOperator() = state().ircOperator
fun lastAwayMessageTime() = state().lastAwayMessageTime
fun loginTime() = state.value.loginTime fun whoisServiceReply() = state().whoisServiceReply
fun ircOperator() = state.value.ircOperator fun suserHost() = state().suserHost
fun lastAwayMessageTime() = state.value.lastAwayMessageTime fun encrypted() = state().encrypted
fun whoisServiceReply() = state.value.whoisServiceReply fun userModes() = state().userModes
fun suserHost() = state.value.suserHost fun channels() = state().channels
fun encrypted() = state.value.encrypted
fun userModes() = state.value.userModes @Suppress("NOTHING_TO_INLINE")
fun channels() = state.value.channels inline fun state() = flow().value
private val state = MutableStateFlow( @Suppress("NOTHING_TO_INLINE")
inline fun flow(): StateFlow<IrcUserState> = state
@PublishedApi
internal val state = MutableStateFlow(
IrcUserState( IrcUserState(
network = network, network = network,
nick = HostmaskHelper.nick(hostmask), nick = HostmaskHelper.nick(hostmask),
......
...@@ -24,8 +24,7 @@ import de.justjanne.libquassel.protocol.syncables.state.NetworkState ...@@ -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.syncables.stubs.NetworkStub
import de.justjanne.libquassel.protocol.util.indices import de.justjanne.libquassel.protocol.util.indices
import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper
import de.justjanne.libquassel.protocol.util.irc.IrcCapability import de.justjanne.libquassel.protocol.util.irc.IrcISupport
import de.justjanne.libquassel.protocol.util.irc.IrcCaseMapper
import de.justjanne.libquassel.protocol.util.transpose import de.justjanne.libquassel.protocol.util.transpose
import de.justjanne.libquassel.protocol.util.update import de.justjanne.libquassel.protocol.util.update
import de.justjanne.libquassel.protocol.variant.QVariantList import de.justjanne.libquassel.protocol.variant.QVariantList
...@@ -33,16 +32,15 @@ import de.justjanne.libquassel.protocol.variant.QVariantMap ...@@ -33,16 +32,15 @@ import de.justjanne.libquassel.protocol.variant.QVariantMap
import de.justjanne.libquassel.protocol.variant.into import de.justjanne.libquassel.protocol.variant.into
import de.justjanne.libquassel.protocol.variant.qVariant import de.justjanne.libquassel.protocol.variant.qVariant
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.Locale
class Network constructor( class Network constructor(
networkId: NetworkId, networkId: NetworkId,
session: Session session: Session
) : SyncableObject(session, "Network"), NetworkStub { ) : SyncableObject(session, "Network"), NetworkStub {
override fun init() { override fun init() {
renameObject("${networkId().id}") renameObject(state().identifier())
super.init()
} }
override fun fromVariantMap(properties: QVariantMap) { override fun fromVariantMap(properties: QVariantMap) {
...@@ -233,83 +231,70 @@ class Network constructor( ...@@ -233,83 +231,70 @@ class Network constructor(
) )
) )
fun me() = ircUser(myNick() ?: "") fun networkId() = state().networkId
fun networkName() = state().networkName
fun networkId() = state.value.id fun isConnected() = state().connected
fun networkName() = state.value.networkName fun connectionState() = state().connectionState
fun isConnected() = state.value.connected fun currentServer() = state().currentServer
fun connectionState() = state.value.connectionState fun myNick() = state().myNick
fun currentServer() = state.value.currentServer fun latency() = state().latency
fun myNick() = state.value.myNick fun identity() = state().identity
fun latency() = state.value.latency fun nicks() = state().ircUsers.keys
fun identity() = state.value.identity fun channels() = state().ircChannels.keys
fun nicks() = state.value.ircUsers.keys fun caps() = state().caps
fun channels() = state.value.ircChannels.keys fun capsEnabled() = state().capsEnabled
fun caps() = state.value.caps
fun capsEnabled() = state.value.capsEnabled fun serverList() = state().serverList
fun serverList() = state.value.serverList fun useRandomServer() = state().useRandomServer
fun useRandomServer() = state.value.useRandomServer fun perform() = state().perform
fun perform() = state.value.perform
fun useAutoIdentify() = state.value.useAutoIdentify fun useAutoIdentify() = state().useAutoIdentify
fun autoIdentifyService() = state.value.autoIdentifyService fun autoIdentifyService() = state().autoIdentifyService
fun autoIdentifyPassword() = state.value.autoIdentifyPassword fun autoIdentifyPassword() = state().autoIdentifyPassword
fun useSasl() = state.value.useSasl
fun saslAccount() = state.value.saslAccount fun useSasl() = state().useSasl
fun saslPassword() = state.value.saslPassword fun saslAccount() = state().saslAccount
fun useAutoReconnect() = state.value.useAutoReconnect fun saslPassword() = state().saslPassword
fun autoReconnectInterval() = state.value.autoReconnectInterval
fun autoReconnectRetries() = state.value.autoReconnectRetries fun useAutoReconnect() = state().useAutoReconnect
fun unlimitedReconnectRetries() = state.value.unlimitedReconnectRetries fun autoReconnectInterval() = state().autoReconnectInterval
fun rejoinChannels() = state.value.rejoinChannels fun autoReconnectRetries() = state().autoReconnectRetries
fun useCustomMessageRate() = state.value.useCustomMessageRate fun unlimitedReconnectRetries() = state().unlimitedReconnectRetries
fun messageRateBurstSize() = state.value.messageRateBurstSize fun rejoinChannels() = state().rejoinChannels
fun messageRateDelay() = state.value.messageRateDelay
fun unlimitedMessageRate() = state.value.unlimitedMessageRate fun useCustomMessageRate() = state().useCustomMessageRate
fun prefixes() = state.value.prefixes fun messageRateBurstSize() = state().messageRateBurstSize
fun prefixModes() = state.value.prefixModes fun messageRateDelay() = state().messageRateDelay
fun channelModes() = state.value.channelModes fun unlimitedMessageRate() = state().unlimitedMessageRate
fun supports() = state.value.supports
fun supports(key: String) = fun prefixes() = state().prefixes
state.value.supports.containsKey(key.toUpperCase(Locale.ROOT)) fun prefixModes() = state().prefixModes
fun channelModes() = state().channelModes
fun supportValue(key: String) =
state.value.supports[key.toUpperCase(Locale.ROOT)] fun supports() = state().supports
fun supports(key: String) = state().supports(key)
fun capAvailable(capability: String) = fun supportValue(key: String) = state().supportValue(key)
state.value.caps.containsKey(capability.toLowerCase(Locale.ROOT))
fun capAvailable(capability: String) = state().capAvailable(capability)
fun capEnabled(capability: String) = fun capEnabled(capability: String) = state().capEnabled(capability)
state.value.capsEnabled.contains(capability.toLowerCase(Locale.ROOT)) fun capValue(capability: String) = state().capValue(capability)
fun skipCaps() = state().skipCaps
fun capValue(capability: String) =
state.value.caps[capability.toLowerCase(Locale.ROOT)] ?: "" fun isSaslSupportLikely(mechanism: String) = state().isSaslSupportLikely(mechanism)
fun skipCaps() = state.value.skipCaps fun ircUser(nickName: String) = state().ircUser(nickName)
fun ircUsers() = state().ircUsers.values
fun isSaslSupportLikely(mechanism: String): Boolean { fun ircUserCount() = state().ircUsers.size
if (!capAvailable(IrcCapability.SASL)) {
return false fun ircChannel(name: String) = state().ircChannel(name)
} fun ircChannels() = state().ircChannels.values
val capValue = capValue(IrcCapability.SASL) fun ircChannelCount() = state().ircChannels.size
return (capValue.isBlank() || capValue.contains(mechanism, ignoreCase = true))
} fun codecForServer() = state().codecForServer
fun codecForEncoding() = state().codecForEncoding
fun ircUser(nickName: String) = fun codecForDecoding() = state().codecForDecoding
state.value.ircUsers[caseMapper().toLowerCase(nickName)]
fun caseMapper() = state().caseMapper()
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 networkInfo() = NetworkInfo( fun networkInfo() = NetworkInfo(
networkName = networkName(), networkName = networkName(),
...@@ -405,15 +390,10 @@ class Network constructor( ...@@ -405,15 +390,10 @@ class Network constructor(
} }
} }
fun isMe(user: IrcUser): Boolean { fun me() = state().me()
return caseMapper().equalsIgnoreCase(user.nick(), myNick()) fun isMe(user: IrcUser) = state().isMe(user)
}
fun channelModeType(mode: Char): ChannelModeType? { fun channelModeType(mode: Char) = state().channelModeType(mode)
return channelModes().entries.find {
it.value.contains(mode)
}?.key
}
override fun addSupport(param: String, value: String) { override fun addSupport(param: String, value: String) {
state.update { state.update {
...@@ -477,7 +457,7 @@ class Network constructor( ...@@ -477,7 +457,7 @@ class Network constructor(
fun ircUserNickChanged(old: String, new: String) { fun ircUserNickChanged(old: String, new: String) {
val oldNick = caseMapper().toLowerCase(old) val oldNick = caseMapper().toLowerCase(old)
val newNick = caseMapper().toLowerCase(new) val newNick = caseMapper().toLowerCase(new)
val user = state.value.ircUsers[oldNick] val user = state().ircUsers[oldNick]
if (user != null) { if (user != null) {
state.update { state.update {
copy(ircUsers = ircUsers - oldNick + Pair(newNick, user)) copy(ircUsers = ircUsers - oldNick + Pair(newNick, user))
...@@ -488,7 +468,7 @@ class Network constructor( ...@@ -488,7 +468,7 @@ class Network constructor(
private fun determineChannelModeTypes(): Map<ChannelModeType, Set<Char>> { private fun determineChannelModeTypes(): Map<ChannelModeType, Set<Char>> {
return ChannelModeType.values() return ChannelModeType.values()
.zip( .zip(
supportValue("CHANMODES") supportValue(IrcISupport.CHANMODES)
?.split(',', limit = ChannelModeType.values().size) ?.split(',', limit = ChannelModeType.values().size)
?.map(String::toSet) ?.map(String::toSet)
.orEmpty() .orEmpty()
...@@ -500,7 +480,7 @@ class Network constructor( ...@@ -500,7 +480,7 @@ class Network constructor(
val defaultPrefixes = listOf('~', '&', '@', '%', '+') val defaultPrefixes = listOf('~', '&', '@', '%', '+')
val defaultPrefixModes = listOf('q', 'a', 'o', 'h', 'v') val defaultPrefixModes = listOf('q', 'a', 'o', 'h', 'v')
val prefix = supportValue("PREFIX") val prefix = supportValue(IrcISupport.PREFIX)
?: return Pair(defaultPrefixes, defaultPrefixModes) ?: return Pair(defaultPrefixes, defaultPrefixModes)
if (prefix.startsWith("(") && prefix.contains(")")) { if (prefix.startsWith("(") && prefix.contains(")")) {
...@@ -743,9 +723,16 @@ class Network constructor( ...@@ -743,9 +723,16 @@ class Network constructor(
super.setCodecForDecoding(codecForDecoding) 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( NetworkState(
id = networkId networkId = networkId
) )
) )
} }
...@@ -11,7 +11,124 @@ ...@@ -11,7 +11,124 @@
package de.justjanne.libquassel.protocol.syncables.state package de.justjanne.libquassel.protocol.syncables.state
import de.justjanne.libquassel.protocol.models.Alias 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( data class AliasManagerState(
val aliases: List<Alias> = emptyList() 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(' ')
) )
}
}
}
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
package de.justjanne.libquassel.protocol.syncables.state 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.ChannelModes
import de.justjanne.libquassel.protocol.models.ids.NetworkId import de.justjanne.libquassel.protocol.models.ids.NetworkId
...@@ -21,4 +22,41 @@ data class IrcChannelState( ...@@ -21,4 +22,41 @@ data class IrcChannelState(
val encrypted: Boolean = false, val encrypted: Boolean = false,
val channelModes: ChannelModes = ChannelModes(), val channelModes: ChannelModes = ChannelModes(),
val userModes: Map<String, Set<Char>> = emptyMap() 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()
}
}
...@@ -32,4 +32,13 @@ data class IrcUserState( ...@@ -32,4 +32,13 @@ data class IrcUserState(
val encrypted: Boolean = false, val encrypted: Boolean = false,
val channels: Set<String> = emptySet(), val channels: Set<String> = emptySet(),
val userModes: Set<Char> = 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}"
}
...@@ -17,9 +17,13 @@ import de.justjanne.libquassel.protocol.models.ids.IdentityId ...@@ -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.models.ids.NetworkId
import de.justjanne.libquassel.protocol.syncables.IrcChannel import de.justjanne.libquassel.protocol.syncables.IrcChannel
import de.justjanne.libquassel.protocol.syncables.IrcUser 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( data class NetworkState(
val id: NetworkId, val networkId: NetworkId,
val identity: IdentityId = IdentityId(-1), val identity: IdentityId = IdentityId(-1),
val myNick: String? = "", val myNick: String? = "",
val latency: Int = 0, val latency: Int = 0,
...@@ -57,4 +61,37 @@ data class NetworkState( ...@@ -57,4 +61,37 @@ data class NetworkState(
val codecForServer: String = "UTF_8", val codecForServer: String = "UTF_8",
val codecForEncoding: String = "UTF_8", val codecForEncoding: String = "UTF_8",
val codecForDecoding: 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
}
}
/*
* 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"
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment