mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-22 11:59:40 +01:00
Merge pull request #499 from slingamn/prefs.11
user preference system plus two small fixes
This commit is contained in:
commit
6291a44350
213
irc/accounts.go
213
irc/accounts.go
@ -29,7 +29,7 @@ const (
|
||||
keyAccountRegTime = "account.registered.time %s"
|
||||
keyAccountCredentials = "account.credentials %s"
|
||||
keyAccountAdditionalNicks = "account.additionalnicks %s"
|
||||
keyAccountEnforcement = "account.customenforcement %s"
|
||||
keyAccountSettings = "account.settings %s"
|
||||
keyAccountVHost = "account.vhost %s"
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
keyAccountChannels = "account.channels %s"
|
||||
@ -55,14 +55,14 @@ type AccountManager struct {
|
||||
accountToClients map[string][]*Client
|
||||
nickToAccount map[string]string
|
||||
skeletonToAccount map[string]string
|
||||
accountToMethod map[string]NickReservationMethod
|
||||
accountToMethod map[string]NickEnforcementMethod
|
||||
}
|
||||
|
||||
func (am *AccountManager) Initialize(server *Server) {
|
||||
am.accountToClients = make(map[string][]*Client)
|
||||
am.nickToAccount = make(map[string]string)
|
||||
am.skeletonToAccount = make(map[string]string)
|
||||
am.accountToMethod = make(map[string]NickReservationMethod)
|
||||
am.accountToMethod = make(map[string]NickEnforcementMethod)
|
||||
am.server = server
|
||||
|
||||
am.buildNickToAccountIndex()
|
||||
@ -76,7 +76,7 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
||||
|
||||
nickToAccount := make(map[string]string)
|
||||
skeletonToAccount := make(map[string]string)
|
||||
accountToMethod := make(map[string]NickReservationMethod)
|
||||
accountToMethod := make(map[string]NickEnforcementMethod)
|
||||
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
||||
|
||||
am.serialCacheUpdateMutex.Lock()
|
||||
@ -109,12 +109,16 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
||||
}
|
||||
}
|
||||
|
||||
if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil {
|
||||
method, err := nickReservationFromString(methodStr)
|
||||
if err == nil {
|
||||
accountToMethod[account] = method
|
||||
if rawPrefs, err := tx.Get(fmt.Sprintf(keyAccountSettings, account)); err == nil {
|
||||
var prefs AccountSettings
|
||||
err := json.Unmarshal([]byte(rawPrefs), &prefs)
|
||||
if err == nil && prefs.NickEnforcement != NickEnforcementOptional {
|
||||
accountToMethod[account] = prefs.NickEnforcement
|
||||
} else {
|
||||
am.server.logger.Error("internal", "corrupt account creds", account)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return err
|
||||
@ -180,36 +184,44 @@ func (am *AccountManager) NickToAccount(nick string) string {
|
||||
return am.nickToAccount[cfnick]
|
||||
}
|
||||
|
||||
// given an account, combine stored enforcement method with the config settings
|
||||
// to compute the actual enforcement method
|
||||
func configuredEnforcementMethod(config *Config, storedMethod NickEnforcementMethod) (result NickEnforcementMethod) {
|
||||
if !config.Accounts.NickReservation.Enabled {
|
||||
return NickEnforcementNone
|
||||
}
|
||||
result = storedMethod
|
||||
// if they don't have a custom setting, or customization is disabled, use the default
|
||||
if result == NickEnforcementOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
|
||||
result = config.Accounts.NickReservation.Method
|
||||
}
|
||||
if result == NickEnforcementOptional {
|
||||
// enforcement was explicitly enabled neither in the config or by the user
|
||||
result = NickEnforcementNone
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
|
||||
// used to enforce ownership.
|
||||
func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickReservationMethod) {
|
||||
func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickEnforcementMethod) {
|
||||
config := am.server.Config()
|
||||
if !config.Accounts.NickReservation.Enabled {
|
||||
return "", NickReservationNone
|
||||
return "", NickEnforcementNone
|
||||
}
|
||||
|
||||
am.RLock()
|
||||
defer am.RUnlock()
|
||||
|
||||
// given an account, combine stored enforcement method with the config settings
|
||||
// to compute the actual enforcement method
|
||||
finalEnforcementMethod := func(account_ string) (result NickReservationMethod) {
|
||||
result = am.accountToMethod[account_]
|
||||
// if they don't have a custom setting, or customization is disabled, use the default
|
||||
if result == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
|
||||
result = config.Accounts.NickReservation.Method
|
||||
}
|
||||
if result == NickReservationOptional {
|
||||
// enforcement was explicitly enabled neither in the config or by the user
|
||||
result = NickReservationNone
|
||||
}
|
||||
return
|
||||
finalEnforcementMethod := func(account_ string) (result NickEnforcementMethod) {
|
||||
storedMethod := am.accountToMethod[account_]
|
||||
return configuredEnforcementMethod(config, storedMethod)
|
||||
}
|
||||
|
||||
nickAccount := am.nickToAccount[cfnick]
|
||||
skelAccount := am.skeletonToAccount[skeleton]
|
||||
if nickAccount == "" && skelAccount == "" {
|
||||
return "", NickReservationNone
|
||||
return "", NickEnforcementNone
|
||||
} else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") {
|
||||
return nickAccount, finalEnforcementMethod(nickAccount)
|
||||
} else if skelAccount != "" && nickAccount == "" {
|
||||
@ -220,75 +232,47 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
|
||||
nickMethod := finalEnforcementMethod(nickAccount)
|
||||
skelMethod := finalEnforcementMethod(skelAccount)
|
||||
switch {
|
||||
case skelMethod == NickReservationNone:
|
||||
case skelMethod == NickEnforcementNone:
|
||||
return nickAccount, nickMethod
|
||||
case nickMethod == NickReservationNone:
|
||||
case nickMethod == NickEnforcementNone:
|
||||
return skelAccount, skelMethod
|
||||
default:
|
||||
// nobody can use this nick
|
||||
return "!", NickReservationStrict
|
||||
return "!", NickEnforcementStrict
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) BouncerAllowed(account string, session *Session) bool {
|
||||
// TODO stub
|
||||
config := am.server.Config()
|
||||
if !config.Accounts.Bouncer.Enabled {
|
||||
return false
|
||||
}
|
||||
if config.Accounts.Bouncer.AllowedByDefault {
|
||||
return true
|
||||
}
|
||||
return session != nil && session.capabilities.Has(caps.Bouncer)
|
||||
}
|
||||
|
||||
// Looks up the enforcement method stored in the database for an account
|
||||
// (typically you want EnforcementStatus instead, which respects the config)
|
||||
func (am *AccountManager) getStoredEnforcementStatus(account string) string {
|
||||
am.RLock()
|
||||
defer am.RUnlock()
|
||||
return nickReservationToString(am.accountToMethod[account])
|
||||
}
|
||||
|
||||
// Sets a custom enforcement method for an account and stores it in the database.
|
||||
func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) {
|
||||
func (am *AccountManager) SetEnforcementStatus(account string, method NickEnforcementMethod) (finalSettings AccountSettings, err error) {
|
||||
config := am.server.Config()
|
||||
if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
|
||||
return errFeatureDisabled
|
||||
err = errFeatureDisabled
|
||||
return
|
||||
}
|
||||
|
||||
var serialized string
|
||||
if method == NickReservationOptional {
|
||||
serialized = "" // normally this is "default", but we're going to delete the key
|
||||
} else {
|
||||
serialized = nickReservationToString(method)
|
||||
setter := func(in AccountSettings) (out AccountSettings, err error) {
|
||||
out = in
|
||||
out.NickEnforcement = method
|
||||
return out, nil
|
||||
}
|
||||
|
||||
key := fmt.Sprintf(keyAccountEnforcement, account)
|
||||
_, err = am.ModifyAccountSettings(account, setter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// this update of the data plane is racey, but it's probably fine
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
|
||||
currentMethod := am.accountToMethod[account]
|
||||
if method != currentMethod {
|
||||
if method == NickReservationOptional {
|
||||
delete(am.accountToMethod, account)
|
||||
} else {
|
||||
am.accountToMethod[account] = method
|
||||
}
|
||||
|
||||
return am.server.store.Update(func(tx *buntdb.Tx) (err error) {
|
||||
if serialized != "" {
|
||||
_, _, err = tx.Set(key, nickReservationToString(method), nil)
|
||||
} else {
|
||||
_, err = tx.Delete(key)
|
||||
}
|
||||
return
|
||||
})
|
||||
if method == NickEnforcementOptional {
|
||||
delete(am.accountToMethod, account)
|
||||
} else {
|
||||
am.accountToMethod[account] = method
|
||||
}
|
||||
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) AccountToClients(account string) (result []*Client) {
|
||||
@ -813,6 +797,12 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
|
||||
// pretend they have no vhost and move on
|
||||
}
|
||||
}
|
||||
if raw.Settings != "" {
|
||||
e := json.Unmarshal([]byte(raw.Settings), &result.Settings)
|
||||
if e != nil {
|
||||
am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -825,6 +815,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
|
||||
_, e := tx.Get(accountKey)
|
||||
if e == buntdb.ErrNotFound {
|
||||
@ -838,6 +829,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
result.Callback, _ = tx.Get(callbackKey)
|
||||
result.AdditionalNicks, _ = tx.Get(nicksKey)
|
||||
result.VHost, _ = tx.Get(vhostKey)
|
||||
result.Settings, _ = tx.Get(settingsKey)
|
||||
|
||||
if _, e = tx.Get(verifiedKey); e == nil {
|
||||
result.Verified = true
|
||||
@ -861,7 +853,7 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
||||
enforcementKey := fmt.Sprintf(keyAccountEnforcement, casefoldedAccount)
|
||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
|
||||
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
||||
@ -892,7 +884,7 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
tx.Delete(registeredTimeKey)
|
||||
tx.Delete(callbackKey)
|
||||
tx.Delete(verificationCodeKey)
|
||||
tx.Delete(enforcementKey)
|
||||
tx.Delete(settingsKey)
|
||||
rawNicks, _ = tx.Get(nicksKey)
|
||||
tx.Delete(nicksKey)
|
||||
credText, err = tx.Get(credentialsKey)
|
||||
@ -980,19 +972,13 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
|
||||
}
|
||||
|
||||
var account string
|
||||
var rawAccount rawClientAccount
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
|
||||
|
||||
err := am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
var err error
|
||||
err := am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
account, _ = tx.Get(certFPKey)
|
||||
if account == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
rawAccount, err = am.loadRawAccount(tx, account)
|
||||
if err != nil || !rawAccount.Verified {
|
||||
return errAccountUnverified
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -1001,14 +987,57 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
|
||||
}
|
||||
|
||||
// ok, we found an account corresponding to their certificate
|
||||
clientAccount, err := am.deserializeRawAccount(rawAccount)
|
||||
clientAccount, err := am.LoadAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !clientAccount.Verified {
|
||||
return errAccountUnverified
|
||||
}
|
||||
am.Login(client, clientAccount)
|
||||
return nil
|
||||
}
|
||||
|
||||
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
|
||||
|
||||
func (am *AccountManager) ModifyAccountSettings(account string, munger settingsMunger) (newSettings AccountSettings, err error) {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return newSettings, errAccountDoesNotExist
|
||||
}
|
||||
// TODO implement this in general via a compare-and-swap API
|
||||
accountData, err := am.LoadAccount(casefoldedAccount)
|
||||
if err != nil {
|
||||
return
|
||||
} else if !accountData.Verified {
|
||||
return newSettings, errAccountUnverified
|
||||
}
|
||||
newSettings, err = munger(accountData.Settings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, err := json.Marshal(newSettings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
serializedValue := string(text)
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) (err error) {
|
||||
_, _, err = tx.Set(key, serializedValue, nil)
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
// success, push new settings into the client objects
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
for _, client := range am.accountToClients[casefoldedAccount] {
|
||||
client.SetAccountSettings(newSettings)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// represents someone's status in hostserv
|
||||
type VHostInfo struct {
|
||||
ApprovedVHost string
|
||||
@ -1237,6 +1266,9 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
|
||||
for _, client := range am.accountToClients[casefoldedAccount] {
|
||||
client.SetAccountSettings(account.Settings)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) Logout(client *Client) {
|
||||
@ -1283,6 +1315,21 @@ type AccountCredentials struct {
|
||||
Certificate string // fingerprint
|
||||
}
|
||||
|
||||
type BouncerAllowedSetting int
|
||||
|
||||
const (
|
||||
BouncerAllowedServerDefault BouncerAllowedSetting = iota
|
||||
BouncerDisallowedByUser
|
||||
BouncerAllowedByUser
|
||||
)
|
||||
|
||||
type AccountSettings struct {
|
||||
AutoreplayLines *int
|
||||
NickEnforcement NickEnforcementMethod
|
||||
AllowBouncer BouncerAllowedSetting
|
||||
AutoreplayJoins bool
|
||||
}
|
||||
|
||||
// ClientAccount represents a user account.
|
||||
type ClientAccount struct {
|
||||
// Name of the account.
|
||||
@ -1293,6 +1340,7 @@ type ClientAccount struct {
|
||||
Verified bool
|
||||
AdditionalNicks []string
|
||||
VHost VHostInfo
|
||||
Settings AccountSettings
|
||||
}
|
||||
|
||||
// convenience for passing around raw serialized account data
|
||||
@ -1304,6 +1352,7 @@ type rawClientAccount struct {
|
||||
Verified bool
|
||||
AdditionalNicks string
|
||||
VHost string
|
||||
Settings string
|
||||
}
|
||||
|
||||
// logoutOfAccount logs the client out of their current account.
|
||||
|
@ -114,9 +114,3 @@ func (s *Set) String(version Version, values *Values) string {
|
||||
|
||||
return strings.Join(strs, " ")
|
||||
}
|
||||
|
||||
// returns whether we should send `znc.in/self-message`-style echo messages
|
||||
// to sessions other than that which originated the message
|
||||
func (capabs *Set) SelfMessagesEnabled() bool {
|
||||
return capabs.Has(EchoMessage) || capabs.Has(ZNCSelfMessage)
|
||||
}
|
||||
|
@ -620,7 +620,17 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
||||
rb.Flush(true)
|
||||
|
||||
replayLimit := channel.server.Config().History.AutoreplayOnJoin
|
||||
var replayLimit int
|
||||
customReplayLimit := client.AccountSettings().AutoreplayLines
|
||||
if customReplayLimit != nil {
|
||||
replayLimit = *customReplayLimit
|
||||
maxLimit := channel.server.Config().History.ChathistoryMax
|
||||
if maxLimit < replayLimit {
|
||||
replayLimit = maxLimit
|
||||
}
|
||||
} else {
|
||||
replayLimit = channel.server.Config().History.AutoreplayOnJoin
|
||||
}
|
||||
if 0 < replayLimit {
|
||||
// TODO don't replay the client's own JOIN line?
|
||||
items := channel.history.Latest(replayLimit)
|
||||
@ -782,6 +792,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||
client := rb.target
|
||||
eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
|
||||
extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin)
|
||||
playJoinsAsPrivmsg := (!autoreplay || client.AccountSettings().AutoreplayJoins)
|
||||
|
||||
if len(items) == 0 {
|
||||
return
|
||||
@ -808,7 +819,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
|
||||
}
|
||||
} else {
|
||||
if autoreplay {
|
||||
if !playJoinsAsPrivmsg {
|
||||
continue // #474
|
||||
}
|
||||
var message string
|
||||
@ -823,7 +834,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||
if eventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
|
||||
} else {
|
||||
if autoreplay {
|
||||
if !playJoinsAsPrivmsg {
|
||||
continue // #474
|
||||
}
|
||||
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
|
||||
@ -840,7 +851,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||
if eventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
|
||||
} else {
|
||||
if autoreplay {
|
||||
if !playJoinsAsPrivmsg {
|
||||
continue // #474
|
||||
}
|
||||
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
|
||||
@ -989,7 +1000,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
||||
}
|
||||
// send echo-message to other connected sessions
|
||||
for _, session := range client.Sessions() {
|
||||
if session == rb.session || !session.capabilities.SelfMessagesEnabled() {
|
||||
if session == rb.session {
|
||||
continue
|
||||
}
|
||||
var tagsToUse map[string]string
|
||||
@ -998,7 +1009,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
||||
}
|
||||
if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) {
|
||||
session.sendFromClientInternal(false, message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname)
|
||||
} else {
|
||||
} else if histType != history.Tagmsg {
|
||||
session.sendSplitMsgFromClientInternal(false, nickmask, account, tagsToUse, command, chname, message)
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ referenced by their registered account names, not their nicknames.`,
|
||||
|
||||
// csNotice sends the client a notice from ChanServ
|
||||
func csNotice(rb *ResponseBuffer, text string) {
|
||||
rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text)
|
||||
rb.Add(nil, "ChanServ!ChanServ@localhost", "NOTICE", rb.target.Nick(), text)
|
||||
}
|
||||
|
||||
func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
|
@ -48,6 +48,7 @@ type ResumeDetails struct {
|
||||
type Client struct {
|
||||
account string
|
||||
accountName string // display name of the account: uncasefolded, '*' if not logged in
|
||||
accountSettings AccountSettings
|
||||
atime time.Time
|
||||
away bool
|
||||
awayMessage string
|
||||
@ -600,7 +601,8 @@ func (client *Client) tryResumeChannels() {
|
||||
|
||||
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
||||
var batchID string
|
||||
nick := client.Nick()
|
||||
details := client.Details()
|
||||
nick := details.nick
|
||||
if 0 < len(items) {
|
||||
batchID = rb.StartNestedHistoryBatch(nick)
|
||||
}
|
||||
@ -626,7 +628,14 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
|
||||
if allowTags {
|
||||
tags = item.Tags
|
||||
}
|
||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
|
||||
if item.Params[0] == "" {
|
||||
// this message was sent *to* the client from another nick
|
||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
|
||||
} else {
|
||||
// this message was sent *from* the client to another nick; the target is item.Params[0]
|
||||
// substitute the client's current nickmask in case they changed nick
|
||||
rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
|
||||
}
|
||||
}
|
||||
|
||||
rb.EndNestedBatch(batchID)
|
||||
|
@ -145,7 +145,20 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
|
||||
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
|
||||
account := client.Account()
|
||||
bouncerAllowed := client.server.accounts.BouncerAllowed(account, session)
|
||||
config := client.server.Config()
|
||||
var bouncerAllowed bool
|
||||
if config.Accounts.Bouncer.Enabled {
|
||||
if session != nil && session.capabilities.Has(caps.Bouncer) {
|
||||
bouncerAllowed = true
|
||||
} else {
|
||||
settings := client.AccountSettings()
|
||||
if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
|
||||
bouncerAllowed = true
|
||||
} else if settings.AllowBouncer == BouncerAllowedByUser {
|
||||
bouncerAllowed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
@ -168,7 +181,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
if skeletonHolder != nil && skeletonHolder != client {
|
||||
return errNicknameInUse
|
||||
}
|
||||
if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account {
|
||||
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
|
||||
return errNicknameReserved
|
||||
}
|
||||
clients.removeInternal(client)
|
||||
|
@ -112,63 +112,63 @@ type VHostConfig struct {
|
||||
} `yaml:"user-requests"`
|
||||
}
|
||||
|
||||
type NickReservationMethod int
|
||||
type NickEnforcementMethod int
|
||||
|
||||
const (
|
||||
// NickReservationOptional is the zero value; it serializes to
|
||||
// NickEnforcementOptional is the zero value; it serializes to
|
||||
// "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`.
|
||||
// in both cases, it means "defer to the other source of truth", i.e.,
|
||||
// in the config, defer to the user's custom setting, and as a custom setting,
|
||||
// defer to the default in the config. if both are NickReservationOptional then
|
||||
// defer to the default in the config. if both are NickEnforcementOptional then
|
||||
// there is no enforcement.
|
||||
NickReservationOptional NickReservationMethod = iota
|
||||
NickReservationNone
|
||||
NickReservationWithTimeout
|
||||
NickReservationStrict
|
||||
// XXX: these are serialized as numbers in the database, so beware of collisions
|
||||
// when refactoring (any numbers currently in use must keep their meanings, or
|
||||
// else be fixed up by a schema change)
|
||||
NickEnforcementOptional NickEnforcementMethod = iota
|
||||
NickEnforcementNone
|
||||
NickEnforcementWithTimeout
|
||||
NickEnforcementStrict
|
||||
)
|
||||
|
||||
func nickReservationToString(method NickReservationMethod) string {
|
||||
func nickReservationToString(method NickEnforcementMethod) string {
|
||||
switch method {
|
||||
case NickReservationOptional:
|
||||
case NickEnforcementOptional:
|
||||
return "default"
|
||||
case NickReservationNone:
|
||||
case NickEnforcementNone:
|
||||
return "none"
|
||||
case NickReservationWithTimeout:
|
||||
case NickEnforcementWithTimeout:
|
||||
return "timeout"
|
||||
case NickReservationStrict:
|
||||
case NickEnforcementStrict:
|
||||
return "strict"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func nickReservationFromString(method string) (NickReservationMethod, error) {
|
||||
switch method {
|
||||
func nickReservationFromString(method string) (NickEnforcementMethod, error) {
|
||||
switch strings.ToLower(method) {
|
||||
case "default":
|
||||
return NickReservationOptional, nil
|
||||
return NickEnforcementOptional, nil
|
||||
case "optional":
|
||||
return NickReservationOptional, nil
|
||||
return NickEnforcementOptional, nil
|
||||
case "none":
|
||||
return NickReservationNone, nil
|
||||
return NickEnforcementNone, nil
|
||||
case "timeout":
|
||||
return NickReservationWithTimeout, nil
|
||||
return NickEnforcementWithTimeout, nil
|
||||
case "strict":
|
||||
return NickReservationStrict, nil
|
||||
return NickEnforcementStrict, nil
|
||||
default:
|
||||
return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
|
||||
return NickEnforcementOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var orig, raw string
|
||||
func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var orig string
|
||||
var err error
|
||||
if err = unmarshal(&orig); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, err = Casefold(orig); err != nil {
|
||||
return err
|
||||
}
|
||||
method, err := nickReservationFromString(raw)
|
||||
method, err := nickReservationFromString(orig)
|
||||
if err == nil {
|
||||
*nr = method
|
||||
}
|
||||
@ -178,7 +178,7 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
|
||||
type NickReservationConfig struct {
|
||||
Enabled bool
|
||||
AdditionalNickLimit int `yaml:"additional-nick-limit"`
|
||||
Method NickReservationMethod
|
||||
Method NickEnforcementMethod
|
||||
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
|
||||
RenameTimeout time.Duration `yaml:"rename-timeout"`
|
||||
RenamePrefix string `yaml:"rename-prefix"`
|
||||
|
@ -22,7 +22,7 @@ const (
|
||||
// 'version' of the database schema
|
||||
keySchemaVersion = "db.version"
|
||||
// latest schema of the db
|
||||
latestDbSchema = "5"
|
||||
latestDbSchema = "6"
|
||||
)
|
||||
|
||||
type SchemaChanger func(*Config, *buntdb.Tx) error
|
||||
@ -409,6 +409,37 @@ func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// custom nick enforcement was a separate db key, now it's part of settings
|
||||
func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error {
|
||||
accountToEnforcement := make(map[string]NickEnforcementMethod)
|
||||
prefix := "account.customenforcement "
|
||||
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
return false
|
||||
}
|
||||
account := strings.TrimPrefix(key, prefix)
|
||||
method, err := nickReservationFromString(value)
|
||||
if err == nil {
|
||||
accountToEnforcement[account] = method
|
||||
} else {
|
||||
log.Printf("skipping corrupt custom enforcement value for %s\n", account)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for account, method := range accountToEnforcement {
|
||||
var settings AccountSettings
|
||||
settings.NickEnforcement = method
|
||||
text, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Delete(prefix + account)
|
||||
tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
allChanges := []SchemaChange{
|
||||
{
|
||||
@ -431,6 +462,11 @@ func init() {
|
||||
TargetVersion: "5",
|
||||
Changer: schemaChangeV4ToV5,
|
||||
},
|
||||
{
|
||||
InitialVersion: "5",
|
||||
TargetVersion: "6",
|
||||
Changer: schemaChangeV5ToV6,
|
||||
},
|
||||
}
|
||||
|
||||
// build the index
|
||||
|
@ -5,7 +5,10 @@
|
||||
|
||||
package irc
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
// Runtime Errors
|
||||
var (
|
||||
@ -19,10 +22,10 @@ var (
|
||||
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
|
||||
errAccountNotLoggedIn = errors.New("You're not logged into an account")
|
||||
errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
|
||||
errAccountUnverified = errors.New("Account is not yet verified")
|
||||
errAccountUnverified = errors.New(`Account is not yet verified`)
|
||||
errAccountVerificationFailed = errors.New("Account verification failed")
|
||||
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
|
||||
errAccountUpdateFailed = errors.New("Error while updating your account information")
|
||||
errAccountUpdateFailed = errors.New(`Error while updating your account information`)
|
||||
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
|
||||
errCallbackFailed = errors.New("Account verification could not be sent")
|
||||
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
|
||||
@ -40,7 +43,7 @@ var (
|
||||
errInvalidUsername = errors.New("Invalid username")
|
||||
errFeatureDisabled = errors.New(`That feature is disabled`)
|
||||
errBanned = errors.New("IP or nickmask banned")
|
||||
errInvalidParams = errors.New("Invalid parameters")
|
||||
errInvalidParams = utils.ErrInvalidParams
|
||||
)
|
||||
|
||||
// Socket Errors
|
||||
|
@ -275,6 +275,19 @@ func (client *Client) SetAccountName(account string) (changed bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) AccountSettings() (result AccountSettings) {
|
||||
client.stateMutex.RLock()
|
||||
result = client.accountSettings
|
||||
client.stateMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) SetAccountSettings(settings AccountSettings) {
|
||||
client.stateMutex.Lock()
|
||||
client.accountSettings = settings
|
||||
client.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (client *Client) Languages() (languages []string) {
|
||||
client.stateMutex.RLock()
|
||||
languages = client.languages
|
||||
|
@ -2046,12 +2046,12 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
}
|
||||
// an echo-message may need to go out to other client sessions:
|
||||
for _, session := range client.Sessions() {
|
||||
if session == rb.session || !rb.session.capabilities.SelfMessagesEnabled() {
|
||||
if session == rb.session {
|
||||
continue
|
||||
}
|
||||
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
||||
session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
||||
} else {
|
||||
} else if histType != history.Tagmsg {
|
||||
session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
||||
}
|
||||
}
|
||||
@ -2060,12 +2060,17 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage())
|
||||
}
|
||||
|
||||
user.history.Add(history.Item{
|
||||
item := history.Item{
|
||||
Type: histType,
|
||||
Message: splitMsg,
|
||||
Nick: nickMaskString,
|
||||
AccountName: accountName,
|
||||
})
|
||||
}
|
||||
// add to the target's history:
|
||||
user.history.Add(item)
|
||||
// add this to the client's history as well, recording the target:
|
||||
item.Params[0] = tnick
|
||||
client.history.Add(item)
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
@ -131,7 +131,7 @@ for the rejection.`,
|
||||
|
||||
// hsNotice sends the client a notice from HostServ
|
||||
func hsNotice(rb *ResponseBuffer, text string) {
|
||||
rb.Add(nil, "HostServ", "NOTICE", rb.target.Nick(), text)
|
||||
rb.Add(nil, "HostServ!HostServ@localhost", "NOTICE", rb.target.Nick(), text)
|
||||
}
|
||||
|
||||
// hsNotifyChannel notifies the designated channel of new vhost activity
|
||||
|
@ -198,7 +198,7 @@ func (nt *NickTimer) Initialize(client *Client) {
|
||||
}
|
||||
|
||||
config := &client.server.Config().Accounts.NickReservation
|
||||
enabled := config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)
|
||||
enabled := config.Enabled && (config.Method == NickEnforcementWithTimeout || config.AllowCustomEnforcement)
|
||||
|
||||
nt.Lock()
|
||||
defer nt.Unlock()
|
||||
@ -235,7 +235,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) {
|
||||
cfnick, skeleton := nt.client.uniqueIdentifiers()
|
||||
account := nt.client.Account()
|
||||
accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton)
|
||||
enforceTimeout := method == NickReservationWithTimeout
|
||||
enforceTimeout := method == NickEnforcementWithTimeout
|
||||
|
||||
var shouldWarn, shouldRename bool
|
||||
|
||||
@ -258,7 +258,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) {
|
||||
if enforceTimeout && delinquent && (accountChanged || nt.timer == nil) {
|
||||
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
|
||||
shouldWarn = true
|
||||
} else if method == NickReservationStrict && delinquent {
|
||||
} else if method == NickEnforcementStrict && delinquent {
|
||||
shouldRename = true // this can happen if reservation was enabled by rehash
|
||||
}
|
||||
}()
|
||||
|
277
irc/nickserv.go
277
irc/nickserv.go
@ -5,11 +5,14 @@ package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
// "enabled" callbacks for specific nickserv commands
|
||||
@ -25,10 +28,6 @@ func servCmdRequiresNickRes(config *Config) bool {
|
||||
return config.Accounts.AuthenticationEnabled && config.Accounts.NickReservation.Enabled
|
||||
}
|
||||
|
||||
func nsEnforceEnabled(config *Config) bool {
|
||||
return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement
|
||||
}
|
||||
|
||||
func servCmdRequiresBouncerEnabled(config *Config) bool {
|
||||
return config.Accounts.Bouncer.Enabled
|
||||
}
|
||||
@ -61,20 +60,14 @@ DROP de-links the given (or your current) nickname from your user account.`,
|
||||
authRequired: true,
|
||||
},
|
||||
"enforce": {
|
||||
hidden: true,
|
||||
handler: nsEnforceHandler,
|
||||
help: `Syntax: $bENFORCE [method]$b
|
||||
|
||||
ENFORCE lets you specify a custom enforcement mechanism for your registered
|
||||
nicknames. Your options are:
|
||||
1. 'none' [no enforcement, overriding the server default]
|
||||
2. 'timeout' [anyone using the nick must authenticate before a deadline,
|
||||
or else they will be renamed]
|
||||
3. 'strict' [you must already be authenticated to use the nick]
|
||||
4. 'default' [use the server default]
|
||||
With no arguments, queries your current enforcement status.`,
|
||||
helpShort: `$bENFORCE$b lets you change how your nicknames are reserved.`,
|
||||
ENFORCE is an alias for $bGET enforce$b and $bSET enforce$b. See the help
|
||||
entry for $bSET$b for more information.`,
|
||||
authRequired: true,
|
||||
enabled: nsEnforceEnabled,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
},
|
||||
"ghost": {
|
||||
handler: nsGhostHandler,
|
||||
@ -194,12 +187,246 @@ password by supplying their username and then the desired password.`,
|
||||
enabled: servCmdRequiresAuthEnabled,
|
||||
minParams: 2,
|
||||
},
|
||||
"get": {
|
||||
handler: nsGetHandler,
|
||||
help: `Syntax: $bGET <setting>$b
|
||||
|
||||
GET queries the current values of your account settings. For more information
|
||||
on the settings and their possible values, see HELP SET.`,
|
||||
helpShort: `$bGET$b queries the current values of your account settings`,
|
||||
authRequired: true,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
minParams: 1,
|
||||
},
|
||||
"saget": {
|
||||
handler: nsGetHandler,
|
||||
help: `Syntax: $bSAGET <account> <setting>$b
|
||||
|
||||
SAGET queries the values of someone else's account settings. For more
|
||||
information on the settings and their possible values, see HELP SET.`,
|
||||
helpShort: `$bSAGET$b queries the current values of another user's account settings`,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
minParams: 2,
|
||||
capabs: []string{"accreg"},
|
||||
},
|
||||
"set": {
|
||||
handler: nsSetHandler,
|
||||
helpShort: `$bSET$b modifies your account settings`,
|
||||
// these are broken out as separate strings so they can be translated separately
|
||||
helpStrings: []string{
|
||||
`Syntax $bSET <setting> <value>$b
|
||||
|
||||
Set modifies your account settings. The following settings are available:`,
|
||||
|
||||
`$bENFORCE$b
|
||||
'enforce' lets you specify a custom enforcement mechanism for your registered
|
||||
nicknames. Your options are:
|
||||
1. 'none' [no enforcement, overriding the server default]
|
||||
2. 'timeout' [anyone using the nick must authenticate before a deadline,
|
||||
or else they will be renamed]
|
||||
3. 'strict' [you must already be authenticated to use the nick]
|
||||
4. 'default' [use the server default]`,
|
||||
|
||||
`$bBOUNCER$b
|
||||
If 'bouncer' is enabled and you are already logged in and using a nick, a
|
||||
second client of yours that authenticates with SASL and requests the same nick
|
||||
is allowed to attach to the nick as well (this is comparable to the behavior
|
||||
of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior),
|
||||
'off' (disallow it), and 'default' (use the server default value).`,
|
||||
|
||||
`$bAUTOREPLAY-LINES$b
|
||||
'autoreplay-lines' controls the number of lines of channel history that will
|
||||
be replayed to you automatically when joining a channel. Your options are any
|
||||
positive number, 0 to disable the feature, and 'default' to use the server
|
||||
default.`,
|
||||
|
||||
`$bAUTOREPLAY-JOINS$b
|
||||
'autoreplay-joins' controls whether autoreplayed channel history will include
|
||||
lines for join and part. This provides more information about the context of
|
||||
messages, but may be spammy. Your options are 'on' and 'off'.`,
|
||||
},
|
||||
authRequired: true,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
minParams: 2,
|
||||
},
|
||||
"saset": {
|
||||
handler: nsSetHandler,
|
||||
help: `Syntax: $bSASET <account> <setting> <value>$b
|
||||
|
||||
SASET modifies the values of someone else's account settings. For more
|
||||
information on the settings and their possible values, see HELP SET.`,
|
||||
helpShort: `$bSASET$b modifies another user's account settings`,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
minParams: 3,
|
||||
capabs: []string{"accreg"},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// nsNotice sends the client a notice from NickServ
|
||||
func nsNotice(rb *ResponseBuffer, text string) {
|
||||
rb.Add(nil, "NickServ", "NOTICE", rb.target.Nick(), text)
|
||||
// XXX i can't figure out how to use OragonoServices[servicename].prefix here
|
||||
// without creating a compile-time initialization loop
|
||||
rb.Add(nil, "NickServ!NickServ@localhost", "NOTICE", rb.target.Nick(), text)
|
||||
}
|
||||
|
||||
func nsGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
var account string
|
||||
if command == "saget" {
|
||||
account = params[0]
|
||||
params = params[1:]
|
||||
} else {
|
||||
account = client.Account()
|
||||
}
|
||||
|
||||
accountData, err := server.accounts.LoadAccount(account)
|
||||
if err == errAccountDoesNotExist {
|
||||
nsNotice(rb, client.t("No such account"))
|
||||
return
|
||||
} else if err != nil {
|
||||
nsNotice(rb, client.t("Error loading account data"))
|
||||
return
|
||||
}
|
||||
|
||||
displaySetting(params[0], accountData.Settings, client, rb)
|
||||
}
|
||||
|
||||
func displaySetting(settingName string, settings AccountSettings, client *Client, rb *ResponseBuffer) {
|
||||
config := client.server.Config()
|
||||
switch strings.ToLower(settingName) {
|
||||
case "enforce":
|
||||
storedValue := settings.NickEnforcement
|
||||
serializedStoredValue := nickReservationToString(storedValue)
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Your stored nickname enforcement setting is: %s"), serializedStoredValue))
|
||||
serializedActualValue := nickReservationToString(configuredEnforcementMethod(config, storedValue))
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Given current server settings, your nickname is enforced with: %s"), serializedActualValue))
|
||||
case "autoreplay-lines":
|
||||
if settings.AutoreplayLines == nil {
|
||||
nsNotice(rb, fmt.Sprintf(client.t("You will receive the server default of %d lines of autoreplayed history"), config.History.AutoreplayOnJoin))
|
||||
} else {
|
||||
nsNotice(rb, fmt.Sprintf(client.t("You will receive %d lines of autoreplayed history"), *settings.AutoreplayLines))
|
||||
}
|
||||
case "autoreplay-joins":
|
||||
if settings.AutoreplayJoins {
|
||||
nsNotice(rb, client.t("You will see JOINs and PARTs in autoreplayed history lines"))
|
||||
} else {
|
||||
nsNotice(rb, client.t("You will not see JOINs and PARTs in autoreplayed history lines"))
|
||||
}
|
||||
case "bouncer":
|
||||
if !config.Accounts.Bouncer.Enabled {
|
||||
nsNotice(rb, fmt.Sprintf(client.t("This feature has been disabled by the server administrators")))
|
||||
} else {
|
||||
switch settings.AllowBouncer {
|
||||
case BouncerAllowedServerDefault:
|
||||
if config.Accounts.Bouncer.AllowedByDefault {
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account, but you can opt out")))
|
||||
} else {
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account, but you can opt in")))
|
||||
}
|
||||
case BouncerDisallowedByUser:
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account")))
|
||||
case BouncerAllowedByUser:
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account")))
|
||||
}
|
||||
}
|
||||
default:
|
||||
nsNotice(rb, client.t("No such setting"))
|
||||
}
|
||||
}
|
||||
|
||||
func nsSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
var account string
|
||||
if command == "saset" {
|
||||
account = params[0]
|
||||
params = params[1:]
|
||||
} else {
|
||||
account = client.Account()
|
||||
}
|
||||
|
||||
var munger settingsMunger
|
||||
var finalSettings AccountSettings
|
||||
var err error
|
||||
switch strings.ToLower(params[0]) {
|
||||
case "pass":
|
||||
nsNotice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD"))
|
||||
return
|
||||
case "enforce":
|
||||
var method NickEnforcementMethod
|
||||
method, err = nickReservationFromString(params[1])
|
||||
if err != nil {
|
||||
err = errInvalidParams
|
||||
break
|
||||
}
|
||||
// updating enforcement settings is special-cased, because it requires
|
||||
// an update to server.accounts.accountToMethod
|
||||
finalSettings, err = server.accounts.SetEnforcementStatus(account, method)
|
||||
if err == nil {
|
||||
finalSettings.NickEnforcement = method // success
|
||||
}
|
||||
case "autoreplay-lines":
|
||||
var newValue *int
|
||||
if strings.ToLower(params[1]) != "default" {
|
||||
val, err_ := strconv.Atoi(params[1])
|
||||
if err_ != nil || val < 0 {
|
||||
err = errInvalidParams
|
||||
break
|
||||
}
|
||||
newValue = new(int)
|
||||
*newValue = val
|
||||
}
|
||||
munger = func(in AccountSettings) (out AccountSettings, err error) {
|
||||
out = in
|
||||
out.AutoreplayLines = newValue
|
||||
return
|
||||
}
|
||||
case "bouncer":
|
||||
var newValue BouncerAllowedSetting
|
||||
if strings.ToLower(params[1]) == "default" {
|
||||
newValue = BouncerAllowedServerDefault
|
||||
} else {
|
||||
var enabled bool
|
||||
enabled, err = utils.StringToBool(params[1])
|
||||
if enabled {
|
||||
newValue = BouncerAllowedByUser
|
||||
} else {
|
||||
newValue = BouncerDisallowedByUser
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
munger = func(in AccountSettings) (out AccountSettings, err error) {
|
||||
out = in
|
||||
out.AllowBouncer = newValue
|
||||
return
|
||||
}
|
||||
}
|
||||
case "autoreplay-joins":
|
||||
var newValue bool
|
||||
newValue, err = utils.StringToBool(params[1])
|
||||
if err == nil {
|
||||
munger = func(in AccountSettings) (out AccountSettings, err error) {
|
||||
out = in
|
||||
out.AutoreplayJoins = newValue
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
err = errInvalidParams
|
||||
}
|
||||
|
||||
if munger != nil {
|
||||
finalSettings, err = server.accounts.ModifyAccountSettings(account, munger)
|
||||
}
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
nsNotice(rb, client.t("Successfully changed your account settings"))
|
||||
displaySetting(params[0], finalSettings, client, rb)
|
||||
case errInvalidParams, errAccountDoesNotExist, errFeatureDisabled, errAccountUnverified, errAccountUpdateFailed:
|
||||
nsNotice(rb, client.t(err.Error()))
|
||||
default:
|
||||
// unknown error
|
||||
nsNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func nsDropHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
@ -550,7 +777,7 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
|
||||
}
|
||||
}
|
||||
default:
|
||||
errorMessage = "Invalid parameters"
|
||||
errorMessage = `Invalid parameters`
|
||||
}
|
||||
|
||||
if errorMessage != "" {
|
||||
@ -568,22 +795,12 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
|
||||
}
|
||||
|
||||
func nsEnforceHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
newParams := []string{"enforce"}
|
||||
if len(params) == 0 {
|
||||
status := server.accounts.getStoredEnforcementStatus(client.Account())
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status))
|
||||
nsGetHandler(server, client, "get", newParams, rb)
|
||||
} else {
|
||||
method, err := nickReservationFromString(params[0])
|
||||
if err != nil {
|
||||
nsNotice(rb, client.t("Invalid parameters"))
|
||||
return
|
||||
}
|
||||
err = server.accounts.SetEnforcementStatus(client.Account(), method)
|
||||
if err == nil {
|
||||
nsNotice(rb, client.t("Enforcement method set"))
|
||||
} else {
|
||||
server.logger.Error("internal", "couldn't store NS ENFORCE data", err.Error())
|
||||
nsNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
newParams = append(newParams, params[0])
|
||||
nsSetHandler(server, client, "set", newParams, rb)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ type resumeTokenPair struct {
|
||||
}
|
||||
|
||||
type ResumeManager struct {
|
||||
sync.RWMutex // level 2
|
||||
sync.Mutex // level 2
|
||||
|
||||
resumeIDtoCreds map[string]resumeTokenPair
|
||||
server *Server
|
||||
@ -59,8 +59,8 @@ func (rm *ResumeManager) VerifyToken(token string) (client *Client) {
|
||||
return
|
||||
}
|
||||
|
||||
rm.RLock()
|
||||
defer rm.RUnlock()
|
||||
rm.Lock()
|
||||
defer rm.Unlock()
|
||||
|
||||
id := token[:utils.SecretTokenLength]
|
||||
pair, ok := rm.resumeIDtoCreds[id]
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
type ircService struct {
|
||||
Name string
|
||||
ShortName string
|
||||
prefix string
|
||||
CommandAliases []string
|
||||
Commands map[string]*serviceCommand
|
||||
HelpBanner string
|
||||
@ -29,8 +30,10 @@ type serviceCommand struct {
|
||||
capabs []string // oper capabs the given user has to have to access this command
|
||||
handler func(server *Server, client *Client, command string, params []string, rb *ResponseBuffer)
|
||||
help string
|
||||
helpStrings []string
|
||||
helpShort string
|
||||
authRequired bool
|
||||
hidden bool
|
||||
enabled func(*Config) bool // is this command enabled in the server config?
|
||||
minParams int
|
||||
maxParams int // split into at most n params, with last param containing remaining unsplit text
|
||||
@ -139,7 +142,7 @@ func servicePrivmsgHandler(service *ircService, server *Server, client *Client,
|
||||
func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) {
|
||||
nick := rb.target.Nick()
|
||||
sendNotice := func(notice string) {
|
||||
rb.Add(nil, service.Name, "NOTICE", nick, notice)
|
||||
rb.Add(nil, service.prefix, "NOTICE", nick, notice)
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
@ -180,7 +183,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
|
||||
nick := rb.target.Nick()
|
||||
config := server.Config()
|
||||
sendNotice := func(notice string) {
|
||||
rb.Add(nil, service.Name, "NOTICE", nick, notice)
|
||||
rb.Add(nil, service.prefix, "NOTICE", nick, notice)
|
||||
}
|
||||
|
||||
sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
|
||||
@ -194,7 +197,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
|
||||
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
|
||||
continue
|
||||
}
|
||||
if commandInfo.aliasOf != "" {
|
||||
if commandInfo.aliasOf != "" || commandInfo.hidden {
|
||||
continue // don't show help lines for aliases
|
||||
}
|
||||
if commandInfo.enabled != nil && !commandInfo.enabled(config) {
|
||||
@ -226,8 +229,18 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
|
||||
if commandInfo == nil {
|
||||
sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName)))
|
||||
} else {
|
||||
for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
|
||||
sendNotice(line)
|
||||
helpStrings := commandInfo.helpStrings
|
||||
if helpStrings == nil {
|
||||
hsArray := [1]string{commandInfo.help}
|
||||
helpStrings = hsArray[:]
|
||||
}
|
||||
for i, helpString := range helpStrings {
|
||||
if 0 < i {
|
||||
sendNotice("")
|
||||
}
|
||||
for _, line := range strings.Split(ircfmt.Unescape(client.t(helpString)), "\n") {
|
||||
sendNotice(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,6 +254,8 @@ func initializeServices() {
|
||||
oragonoServicesByCommandAlias = make(map[string]*ircService)
|
||||
|
||||
for serviceName, service := range OragonoServices {
|
||||
service.prefix = fmt.Sprintf("%s!%s@localhost", service.Name, service.Name)
|
||||
|
||||
// make `/MSG ServiceName HELP` work correctly
|
||||
service.Commands["help"] = &servHelpCmd
|
||||
|
||||
@ -257,8 +272,10 @@ func initializeServices() {
|
||||
|
||||
// force devs to write a help entry for every command
|
||||
for commandName, commandInfo := range service.Commands {
|
||||
if commandInfo.aliasOf == "" && (commandInfo.help == "" || commandInfo.helpShort == "") {
|
||||
log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
|
||||
if commandInfo.aliasOf == "" && !commandInfo.hidden {
|
||||
if (commandInfo.help == "" && commandInfo.helpStrings == nil) || commandInfo.helpShort == "" {
|
||||
log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,15 @@
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidParams = errors.New("Invalid parameters")
|
||||
)
|
||||
|
||||
// ArgsToStrings takes the arguments and splits them into a series of strings,
|
||||
// each argument separated by delim and each string bounded by maxLength.
|
||||
func ArgsToStrings(maxLength int, arguments []string, delim string) []string {
|
||||
@ -33,3 +42,15 @@ func ArgsToStrings(maxLength int, arguments []string, delim string) []string {
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
func StringToBool(str string) (result bool, err error) {
|
||||
switch strings.ToLower(str) {
|
||||
case "on", "true", "t", "yes", "y":
|
||||
result = true
|
||||
case "off", "false", "f", "no", "n":
|
||||
result = false
|
||||
default:
|
||||
err = ErrInvalidParams
|
||||
}
|
||||
return
|
||||
}
|
||||
|
23
irc/utils/args_test.go
Normal file
23
irc/utils/args_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStringToBool(t *testing.T) {
|
||||
val, err := StringToBool("on")
|
||||
assertEqual(val, true, t)
|
||||
assertEqual(err, nil, t)
|
||||
|
||||
val, err = StringToBool("n")
|
||||
assertEqual(val, false, t)
|
||||
assertEqual(err, nil, t)
|
||||
|
||||
val, err = StringToBool("OFF")
|
||||
assertEqual(val, false, t)
|
||||
assertEqual(err, nil, t)
|
||||
|
||||
val, err = StringToBool("default")
|
||||
assertEqual(err, ErrInvalidParams, t)
|
||||
}
|
@ -134,7 +134,7 @@ server:
|
||||
# defaults to true when unset for that reason.
|
||||
force-trailing: true
|
||||
|
||||
# some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower, Adium) do not
|
||||
# some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower) do not
|
||||
# respond correctly to SASL messages with the server name as a prefix:
|
||||
# https://github.com/znc/znc/issues/1212
|
||||
# this works around that bug, allowing them to use SASL.
|
||||
|
Loading…
Reference in New Issue
Block a user