mirror of
https://github.com/IllusionMan1212/gorc.git
synced 2026-10-24 02:12:33 +00:00
feat: handle CAP command and support a few capabilities
This commit is contained in:
+8
-4
@@ -82,8 +82,13 @@ type Client struct {
|
||||
|
||||
// Index of the last visible tab in the tab bar
|
||||
LastTabIndexInTabBar int
|
||||
|
||||
// The acknowledged capabilities
|
||||
EnabledCapabilities Capabilities
|
||||
}
|
||||
|
||||
type Capabilities map[string]string
|
||||
|
||||
type MsgFmtOpts struct {
|
||||
WithTimestamp bool
|
||||
AsServerMsg bool
|
||||
@@ -138,6 +143,7 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
|
||||
s.Port = port
|
||||
s.ActiveChannel = host
|
||||
s.Channels = make([]Channel, 0)
|
||||
s.EnabledCapabilities = make(Capabilities, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,6 +162,7 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
|
||||
s.Port = port
|
||||
s.ActiveChannel = host
|
||||
s.Channels = make([]Channel, 0)
|
||||
s.EnabledCapabilities = make(Capabilities, 0)
|
||||
}
|
||||
|
||||
func (c *Client) Register(nick string, password string, channel string) {
|
||||
@@ -164,17 +171,14 @@ func (c *Client) Register(nick string, password string, channel string) {
|
||||
Users: make(map[string]User),
|
||||
})
|
||||
|
||||
c.SendCommand(commands.CAP, "LS")
|
||||
c.SendCommand(commands.CAP, "LS", "302")
|
||||
if password != "" {
|
||||
c.SendCommand(commands.PASS, password)
|
||||
}
|
||||
// TODO: check if nickname has spaces and remove them
|
||||
c.SendCommand(commands.NICK, nick)
|
||||
// set user-wanted nickname
|
||||
c.Nickname = nick
|
||||
c.SendCommand(commands.USER, nick, "0", "*", nick)
|
||||
// TODO: CAP REQ :whatever capability the client recognizes and supports
|
||||
c.SendCommand(commands.CAP, "END")
|
||||
// joining a channel when registering is optional
|
||||
if channel != "" {
|
||||
c.SendCommand(commands.JOIN, channel)
|
||||
|
||||
@@ -116,6 +116,7 @@ const (
|
||||
RPL_MYINFO = "004" // RFC2812 - Implemented
|
||||
RPL_ISUPPORT = "005" // ??????? - Not Implemented (TODO:)
|
||||
RPL_BOUNCE = "010" // ??????? - Not Implemented (TODO:)
|
||||
RPL_REMOTEISUPPORT = "105" // ??????? - Not Implemented (TODO:) // Exact same as RPL_ISUPPORT but for remote servers
|
||||
RPL_TRACELINK = "200" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:)
|
||||
RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:)
|
||||
@@ -240,7 +241,7 @@ const (
|
||||
ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:)
|
||||
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Implemented
|
||||
ERR_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
|
||||
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
|
||||
@@ -312,7 +313,7 @@ var Features = map[string]bool{
|
||||
"CHANLIMIT": true,
|
||||
"CHANMODES": true,
|
||||
"CHANNELLEN": true,
|
||||
"CHANTYPES": 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,
|
||||
@@ -364,7 +365,42 @@ var Features = map[string]bool{
|
||||
"WHOX": true,
|
||||
}
|
||||
|
||||
// Messages MUST end in CRLF and SHOULD be encoded and decoded using UTF-8 (with fallbacks such as Latin-1).
|
||||
// Names are casemapped, read the casemapping from the RPL_ISUPPORT the server sends when registration is completed.
|
||||
var Capabilities = map[string]bool{
|
||||
// Twitch-specific capabilities
|
||||
"twitch.tv/membership": false,
|
||||
"twitch.tv/commands": false,
|
||||
"twitch.tv/tags": false,
|
||||
|
||||
// When receiving a numeric reply, client MUST be able to handle any number of parameters on a numeric reply.
|
||||
// Solanum-specific capabilities
|
||||
"solanum.chat/identify-msg": false,
|
||||
"solanum.chat/oper": false,
|
||||
"solanum.chat/realhost": false,
|
||||
|
||||
// IRCv3 capabilities
|
||||
"account-notify": false,
|
||||
"account-registration": false, // Draft
|
||||
"account-tag": false,
|
||||
"away-notify": false,
|
||||
"batch": false,
|
||||
"cap-notify": true,
|
||||
"channel-rename": false, // Draft
|
||||
"chathistory": false, // Draft
|
||||
"chghost": false,
|
||||
"echo-message": false,
|
||||
"event-playback": false, // Draft
|
||||
"extended-join": false,
|
||||
"extended-monitor": false,
|
||||
"invite-notify": false,
|
||||
"labeled-response": false,
|
||||
"message-tags": false,
|
||||
"metadata": false,
|
||||
"monitor": false,
|
||||
"multi-prefix": false,
|
||||
"multiline": false, // Draft
|
||||
"read-marker": false, // Draft
|
||||
"sasl": false,
|
||||
"server-time": true,
|
||||
"setname": false,
|
||||
"tls": false, // Deprecated
|
||||
"userhost-in-names": false,
|
||||
}
|
||||
|
||||
@@ -169,6 +169,87 @@ func handlePart(msg parser.IRCMessage, client *irc.Client) {
|
||||
client.Tea.Send(cmds.SwitchChannels())
|
||||
}
|
||||
|
||||
func handleCAP(msg parser.IRCMessage, client *irc.Client) {
|
||||
switch msg.Parameters[1] {
|
||||
case "LIST":
|
||||
fallthrough
|
||||
case "NEW":
|
||||
fallthrough
|
||||
case "LS":
|
||||
caps := strings.Split(msg.Parameters[2], " ")
|
||||
recognizedCaps := make([]irc.Capabilities, 0)
|
||||
capabilities := make(irc.Capabilities, 0)
|
||||
length := 0
|
||||
|
||||
for _, capability := range caps {
|
||||
// NOTE: not sure if I should store the cap value the server advertises or use my own
|
||||
parts := strings.Split(capability, "=")
|
||||
key := parts[0]
|
||||
value := ""
|
||||
if len(parts) > 1 {
|
||||
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
|
||||
}
|
||||
|
||||
capabilities[key] = value
|
||||
length += len(capability)
|
||||
} else {
|
||||
log.Println("Unknown capability:", capability)
|
||||
}
|
||||
}
|
||||
recognizedCaps = append(recognizedCaps, capabilities)
|
||||
|
||||
for _, capsList := range recognizedCaps {
|
||||
list := make([]string, 0)
|
||||
for key, value := range capsList {
|
||||
if len(value) > 0 {
|
||||
list = append(list, key+"="+value)
|
||||
} else {
|
||||
list = append(list, key)
|
||||
}
|
||||
}
|
||||
client.SendCommand(commands.CAP, "REQ", strings.Join(list, " "))
|
||||
}
|
||||
client.SendCommand(commands.CAP, "END")
|
||||
case "ACK":
|
||||
caps := strings.Split(msg.Parameters[2], " ")
|
||||
for _, capability := range caps {
|
||||
if capability[0] == '-' {
|
||||
delete(client.EnabledCapabilities, capability[1:])
|
||||
} else {
|
||||
parts := strings.Split(capability, "=")
|
||||
key := parts[0]
|
||||
value := ""
|
||||
if len(parts) > 1 {
|
||||
value = parts[1]
|
||||
}
|
||||
client.EnabledCapabilities[key] = value
|
||||
}
|
||||
}
|
||||
case "NAK":
|
||||
caps := strings.Split(msg.Parameters[2], " ")
|
||||
msgOpts := irc.MsgFmtOpts{
|
||||
WithTimestamp: true,
|
||||
AsServerMsg: true,
|
||||
}
|
||||
client.Channels[0].AppendMsg(msg.Timestamp, "Unrecognized capabilities: "+strings.Join(caps, " "), msgOpts)
|
||||
case "DEL":
|
||||
caps := strings.Split(msg.Parameters[2], " ")
|
||||
for _, capability := range caps {
|
||||
delete(client.EnabledCapabilities, capability)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleWELCOME(msg parser.IRCMessage, client *irc.Client) {
|
||||
nick := msg.Parameters[0]
|
||||
welcomeMsg := msg.Parameters[1]
|
||||
@@ -467,6 +548,17 @@ func handleNOSUCHSERVER(msg parser.IRCMessage, client *irc.Client) {
|
||||
client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts)
|
||||
}
|
||||
|
||||
func handleUNKNOWNCOMMAND(msg parser.IRCMessage, client *irc.Client) {
|
||||
message := msg.Parameters[1] + " " + msg.Parameters[2]
|
||||
|
||||
msgOpts := irc.MsgFmtOpts{
|
||||
WithTimestamp: true,
|
||||
AsServerMsg: true,
|
||||
}
|
||||
|
||||
client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts)
|
||||
}
|
||||
|
||||
func handleNONICKNAMEGIVEN(msg parser.IRCMessage, client *irc.Client) {
|
||||
message := msg.Parameters[1]
|
||||
|
||||
@@ -513,6 +605,8 @@ func HandleCommand(msg parser.IRCMessage, client *irc.Client) {
|
||||
handleQuit(msg, client)
|
||||
case commands.PART:
|
||||
handlePart(msg, client)
|
||||
case commands.CAP:
|
||||
handleCAP(msg, client)
|
||||
case commands.RPL_WELCOME:
|
||||
handleWELCOME(msg, client)
|
||||
case commands.RPL_YOURHOST:
|
||||
@@ -569,6 +663,8 @@ func HandleCommand(msg parser.IRCMessage, client *irc.Client) {
|
||||
handleWHOISMODES(msg, client)
|
||||
case commands.ERR_NOSUCHSERVER:
|
||||
handleNOSUCHSERVER(msg, client)
|
||||
case commands.ERR_UNKNOWNCOMMAND:
|
||||
handleUNKNOWNCOMMAND(msg, client)
|
||||
case commands.ERR_NONICKNAMEGIVEN:
|
||||
handleNONICKNAMEGIVEN(msg, client)
|
||||
case commands.ERR_NEEDMOREPARAMS:
|
||||
|
||||
+10
-1
@@ -18,6 +18,7 @@ package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -34,10 +35,18 @@ type IRCMessage struct {
|
||||
type Tags map[string]string
|
||||
|
||||
func (m *IRCMessage) setTimestamp() {
|
||||
// TODO: get timestamp from time tag
|
||||
if serverTime, ok := m.Tags["time"]; ok {
|
||||
t, err := time.Parse("2006-01-02T15:04:05.000Z", serverTime)
|
||||
if err != nil {
|
||||
// TODO: properly handle error
|
||||
log.Fatalln(err)
|
||||
}
|
||||
m.Timestamp = fmt.Sprintf("[%02d:%02d]", t.Local().Hour(), t.Local().Minute())
|
||||
} else {
|
||||
now := time.Now()
|
||||
m.Timestamp = fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
|
||||
}
|
||||
}
|
||||
|
||||
func escapeCharacters(char rune) string {
|
||||
switch char {
|
||||
|
||||
Reference in New Issue
Block a user