diff --git a/irc/client.go b/irc/client.go index 52785f1..40e1c5a 100644 --- a/irc/client.go +++ b/irc/client.go @@ -95,9 +95,13 @@ type Client struct { // The acknowledged capabilities EnabledCapabilities Capabilities + + // The features currently enabled for this client + EnabledFeatures Features } type Capabilities map[string]string +type Features map[string]string type MsgFmtOpts struct { WithTimestamp bool diff --git a/irc/commands/commands.go b/irc/commands/commands.go index 4272701..67b9bfa 100644 --- a/irc/commands/commands.go +++ b/irc/commands/commands.go @@ -58,7 +58,7 @@ const ( NOTICE = "NOTICE" // Send notices between users and to channels. // Operator Messages - KILL = "KILL" // CLose connection between a client and the server. IRC Operators only (do we need this ??) + KILL = "KILL" // Close connection between a client and the server. IRC Operators only (do we need this ??) // Optional Messages AWAY = "AWAY" // Indicate that the client (user) is away/afk/etc.. @@ -305,64 +305,67 @@ const ( /* --- RPL_ISUPPORT Parameters --- */ var Features = map[string]bool{ - "ACCEPT": true, + "ACCEPT": false, + // The AWAYLEN parameter indicates the maximum length for the of an AWAY command. + // If an AWAY has more characters than this parameter, it may be silently truncated by the server before being passed on to other clients. + // Clients MAY receive an AWAY that has more characters than this parameter "AWAYLEN": true, - "BOT": true, - "CALLERID": true, - "CASEMAPPING": true, - "CHANLIMIT": true, - "CHANMODES": true, - "CHANNELLEN": true, - "CHANTYPES": true, // Channel Types. Default is #. Available are #& - "CHARSET": true, // Deprecated but might still be used - "CLIENTVER": true, // Deprecated but might still be used - "CNOTICE": true, - "CPRIVMSG": true, - "DEAF": true, - "ELIST": true, - "ESILENCE": true, - "ETRACE": true, - "EXCEPTS": true, - "EXTBAN": true, - "FNC": true, // Deprecated but might still be used - "INVEX": true, - "KEYLEN": true, + "BOT": false, + "CALLERID": false, + "CASEMAPPING": false, + "CHANLIMIT": false, + "CHANMODES": false, + "CHANNELLEN": false, + "CHANTYPES": false, // Channel Types. Default is #. Available are #& + "CHARSET": false, // Deprecated but might still be used + "CLIENTVER": false, // Deprecated but might still be used + "CNOTICE": false, + "CPRIVMSG": false, + "DEAF": false, + "ELIST": false, + "ESILENCE": false, + "ETRACE": false, + "EXCEPTS": false, + "EXTBAN": false, + "FNC": false, // Deprecated but might still be used + "INVEX": false, + "KEYLEN": false, "KICKLEN": true, - "KNOCK": true, - "LINELEN": true, // Proposed - "MAP": true, // Deprecated but might still be used - "MAXBANS": true, // Deprecated but might still be used - "MAXCHANNELS": true, // Deprecated but might still be used - "MAXLIST": true, - "MAXNICKLEN": true, - "MAXPARA": true, // Deprecated but might still be used - "MAXTARGETS": true, - "METADATA": true, - "MODES": true, - "MONITOR": true, - "NAMESX": true, // Deprecated but might still be used + "KNOCK": false, + "LINELEN": false, // Proposed + "MAP": false, // Deprecated but might still be used + "MAXBANS": false, // Deprecated but might still be used + "MAXCHANNELS": false, // Deprecated but might still be used + "MAXLIST": false, + "MAXNICKLEN": false, + "MAXPARA": false, // Deprecated but might still be used + "MAXTARGETS": false, + "METADATA": false, + "MODES": false, + "MONITOR": false, + "NAMESX": false, // Deprecated but might still be used "NETWORK": true, "NICKLEN": true, - "OVERRIDE": true, - "PREFIX": true, - "SAFELIST": true, - "SECURELIST": true, - "SILENCE": true, - "SSL": true, // Deprecated but might still be used - "STARTTLS": true, // Deprecated but might still be used - "STATUSMSG": true, - "STD": true, // Deprecated but might still be used - "TARGMAX": true, + "OVERRIDE": false, + "PREFIX": false, + "SAFELIST": false, + "SECURELIST": false, + "SILENCE": false, + "SSL": false, // Deprecated but might still be used + "STARTTLS": false, // Deprecated but might still be used + "STATUSMSG": false, + "STD": false, // Deprecated but might still be used + "TARGMAX": false, "TOPICLEN": true, - "UHNAMES": true, // Deprecated but might still be used - "USERIP": true, - "USERLEN": true, // Proposed - "VBANLIST": true, // Deprecared but might still be used - "VLIST": true, - "WALLCHOPS": true, // Deprecated but might still be used - "WALLVOICES": true, // Deprecated but might still be used - "WATCH": true, - "WHOX": true, + "UHNAMES": false, // Deprecated but might still be used + "USERIP": false, + "USERLEN": false, // Proposed + "VBANLIST": false, // Deprecared but might still be used + "VLIST": false, + "WALLCHOPS": false, // Deprecated but might still be used + "WALLVOICES": false, // Deprecated but might still be used + "WATCH": false, + "WHOX": false, } var Capabilities = map[string]bool{ diff --git a/irc/handler/handler.go b/irc/handler/handler.go index 2f2ddb7..99c192c 100644 --- a/irc/handler/handler.go +++ b/irc/handler/handler.go @@ -35,6 +35,7 @@ import ( func ReadLoop(client *irc.Client) { // 512 bytes as a base + 8192 additional bytes for tags + // as specified here: https://modern.ircdocs.horse/#message-format r := bufio.NewReaderSize(client.TCPConn, 8192+512) for { @@ -235,18 +236,22 @@ func handleCAP(msg irc.Message, client *irc.Client) { value = parts[1] } - if commands.Capabilities[key] { - // When the final parameter approaches 510 bytes, - // we send multiple REQ commands - // (https://ircv3.net/specs/extensions/capability-negotiation.html#the-cap-req-subcommand) - if length+len(capability) >= 500 { - recognizedCaps = append(recognizedCaps, capabilities) - capabilities = make(irc.Capabilities, 0) - length = 0 - } + if capEnabled, ok := commands.Capabilities[key]; ok { + if capEnabled { + // When the final parameter approaches 510 bytes, + // we send multiple REQ commands + // (https://ircv3.net/specs/extensions/capability-negotiation.html#the-cap-req-subcommand) + if length+len(capability) >= 500 { + recognizedCaps = append(recognizedCaps, capabilities) + capabilities = make(irc.Capabilities, 0) + length = 0 + } - capabilities[key] = value - length += len(capability) + capabilities[key] = value + length += len(capability) + } else { + log.Println("Unsupported capability:", capability) + } } else { log.Println("Unknown capability:", capability) } @@ -266,6 +271,9 @@ func handleCAP(msg irc.Message, client *irc.Client) { } client.SendCommand(commands.CAP, "END") case "ACK": + if msg.Parameters[2] == "" { + return + } caps := strings.Split(msg.Parameters[2], " ") for _, capability := range caps { if capability[0] == '-' { @@ -342,6 +350,34 @@ func handleMYINFO(msg irc.Message, client *irc.Client) { client.Channels[0].AppendMsg(msg.Timestamp, info, msgOpts) } +func handleISUPPORT(msg irc.Message, client *irc.Client) { + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + for _, token := range msg.Parameters[1 : len(msg.Parameters)-1] { + splits := strings.Split(token, "=") + key := splits[0] + value := "" + if len(splits) > 1 { + value = splits[1] + } + + if supported, known := commands.Features[key]; known { + if supported { + client.EnabledFeatures[key] = value + } else { + log.Printf("Unsupported feature: \"%v\" with value: \"%v\"\n", key, value) + } + } else { + log.Printf("Unknown feature: \"%v\" with value: \"%v\"\n", key, value) + } + + client.Channels[0].AppendMsg(msg.Timestamp, token, msgOpts) + } +} + func handleLUSERCLIENT(msg irc.Message, client *irc.Client) { message := msg.Parameters[1] @@ -695,6 +731,8 @@ func HandleCommand(msg irc.Message, client *irc.Client) { handleCREATED(msg, client) case commands.RPL_MYINFO: handleMYINFO(msg, client) + case commands.RPL_ISUPPORT: + handleISUPPORT(msg, client) case commands.RPL_LUSERCLIENT: handleLUSERCLIENT(msg, client) case commands.RPL_LUSEROP: diff --git a/ui/mainscreen/mainscreen.go b/ui/mainscreen/mainscreen.go index afad6fe..0d9d79c 100644 --- a/ui/mainscreen/mainscreen.go +++ b/ui/mainscreen/mainscreen.go @@ -18,6 +18,7 @@ package mainscreen import ( "math" + "strconv" "strings" "github.com/charmbracelet/bubbles/textinput" @@ -194,6 +195,36 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) { cmdsToProcess = append(cmdsToProcess, cmd) case InputBox: s.InputBox, cmd = s.InputBox.Update(msg) + width := 0 + + command := strings.Split(s.InputBox.Input.Value(), " ")[0] + if len(command) > 0 && command[0] == '/' { + switch strings.ToUpper(command)[1:] { + case commands.AWAY: + w, err := strconv.ParseInt(s.Client.EnabledFeatures["AWAYLEN"], 10, 32) + if err == nil { + width = int(w) + } + case commands.TOPIC: + w, err := strconv.ParseInt(s.Client.EnabledFeatures["TOPICLEN"], 10, 32) + if err == nil { + width = int(w) + } + case commands.NICK: + w, err := strconv.ParseInt(s.Client.EnabledFeatures["NICKLEN"], 10, 32) + if err == nil { + width = int(w) + } + case commands.KICK: + w, err := strconv.ParseInt(s.Client.EnabledFeatures["KICKLEN"], 10, 32) + if err == nil { + width = int(w) + } + } + } + + s.InputBox.Input.CharLimit = width + cmdsToProcess = append(cmdsToProcess, cmd) }