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"
+}