3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-10 22:19:31 +01:00

implement user preferences system

This commit is contained in:
Shivaram Lingamneni 2019-05-19 04:27:44 -04:00
parent 25974b6881
commit 8fc588375b
16 changed files with 515 additions and 167 deletions

View File

@ -29,7 +29,7 @@ const (
keyAccountRegTime = "account.registered.time %s" keyAccountRegTime = "account.registered.time %s"
keyAccountCredentials = "account.credentials %s" keyAccountCredentials = "account.credentials %s"
keyAccountAdditionalNicks = "account.additionalnicks %s" keyAccountAdditionalNicks = "account.additionalnicks %s"
keyAccountEnforcement = "account.customenforcement %s" keyAccountSettings = "account.settings %s"
keyAccountVHost = "account.vhost %s" keyAccountVHost = "account.vhost %s"
keyCertToAccount = "account.creds.certfp %s" keyCertToAccount = "account.creds.certfp %s"
keyAccountChannels = "account.channels %s" keyAccountChannels = "account.channels %s"
@ -55,14 +55,14 @@ type AccountManager struct {
accountToClients map[string][]*Client accountToClients map[string][]*Client
nickToAccount map[string]string nickToAccount map[string]string
skeletonToAccount map[string]string skeletonToAccount map[string]string
accountToMethod map[string]NickReservationMethod accountToMethod map[string]NickEnforcementMethod
} }
func (am *AccountManager) Initialize(server *Server) { func (am *AccountManager) Initialize(server *Server) {
am.accountToClients = make(map[string][]*Client) am.accountToClients = make(map[string][]*Client)
am.nickToAccount = make(map[string]string) am.nickToAccount = make(map[string]string)
am.skeletonToAccount = 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.server = server
am.buildNickToAccountIndex() am.buildNickToAccountIndex()
@ -76,7 +76,7 @@ func (am *AccountManager) buildNickToAccountIndex() {
nickToAccount := make(map[string]string) nickToAccount := make(map[string]string)
skeletonToAccount := make(map[string]string) skeletonToAccount := make(map[string]string)
accountToMethod := make(map[string]NickReservationMethod) accountToMethod := make(map[string]NickEnforcementMethod)
existsPrefix := fmt.Sprintf(keyAccountExists, "") existsPrefix := fmt.Sprintf(keyAccountExists, "")
am.serialCacheUpdateMutex.Lock() am.serialCacheUpdateMutex.Lock()
@ -109,12 +109,16 @@ func (am *AccountManager) buildNickToAccountIndex() {
} }
} }
if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil { if rawPrefs, err := tx.Get(fmt.Sprintf(keyAccountSettings, account)); err == nil {
method, err := nickReservationFromString(methodStr) var prefs AccountSettings
if err == nil { err := json.Unmarshal([]byte(rawPrefs), &prefs)
accountToMethod[account] = method if err == nil && prefs.NickEnforcement != NickEnforcementOptional {
accountToMethod[account] = prefs.NickEnforcement
} else {
am.server.logger.Error("internal", "corrupt account creds", account)
} }
} }
return true return true
}) })
return err return err
@ -180,36 +184,44 @@ func (am *AccountManager) NickToAccount(nick string) string {
return am.nickToAccount[cfnick] 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) // Given a nick, looks up the account that owns it and the method (none/timeout/strict)
// used to enforce ownership. // 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() config := am.server.Config()
if !config.Accounts.NickReservation.Enabled { if !config.Accounts.NickReservation.Enabled {
return "", NickReservationNone return "", NickEnforcementNone
} }
am.RLock() am.RLock()
defer am.RUnlock() defer am.RUnlock()
// given an account, combine stored enforcement method with the config settings finalEnforcementMethod := func(account_ string) (result NickEnforcementMethod) {
// to compute the actual enforcement method storedMethod := am.accountToMethod[account_]
finalEnforcementMethod := func(account_ string) (result NickReservationMethod) { return configuredEnforcementMethod(config, storedMethod)
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
} }
nickAccount := am.nickToAccount[cfnick] nickAccount := am.nickToAccount[cfnick]
skelAccount := am.skeletonToAccount[skeleton] skelAccount := am.skeletonToAccount[skeleton]
if nickAccount == "" && skelAccount == "" { if nickAccount == "" && skelAccount == "" {
return "", NickReservationNone return "", NickEnforcementNone
} else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") { } else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") {
return nickAccount, finalEnforcementMethod(nickAccount) return nickAccount, finalEnforcementMethod(nickAccount)
} else if skelAccount != "" && nickAccount == "" { } else if skelAccount != "" && nickAccount == "" {
@ -220,75 +232,47 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
nickMethod := finalEnforcementMethod(nickAccount) nickMethod := finalEnforcementMethod(nickAccount)
skelMethod := finalEnforcementMethod(skelAccount) skelMethod := finalEnforcementMethod(skelAccount)
switch { switch {
case skelMethod == NickReservationNone: case skelMethod == NickEnforcementNone:
return nickAccount, nickMethod return nickAccount, nickMethod
case nickMethod == NickReservationNone: case nickMethod == NickEnforcementNone:
return skelAccount, skelMethod return skelAccount, skelMethod
default: default:
// nobody can use this nick // 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. // 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() config := am.server.Config()
if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) { if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
return errFeatureDisabled err = errFeatureDisabled
return
} }
var serialized string setter := func(in AccountSettings) (out AccountSettings, err error) {
if method == NickReservationOptional { out = in
serialized = "" // normally this is "default", but we're going to delete the key out.NickEnforcement = method
} else { return out, nil
serialized = nickReservationToString(method)
} }
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() am.Lock()
defer am.Unlock() defer am.Unlock()
currentMethod := am.accountToMethod[account] if method == NickEnforcementOptional {
if method != currentMethod {
if method == NickReservationOptional {
delete(am.accountToMethod, account) delete(am.accountToMethod, account)
} else { } else {
am.accountToMethod[account] = method 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 return
})
}
return nil
} }
func (am *AccountManager) AccountToClients(account string) (result []*Client) { 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 // 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 return
} }
@ -825,6 +815,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
_, e := tx.Get(accountKey) _, e := tx.Get(accountKey)
if e == buntdb.ErrNotFound { if e == buntdb.ErrNotFound {
@ -838,6 +829,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
result.Callback, _ = tx.Get(callbackKey) result.Callback, _ = tx.Get(callbackKey)
result.AdditionalNicks, _ = tx.Get(nicksKey) result.AdditionalNicks, _ = tx.Get(nicksKey)
result.VHost, _ = tx.Get(vhostKey) result.VHost, _ = tx.Get(vhostKey)
result.Settings, _ = tx.Get(settingsKey)
if _, e = tx.Get(verifiedKey); e == nil { if _, e = tx.Get(verifiedKey); e == nil {
result.Verified = true result.Verified = true
@ -861,7 +853,7 @@ func (am *AccountManager) Unregister(account string) error {
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount) verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
enforcementKey := fmt.Sprintf(keyAccountEnforcement, casefoldedAccount) settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount) vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount) channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
@ -892,7 +884,7 @@ func (am *AccountManager) Unregister(account string) error {
tx.Delete(registeredTimeKey) tx.Delete(registeredTimeKey)
tx.Delete(callbackKey) tx.Delete(callbackKey)
tx.Delete(verificationCodeKey) tx.Delete(verificationCodeKey)
tx.Delete(enforcementKey) tx.Delete(settingsKey)
rawNicks, _ = tx.Get(nicksKey) rawNicks, _ = tx.Get(nicksKey)
tx.Delete(nicksKey) tx.Delete(nicksKey)
credText, err = tx.Get(credentialsKey) credText, err = tx.Get(credentialsKey)
@ -980,19 +972,13 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
} }
var account string var account string
var rawAccount rawClientAccount
certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp) certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
err := am.server.store.Update(func(tx *buntdb.Tx) error { err := am.server.store.View(func(tx *buntdb.Tx) error {
var err error
account, _ = tx.Get(certFPKey) account, _ = tx.Get(certFPKey)
if account == "" { if account == "" {
return errAccountInvalidCredentials return errAccountInvalidCredentials
} }
rawAccount, err = am.loadRawAccount(tx, account)
if err != nil || !rawAccount.Verified {
return errAccountUnverified
}
return nil return nil
}) })
@ -1001,14 +987,57 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
} }
// ok, we found an account corresponding to their certificate // ok, we found an account corresponding to their certificate
clientAccount, err := am.deserializeRawAccount(rawAccount) clientAccount, err := am.LoadAccount(account)
if err != nil { if err != nil {
return err return err
} else if !clientAccount.Verified {
return errAccountUnverified
} }
am.Login(client, clientAccount) am.Login(client, clientAccount)
return nil 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 // represents someone's status in hostserv
type VHostInfo struct { type VHostInfo struct {
ApprovedVHost string ApprovedVHost string
@ -1237,6 +1266,9 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
am.Lock() am.Lock()
defer am.Unlock() defer am.Unlock()
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client) am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
for _, client := range am.accountToClients[casefoldedAccount] {
client.SetAccountSettings(account.Settings)
}
} }
func (am *AccountManager) Logout(client *Client) { func (am *AccountManager) Logout(client *Client) {
@ -1283,6 +1315,21 @@ type AccountCredentials struct {
Certificate string // fingerprint 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. // ClientAccount represents a user account.
type ClientAccount struct { type ClientAccount struct {
// Name of the account. // Name of the account.
@ -1293,6 +1340,7 @@ type ClientAccount struct {
Verified bool Verified bool
AdditionalNicks []string AdditionalNicks []string
VHost VHostInfo VHost VHostInfo
Settings AccountSettings
} }
// convenience for passing around raw serialized account data // convenience for passing around raw serialized account data
@ -1304,6 +1352,7 @@ type rawClientAccount struct {
Verified bool Verified bool
AdditionalNicks string AdditionalNicks string
VHost string VHost string
Settings string
} }
// logoutOfAccount logs the client out of their current account. // logoutOfAccount logs the client out of their current account.

View File

@ -114,9 +114,3 @@ func (s *Set) String(version Version, values *Values) string {
return strings.Join(strs, " ") 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)
}

View File

@ -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 // TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
rb.Flush(true) 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 { if 0 < replayLimit {
// TODO don't replay the client's own JOIN line? // TODO don't replay the client's own JOIN line?
items := channel.history.Latest(replayLimit) items := channel.history.Latest(replayLimit)
@ -782,6 +792,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
client := rb.target client := rb.target
eventPlayback := rb.session.capabilities.Has(caps.EventPlayback) eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin) extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin)
playJoinsAsPrivmsg := (!autoreplay || client.AccountSettings().AutoreplayJoins)
if len(items) == 0 { if len(items) == 0 {
return 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) rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
} }
} else { } else {
if autoreplay { if !playJoinsAsPrivmsg {
continue // #474 continue // #474
} }
var message string var message string
@ -823,7 +834,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
if eventPlayback { if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message) rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
} else { } else {
if autoreplay { if !playJoinsAsPrivmsg {
continue // #474 continue // #474
} }
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) 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 { if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message) rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
} else { } else {
if autoreplay { if !playJoinsAsPrivmsg {
continue // #474 continue // #474
} }
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) 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 // send echo-message to other connected sessions
for _, session := range client.Sessions() { for _, session := range client.Sessions() {
if session == rb.session || !session.capabilities.SelfMessagesEnabled() { if session == rb.session {
continue continue
} }
var tagsToUse map[string]string 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) { if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) {
session.sendFromClientInternal(false, message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname) 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) session.sendSplitMsgFromClientInternal(false, nickmask, account, tagsToUse, command, chname, message)
} }
} }

