Add MODE arguments support

- Add RPL_ISUPPORT support with CHANMODES, CHANTYPES, PREFIX parsing
- Add support for channel mode state with mode arguments
- Add upstream support for RPL_UMODEIS, RPL_CHANNELMODEIS
- Request channel MODE on upstream channel JOIN
- Use sane default channel mode and channel mode types
This commit is contained in:
delthas 2020-03-21 00:48:19 +01:00 committed by Simon Ser
parent b0ab43e5d8
commit 98a95e9955
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
4 changed files with 338 additions and 126 deletions

View File

@ -29,10 +29,7 @@ func forwardChannel(dc *downstreamConn, ch *upstreamChannel) {
// TODO: send multiple members in each message
for nick, membership := range ch.Members {
s := dc.marshalNick(ch.conn, nick)
if membership != 0 {
s = string(membership) + s
}
s := membership.String() + dc.marshalNick(ch.conn, nick)
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),

View File

@ -851,41 +851,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
modeStr = msg.Params[1]
}
uc, upstreamName, err := dc.unmarshalEntity(name)
if err != nil {
return err
}
if uc.isChannel(upstreamName) {
// TODO: handle MODE channel mode arguments
if modeStr != "" {
uc.SendMessage(&irc.Message{
Command: "MODE",
Params: []string{upstreamName, modeStr},
})
} else {
ch, ok := uc.channels[upstreamName]
if !ok {
return ircError{&irc.Message{
Command: irc.ERR_NOSUCHCHANNEL,
Params: []string{dc.nick, name, "No such channel"},
}}
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_CHANNELMODEIS,
Params: []string{dc.nick, name, string(ch.modes)},
})
}
} else {
if name != dc.nick {
return ircError{&irc.Message{
Command: irc.ERR_USERSDONTMATCH,
Params: []string{dc.nick, "Cannot change mode for other users"},
}}
}
if name == dc.nick {
if modeStr != "" {
dc.forEachUpstream(func(uc *upstreamConn) {
uc.SendMessage(&irc.Message{
@ -900,6 +866,52 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
Params: []string{dc.nick, ""}, // TODO
})
}
return nil
}
uc, upstreamName, err := dc.unmarshalEntity(name)
if err != nil {
return err
}
if !uc.isChannel(upstreamName) {
return ircError{&irc.Message{
Command: irc.ERR_USERSDONTMATCH,
Params: []string{dc.nick, "Cannot change mode for other users"},
}}
}
if modeStr != "" {
params := []string{upstreamName, modeStr}
params = append(params, msg.Params[2:]...)
uc.SendMessage(&irc.Message{
Command: "MODE",
Params: params,
})
} else {
ch, ok := uc.channels[upstreamName]
if !ok {
return ircError{&irc.Message{
Command: irc.ERR_NOSUCHCHANNEL,
Params: []string{dc.nick, name, "No such channel"},
}}
}
if ch.modes == nil {
// we haven't received the initial RPL_CHANNELMODEIS yet
// ignore the request, we will broadcast the modes later when we receive RPL_CHANNELMODEIS
return nil
}
modeStr, modeParams := ch.modes.Format()
params := []string{dc.nick, name, modeStr}
params = append(params, modeParams...)
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_CHANNELMODEIS,
Params: params,
})
}
case "WHO":
if len(msg.Params) == 0 {

138
irc.go
View File

@ -15,26 +15,26 @@ const (
err_invalidcapcmd = "410"
)
type modeSet string
type userModes string
func (ms modeSet) Has(c byte) bool {
func (ms userModes) Has(c byte) bool {
return strings.IndexByte(string(ms), c) >= 0
}
func (ms *modeSet) Add(c byte) {
func (ms *userModes) Add(c byte) {
if !ms.Has(c) {
*ms += modeSet(c)
*ms += userModes(c)
}
}
func (ms *modeSet) Del(c byte) {
func (ms *userModes) Del(c byte) {
i := strings.IndexByte(string(*ms), c)
if i >= 0 {
*ms = (*ms)[:i] + (*ms)[i+1:]
}
}
func (ms *modeSet) Apply(s string) error {
func (ms *userModes) Apply(s string) error {
var plusMinus byte
for i := 0; i < len(s); i++ {
switch c := s[i]; c {
@ -54,6 +54,94 @@ func (ms *modeSet) Apply(s string) error {
return nil
}
type channelModeType byte
// standard channel mode types, as explained in https://modern.ircdocs.horse/#mode-message
const (
// modes that add or remove an address to or from a list
modeTypeA channelModeType = iota
// modes that change a setting on a channel, and must always have a parameter
modeTypeB
// modes that change a setting on a channel, and must have a parameter when being set, and no parameter when being unset
modeTypeC
// modes that change a setting on a channel, and must not have a parameter
modeTypeD
)
var stdChannelModes = map[byte]channelModeType{
'b': modeTypeA, // ban list
'e': modeTypeA, // ban exception list
'I': modeTypeA, // invite exception list
'k': modeTypeB, // channel key
'l': modeTypeC, // channel user limit
'i': modeTypeD, // channel is invite-only
'm': modeTypeD, // channel is moderated
'n': modeTypeD, // channel has no external messages
's': modeTypeD, // channel is secret
't': modeTypeD, // channel has protected topic
}
type channelModes map[byte]string
func (cm channelModes) Apply(modeTypes map[byte]channelModeType, modeStr string, arguments ...string) error {
nextArgument := 0
var plusMinus byte
for i := 0; i < len(modeStr); i++ {
mode := modeStr[i]
if mode == '+' || mode == '-' {
plusMinus = mode
continue
}
if plusMinus != '+' && plusMinus != '-' {
return fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
}
mt, ok := modeTypes[mode]
if !ok {
continue
}
if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
if plusMinus == '+' {
var argument string
// some sentitive arguments (such as channel keys) can be omitted for privacy
// (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
if nextArgument < len(arguments) {
argument = arguments[nextArgument]
}
cm[mode] = argument
} else {
delete(cm, mode)
}
nextArgument++
} else if mt == modeTypeC || mt == modeTypeD {
if plusMinus == '+' {
cm[mode] = ""
} else {
delete(cm, mode)
}
}
}
return nil
}
func (cm channelModes) Format() (modeString string, parameters []string) {
var modesWithValues strings.Builder
var modesWithoutValues strings.Builder
parameters = make([]string, 0, 16)
for mode, value := range cm {
if value != "" {
modesWithValues.WriteString(string(mode))
parameters = append(parameters, value)
} else {
modesWithoutValues.WriteString(string(mode))
}
}
modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
return
}
const stdChannelTypes = "#&+!"
type channelStatus byte
const (
@ -74,32 +162,24 @@ func parseChannelStatus(s string) (channelStatus, error) {
}
}
type membership byte
const (
membershipFounder membership = '~'
membershipProtected membership = '&'
membershipOperator membership = '@'
membershipHalfOp membership = '%'
membershipVoice membership = '+'
)
const stdMembershipPrefixes = "~&@%+"
func (m membership) String() string {
if m == 0 {
return ""
}
return string(m)
type membership struct {
Mode byte
Prefix byte
}
func parseMembershipPrefix(s string) (prefix membership, nick string) {
// TODO: any prefix from PREFIX RPL_ISUPPORT
if strings.IndexByte(stdMembershipPrefixes, s[0]) >= 0 {
return membership(s[0]), s[1:]
} else {
return 0, s
var stdMemberships = []membership{
{'q', '~'}, // founder
{'a', '&'}, // protected
{'o', '@'}, // operator
{'h', '%'}, // halfop
{'v', '+'}, // voice
}
func (m *membership) String() string {
if m == nil {
return ""
}
return string(m.Prefix)
}
func parseMessageParams(msg *irc.Message, out ...*string) error {

View File

@ -21,8 +21,8 @@ type upstreamChannel struct {
TopicWho string
TopicTime time.Time
Status channelStatus
modes modeSet
Members map[string]membership
modes channelModes
Members map[string]*membership
complete bool
}
@ -38,15 +38,16 @@ type upstreamConn struct {
serverName string
availableUserModes string
availableChannelModes string
channelModesWithParam string
availableChannelModes map[byte]channelModeType
availableChannelTypes string
availableMemberships []membership
registered bool
nick string
username string
realname string
closed bool
modes modeSet
modes userModes
channels map[string]*upstreamChannel
caps map[string]string
@ -72,16 +73,19 @@ func connectToUpstream(network *network) (*upstreamConn, error) {
outgoing := make(chan *irc.Message, 64)
uc := &upstreamConn{
network: network,
logger: logger,
net: netConn,
irc: irc.NewConn(netConn),
srv: network.user.srv,
user: network.user,
outgoing: outgoing,
ring: NewRing(network.user.srv.RingCap),
channels: make(map[string]*upstreamChannel),
caps: make(map[string]string),
network: network,
logger: logger,
net: netConn,
irc: irc.NewConn(netConn),
srv: network.user.srv,
user: network.user,
outgoing: outgoing,
ring: NewRing(network.user.srv.RingCap),
channels: make(map[string]*upstreamChannel),
caps: make(map[string]string),
availableChannelTypes: stdChannelTypes,
availableChannelModes: stdChannelModes,
availableMemberships: stdMemberships,
}
go func() {
@ -130,17 +134,21 @@ func (uc *upstreamConn) getChannel(name string) (*upstreamChannel, error) {
}
func (uc *upstreamConn) isChannel(entity string) bool {
for _, r := range entity {
switch r {
// TODO: support upstream ISUPPORT channel prefixes
case '#', '&', '+', '!':
return true
}
break
if i := strings.IndexByte(uc.availableChannelTypes, entity[0]); i >= 0 {
return true
}
return false
}
func (uc *upstreamConn) parseMembershipPrefix(s string) (membership *membership, nick string) {
for _, m := range uc.availableMemberships {
if m.Prefix == s[0] {
return &m, s[1:]
}
}
return nil, s
}
func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
switch msg.Command {
case "PING":
@ -149,35 +157,6 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
Params: msg.Params,
})
return nil
case "MODE":
var name, modeStr string
if err := parseMessageParams(msg, &name, &modeStr); err != nil {
return err
}
if !uc.isChannel(name) { // user mode change
if name != uc.nick {
return fmt.Errorf("received MODE message for unknown nick %q", name)
}
return uc.modes.Apply(modeStr)
} else { // channel mode change
// TODO: handle MODE channel mode arguments
ch, err := uc.getChannel(name)
if err != nil {
return err
}
if err := ch.modes.Apply(modeStr); err != nil {
return err
}
uc.forEachDownstream(func(dc *downstreamConn) {
dc.SendMessage(&irc.Message{
Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
Command: "MODE",
Params: []string{dc.marshalChannel(uc, name), modeStr},
})
})
}
case "NOTICE":
uc.logger.Print(msg)
@ -346,11 +325,67 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
})
}
case irc.RPL_MYINFO:
if err := parseMessageParams(msg, nil, &uc.serverName, nil, &uc.availableUserModes, &uc.availableChannelModes); err != nil {
if err := parseMessageParams(msg, nil, &uc.serverName, nil, &uc.availableUserModes, nil); err != nil {
return err
}
if len(msg.Params) > 5 {
uc.channelModesWithParam = msg.Params[5]
case irc.RPL_ISUPPORT:
if err := parseMessageParams(msg, nil, nil); err != nil {
return err
}
for _, token := range msg.Params[1 : len(msg.Params)-1] {
negate := false
parameter := token
value := ""
if strings.HasPrefix(token, "-") {
negate = true
token = token[1:]
} else {
if i := strings.IndexByte(token, '='); i >= 0 {
parameter = token[:i]
value = token[i+1:]
}
}
if !negate {
switch parameter {
case "CHANMODES":
parts := strings.SplitN(value, ",", 5)
if len(parts) < 4 {
return fmt.Errorf("malformed ISUPPORT CHANMODES value: %v", value)
}
modes := make(map[byte]channelModeType)
for i, mt := range []channelModeType{modeTypeA, modeTypeB, modeTypeC, modeTypeD} {
for j := 0; j < len(parts[i]); j++ {
mode := parts[i][j]
modes[mode] = mt
}
}
uc.availableChannelModes = modes
case "CHANTYPES":
uc.availableChannelTypes = value
case "PREFIX":
if value == "" {
uc.availableMemberships = nil
} else {
if value[0] != '(' {
return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value)
}
sep := strings.IndexByte(value, ')')
if sep < 0 || len(value) != sep*2 {
return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value)
}
memberships := make([]membership, len(value)/2-1)
for i := range memberships {
memberships[i] = membership{
Mode: value[i+1],
Prefix: value[sep+i+1],
}
}
uc.availableMemberships = memberships
}
}
} else {
// TODO: handle ISUPPORT negations
}
}
case "NICK":
if msg.Prefix == nil {
@ -399,14 +434,19 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
uc.channels[ch] = &upstreamChannel{
Name: ch,
conn: uc,
Members: make(map[string]membership),
Members: make(map[string]*membership),
}
uc.SendMessage(&irc.Message{
Command: "MODE",
Params: []string{ch},
})
} else {
ch, err := uc.getChannel(ch)
if err != nil {
return err
}
ch.Members[msg.Prefix.Name] = 0
ch.Members[msg.Prefix.Name] = nil
}
uc.forEachDownstream(func(dc *downstreamConn) {
@ -508,6 +548,89 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
Params: params,
})
})
case "MODE":
var name, modeStr string
if err := parseMessageParams(msg, &name, &modeStr); err != nil {
return err
}
if !uc.isChannel(name) { // user mode change
if name != uc.nick {
return fmt.Errorf("received MODE message for unknown nick %q", name)
}
return uc.modes.Apply(modeStr)
// TODO: notify downstreams about user mode change?
} else { // channel mode change
ch, err := uc.getChannel(name)
if err != nil {
return err
}
if ch.modes != nil {
if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[2:]...); err != nil {
return err
}
}
uc.forEachDownstream(func(dc *downstreamConn) {
params := []string{dc.marshalChannel(uc, name), modeStr}
params = append(params, msg.Params[2:]...)
dc.SendMessage(&irc.Message{
Prefix: dc.marshalUserPrefix(uc, msg.Prefix),
Command: "MODE",
Params: params,
})
})
}
case irc.RPL_UMODEIS:
if err := parseMessageParams(msg, nil); err != nil {
return err
}
modeStr := ""
if len(msg.Params) > 1 {
modeStr = msg.Params[1]
}
uc.modes = ""
if err := uc.modes.Apply(modeStr); err != nil {
return err
}
// TODO: send RPL_UMODEIS to downstream connections when applicable
case irc.RPL_CHANNELMODEIS:
var channel string
if err := parseMessageParams(msg, nil, &channel); err != nil {
return err
}
modeStr := ""
if len(msg.Params) > 2 {
modeStr = msg.Params[2]
}
ch, err := uc.getChannel(channel)
if err != nil {
return err
}
firstMode := ch.modes == nil
ch.modes = make(map[byte]string)
if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[3:]...); err != nil {
return err
}
if firstMode {
modeStr, modeParams := ch.modes.Format()
uc.forEachDownstream(func(dc *downstreamConn) {
params := []string{dc.nick, dc.marshalChannel(uc, channel), modeStr}
params = append(params, modeParams...)
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_CHANNELMODEIS,
Params: params,
})
})
}
case rpl_topicwhotime:
var name, who, timeStr string
if err := parseMessageParams(msg, nil, &name, &who, &timeStr); err != nil {
@ -540,7 +663,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
ch.Status = status
for _, s := range strings.Split(members, " ") {
membership, nick := parseMembershipPrefix(s)
membership, nick := uc.parseMembershipPrefix(s)
ch.Members[nick] = membership
}
case irc.RPL_ENDOFNAMES:
@ -679,7 +802,7 @@ func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
nick := dc.marshalNick(uc, nick)
channelList := make([]string, len(channels))
for i, channel := range channels {
prefix, channel := parseMembershipPrefix(channel)
prefix, channel := uc.parseMembershipPrefix(channel)
channel = dc.marshalChannel(uc, channel)
channelList[i] = prefix.String() + channel
}