// Copyright (c) 2012-2014 Jeremy Latt // Copyright (c) 2014-2015 Edmund Huber // Copyright (c) 2016-2017 Daniel Oaks // released under the MIT license package irc import ( "strconv" "strings" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" ) var ( // DefaultChannelModes are enabled on brand new channels when they're created. // this can be overridden in the `channels` config, with the `default-modes` key DefaultChannelModes = modes.Modes{ modes.NoOutside, modes.OpOnlyTopic, } ) // applyUserModeChanges applies the given changes, and returns the applied changes. func (client *Client) applyUserModeChanges(force bool, changes modes.ModeChanges) modes.ModeChanges { applied := make(modes.ModeChanges, 0) for _, change := range changes { switch change.Mode { case modes.Bot, modes.Invisible, modes.WallOps, modes.UserRoleplaying, modes.Operator, modes.LocalOperator, modes.RegisteredOnly: switch change.Op { case modes.Add: if change.Mode == modes.Operator || change.Mode == modes.LocalOperator { client.server.stats.ChangeOperators(1) } if !force && (change.Mode == modes.Operator || change.Mode == modes.LocalOperator) { continue } if client.flags[change.Mode] { continue } if change.Mode == modes.Invisible { client.server.stats.ChangeInvisible(1) } client.flags[change.Mode] = true applied = append(applied, change) case modes.Remove: if !client.flags[change.Mode] { continue } if change.Mode == modes.Invisible { client.server.stats.ChangeInvisible(-1) } if change.Mode == modes.Operator || change.Mode == modes.LocalOperator { client.server.stats.ChangeOperators(-1) } delete(client.flags, change.Mode) applied = append(applied, change) } case modes.ServerNotice: if !client.flags[modes.Operator] { continue } var masks []sno.Mask if change.Op == modes.Add || change.Op == modes.Remove { var newArg string for _, char := range change.Arg { mask := sno.Mask(char) if sno.ValidMasks[mask] { masks = append(masks, mask) newArg += string(char) } } change.Arg = newArg } if change.Op == modes.Add { client.server.snomasks.AddMasks(client, masks...) applied = append(applied, change) } else if change.Op == modes.Remove { client.server.snomasks.RemoveMasks(client, masks...) applied = append(applied, change) } } // can't do anything to TLS mode } // return the changes we could actually apply return applied } // ParseDefaultChannelModes parses the `default-modes` line of the config func ParseDefaultChannelModes(config *Config) modes.Modes { if config.Channels.DefaultModes == nil { // not present in config, fall back to compile-time default return DefaultChannelModes } modeChangeStrings := strings.Split(strings.TrimSpace(*config.Channels.DefaultModes), " ") modeChanges, _ := ParseChannelModeChanges(modeChangeStrings...) defaultChannelModes := make(modes.Modes, 0) for _, modeChange := range modeChanges { if modeChange.Op == modes.Add { defaultChannelModes = append(defaultChannelModes, modeChange.Mode) } } return defaultChannelModes } // ParseChannelModeChanges returns the valid changes, and the list of unknown chars. func ParseChannelModeChanges(params ...string) (modes.ModeChanges, map[rune]bool) { changes := make(modes.ModeChanges, 0) unknown := make(map[rune]bool) op := modes.List if 0 < len(params) { modeArg := params[0] skipArgs := 1 for _, mode := range modeArg { if mode == '-' || mode == '+' { op = modes.ModeOp(mode) continue } change := modes.ModeChange{ Mode: modes.Mode(mode), Op: op, } // put arg into modechange if needed switch modes.Mode(mode) { case modes.BanMask, modes.ExceptMask, modes.InviteMask: if len(params) > skipArgs { change.Arg = params[skipArgs] skipArgs++ } else { change.Op = modes.List } case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: if len(params) > skipArgs { change.Arg = params[skipArgs] skipArgs++ } else { continue } case modes.Key, modes.UserLimit: // don't require value when removing if change.Op == modes.Add { if len(params) > skipArgs { change.Arg = params[skipArgs] skipArgs++ } else { continue } } } var isKnown bool for _, supportedMode := range modes.SupportedChannelModes { if rune(supportedMode) == mode { isKnown = true break } } for _, supportedMode := range modes.ChannelPrivModes { if rune(supportedMode) == mode { isKnown = true break } } if mode == rune(modes.Voice) { isKnown = true } if !isKnown { unknown[mode] = true continue } changes = append(changes, change) } } return changes, unknown } // ApplyChannelModeChanges applies a given set of mode changes. func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, changes modes.ModeChanges, rb *ResponseBuffer) modes.ModeChanges { // so we only output one warning for each list type when full listFullWarned := make(map[modes.Mode]bool) clientIsOp := channel.ClientIsAtLeast(client, modes.ChannelOperator) var alreadySentPrivError bool applied := make(modes.ModeChanges, 0) isListOp := func(change modes.ModeChange) bool { return (change.Op == modes.List) || (change.Arg == "") } hasPrivs := func(change modes.ModeChange) bool { if isSamode { return true } switch change.Mode { case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: // Admins can't give other people Admin or remove it from others if change.Mode == modes.ChannelAdmin { return false } if change.Op == modes.List { return true } cfarg, _ := CasefoldName(change.Arg) if change.Op == modes.Remove && cfarg == client.nickCasefolded { // "There is no restriction, however, on anyone `deopping' themselves" // return true } return channel.ClientIsAtLeast(client, change.Mode) case modes.BanMask: // #163: allow unprivileged users to list ban masks return clientIsOp || isListOp(change) default: return clientIsOp } } for _, change := range changes { if !hasPrivs(change) { if !alreadySentPrivError { alreadySentPrivError = true rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, client.t("You're not a channel operator")) } continue } switch change.Mode { case modes.BanMask, modes.ExceptMask, modes.InviteMask: if isListOp(change) { channel.ShowMaskList(client, change.Mode, rb) continue } // confirm mask looks valid mask, err := Casefold(change.Arg) if err != nil { continue } switch change.Op { case modes.Add: if channel.lists[change.Mode].Length() >= client.server.Limits().ChanListModes { if !listFullWarned[change.Mode] { rb.Add(nil, client.server.name, ERR_BANLISTFULL, client.Nick(), channel.Name(), change.Mode.String(), client.t("Channel list is full")) listFullWarned[change.Mode] = true } continue } channel.lists[change.Mode].Add(mask) applied = append(applied, change) case modes.Remove: channel.lists[change.Mode].Remove(mask) applied = append(applied, change) } case modes.UserLimit: switch change.Op { case modes.Add: val, err := strconv.ParseUint(change.Arg, 10, 64) if err == nil { channel.setUserLimit(val) applied = append(applied, change) } case modes.Remove: channel.setUserLimit(0) applied = append(applied, change) } case modes.Key: switch change.Op { case modes.Add: channel.setKey(change.Arg) case modes.Remove: channel.setKey("") } applied = append(applied, change) case modes.InviteOnly, modes.Moderated, modes.NoOutside, modes.OpOnlyTopic, modes.RegisteredOnly, modes.Secret, modes.ChanRoleplaying: if change.Op == modes.List { continue } already := channel.setMode(change.Mode, change.Op == modes.Add) if !already { applied = append(applied, change) } case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: if change.Op == modes.List { continue } change := channel.applyModeMemberNoMutex(client, change.Mode, change.Op, change.Arg, rb) if change != nil { applied = append(applied, *change) } } } return applied }