View File

@ -86,7 +86,7 @@ referenced by their registered account names, not their nicknames.`,
// csNotice sends the client a notice from ChanServ // csNotice sends the client a notice from ChanServ
func csNotice(rb *ResponseBuffer, text string) { 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) { func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {

View File

@ -48,6 +48,7 @@ type ResumeDetails struct {
type Client struct { type Client struct {
account string account string
accountName string // display name of the account: uncasefolded, '*' if not logged in accountName string // display name of the account: uncasefolded, '*' if not logged in
accountSettings AccountSettings
atime time.Time atime time.Time
away bool away bool
awayMessage string awayMessage string

View File

@ -145,7 +145,20 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton) reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
account := client.Account() 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() clients.Lock()
defer clients.Unlock() defer clients.Unlock()
@ -168,7 +181,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
if skeletonHolder != nil && skeletonHolder != client { if skeletonHolder != nil && skeletonHolder != client {
return errNicknameInUse return errNicknameInUse
} }
if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account { if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
return errNicknameReserved return errNicknameReserved
} }
clients.removeInternal(client) clients.removeInternal(client)

View File

@ -112,63 +112,63 @@ type VHostConfig struct {
} `yaml:"user-requests"` } `yaml:"user-requests"`
} }
type NickReservationMethod int type NickEnforcementMethod int
const ( 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`. // "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 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, // 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. // there is no enforcement.
NickReservationOptional NickReservationMethod = iota // XXX: these are serialized as numbers in the database, so beware of collisions
NickReservationNone // when refactoring (any numbers currently in use must keep their meanings, or
NickReservationWithTimeout // else be fixed up by a schema change)
NickReservationStrict NickEnforcementOptional NickEnforcementMethod = iota
NickEnforcementNone
NickEnforcementWithTimeout
NickEnforcementStrict
) )
func nickReservationToString(method NickReservationMethod) string { func nickReservationToString(method NickEnforcementMethod) string {
switch method { switch method {
case NickReservationOptional: case NickEnforcementOptional:
return "default" return "default"
case NickReservationNone: case NickEnforcementNone:
return "none" return "none"
case NickReservationWithTimeout: case NickEnforcementWithTimeout:
return "timeout" return "timeout"
case NickReservationStrict: case NickEnforcementStrict:
return "strict" return "strict"
default: default:
return "" return ""
} }
} }
func nickReservationFromString(method string) (NickReservationMethod, error) { func nickReservationFromString(method string) (NickEnforcementMethod, error) {
switch method { switch strings.ToLower(method) {
case "default": case "default":
return NickReservationOptional, nil return NickEnforcementOptional, nil
case "optional": case "optional":
return NickReservationOptional, nil return NickEnforcementOptional, nil
case "none": case "none":
return NickReservationNone, nil return NickEnforcementNone, nil
case "timeout": case "timeout":
return NickReservationWithTimeout, nil return NickEnforcementWithTimeout, nil
case "strict": case "strict":
return NickReservationStrict, nil return NickEnforcementStrict, nil
default: 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 { func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
var orig, raw string var orig string
var err error var err error
if err = unmarshal(&orig); err != nil { if err = unmarshal(&orig); err != nil {
return err return err
} }
if raw, err = Casefold(orig); err != nil { method, err := nickReservationFromString(orig)
return err
}
method, err := nickReservationFromString(raw)
if err == nil { if err == nil {
*nr = method *nr = method
} }
@ -178,7 +178,7 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
type NickReservationConfig struct { type NickReservationConfig struct {
Enabled bool Enabled bool
AdditionalNickLimit int `yaml:"additional-nick-limit"` AdditionalNickLimit int `yaml:"additional-nick-limit"`
Method NickReservationMethod Method NickEnforcementMethod
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"` AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
RenameTimeout time.Duration `yaml:"rename-timeout"` RenameTimeout time.Duration `yaml:"rename-timeout"`
RenamePrefix string `yaml:"rename-prefix"` RenamePrefix string `yaml:"rename-prefix"`

View File

@ -22,7 +22,7 @@ const (
// 'version' of the database schema // 'version' of the database schema
keySchemaVersion = "db.version" keySchemaVersion = "db.version"
// latest schema of the db // latest schema of the db
latestDbSchema = "5" latestDbSchema = "6"
) )
type SchemaChanger func(*Config, *buntdb.Tx) error type SchemaChanger func(*Config, *buntdb.Tx) error
@ -409,6 +409,37 @@ func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error {
return nil 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() { func init() {
allChanges := []SchemaChange{ allChanges := []SchemaChange{
{ {
@ -431,6 +462,11 @@ func init() {
TargetVersion: "5", TargetVersion: "5",
Changer: schemaChangeV4ToV5, Changer: schemaChangeV4ToV5,
}, },
{
InitialVersion: "5",
TargetVersion: "6",
Changer: schemaChangeV5ToV6,
},
} }
// build the index // build the index

View File

@ -19,10 +19,10 @@ var (
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick") errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
errAccountNotLoggedIn = errors.New("You're not logged into an account") errAccountNotLoggedIn = errors.New("You're not logged into an account")
errAccountTooManyNicks = errors.New("Account has too many reserved nicks") 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") errAccountVerificationFailed = errors.New("Account verification failed")
errAccountVerificationInvalidCode = errors.New("Invalid account verification code") 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`) errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
errCallbackFailed = errors.New("Account verification could not be sent") errCallbackFailed = errors.New("Account verification could not be sent")
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`) errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)

View File

@ -275,6 +275,19 @@ func (client *Client) SetAccountName(account string) (changed bool) {
return 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) { func (client *Client) Languages() (languages []string) {
client.stateMutex.RLock() client.stateMutex.RLock()
languages = client.languages languages = client.languages

View File

@ -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: // an echo-message may need to go out to other client sessions:
for _, session := range client.Sessions() { for _, session := range client.Sessions() {
if session == rb.session || !rb.session.capabilities.SelfMessagesEnabled() { if session == rb.session {
continue continue
} }
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) 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) session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
} }
} }

View File

@ -131,7 +131,7 @@ for the rejection.`,
// hsNotice sends the client a notice from HostServ // hsNotice sends the client a notice from HostServ
func hsNotice(rb *ResponseBuffer, text string) { 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 // hsNotifyChannel notifies the designated channel of new vhost activity

View File

@ -198,7 +198,7 @@ func (nt *NickTimer) Initialize(client *Client) {
} }
config := &client.server.Config().Accounts.NickReservation config := &client.server.Config().Accounts.NickReservation
enabled := config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement) enabled := config.Enabled && (config.Method == NickEnforcementWithTimeout || config.AllowCustomEnforcement)
nt.Lock() nt.Lock()
defer nt.Unlock() defer nt.Unlock()
@ -235,7 +235,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) {
cfnick, skeleton := nt.client.uniqueIdentifiers() cfnick, skeleton := nt.client.uniqueIdentifiers()
account := nt.client.Account() account := nt.client.Account()
accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton) accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton)
enforceTimeout := method == NickReservationWithTimeout enforceTimeout := method == NickEnforcementWithTimeout
var shouldWarn, shouldRename bool var shouldWarn, shouldRename bool
@ -258,7 +258,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) {
if enforceTimeout && delinquent && (accountChanged || nt.timer == nil) { if enforceTimeout && delinquent && (accountChanged || nt.timer == nil) {
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout) nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
shouldWarn = true shouldWarn = true
} else if method == NickReservationStrict && delinquent { } else if method == NickEnforcementStrict && delinquent {
shouldRename = true // this can happen if reservation was enabled by rehash shouldRename = true // this can happen if reservation was enabled by rehash
} }
}() }()

View File

@ -5,6 +5,8 @@ package irc
import ( import (
"fmt" "fmt"
"strconv"
"strings"
"time" "time"
"github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircfmt"
@ -25,10 +27,6 @@ func servCmdRequiresNickRes(config *Config) bool {
return config.Accounts.AuthenticationEnabled && config.Accounts.NickReservation.Enabled 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 { func servCmdRequiresBouncerEnabled(config *Config) bool {
return config.Accounts.Bouncer.Enabled return config.Accounts.Bouncer.Enabled
} }
@ -61,20 +59,14 @@ DROP de-links the given (or your current) nickname from your user account.`,
authRequired: true, authRequired: true,
}, },
"enforce": { "enforce": {
hidden: true,
handler: nsEnforceHandler, handler: nsEnforceHandler,
help: `Syntax: $bENFORCE [method]$b help: `Syntax: $bENFORCE [method]$b
ENFORCE lets you specify a custom enforcement mechanism for your registered ENFORCE is an alias for $bGET enforce$b and $bSET enforce$b. See the help
nicknames. Your options are: entry for $bSET$b for more information.`,
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.`,
authRequired: true, authRequired: true,
enabled: nsEnforceEnabled, enabled: servCmdRequiresAccreg,
}, },
"ghost": { "ghost": {
handler: nsGhostHandler, handler: nsGhostHandler,
@ -194,12 +186,257 @@ password by supplying their username and then the desired password.`,
enabled: servCmdRequiresAuthEnabled, enabled: servCmdRequiresAuthEnabled,
minParams: 2, 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,
help: `Syntax $bSET <setting> <value>$b
Set modifies your account settings. The following settings ara 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'.
`,
helpShort: `$bSET$b modifies your account settings`,
authRequired: true,
enabled: servCmdRequiresAccreg,
minParams: 2,
},
"saset": {
handler: nsSetHandler,
help: `Syntax: $bSASET <account> <setting> <value>$b`,
helpShort: `$bSASET$b modifies another user's account settings`,
enabled: servCmdRequiresAccreg,
minParams: 3,
capabs: []string{"accreg"},
},
} }
) )
// nsNotice sends the client a notice from NickServ // nsNotice sends the client a notice from NickServ
func nsNotice(rb *ResponseBuffer, text string) { 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 stringToBool(str string) (result bool, err error) {
switch strings.ToLower(str) {
case "on":
result = true
case "off":
result = false
case "true":
result = true
case "false":
result = false
default:
err = errInvalidParams
}
return
}
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 = 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 = 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) { func nsDropHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
@ -568,22 +805,12 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
} }
func nsEnforceHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { func nsEnforceHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
newParams := []string{"enforce"}
if len(params) == 0 { if len(params) == 0 {
status := server.accounts.getStoredEnforcementStatus(client.Account()) nsGetHandler(server, client, "get", newParams, rb)
nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status))
} else { } else {
method, err := nickReservationFromString(params[0]) newParams = append(newParams, params[0])
if err != nil { nsSetHandler(server, client, "set", newParams, rb)
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"))
}
} }
} }

