mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-11 06:29:29 +01:00
initial UBAN implementation
This commit is contained in:
parent
64bc363cf1
commit
bb5276553d
@ -175,6 +175,17 @@ SET modifies a channel's settings. The following settings are available:`,
|
|||||||
enabled: chanregEnabled,
|
enabled: chanregEnabled,
|
||||||
minParams: 3,
|
minParams: 3,
|
||||||
},
|
},
|
||||||
|
"howtoban": {
|
||||||
|
handler: csHowToBanHandler,
|
||||||
|
helpShort: `$bHOWTOBAN$b suggests the best available way of banning a user`,
|
||||||
|
help: `Syntax: $bHOWTOBAN #channel <nick>
|
||||||
|
|
||||||
|
The best way to ban a user from a channel will depend on how they are
|
||||||
|
connected to the server. $bHOWTOBAN$b suggests a ban command that will
|
||||||
|
(ideally) prevent the user from returning to the channel.`,
|
||||||
|
enabled: chanregEnabled,
|
||||||
|
minParams: 2,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -809,3 +820,83 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
|
|||||||
service.Notice(rb, client.t("An error occurred"))
|
service.Notice(rb, client.t("An error occurred"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func csHowToBanHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
success := false
|
||||||
|
defer func() {
|
||||||
|
if success {
|
||||||
|
service.Notice(rb, client.t("Note that if the user is currently in the channel, you must /KICK them after you ban them"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
chname, nick := params[0], params[1]
|
||||||
|
channel := server.channels.Get(chname)
|
||||||
|
if channel == nil {
|
||||||
|
service.Notice(rb, client.t("No such channel"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !channel.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("samode") {
|
||||||
|
service.Notice(rb, client.t("Insufficient privileges"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var details WhoWas
|
||||||
|
target := server.clients.Get(nick)
|
||||||
|
if target == nil {
|
||||||
|
whowasList := server.whoWas.Find(nick, 1)
|
||||||
|
if len(whowasList) == 0 {
|
||||||
|
service.Notice(rb, client.t("No such nick"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
service.Notice(rb, fmt.Sprintf(client.t("Warning: %s is not currently connected to the server. Using WHOWAS data, which may be inaccurate:"), nick))
|
||||||
|
details = whowasList[0]
|
||||||
|
} else {
|
||||||
|
details = target.Details().WhoWas
|
||||||
|
}
|
||||||
|
|
||||||
|
if details.account != "" {
|
||||||
|
if channel.getAmode(details.account) != modes.Mode(0) {
|
||||||
|
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s currently has a persistent channel privilege granted with CS AMODE. If this mode is not removed, bans will not be respected"), details.accountName))
|
||||||
|
return
|
||||||
|
} else if details.account == channel.Founder() {
|
||||||
|
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s is the channel founder and cannot be banned"), details.accountName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := server.Config()
|
||||||
|
if !config.Server.Cloaks.EnabledForAlwaysOn {
|
||||||
|
service.Notice(rb, client.t("Warning: server.ip-cloaking.enabled-for-always-on is disabled. This reduces the precision of channel bans."))
|
||||||
|
}
|
||||||
|
|
||||||
|
if details.account != "" {
|
||||||
|
if config.Accounts.NickReservation.ForceNickEqualsAccount || target.AlwaysOn() {
|
||||||
|
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s is authenticated and can be banned by nickname: /MODE %[2]s +b %[3]s!*@*"), details.nick, channel.Name(), details.nick))
|
||||||
|
success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ban := fmt.Sprintf("*!*@%s", strings.ToLower(details.hostname))
|
||||||
|
banRe, err := utils.CompileGlob(ban, false)
|
||||||
|
if err != nil {
|
||||||
|
server.logger.Error("internal", "couldn't compile ban regex", ban, err.Error())
|
||||||
|
service.Notice(rb, "An error occurred")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var collateralDamage []string
|
||||||
|
for _, mcl := range channel.Members() {
|
||||||
|
if mcl != target && banRe.MatchString(mcl.NickMaskCasefolded()) {
|
||||||
|
collateralDamage = append(collateralDamage, mcl.Nick())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s can be banned by hostname: /MODE %[2]s +b %[3]s"), details.nick, channel.Name(), ban))
|
||||||
|
success = true
|
||||||
|
if len(collateralDamage) != 0 {
|
||||||
|
service.Notice(rb, fmt.Sprintf(client.t("Warning: this ban will affect %d other users:"), len(collateralDamage)))
|
||||||
|
for _, line := range utils.BuildTokenLines(400, collateralDamage, " ") {
|
||||||
|
service.Notice(rb, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -292,6 +292,9 @@ type WhoWas struct {
|
|||||||
username string
|
username string
|
||||||
hostname string
|
hostname string
|
||||||
realname string
|
realname string
|
||||||
|
// technically not required for WHOWAS:
|
||||||
|
account string
|
||||||
|
accountName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientDetails is a standard set of details about a client
|
// ClientDetails is a standard set of details about a client
|
||||||
@ -300,8 +303,6 @@ type ClientDetails struct {
|
|||||||
|
|
||||||
nickMask string
|
nickMask string
|
||||||
nickMaskCasefolded string
|
nickMaskCasefolded string
|
||||||
account string
|
|
||||||
accountName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunClient sets up a new client and runs its goroutine.
|
// RunClient sets up a new client and runs its goroutine.
|
||||||
|
@ -319,6 +319,11 @@ func init() {
|
|||||||
handler: topicHandler,
|
handler: topicHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
|
"UBAN": {
|
||||||
|
handler: ubanHandler,
|
||||||
|
minParams: 1,
|
||||||
|
capabs: []string{"ban"},
|
||||||
|
},
|
||||||
"UNDLINE": {
|
"UNDLINE": {
|
||||||
handler: unDLineHandler,
|
handler: unDLineHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
|
@ -209,6 +209,38 @@ func (cl *Limiter) RemoveClient(addr flatip.IP) {
|
|||||||
cl.limiter[addrString] = count
|
cl.limiter[addrString] = count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LimiterStatus struct {
|
||||||
|
Exempt bool
|
||||||
|
|
||||||
|
Count int
|
||||||
|
MaxCount int
|
||||||
|
|
||||||
|
Throttle int
|
||||||
|
MaxPerWindow int
|
||||||
|
ThrottleDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Limiter) Status(addr flatip.IP) (status LimiterStatus) {
|
||||||
|
cl.Lock()
|
||||||
|
defer cl.Unlock()
|
||||||
|
|
||||||
|
if flatip.IPInNets(addr, cl.config.exemptedNets) {
|
||||||
|
status.Exempt = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status.ThrottleDuration = cl.config.Window
|
||||||
|
|
||||||
|
addrString, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
|
||||||
|
status.MaxCount = maxConcurrent
|
||||||
|
status.MaxPerWindow = maxPerWindow
|
||||||
|
|
||||||
|
status.Count = cl.limiter[addrString]
|
||||||
|
status.Throttle = cl.throttler[addrString].Count
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ResetThrottle resets the throttle count for an IP
|
// ResetThrottle resets the throttle count for an IP
|
||||||
func (cl *Limiter) ResetThrottle(addr flatip.IP) {
|
func (cl *Limiter) ResetThrottle(addr flatip.IP) {
|
||||||
cl.Lock()
|
cl.Lock()
|
||||||
|
35
irc/dline.go
35
irc/dline.go
@ -6,13 +6,11 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/oragono/oragono/irc/flatip"
|
"github.com/oragono/oragono/irc/flatip"
|
||||||
"github.com/oragono/oragono/irc/utils"
|
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -48,7 +46,11 @@ func (info IPBanInfo) TimeLeft() string {
|
|||||||
|
|
||||||
// BanMessage returns the ban message.
|
// BanMessage returns the ban message.
|
||||||
func (info IPBanInfo) BanMessage(message string) string {
|
func (info IPBanInfo) BanMessage(message string) string {
|
||||||
message = fmt.Sprintf(message, info.Reason)
|
reason := info.Reason
|
||||||
|
if reason == "" {
|
||||||
|
reason = "No reason given"
|
||||||
|
}
|
||||||
|
message = fmt.Sprintf(message, reason)
|
||||||
if info.Duration != 0 {
|
if info.Duration != 0 {
|
||||||
message += fmt.Sprintf(" [%s]", info.TimeLeft())
|
message += fmt.Sprintf(" [%s]", info.TimeLeft())
|
||||||
}
|
}
|
||||||
@ -86,14 +88,14 @@ func (dm *DLineManager) AllBans() map[string]IPBanInfo {
|
|||||||
defer dm.RUnlock()
|
defer dm.RUnlock()
|
||||||
|
|
||||||
for key, info := range dm.networks {
|
for key, info := range dm.networks {
|
||||||
allb[key.String()] = info
|
allb[key.HumanReadableString()] = info
|
||||||
}
|
}
|
||||||
|
|
||||||
return allb
|
return allb
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddNetwork adds a network to the blocked list.
|
// AddNetwork adds a network to the blocked list.
|
||||||
func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, reason, operReason, operName string) error {
|
func (dm *DLineManager) AddNetwork(network flatip.IPNet, duration time.Duration, reason, operReason, operName string) error {
|
||||||
dm.persistenceMutex.Lock()
|
dm.persistenceMutex.Lock()
|
||||||
defer dm.persistenceMutex.Unlock()
|
defer dm.persistenceMutex.Unlock()
|
||||||
|
|
||||||
@ -110,8 +112,7 @@ func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, re
|
|||||||
return dm.persistDline(id, info)
|
return dm.persistDline(id, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (id flatip.IPNet) {
|
func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo) (id flatip.IPNet) {
|
||||||
flatnet := flatip.FromNetIPNet(network)
|
|
||||||
id = flatnet
|
id = flatnet
|
||||||
|
|
||||||
var timeLeft time.Duration
|
var timeLeft time.Duration
|
||||||
@ -193,11 +194,11 @@ func (dm *DLineManager) unpersistDline(id flatip.IPNet) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveNetwork removes a network from the blocked list.
|
// RemoveNetwork removes a network from the blocked list.
|
||||||
func (dm *DLineManager) RemoveNetwork(network net.IPNet) error {
|
func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error {
|
||||||
dm.persistenceMutex.Lock()
|
dm.persistenceMutex.Lock()
|
||||||
defer dm.persistenceMutex.Unlock()
|
defer dm.persistenceMutex.Unlock()
|
||||||
|
|
||||||
id := flatip.FromNetIPNet(network)
|
id := network
|
||||||
|
|
||||||
present := func() bool {
|
present := func() bool {
|
||||||
dm.Lock()
|
dm.Lock()
|
||||||
@ -215,22 +216,8 @@ func (dm *DLineManager) RemoveNetwork(network net.IPNet) error {
|
|||||||
return dm.unpersistDline(id)
|
return dm.unpersistDline(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddIP adds an IP address to the blocked list.
|
|
||||||
func (dm *DLineManager) AddIP(addr net.IP, duration time.Duration, reason, operReason, operName string) error {
|
|
||||||
return dm.AddNetwork(utils.NormalizeIPToNet(addr), duration, reason, operReason, operName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveIP removes an IP address from the blocked list.
|
|
||||||
func (dm *DLineManager) RemoveIP(addr net.IP) error {
|
|
||||||
return dm.RemoveNetwork(utils.NormalizeIPToNet(addr))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckIP returns whether or not an IP address was banned, and how long it is banned for.
|
// CheckIP returns whether or not an IP address was banned, and how long it is banned for.
|
||||||
func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) {
|
func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) {
|
||||||
if addr.IsLoopback() {
|
|
||||||
return // #671
|
|
||||||
}
|
|
||||||
|
|
||||||
dm.RLock()
|
dm.RLock()
|
||||||
defer dm.RUnlock()
|
defer dm.RUnlock()
|
||||||
|
|
||||||
@ -257,7 +244,7 @@ func (dm *DLineManager) loadFromDatastore() {
|
|||||||
key = strings.TrimPrefix(key, dlinePrefix)
|
key = strings.TrimPrefix(key, dlinePrefix)
|
||||||
|
|
||||||
// load addr/net
|
// load addr/net
|
||||||
hostNet, err := utils.NormalizedNetFromString(key)
|
hostNet, err := flatip.ParseToNormalizedNet(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dm.server.logger.Error("internal", "bad dline cidr", err.Error())
|
dm.server.logger.Error("internal", "bad dline cidr", err.Error())
|
||||||
return true
|
return true
|
||||||
|
@ -183,6 +183,16 @@ func (cidr IPNet) String() string {
|
|||||||
return ipnet.String()
|
return ipnet.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HumanReadableString returns a string representation of an IPNet;
|
||||||
|
// if the network contains only a single IP address, it returns
|
||||||
|
// a representation of that address.
|
||||||
|
func (cidr IPNet) HumanReadableString() string {
|
||||||
|
if cidr.PrefixLen == 128 {
|
||||||
|
return cidr.IP.String()
|
||||||
|
}
|
||||||
|
return cidr.String()
|
||||||
|
}
|
||||||
|
|
||||||
// IsZero tests whether ipnet is the zero value of an IPNet, 0::0/0.
|
// IsZero tests whether ipnet is the zero value of an IPNet, 0::0/0.
|
||||||
// Although this is a valid subnet, it can still be used as a sentinel
|
// Although this is a valid subnet, it can still be used as a sentinel
|
||||||
// value in some contexts.
|
// value in some contexts.
|
||||||
|
@ -314,6 +314,12 @@ func (client *Client) setCloakedHostname(cloak string) {
|
|||||||
client.updateNickMaskNoMutex()
|
client.updateNickMaskNoMutex()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) CloakedHostname() string {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
|
return client.cloakedHostname
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) historyCutoff() (cutoff time.Time) {
|
func (client *Client) historyCutoff() (cutoff time.Time) {
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
if client.account != "" {
|
if client.account != "" {
|
||||||
@ -553,3 +559,9 @@ func (channel *Channel) Ctime() (ctime time.Time) {
|
|||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
|
||||||
|
channel.stateMutex.RLock()
|
||||||
|
defer channel.stateMutex.RUnlock()
|
||||||
|
return channel.accountToUMode[cfaccount]
|
||||||
|
}
|
||||||
|
@ -906,7 +906,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
|||||||
operName = server.name
|
operName = server.name
|
||||||
}
|
}
|
||||||
|
|
||||||
err = server.dlines.AddNetwork(hostNet, duration, reason, operReason, operName)
|
err = server.dlines.AddNetwork(flatip.FromNetIPNet(hostNet), duration, reason, operReason, operName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rb.Notice(fmt.Sprintf(client.t("Could not successfully save new D-LINE: %s"), err.Error()))
|
rb.Notice(fmt.Sprintf(client.t("Could not successfully save new D-LINE: %s"), err.Error()))
|
||||||
@ -2833,13 +2833,8 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
// get host
|
// get host
|
||||||
hostString := msg.Params[0]
|
hostString := msg.Params[0]
|
||||||
|
|
||||||
// TODO(#1447) consolidate this into the "unban" command
|
|
||||||
if flatip, ipErr := flatip.ParseIP(hostString); ipErr == nil {
|
|
||||||
server.connectionLimiter.ResetThrottle(flatip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check host
|
// check host
|
||||||
hostNet, err := utils.NormalizedNetFromString(hostString)
|
hostNet, err := flatip.ParseToNormalizedNet(hostString)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("Could not parse IP address or CIDR network"))
|
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("Could not parse IP address or CIDR network"))
|
||||||
@ -2853,7 +2848,7 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
hostString = utils.NetToNormalizedString(hostNet)
|
hostString = hostNet.String()
|
||||||
rb.Notice(fmt.Sprintf(client.t("Removed D-Line for %s"), hostString))
|
rb.Notice(fmt.Sprintf(client.t("Removed D-Line for %s"), hostString))
|
||||||
server.snomasks.Send(sno.LocalXline, fmt.Sprintf(ircfmt.Unescape("%s$r removed D-Line for %s"), client.nick, hostString))
|
server.snomasks.Send(sno.LocalXline, fmt.Sprintf(ircfmt.Unescape("%s$r removed D-Line for %s"), client.nick, hostString))
|
||||||
return false
|
return false
|
||||||
|
13
irc/help.go
13
irc/help.go
@ -520,6 +520,19 @@ Shows the time of the current, or the given, server.`,
|
|||||||
|
|
||||||
If [topic] is given, sets the topic in the channel to that. If [topic] is not
|
If [topic] is given, sets the topic in the channel to that. If [topic] is not
|
||||||
given, views the current topic on the channel.`,
|
given, views the current topic on the channel.`,
|
||||||
|
},
|
||||||
|
"uban": {
|
||||||
|
text: `UBAN <subcommand> [arguments]
|
||||||
|
|
||||||
|
Oragono's "unified ban" system. Accepts the following subcommands:
|
||||||
|
|
||||||
|
1. UBAN ADD <target> [DURATION <duration>] [REASON...]
|
||||||
|
2. UBAN DEL <target>
|
||||||
|
3. UBAN LIST
|
||||||
|
4. UBAN INFO <target>
|
||||||
|
|
||||||
|
<target> may be an IP, a CIDR, a nickmask with wildcards, or the name of an
|
||||||
|
account to suspend.`,
|
||||||
},
|
},
|
||||||
"undline": {
|
"undline": {
|
||||||
oper: true,
|
oper: true,
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
11
irc/kline.go
11
irc/kline.go
@ -189,6 +189,17 @@ func (km *KLineManager) RemoveMask(mask string) error {
|
|||||||
return km.unpersistKLine(mask)
|
return km.unpersistKLine(mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (km *KLineManager) ContainsMask(mask string) (isBanned bool, info IPBanInfo) {
|
||||||
|
km.RLock()
|
||||||
|
defer km.RUnlock()
|
||||||
|
|
||||||
|
klineInfo, isBanned := km.entries[mask]
|
||||||
|
if isBanned {
|
||||||
|
info = klineInfo.Info
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// CheckMasks returns whether or not the hostmask(s) are banned, and how long they are banned for.
|
// CheckMasks returns whether or not the hostmask(s) are banned, and how long they are banned for.
|
||||||
func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info IPBanInfo) {
|
func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info IPBanInfo) {
|
||||||
km.RLock()
|
km.RLock()
|
||||||
|
@ -1361,11 +1361,16 @@ func (a ByCreationTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|||||||
func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) }
|
func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) }
|
||||||
|
|
||||||
func nsSuspendListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func nsSuspendListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
suspensions := server.accounts.ListSuspended()
|
listAccountSuspensions(client, rb, service.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listAccountSuspensions(client *Client, rb *ResponseBuffer, source string) {
|
||||||
|
suspensions := client.server.accounts.ListSuspended()
|
||||||
sort.Sort(ByCreationTime(suspensions))
|
sort.Sort(ByCreationTime(suspensions))
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("There are %d active suspensions."), len(suspensions)))
|
nick := client.Nick()
|
||||||
|
rb.Add(nil, source, "NOTICE", nick, fmt.Sprintf(client.t("There are %d active account suspensions."), len(suspensions)))
|
||||||
for _, suspension := range suspensions {
|
for _, suspension := range suspensions {
|
||||||
service.Notice(rb, suspensionToString(client, suspension))
|
rb.Add(nil, source, "NOTICE", nick, suspensionToString(client, suspension))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,8 +162,14 @@ func (server *Server) Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool) (banned bool, requireSASL bool, message string) {
|
func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool) (banned bool, requireSASL bool, message string) {
|
||||||
|
// #671: do not enforce bans against loopback, as a failsafe
|
||||||
|
// note that this function is not used for Tor connections (checkTorLimits is used instead)
|
||||||
|
if ipaddr.IsLoopback() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if server.Defcon() == 1 {
|
if server.Defcon() == 1 {
|
||||||
if !(ipaddr.IsLoopback() || utils.IPInNets(ipaddr, server.Config().Server.secureNets)) {
|
if !utils.IPInNets(ipaddr, server.Config().Server.secureNets) {
|
||||||
return true, false, "New connections to this server are temporarily restricted"
|
return true, false, "New connections to this server are temporarily restricted"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,7 +204,7 @@ func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool
|
|||||||
}
|
}
|
||||||
// TODO: currently no way to cache results other than IPBanned
|
// TODO: currently no way to cache results other than IPBanned
|
||||||
if output.Result == IPBanned && output.CacheSeconds != 0 {
|
if output.Result == IPBanned && output.CacheSeconds != 0 {
|
||||||
network, err := utils.NormalizedNetFromString(output.CacheNet)
|
network, err := flatip.ParseToNormalizedNet(output.CacheNet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.logger.Error("internal", "invalid dline net from IP ban script", ipaddr.String(), output.CacheNet)
|
server.logger.Error("internal", "invalid dline net from IP ban script", ipaddr.String(), output.CacheNet)
|
||||||
} else {
|
} else {
|
||||||
@ -339,12 +345,14 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
|||||||
// count new user in statistics (before checking KLINEs, see #1303)
|
// count new user in statistics (before checking KLINEs, see #1303)
|
||||||
server.stats.Register(c.HasMode(modes.Invisible))
|
server.stats.Register(c.HasMode(modes.Invisible))
|
||||||
|
|
||||||
// check KLINEs
|
// check KLINEs (#671: ignore KLINEs for loopback connections)
|
||||||
|
if !session.IP().IsLoopback() || session.isTor {
|
||||||
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
|
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
|
||||||
if isBanned {
|
if isBanned {
|
||||||
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
|
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
server.playRegistrationBurst(session)
|
server.playRegistrationBurst(session)
|
||||||
return false
|
return false
|
||||||
|
381
irc/uban.go
Normal file
381
irc/uban.go
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
// Copyright (c) 2021 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goshuirc/irc-go/ircmsg"
|
||||||
|
|
||||||
|
"github.com/oragono/oragono/irc/custime"
|
||||||
|
"github.com/oragono/oragono/irc/flatip"
|
||||||
|
"github.com/oragono/oragono/irc/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func consumeDuration(params []string, rb *ResponseBuffer) (duration time.Duration, remainingParams []string, err error) {
|
||||||
|
remainingParams = params
|
||||||
|
if 2 <= len(remainingParams) && strings.ToLower(remainingParams[0]) == "duration" {
|
||||||
|
duration, err = custime.ParseDuration(remainingParams[1])
|
||||||
|
if err != nil {
|
||||||
|
rb.Notice(rb.session.client.t("Invalid time duration for NS SUSPEND"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remainingParams = remainingParams[2:]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// a UBAN target is one of these syntactically unambiguous entities:
|
||||||
|
// an IP, a CIDR, a NUH mask, or an account name
|
||||||
|
type ubanType uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
ubanCIDR ubanType = iota
|
||||||
|
ubanNickmask
|
||||||
|
ubanNick
|
||||||
|
)
|
||||||
|
|
||||||
|
// tagged union, i guess
|
||||||
|
type ubanTarget struct {
|
||||||
|
banType ubanType
|
||||||
|
|
||||||
|
cidr flatip.IPNet
|
||||||
|
matcher *regexp.Regexp
|
||||||
|
nickOrMask string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUbanTarget(param string) (target ubanTarget, err error) {
|
||||||
|
if utils.SafeErrorParam(param) == "*" {
|
||||||
|
err = errInvalidParams
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ipnet, ipErr := flatip.ParseToNormalizedNet(param)
|
||||||
|
if ipErr == nil {
|
||||||
|
target.banType = ubanCIDR
|
||||||
|
target.cidr = ipnet
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.IndexByte(param, '!') != -1 || strings.IndexByte(param, '@') != -1 {
|
||||||
|
canonicalized, cErr := CanonicalizeMaskWildcard(param)
|
||||||
|
if cErr != nil {
|
||||||
|
err = errInvalidParams
|
||||||
|
return
|
||||||
|
}
|
||||||
|
re, reErr := utils.CompileGlob(canonicalized, false)
|
||||||
|
if reErr != nil {
|
||||||
|
err = errInvalidParams
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target.banType = ubanNickmask
|
||||||
|
target.nickOrMask = canonicalized
|
||||||
|
target.matcher = re
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, cErr := CasefoldName(param); cErr == nil {
|
||||||
|
target.banType = ubanNick
|
||||||
|
target.nickOrMask = param
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errInvalidParams
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UBAN <subcommand> [target] [DURATION <duration>] [reason...]
|
||||||
|
func ubanHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
|
subcommand := strings.ToLower(msg.Params[0])
|
||||||
|
params := msg.Params[1:]
|
||||||
|
var target ubanTarget
|
||||||
|
if subcommand != "list" {
|
||||||
|
if len(msg.Params) == 1 {
|
||||||
|
rb.Add(nil, client.server.name, "FAIL", "UBAN", "INVALID_PARAMS", client.t("Not enough parameters"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var parseErr error
|
||||||
|
target, parseErr = parseUbanTarget(params[0])
|
||||||
|
if parseErr != nil {
|
||||||
|
rb.Add(nil, client.server.name, "FAIL", "UBAN", "INVALID_PARAMS", client.t("Couldn't parse ban target"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
params = params[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch subcommand {
|
||||||
|
case "add":
|
||||||
|
return ubanAddHandler(client, target, params, rb)
|
||||||
|
case "del", "remove", "rm":
|
||||||
|
return ubanDelHandler(client, target, params, rb)
|
||||||
|
case "list":
|
||||||
|
return ubanListHandler(client, params, rb)
|
||||||
|
case "info":
|
||||||
|
return ubanInfoHandler(client, target, params, rb)
|
||||||
|
default:
|
||||||
|
rb.Add(nil, server.name, "FAIL", "UBAN", "UNKNOWN_COMMAND", client.t("Unknown command"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionsForCIDR(server *Server, cidr flatip.IPNet, exclude *Session) (sessions []*Session, nicks []string) {
|
||||||
|
for _, client := range server.clients.AllClients() {
|
||||||
|
for _, session := range client.Sessions() {
|
||||||
|
seen := false
|
||||||
|
if session != exclude && cidr.Contains(flatip.FromNetIP(session.IP())) {
|
||||||
|
sessions = append(sessions, session)
|
||||||
|
if !seen {
|
||||||
|
seen = true
|
||||||
|
nicks = append(nicks, session.client.Nick())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanAddHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
|
||||||
|
duration, params, err := consumeDuration(params, rb)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
operReason := strings.Join(params, " ")
|
||||||
|
|
||||||
|
switch target.banType {
|
||||||
|
case ubanCIDR:
|
||||||
|
ubanAddCIDR(client, target, duration, operReason, rb)
|
||||||
|
case ubanNickmask:
|
||||||
|
ubanAddNickmask(client, target, duration, operReason, rb)
|
||||||
|
case ubanNick:
|
||||||
|
account := target.nickOrMask
|
||||||
|
err := client.server.accounts.Suspend(account, duration, client.Oper().Name, "UBAN")
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Successfully suspended account %s"), account))
|
||||||
|
case errAccountDoesNotExist:
|
||||||
|
rb.Notice(client.t("No such account"))
|
||||||
|
default:
|
||||||
|
rb.Notice(client.t("An error occurred"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanAddCIDR(client *Client, target ubanTarget, duration time.Duration, operReason string, rb *ResponseBuffer) {
|
||||||
|
err := client.server.dlines.AddNetwork(target.cidr, duration, "", operReason, client.Oper().Name)
|
||||||
|
if err == nil {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Successfully added UBAN for %s"), target.cidr.HumanReadableString()))
|
||||||
|
} else {
|
||||||
|
client.server.logger.Error("internal", "ubanAddCIDR failed", err.Error())
|
||||||
|
rb.Notice(client.t("An error occurred"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions, nicks := sessionsForCIDR(client.server, target.cidr, rb.session)
|
||||||
|
for _, session := range sessions {
|
||||||
|
session.client.Quit("You have been banned from this server", session)
|
||||||
|
session.client.destroy(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sessions) != 0 {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Killed %[1]d active client(s) from %[2]s, associated with %[3]d nickname(s):"), len(sessions), target.cidr.String(), len(nicks)))
|
||||||
|
for _, line := range utils.BuildTokenLines(400, nicks, " ") {
|
||||||
|
rb.Notice(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanAddNickmask(client *Client, target ubanTarget, duration time.Duration, operReason string, rb *ResponseBuffer) {
|
||||||
|
err := client.server.klines.AddMask(target.nickOrMask, duration, "", operReason, client.Oper().Name)
|
||||||
|
if err == nil {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Successfully added UBAN for %s"), target.nickOrMask))
|
||||||
|
} else {
|
||||||
|
client.server.logger.Error("internal", "ubanAddNickmask failed", err.Error())
|
||||||
|
rb.Notice(client.t("An error occurred"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var killed []string
|
||||||
|
var alwaysOn []string
|
||||||
|
for _, mcl := range client.server.clients.AllClients() {
|
||||||
|
if mcl != client && target.matcher.MatchString(client.NickMaskCasefolded()) {
|
||||||
|
if !mcl.AlwaysOn() {
|
||||||
|
killed = append(killed, mcl.Nick())
|
||||||
|
mcl.destroy(nil)
|
||||||
|
} else {
|
||||||
|
alwaysOn = append(alwaysOn, mcl.Nick())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(killed) != 0 {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Killed %d clients:"), len(killed)))
|
||||||
|
for _, line := range utils.BuildTokenLines(400, killed, " ") {
|
||||||
|
rb.Notice(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(alwaysOn) != 0 {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Warning: %d clients matched this rule, but were not killed due to being always-on:"), len(alwaysOn)))
|
||||||
|
for _, line := range utils.BuildTokenLines(400, alwaysOn, " ") {
|
||||||
|
rb.Notice(line)
|
||||||
|
}
|
||||||
|
rb.Notice(client.t("You can suspend their accounts instead; try /UBAN ADD <nickname>"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanDelHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
|
||||||
|
var err error
|
||||||
|
var targetString string
|
||||||
|
switch target.banType {
|
||||||
|
case ubanCIDR:
|
||||||
|
if target.cidr.PrefixLen == 128 {
|
||||||
|
client.server.connectionLimiter.ResetThrottle(target.cidr.IP)
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Reset throttle for IP: %s"), target.cidr.IP.String()))
|
||||||
|
}
|
||||||
|
targetString = target.cidr.HumanReadableString()
|
||||||
|
err = client.server.dlines.RemoveNetwork(target.cidr)
|
||||||
|
case ubanNickmask:
|
||||||
|
targetString = target.nickOrMask
|
||||||
|
err = client.server.klines.RemoveMask(target.nickOrMask)
|
||||||
|
case ubanNick:
|
||||||
|
targetString = target.nickOrMask
|
||||||
|
err = client.server.accounts.Unsuspend(target.nickOrMask)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Successfully removed ban on %s"), targetString))
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Could not remove ban: %v"), err))
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanListHandler(client *Client, params []string, rb *ResponseBuffer) bool {
|
||||||
|
allDlines := client.server.dlines.AllBans()
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("There are %d active IP/network ban(s) (DLINEs)"), len(allDlines)))
|
||||||
|
for key, info := range allDlines {
|
||||||
|
rb.Notice(formatBanForListing(client, key, info))
|
||||||
|
}
|
||||||
|
rb.Notice(client.t("Some IPs may also be prevented from connecting by the connection limiter and/or throttler"))
|
||||||
|
|
||||||
|
allKlines := client.server.klines.AllBans()
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("There are %d active ban(s) on nick-user-host masks (KLINEs)"), len(allKlines)))
|
||||||
|
for key, info := range allKlines {
|
||||||
|
rb.Notice(formatBanForListing(client, key, info))
|
||||||
|
}
|
||||||
|
|
||||||
|
listAccountSuspensions(client, rb, client.server.name)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanInfoHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
|
||||||
|
switch target.banType {
|
||||||
|
case ubanCIDR:
|
||||||
|
ubanInfoCIDR(client, target, rb)
|
||||||
|
case ubanNickmask:
|
||||||
|
ubanInfoNickmask(client, target, rb)
|
||||||
|
case ubanNick:
|
||||||
|
ubanInfoNick(client, target, rb)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanInfoCIDR(client *Client, target ubanTarget, rb *ResponseBuffer) {
|
||||||
|
if target.cidr.PrefixLen == 128 {
|
||||||
|
status := client.server.connectionLimiter.Status(target.cidr.IP)
|
||||||
|
str := target.cidr.IP.String()
|
||||||
|
if status.Exempt {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("IP %s is exempt from connection limits"), str))
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("IP %[1]s has %[2]d active connections out of a maximum of %[3]d"), str, status.Count, status.MaxCount))
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("IP %[1]s has had %[2]d connection attempts in the past %[3]v, out of a maximum of %[4]d"), str, status.Throttle, status.ThrottleDuration, status.MaxPerWindow))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str := target.cidr.HumanReadableString()
|
||||||
|
isBanned, banInfo := client.server.dlines.CheckIP(target.cidr.IP)
|
||||||
|
if isBanned {
|
||||||
|
rb.Notice(formatBanForListing(client, str, banInfo))
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("There is no active IP ban against %s"), str))
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions, nicks := sessionsForCIDR(client.server, target.cidr, nil)
|
||||||
|
if len(sessions) != 0 {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("There are %[1]d active client(s) from %[2]s, associated with %[3]d nickname(s):"), len(sessions), target.cidr.String(), len(nicks)))
|
||||||
|
for _, line := range utils.BuildTokenLines(400, nicks, " ") {
|
||||||
|
rb.Notice(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanInfoNickmask(client *Client, target ubanTarget, rb *ResponseBuffer) {
|
||||||
|
isBanned, info := client.server.klines.ContainsMask(target.nickOrMask)
|
||||||
|
if isBanned {
|
||||||
|
rb.Notice(formatBanForListing(client, target.nickOrMask, info))
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("No ban exists for %[1]s"), target.nickOrMask))
|
||||||
|
}
|
||||||
|
|
||||||
|
affectedCount := 0
|
||||||
|
alwaysOnCount := 0
|
||||||
|
for _, mcl := range client.server.clients.AllClients() {
|
||||||
|
matches := false
|
||||||
|
for _, mask := range mcl.AllNickmasks() {
|
||||||
|
if target.matcher.MatchString(mask) {
|
||||||
|
matches = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches {
|
||||||
|
if mcl.AlwaysOn() {
|
||||||
|
alwaysOnCount++
|
||||||
|
} else {
|
||||||
|
affectedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Adding this mask would affect %[1]d clients (an additional %[2]d clients are exempt due to always-on)"), affectedCount, alwaysOnCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubanInfoNick(client *Client, target ubanTarget, rb *ResponseBuffer) {
|
||||||
|
mcl := client.server.clients.Get(target.nickOrMask)
|
||||||
|
if mcl != nil {
|
||||||
|
details := mcl.Details()
|
||||||
|
if details.account == "" {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is unauthenticated and connected from %[2]s"), details.nick, client.IP().String()))
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is logged into account %[2]s and has %[3]d active clients (see /NICKSERV CLIENTS LIST %[4]s for more info"), details.nick, details.accountName, len(mcl.Sessions()), details.nick))
|
||||||
|
ip := client.IP()
|
||||||
|
if !ip.IsLoopback() {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is associated with IP %[2]s; you can ban this IP with /UBAN ADD"), details.nick, ip.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("No client is currently using that nickname")))
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := client.server.accounts.LoadAccount(target.nickOrMask)
|
||||||
|
if err != nil {
|
||||||
|
if err == errAccountDoesNotExist {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("There is no account registered for %s"), target.nickOrMask))
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Couldn't load account: %v"), err.Error()))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if account.Verified {
|
||||||
|
if account.Suspended == nil {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Account %[1]s is in good standing; see /NICKSERV INFO %[2]s for more details"), target.nickOrMask, target.nickOrMask))
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Account %[1]s has been suspended: %[2]s"), target.nickOrMask, suspensionToString(client, *account.Suspended)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rb.Notice(fmt.Sprintf(client.t("Account %[1]s was created, but has not been verified"), target.nickOrMask))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user