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
|
// Index of the last visible tab in the tab bar
|
||||||
LastTabIndexInTabBar int
|
LastTabIndexInTabBar int
|
||||||
|
|
||||||
|
// The acknowledged capabilities
|
||||||
|
EnabledCapabilities Capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Capabilities map[string]string
|
||||||
|
|
||||||
type MsgFmtOpts struct {
|
type MsgFmtOpts struct {
|
||||||
WithTimestamp bool
|
WithTimestamp bool
|
||||||
AsServerMsg bool
|
AsServerMsg bool
|
||||||
@@ -138,6 +143,7 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
|
|||||||
s.Port = port
|
s.Port = port
|
||||||
s.ActiveChannel = host
|
s.ActiveChannel = host
|
||||||
s.Channels = make([]Channel, 0)
|
s.Channels = make([]Channel, 0)
|
||||||
|
s.EnabledCapabilities = make(Capabilities, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +162,7 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
|
|||||||
s.Port = port
|
s.Port = port
|
||||||
s.ActiveChannel = host
|
s.ActiveChannel = host
|
||||||
s.Channels = make([]Channel, 0)
|
s.Channels = make([]Channel, 0)
|
||||||
|
s.EnabledCapabilities = make(Capabilities, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Register(nick string, password string, channel string) {
|
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),
|
Users: make(map[string]User),
|
||||||
})
|
})
|
||||||
|
|
||||||
c.SendCommand(commands.CAP, "LS")
|
c.SendCommand(commands.CAP, "LS", "302")
|
||||||
if password != "" {
|
if password != "" {
|
||||||
c.SendCommand(commands.PASS, password)
|
c.SendCommand(commands.PASS, password)
|
||||||
}
|
}
|
||||||
// TODO: check if nickname has spaces and remove them
|
|
||||||
c.SendCommand(commands.NICK, nick)
|
c.SendCommand(commands.NICK, nick)
|
||||||
// set user-wanted nickname
|
// set user-wanted nickname
|
||||||
c.Nickname = nick
|
c.Nickname = nick
|
||||||
c.SendCommand(commands.USER, nick, "0", "*", 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
|
// joining a channel when registering is optional
|
||||||
if channel != "" {
|
if channel != "" {
|
||||||
c.SendCommand(commands.JOIN, channel)
|
c.SendCommand(commands.JOIN, channel)
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ const (
|
|||||||
RPL_MYINFO = "004" // RFC2812 - Implemented
|
RPL_MYINFO = "004" // RFC2812 - Implemented
|
||||||
RPL_ISUPPORT = "005" // ??????? - Not Implemented (TODO:)
|
RPL_ISUPPORT = "005" // ??????? - Not Implemented (TODO:)
|
||||||
RPL_BOUNCE = "010" // ??????? - 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_TRACELINK = "200" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:)
|
RPL_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:)
|
||||||
RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:)
|
RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:)
|
||||||
@@ -240,7 +241,7 @@ const (
|
|||||||
ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:)
|
ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
|
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_BADMASK = "415" // RFC2812 - 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_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
|
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
|
||||||
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
|
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
|
||||||
@@ -312,7 +313,7 @@ var Features = map[string]bool{
|
|||||||
"CHANLIMIT": true,
|
"CHANLIMIT": true,
|
||||||
"CHANMODES": true,
|
"CHANMODES": true,
|
||||||
"CHANNELLEN": true,
|
"CHANNELLEN": true,
|
||||||
"CHANTYPES": true,
|
"CHANTYPES": true, // Channel Types. Default is #. Available are #&
|
||||||
"CHARSET": true, // Deprecated but might still be used
|
"CHARSET": true, // Deprecated but might still be used
|
||||||
"CLIENTVER": true, // Deprecated but might still be used
|
"CLIENTVER": true, // Deprecated but might still be used
|
||||||
"CNOTICE": true,
|
"CNOTICE": true,
|
||||||
@@ -364,7 +365,42 @@ var Features = map[string]bool{
|
|||||||
"WHOX": true,
|
"WHOX": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages MUST end in CRLF and SHOULD be encoded and decoded using UTF-8 (with fallbacks such as Latin-1).
|
var Capabilities = map[string]bool{
|
||||||
// Names are casemapped, read the casemapping from the RPL_ISUPPORT the server sends when registration is completed.
|
// 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())
|
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) {
|
func handleWELCOME(msg parser.IRCMessage, client *irc.Client) {
|
||||||
nick := msg.Parameters[0]
|
nick := msg.Parameters[0]
|
||||||
welcomeMsg := msg.Parameters[1]
|
welcomeMsg := msg.Parameters[1]
|
||||||
@@ -467,6 +548,17 @@ func handleNOSUCHSERVER(msg parser.IRCMessage, client *irc.Client) {
|
|||||||
client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts)
|
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) {
|
func handleNONICKNAMEGIVEN(msg parser.IRCMessage, client *irc.Client) {
|
||||||
message := msg.Parameters[1]
|
message := msg.Parameters[1]
|
||||||
|
|
||||||
@@ -513,6 +605,8 @@ func HandleCommand(msg parser.IRCMessage, client *irc.Client) {
|
|||||||
handleQuit(msg, client)
|
handleQuit(msg, client)
|
||||||
case commands.PART:
|
case commands.PART:
|
||||||
handlePart(msg, client)
|
handlePart(msg, client)
|
||||||
|
case commands.CAP:
|
||||||
|
handleCAP(msg, client)
|
||||||
case commands.RPL_WELCOME:
|
case commands.RPL_WELCOME:
|
||||||
handleWELCOME(msg, client)
|
handleWELCOME(msg, client)
|
||||||
case commands.RPL_YOURHOST:
|
case commands.RPL_YOURHOST:
|
||||||
@@ -569,6 +663,8 @@ func HandleCommand(msg parser.IRCMessage, client *irc.Client) {
|
|||||||
handleWHOISMODES(msg, client)
|
handleWHOISMODES(msg, client)
|
||||||
case commands.ERR_NOSUCHSERVER:
|
case commands.ERR_NOSUCHSERVER:
|
||||||
handleNOSUCHSERVER(msg, client)
|
handleNOSUCHSERVER(msg, client)
|
||||||
|
case commands.ERR_UNKNOWNCOMMAND:
|
||||||
|
handleUNKNOWNCOMMAND(msg, client)
|
||||||
case commands.ERR_NONICKNAMEGIVEN:
|
case commands.ERR_NONICKNAMEGIVEN:
|
||||||
handleNONICKNAMEGIVEN(msg, client)
|
handleNONICKNAMEGIVEN(msg, client)
|
||||||
case commands.ERR_NEEDMOREPARAMS:
|
case commands.ERR_NEEDMOREPARAMS:
|
||||||
|
|||||||
+10
-1
@@ -18,6 +18,7 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -34,9 +35,17 @@ type IRCMessage struct {
|
|||||||
type Tags map[string]string
|
type Tags map[string]string
|
||||||
|
|
||||||
func (m *IRCMessage) setTimestamp() {
|
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()
|
now := time.Now()
|
||||||
m.Timestamp = fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
|
m.Timestamp = fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeCharacters(char rune) string {
|
func escapeCharacters(char rune) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user