View File

@ -18,6 +18,7 @@ import (
type ircService struct { type ircService struct {
Name string Name string
ShortName string ShortName string
prefix string
CommandAliases []string CommandAliases []string
Commands map[string]*serviceCommand Commands map[string]*serviceCommand
HelpBanner string HelpBanner string
@ -31,6 +32,7 @@ type serviceCommand struct {
help string help string
helpShort string helpShort string
authRequired bool authRequired bool
hidden bool
enabled func(*Config) bool // is this command enabled in the server config? enabled func(*Config) bool // is this command enabled in the server config?
minParams int minParams int
maxParams int // split into at most n params, with last param containing remaining unsplit text maxParams int // split into at most n params, with last param containing remaining unsplit text
@ -139,7 +141,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) { func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) {
nick := rb.target.Nick() nick := rb.target.Nick()
sendNotice := func(notice string) { sendNotice := func(notice string) {
rb.Add(nil, service.Name, "NOTICE", nick, notice) rb.Add(nil, service.prefix, "NOTICE", nick, notice)
} }
if cmd == nil { if cmd == nil {
@ -180,7 +182,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
nick := rb.target.Nick() nick := rb.target.Nick()
config := server.Config() config := server.Config()
sendNotice := func(notice string) { 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))) sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
@ -194,7 +196,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) { if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
continue continue
} }
if commandInfo.aliasOf != "" { if commandInfo.aliasOf != "" || commandInfo.hidden {
continue // don't show help lines for aliases continue // don't show help lines for aliases
} }
if commandInfo.enabled != nil && !commandInfo.enabled(config) { if commandInfo.enabled != nil && !commandInfo.enabled(config) {
@ -241,6 +243,8 @@ func initializeServices() {
oragonoServicesByCommandAlias = make(map[string]*ircService) oragonoServicesByCommandAlias = make(map[string]*ircService)
for serviceName, service := range OragonoServices { for serviceName, service := range OragonoServices {
service.prefix = fmt.Sprintf("%s!%s@localhost", service.Name, service.Name)
// make `/MSG ServiceName HELP` work correctly // make `/MSG ServiceName HELP` work correctly
service.Commands["help"] = &servHelpCmd service.Commands["help"] = &servHelpCmd
@ -257,7 +261,7 @@ func initializeServices() {
// force devs to write a help entry for every command // force devs to write a help entry for every command
for commandName, commandInfo := range service.Commands { for commandName, commandInfo := range service.Commands {
if commandInfo.aliasOf == "" && (commandInfo.help == "" || commandInfo.helpShort == "") { if commandInfo.aliasOf == "" && !commandInfo.hidden && (commandInfo.help == "" || commandInfo.helpShort == "") {
log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName)) log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
} }
} }

View File

@ -134,7 +134,7 @@ server:
# defaults to true when unset for that reason. # defaults to true when unset for that reason.
force-trailing: true 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: # respond correctly to SASL messages with the server name as a prefix:
# https://github.com/znc/znc/issues/1212 # https://github.com/znc/znc/issues/1212
# this works around that bug, allowing them to use SASL. # this works around that bug, allowing them to use SASL.