diff --git a/irc/accounts.go b/irc/accounts.go index f5a06d15..4e782130 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -40,8 +40,9 @@ const ( keyAccountChannels = "account.channels %s" // channels registered to the account keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined keyAccountLastSeen = "account.lastseen %s" - keyAccountModes = "account.modes %s" // user modes for the always-on client as a string - keyAccountRealname = "account.realname %s" // client realname stored as string + keyAccountModes = "account.modes %s" // user modes for the always-on client as a string + keyAccountRealname = "account.realname %s" // client realname stored as string + keyAccountSuspended = "account.suspended %s" // client realname stored as string maxCertfpsPerAccount = 5 ) @@ -117,7 +118,7 @@ func (am *AccountManager) createAlwaysOnClients(config *Config) { for _, accountName := range accounts { account, err := am.LoadAccount(accountName) - if err == nil && account.Verified && + if err == nil && (account.Verified && account.Suspended == nil) && persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) { am.server.AddAlwaysOnClient( account, @@ -1035,6 +1036,9 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou if !account.Verified { err = errAccountUnverified return + } else if account.Suspended != nil { + err = errAccountSuspended + return } switch account.Credentials.Version { @@ -1230,6 +1234,15 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName str am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error()) } } + if raw.Suspended != "" { + sus := new(AccountSuspension) + e := json.Unmarshal([]byte(raw.Suspended), sus) + if e != nil { + am.server.logger.Error("internal", "corrupt suspension data", result.Name, e.Error()) + } else { + result.Suspended = sus + } + } return } @@ -1243,6 +1256,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount) + suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount) _, e := tx.Get(accountKey) if e == buntdb.ErrNotFound { @@ -1257,6 +1271,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string result.AdditionalNicks, _ = tx.Get(nicksKey) result.VHost, _ = tx.Get(vhostKey) result.Settings, _ = tx.Get(settingsKey) + result.Suspended, _ = tx.Get(suspendedKey) if _, e = tx.Get(verifiedKey); e == nil { result.Verified = true @@ -1265,20 +1280,44 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string return } -func (am *AccountManager) Suspend(accountName string) (err error) { +type AccountSuspension struct { + AccountName string `json:"AccountName,omitempty"` + TimeCreated time.Time + Duration time.Duration + OperName string + Reason string +} + +func (am *AccountManager) Suspend(accountName string, duration time.Duration, operName, reason string) (err error) { account, err := CasefoldName(accountName) if err != nil { return errAccountDoesNotExist } + suspension := AccountSuspension{ + TimeCreated: time.Now().UTC(), + Duration: duration, + OperName: operName, + Reason: reason, + } + suspensionStr, err := json.Marshal(suspension) + if err != nil { + am.server.logger.Error("internal", "suspension json unserializable", err.Error()) + return errAccountDoesNotExist + } + existsKey := fmt.Sprintf(keyAccountExists, account) - verifiedKey := fmt.Sprintf(keyAccountVerified, account) + suspensionKey := fmt.Sprintf(keyAccountSuspended, account) + var setOptions *buntdb.SetOptions + if duration != time.Duration(0) { + setOptions = &buntdb.SetOptions{Expires: true, TTL: duration} + } err = am.server.store.Update(func(tx *buntdb.Tx) error { _, err := tx.Get(existsKey) if err != nil { return errAccountDoesNotExist } - _, err = tx.Delete(verifiedKey) + _, _, err = tx.Set(suspensionKey, string(suspensionStr), setOptions) return err }) @@ -1293,7 +1332,13 @@ func (am *AccountManager) Suspend(accountName string) (err error) { delete(am.accountToClients, account) am.Unlock() - am.killClients(clients) + // kill clients, sending them the reason + suspension.AccountName = accountName + for _, client := range clients { + client.Logout() + client.Quit(suspensionToString(client, suspension), nil) + client.destroy(nil) + } return nil } @@ -1312,20 +1357,53 @@ func (am *AccountManager) Unsuspend(account string) (err error) { } existsKey := fmt.Sprintf(keyAccountExists, cfaccount) - verifiedKey := fmt.Sprintf(keyAccountVerified, cfaccount) + suspensionKey := fmt.Sprintf(keyAccountSuspended, account) err = am.server.store.Update(func(tx *buntdb.Tx) error { _, err := tx.Get(existsKey) if err != nil { return errAccountDoesNotExist } - tx.Set(verifiedKey, "1", nil) + _, err = tx.Delete(suspensionKey) + if err != nil { + return errNoop + } return nil }) - if err != nil { - return errAccountDoesNotExist + return err +} + +func (am *AccountManager) ListSuspended() (result []AccountSuspension) { + var names []string + var raw []string + + prefix := fmt.Sprintf(keyAccountSuspended, "") + am.server.store.View(func(tx *buntdb.Tx) error { + err := tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + raw = append(raw, value) + cfname := strings.TrimPrefix(key, prefix) + name, _ := tx.Get(fmt.Sprintf(keyAccountName, cfname)) + names = append(names, name) + return true + }) + return err + }) + + result = make([]AccountSuspension, 0, len(raw)) + for i := 0; i < len(raw); i++ { + var sus AccountSuspension + err := json.Unmarshal([]byte(raw[i]), &sus) + if err != nil { + am.server.logger.Error("internal", "corrupt data for suspension", names[i], err.Error()) + continue + } + sus.AccountName = names[i] + result = append(result, sus) } - return nil + return } func (am *AccountManager) Unregister(account string, erase bool) error { @@ -1351,6 +1429,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error { unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount) modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount) realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount) + suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount) var clients []*Client defer func() { @@ -1410,6 +1489,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error { tx.Delete(lastSeenKey) tx.Delete(modesKey) tx.Delete(realnameKey) + tx.Delete(suspendedKey) return nil }) @@ -1491,6 +1571,9 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin } else if !clientAccount.Verified { err = errAccountUnverified return + } else if clientAccount.Suspended != nil { + err = errAccountSuspended + return } // TODO(#1109) clean this check up? if client.registered { @@ -1882,6 +1965,7 @@ type ClientAccount struct { RegisteredAt time.Time Credentials AccountCredentials Verified bool + Suspended *AccountSuspension AdditionalNicks []string VHost VHostInfo Settings AccountSettings @@ -1897,4 +1981,5 @@ type rawClientAccount struct { AdditionalNicks string VHost string Settings string + Suspended string } diff --git a/irc/database.go b/irc/database.go index 06968652..da46e822 100644 --- a/irc/database.go +++ b/irc/database.go @@ -24,7 +24,7 @@ const ( // 'version' of the database schema keySchemaVersion = "db.version" // latest schema of the db - latestDbSchema = "17" + latestDbSchema = 18 keyCloakSecret = "crypto.cloak_secret" ) @@ -32,14 +32,11 @@ const ( type SchemaChanger func(*Config, *buntdb.Tx) error type SchemaChange struct { - InitialVersion string // the change will take this version - TargetVersion string // and transform it into this version + InitialVersion int // the change will take this version + TargetVersion int // and transform it into this version Changer SchemaChanger } -// maps an initial version to a schema change capable of upgrading it -var schemaChanges map[string]SchemaChange - func checkDBReadyForInit(path string) error { _, err := os.Stat(path) if err == nil { @@ -72,7 +69,7 @@ func initializeDB(path string) error { err = store.Update(func(tx *buntdb.Tx) error { // set schema version - tx.Set(keySchemaVersion, latestDbSchema, nil) + tx.Set(keySchemaVersion, strconv.Itoa(latestDbSchema), nil) tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) return nil }) @@ -100,9 +97,12 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, }() // read the current version string - var version string - err = db.View(func(tx *buntdb.Tx) error { - version, err = tx.Get(keySchemaVersion) + var version int + err = db.View(func(tx *buntdb.Tx) (err error) { + vStr, err := tx.Get(keySchemaVersion) + if err == nil { + version, err = strconv.Atoi(vStr) + } return err }) if err != nil { @@ -130,11 +130,11 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, } } -func performAutoUpgrade(currentVersion string, config *Config) (err error) { +func performAutoUpgrade(currentVersion int, config *Config) (err error) { path := config.Datastore.Path - log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema) + log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema) timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z") - backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp) + backupPath := fmt.Sprintf("%s.v%d.%s.bak", path, currentVersion, timestamp) log.Printf("making a backup of current database at %s\n", backupPath) err = utils.CopyFile(path, backupPath) if err != nil { @@ -164,29 +164,30 @@ func UpgradeDB(config *Config) (err error) { } defer store.Close() - var version string + var version int err = store.Update(func(tx *buntdb.Tx) error { for { - version, _ = tx.Get(keySchemaVersion) - change, schemaNeedsChange := schemaChanges[version] - if !schemaNeedsChange { - if version == latestDbSchema { - // success! - break - } + vStr, _ := tx.Get(keySchemaVersion) + version, _ = strconv.Atoi(vStr) + if version == latestDbSchema { + // success! + break + } + change, ok := getSchemaChange(version) + if !ok { // unable to upgrade to the desired version, roll back return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema} } - log.Println("attempting to update schema from version " + version) + log.Printf("attempting to update schema from version %d\n", version) err := change.Changer(config, tx) if err != nil { return err } - _, _, err = tx.Set(keySchemaVersion, change.TargetVersion, nil) + _, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil) if err != nil { return err } - log.Println("successfully updated schema to version " + change.TargetVersion) + log.Printf("successfully updated schema to version %d\n", change.TargetVersion) } return nil }) @@ -853,93 +854,148 @@ func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error { return nil } -func init() { - allChanges := []SchemaChange{ - { - InitialVersion: "1", - TargetVersion: "2", - Changer: schemaChangeV1toV2, - }, - { - InitialVersion: "2", - TargetVersion: "3", - Changer: schemaChangeV2ToV3, - }, - { - InitialVersion: "3", - TargetVersion: "4", - Changer: schemaChangeV3ToV4, - }, - { - InitialVersion: "4", - TargetVersion: "5", - Changer: schemaChangeV4ToV5, - }, - { - InitialVersion: "5", - TargetVersion: "6", - Changer: schemaChangeV5ToV6, - }, - { - InitialVersion: "6", - TargetVersion: "7", - Changer: schemaChangeV6ToV7, - }, - { - InitialVersion: "7", - TargetVersion: "8", - Changer: schemaChangeV7ToV8, - }, - { - InitialVersion: "8", - TargetVersion: "9", - Changer: schemaChangeV8ToV9, - }, - { - InitialVersion: "9", - TargetVersion: "10", - Changer: schemaChangeV9ToV10, - }, - { - InitialVersion: "10", - TargetVersion: "11", - Changer: schemaChangeV10ToV11, - }, - { - InitialVersion: "11", - TargetVersion: "12", - Changer: schemaChangeV11ToV12, - }, - { - InitialVersion: "12", - TargetVersion: "13", - Changer: schemaChangeV12ToV13, - }, - { - InitialVersion: "13", - TargetVersion: "14", - Changer: schemaChangeV13ToV14, - }, - { - InitialVersion: "14", - TargetVersion: "15", - Changer: schemaChangeV14ToV15, - }, - { - InitialVersion: "15", - TargetVersion: "16", - Changer: schemaChangeV15ToV16, - }, - { - InitialVersion: "16", - TargetVersion: "17", - Changer: schemaChangeV16ToV17, - }, +// #1274: we used to suspend accounts by deleting their "verified" key, +// now we save some metadata under a new key +func schemaChangeV17ToV18(config *Config, tx *buntdb.Tx) error { + now := time.Now().UTC() + + exists := "account.exists " + suspended := "account.suspended " + verif := "account.verified " + verifCode := "account.verificationcode " + + var accounts []string + + tx.AscendGreaterOrEqual("", exists, func(key, value string) bool { + if !strings.HasPrefix(key, exists) { + return false + } + account := strings.TrimPrefix(key, exists) + _, verifiedErr := tx.Get(verif + account) + _, verifCodeErr := tx.Get(verifCode + account) + if verifiedErr != nil && verifCodeErr != nil { + // verified key not present, but there's no code either, + // this is a suspension + accounts = append(accounts, account) + } + return true + }) + + type accountSuspensionV18 struct { + TimeCreated time.Time + Duration time.Duration + OperName string + Reason string } - // build the index - schemaChanges = make(map[string]SchemaChange) - for _, change := range allChanges { - schemaChanges[change.InitialVersion] = change + for _, account := range accounts { + var sus accountSuspensionV18 + sus.TimeCreated = now + sus.OperName = "*" + sus.Reason = "[unknown]" + susBytes, err := json.Marshal(sus) + if err != nil { + return err + } + tx.Set(suspended+account, string(susBytes), nil) } + + return nil +} + +func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) { + for _, change := range allChanges { + if initialVersion == change.InitialVersion { + return change, true + } + } + return +} + +var allChanges = []SchemaChange{ + { + InitialVersion: 1, + TargetVersion: 2, + Changer: schemaChangeV1toV2, + }, + { + InitialVersion: 2, + TargetVersion: 3, + Changer: schemaChangeV2ToV3, + }, + { + InitialVersion: 3, + TargetVersion: 4, + Changer: schemaChangeV3ToV4, + }, + { + InitialVersion: 4, + TargetVersion: 5, + Changer: schemaChangeV4ToV5, + }, + { + InitialVersion: 5, + TargetVersion: 6, + Changer: schemaChangeV5ToV6, + }, + { + InitialVersion: 6, + TargetVersion: 7, + Changer: schemaChangeV6ToV7, + }, + { + InitialVersion: 7, + TargetVersion: 8, + Changer: schemaChangeV7ToV8, + }, + { + InitialVersion: 8, + TargetVersion: 9, + Changer: schemaChangeV8ToV9, + }, + { + InitialVersion: 9, + TargetVersion: 10, + Changer: schemaChangeV9ToV10, + }, + { + InitialVersion: 10, + TargetVersion: 11, + Changer: schemaChangeV10ToV11, + }, + { + InitialVersion: 11, + TargetVersion: 12, + Changer: schemaChangeV11ToV12, + }, + { + InitialVersion: 12, + TargetVersion: 13, + Changer: schemaChangeV12ToV13, + }, + { + InitialVersion: 13, + TargetVersion: 14, + Changer: schemaChangeV13ToV14, + }, + { + InitialVersion: 14, + TargetVersion: 15, + Changer: schemaChangeV14ToV15, + }, + { + InitialVersion: 15, + TargetVersion: 16, + Changer: schemaChangeV15ToV16, + }, + { + InitialVersion: 16, + TargetVersion: 17, + Changer: schemaChangeV16ToV17, + }, + { + InitialVersion: 17, + TargetVersion: 18, + Changer: schemaChangeV17ToV18, + }, } diff --git a/irc/errors.go b/irc/errors.go index aa64e760..e0d0361f 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -28,6 +28,7 @@ var ( errAccountAlreadyLoggedIn = errors.New("You're already logged into an account") errAccountTooManyNicks = errors.New("Account has too many reserved nicks") errAccountUnverified = errors.New(`Account is not yet verified`) + errAccountSuspended = errors.New(`Account has been suspended`) errAccountVerificationFailed = errors.New("Account verification failed") errAccountVerificationInvalidCode = errors.New("Invalid account verification code") errAccountUpdateFailed = errors.New(`Error while updating your account information`) diff --git a/irc/handlers.go b/irc/handlers.go index 4c13f47a..1a79df9f 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -267,7 +267,7 @@ func authErrorToMessage(server *Server, err error) (msg string) { } switch err { - case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch: + case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch, errAccountSuspended: return err.Error() default: // don't expose arbitrary error messages to the user diff --git a/irc/import.go b/irc/import.go index 1499e15b..3f405eb2 100644 --- a/irc/import.go +++ b/irc/import.go @@ -15,6 +15,14 @@ import ( "github.com/oragono/oragono/irc/utils" ) +const ( + // produce a hardcoded version of the database schema + // XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal + // (to ensure that no matter what code changes happen elsewhere, we're still producing a + // db of the hardcoded version) + importDBSchemaVersion = 18 +) + type userImport struct { Name string Hash string @@ -66,11 +74,7 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden return fmt.Errorf("unsupported version of the db for import: version %d is required", requiredVersion) } - // produce a hardcoded version of the database schema - // XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal - // (to ensure that no matter what code changes happen elsewhere, we're still producing a - // db of the hardcoded version) - tx.Set(keySchemaVersion, "17", nil) + tx.Set(keySchemaVersion, strconv.Itoa(importDBSchemaVersion), nil) tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) for username, userInfo := range dbImport.Users { diff --git a/irc/mysql/history.go b/irc/mysql/history.go index 5a6cb000..1e055675 100644 --- a/irc/mysql/history.go +++ b/irc/mysql/history.go @@ -142,7 +142,7 @@ func (mysql *MySQL) fixSchemas() (err error) { return } else if err == nil && schema != latestDbSchema { // TODO figure out what to do about schema changes - return &utils.IncompatibleSchemaError{CurrentVersion: schema, RequiredVersion: latestDbSchema} + return fmt.Errorf("incompatible schema: got %s, expected %s", schema, latestDbSchema) } else if err != nil { return err } diff --git a/irc/nickserv.go b/irc/nickserv.go index 605a7a5a..9bef8fde 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -6,12 +6,14 @@ package irc import ( "fmt" "regexp" + "sort" "strconv" "strings" "time" "github.com/goshuirc/irc-go/ircfmt" + "github.com/oragono/oragono/irc/custime" "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/utils" @@ -333,19 +335,15 @@ example with $bCERT ADD $b.`, }, "suspend": { handler: nsSuspendHandler, - help: `Syntax: $bSUSPEND $b + help: `Syntax: $bSUSPEND ADD [DURATION duration] [reason]$b + $bSUSPEND DEL $b + $bSUSPEND LIST$b -SUSPEND disables an account and disconnects the associated clients.`, - helpShort: `$bSUSPEND$b disables an account and disconnects the clients`, - minParams: 1, - capabs: []string{"accreg"}, - }, - "unsuspend": { - handler: nsUnsuspendHandler, - help: `Syntax: $bUNSUSPEND $b - -UNSUSPEND reverses a previous SUSPEND, restoring access to the account.`, - helpShort: `$bUNSUSPEND$b restores access to a suspended account`, +Suspending an account disables it (preventing new logins) and disconnects +all associated clients. You can specify a time limit or a reason for +the suspension. The $bDEL$b subcommand reverses a suspension, and the $bLIST$b +command lists all current suspensions.`, + helpShort: `$bSUSPEND$b adds or removes an account suspension`, minParams: 1, capabs: []string{"accreg"}, }, @@ -810,6 +808,9 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri for _, channel := range server.accounts.ChannelsForAccount(accountName) { nsNotice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel)) } + if account.Suspended != nil { + nsNotice(rb, suspensionToString(client, *account.Suspended)) + } } func nsRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { @@ -1276,10 +1277,52 @@ func nsCertHandler(server *Server, client *Client, command string, params []stri } func nsSuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { - err := server.accounts.Suspend(params[0]) + subCmd := strings.ToLower(params[0]) + params = params[1:] + switch subCmd { + case "add": + nsSuspendAddHandler(server, client, command, params, rb) + case "del", "delete", "remove": + nsSuspendRemoveHandler(server, client, command, params, rb) + case "list": + nsSuspendListHandler(server, client, command, params, rb) + default: + nsNotice(rb, client.t("Invalid parameters")) + } +} + +func nsSuspendAddHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + if len(params) == 0 { + nsNotice(rb, client.t("Invalid parameters")) + return + } + + account := params[0] + params = params[1:] + + var duration time.Duration + if 2 <= len(params) && strings.ToLower(params[0]) == "duration" { + var err error + cDuration, err := custime.ParseDuration(params[1]) + if err != nil { + nsNotice(rb, client.t("Invalid time duration for NS SUSPEND")) + return + } + duration = time.Duration(cDuration) + params = params[2:] + } + + var reason string + if len(params) != 0 { + reason = strings.Join(params, " ") + } + + name := client.Oper().Name + + err := server.accounts.Suspend(account, duration, name, reason) switch err { case nil: - nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), params[0])) + nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), account)) case errAccountDoesNotExist: nsNotice(rb, client.t("No such account")) default: @@ -1287,14 +1330,50 @@ func nsSuspendHandler(server *Server, client *Client, command string, params []s } } -func nsUnsuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { +func nsSuspendRemoveHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + if len(params) == 0 { + nsNotice(rb, client.t("Invalid parameters")) + return + } + err := server.accounts.Unsuspend(params[0]) switch err { case nil: nsNotice(rb, fmt.Sprintf(client.t("Successfully un-suspended account %s"), params[0])) case errAccountDoesNotExist: nsNotice(rb, client.t("No such account")) + case errNoop: + nsNotice(rb, client.t("Account was not suspended")) default: nsNotice(rb, client.t("An error occurred")) } } + +// sort in reverse order of creation time +type ByCreationTime []AccountSuspension + +func (a ByCreationTime) Len() int { return len(a) } +func (a ByCreationTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) } + +func nsSuspendListHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + suspensions := server.accounts.ListSuspended() + sort.Sort(ByCreationTime(suspensions)) + nsNotice(rb, fmt.Sprintf(client.t("There are %d active suspensions."), len(suspensions))) + for _, suspension := range suspensions { + nsNotice(rb, suspensionToString(client, suspension)) + } +} + +func suspensionToString(client *Client, suspension AccountSuspension) (result string) { + duration := client.t("indefinite") + if suspension.Duration != time.Duration(0) { + duration = suspension.Duration.String() + } + ts := suspension.TimeCreated.Format(time.RFC1123) + reason := client.t("No reason given.") + if suspension.Reason != "" { + reason = fmt.Sprintf(client.t("Reason: %s"), suspension.Reason) + } + return fmt.Sprintf(client.t("Account %s suspended at %s. Duration: %s. %s"), suspension.AccountName, ts, duration, reason) +} diff --git a/irc/utils/args.go b/irc/utils/args.go index ea3f0c96..20884ef3 100644 --- a/irc/utils/args.go +++ b/irc/utils/args.go @@ -71,12 +71,12 @@ func SafeErrorParam(param string) string { } type IncompatibleSchemaError struct { - CurrentVersion string - RequiredVersion string + CurrentVersion int + RequiredVersion int } func (err *IncompatibleSchemaError) Error() string { - return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.RequiredVersion, err.CurrentVersion) + return fmt.Sprintf("Database requires update. Expected schema v%d, got v%d", err.RequiredVersion, err.CurrentVersion) } func NanoToTimestamp(nanotime int64) string {