Add per-user default nickname

The soju username is immutable. Add a separate nickname setting so
that users can change their nickname for all networks.

References: https://todo.sr.ht/~emersion/soju/110
This commit is contained in:
Simon Ser 2022-07-08 18:01:05 +02:00
parent 14cbd63412
commit dc0a847240
7 changed files with 109 additions and 48 deletions

View File

@ -67,6 +67,7 @@ type User struct {
ID int64 ID int64
Username string Username string
Password string // hashed Password string // hashed
Nick string
Realname string Realname string
Admin bool Admin bool
} }
@ -150,6 +151,9 @@ func GetNick(user *User, net *Network) string {
if net != nil && net.Nick != "" { if net != nil && net.Nick != "" {
return net.Nick return net.Nick
} }
if user.Nick != "" {
return user.Nick
}
return user.Username return user.Username
} }

View File

@ -30,6 +30,7 @@ CREATE TABLE "User" (
username VARCHAR(255) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255), password VARCHAR(255),
admin BOOLEAN NOT NULL DEFAULT FALSE, admin BOOLEAN NOT NULL DEFAULT FALSE,
nick VARCHAR(255),
realname VARCHAR(255) realname VARCHAR(255)
); );
@ -153,6 +154,7 @@ var postgresMigrations = []string{
ADD COLUMN "user" INTEGER ADD COLUMN "user" INTEGER
REFERENCES "User"(id) ON DELETE CASCADE REFERENCES "User"(id) ON DELETE CASCADE
`, `,
`ALTER TABLE "User" ADD COLUMN nick VARCHAR(255)`,
} }
type PostgresDB struct { type PostgresDB struct {
@ -282,7 +284,7 @@ func (db *PostgresDB) ListUsers(ctx context.Context) ([]User, error) {
defer cancel() defer cancel()
rows, err := db.db.QueryContext(ctx, rows, err := db.db.QueryContext(ctx,
`SELECT id, username, password, admin, realname FROM "User"`) `SELECT id, username, password, admin, nick, realname FROM "User"`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -291,11 +293,12 @@ func (db *PostgresDB) ListUsers(ctx context.Context) ([]User, error) {
var users []User var users []User
for rows.Next() { for rows.Next() {
var user User var user User
var password, realname sql.NullString var password, nick, realname sql.NullString
if err := rows.Scan(&user.ID, &user.Username, &password, &user.Admin, &realname); err != nil { if err := rows.Scan(&user.ID, &user.Username, &password, &user.Admin, &nick, &realname); err != nil {
return nil, err return nil, err
} }
user.Password = password.String user.Password = password.String
user.Nick = nick.String
user.Realname = realname.String user.Realname = realname.String
users = append(users, user) users = append(users, user)
} }
@ -312,14 +315,15 @@ func (db *PostgresDB) GetUser(ctx context.Context, username string) (*User, erro
user := &User{Username: username} user := &User{Username: username}
var password, realname sql.NullString var password, nick, realname sql.NullString
row := db.db.QueryRowContext(ctx, row := db.db.QueryRowContext(ctx,
`SELECT id, password, admin, realname FROM "User" WHERE username = $1`, `SELECT id, password, admin, nick, realname FROM "User" WHERE username = $1`,
username) username)
if err := row.Scan(&user.ID, &password, &user.Admin, &realname); err != nil { if err := row.Scan(&user.ID, &password, &user.Admin, &nick, &realname); err != nil {
return nil, err return nil, err
} }
user.Password = password.String user.Password = password.String
user.Nick = nick.String
user.Realname = realname.String user.Realname = realname.String
return user, nil return user, nil
} }
@ -329,21 +333,22 @@ func (db *PostgresDB) StoreUser(ctx context.Context, user *User) error {
defer cancel() defer cancel()
password := toNullString(user.Password) password := toNullString(user.Password)
nick := toNullString(user.Nick)
realname := toNullString(user.Realname) realname := toNullString(user.Realname)
var err error var err error
if user.ID == 0 { if user.ID == 0 {
err = db.db.QueryRowContext(ctx, ` err = db.db.QueryRowContext(ctx, `
INSERT INTO "User" (username, password, admin, realname) INSERT INTO "User" (username, password, admin, nick, realname)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
RETURNING id`, RETURNING id`,
user.Username, password, user.Admin, realname).Scan(&user.ID) user.Username, password, user.Admin, nick, realname).Scan(&user.ID)
} else { } else {
_, err = db.db.ExecContext(ctx, ` _, err = db.db.ExecContext(ctx, `
UPDATE "User" UPDATE "User"
SET password = $1, admin = $2, realname = $3 SET password = $1, admin = $2, nick = $3, realname = $4
WHERE id = $4`, WHERE id = $5`,
password, user.Admin, realname, user.ID) password, user.Admin, nick, realname, user.ID)
} }
return err return err
} }

View File

@ -27,7 +27,8 @@ CREATE TABLE User (
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
password TEXT, password TEXT,
admin INTEGER NOT NULL DEFAULT 0, admin INTEGER NOT NULL DEFAULT 0,
realname TEXT realname TEXT,
nick TEXT
); );
CREATE TABLE Network ( CREATE TABLE Network (
@ -243,6 +244,7 @@ var sqliteMigrations = []string{
ALTER TABLE WebPushSubscription ADD COLUMN user INTEGER REFERENCES User(id); ALTER TABLE WebPushSubscription ADD COLUMN user INTEGER REFERENCES User(id);
UPDATE WebPushSubscription AS wps SET user = (SELECT n.user FROM Network AS n WHERE n.id = wps.network); UPDATE WebPushSubscription AS wps SET user = (SELECT n.user FROM Network AS n WHERE n.id = wps.network);
`, `,
"ALTER TABLE User ADD COLUMN nick TEXT;",
} }
type SqliteDB struct { type SqliteDB struct {
@ -349,7 +351,7 @@ func (db *SqliteDB) ListUsers(ctx context.Context) ([]User, error) {
defer cancel() defer cancel()
rows, err := db.db.QueryContext(ctx, rows, err := db.db.QueryContext(ctx,
"SELECT id, username, password, admin, realname FROM User") "SELECT id, username, password, admin, nick, realname FROM User")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -358,11 +360,12 @@ func (db *SqliteDB) ListUsers(ctx context.Context) ([]User, error) {
var users []User var users []User
for rows.Next() { for rows.Next() {
var user User var user User
var password, realname sql.NullString var password, nick, realname sql.NullString
if err := rows.Scan(&user.ID, &user.Username, &password, &user.Admin, &realname); err != nil { if err := rows.Scan(&user.ID, &user.Username, &password, &user.Admin, &nick, &realname); err != nil {
return nil, err return nil, err
} }
user.Password = password.String user.Password = password.String
user.Nick = nick.String
user.Realname = realname.String user.Realname = realname.String
users = append(users, user) users = append(users, user)
} }
@ -379,14 +382,15 @@ func (db *SqliteDB) GetUser(ctx context.Context, username string) (*User, error)
user := &User{Username: username} user := &User{Username: username}
var password, realname sql.NullString var password, nick, realname sql.NullString
row := db.db.QueryRowContext(ctx, row := db.db.QueryRowContext(ctx,
"SELECT id, password, admin, realname FROM User WHERE username = ?", "SELECT id, password, admin, nick, realname FROM User WHERE username = ?",
username) username)
if err := row.Scan(&user.ID, &password, &user.Admin, &realname); err != nil { if err := row.Scan(&user.ID, &password, &user.Admin, &nick, &realname); err != nil {
return nil, err return nil, err
} }
user.Password = password.String user.Password = password.String
user.Nick = nick.String
user.Realname = realname.String user.Realname = realname.String
return user, nil return user, nil
} }
@ -399,21 +403,22 @@ func (db *SqliteDB) StoreUser(ctx context.Context, user *User) error {
sql.Named("username", user.Username), sql.Named("username", user.Username),
sql.Named("password", toNullString(user.Password)), sql.Named("password", toNullString(user.Password)),
sql.Named("admin", user.Admin), sql.Named("admin", user.Admin),
sql.Named("nick", toNullString(user.Nick)),
sql.Named("realname", toNullString(user.Realname)), sql.Named("realname", toNullString(user.Realname)),
} }
var err error var err error
if user.ID != 0 { if user.ID != 0 {
_, err = db.db.ExecContext(ctx, ` _, err = db.db.ExecContext(ctx, `
UPDATE User SET password = :password, admin = :admin, UPDATE User SET password = :password, admin = :admin, nick = :nick,
realname = :realname WHERE username = :username`, realname = :realname WHERE username = :username`,
args...) args...)
} else { } else {
var res sql.Result var res sql.Result
res, err = db.db.ExecContext(ctx, ` res, err = db.db.ExecContext(ctx, `
INSERT INTO INSERT INTO
User(username, password, admin, realname) User(username, password, admin, nick, realname)
VALUES (:username, :password, :admin, :realname)`, VALUES (:username, :password, :admin, :nick, :realname)`,
args...) args...)
if err != nil { if err != nil {
return err return err

View File

@ -416,6 +416,10 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just
*-admin* true|false *-admin* true|false
Make the new user an administrator. Make the new user an administrator.
*-nick* <nick>
Set the user's nickname. This is used as a fallback if there is no
nickname set for a network.
*-realname* <realname> *-realname* <realname>
Set the user's realname. This is used as a fallback if there is no Set the user's realname. This is used as a fallback if there is no
realname set for a network. realname set for a network.
@ -429,7 +433,8 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just
Not all flags are valid in all contexts: Not all flags are valid in all contexts:
- The _-username_ flag is never valid, usernames are immutable. - The _-username_ flag is never valid, usernames are immutable.
- The _-realname_ flag is only valid when updating the current user. - The _-nick_ and _-realname_ flag are only valid when updating the current
user.
- The _-admin_ flag is only valid when updating another user. - The _-admin_ flag is only valid when updating another user.
*user delete* <username> *user delete* <username>

View File

@ -1798,12 +1798,6 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
return err return err
} }
if dc.network == nil {
return ircError{&irc.Message{
Command: xirc.ERR_UNKNOWNERROR,
Params: []string{dc.nick, "NICK", "Cannot change nickname on the bouncer connection"},
}}
}
if nick == "" || strings.ContainsAny(nick, illegalNickChars) { if nick == "" || strings.ContainsAny(nick, illegalNickChars) {
return ircError{&irc.Message{ return ircError{&irc.Message{
Command: irc.ERR_ERRONEUSNICKNAME, Command: irc.ERR_ERRONEUSNICKNAME,
@ -1817,25 +1811,45 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
}} }}
} }
record := dc.network.Network var err error
record.Nick = nick if dc.network != nil {
if err := dc.srv.db.StoreNetwork(ctx, dc.user.ID, &record); err != nil { record := dc.network.Network
return err record.Nick = nick
err = dc.srv.db.StoreNetwork(ctx, dc.user.ID, &record)
} else {
record := dc.user.User
record.Nick = nick
err = dc.user.updateUser(ctx, &record)
}
if err != nil {
dc.logger.Printf("failed to update nick: %v", err)
return ircError{&irc.Message{
Command: xirc.ERR_UNKNOWNERROR,
Params: []string{dc.nick, "NICK", "Failed to update nick"},
}}
} }
if uc := dc.upstream(); uc != nil { if dc.network != nil {
uc.SendMessageLabeled(ctx, dc.id, &irc.Message{ if uc := dc.upstream(); uc != nil {
Command: "NICK", uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
Params: []string{nick}, Command: "NICK",
}) Params: []string{nick},
})
} else {
dc.SendMessage(&irc.Message{
Prefix: dc.prefix(),
Command: "NICK",
Params: []string{nick},
})
dc.nick = nick
dc.nickCM = casemapASCII(dc.nick)
}
} else { } else {
dc.SendMessage(&irc.Message{ for _, c := range dc.user.downstreamConns {
Prefix: dc.prefix(), if c.network == nil {
Command: "NICK", c.updateNick()
Params: []string{nick}, }
}) }
dc.nick = nick
dc.nickCM = casemapASCII(dc.nick)
} }
case "SETNAME": case "SETNAME":
var realname string var realname string

View File

@ -817,6 +817,7 @@ func handleUserCreate(ctx context.Context, dc *downstreamConn, params []string)
fs := newFlagSet() fs := newFlagSet()
username := fs.String("username", "", "") username := fs.String("username", "", "")
password := fs.String("password", "", "") password := fs.String("password", "", "")
nick := fs.String("nick", "", "")
realname := fs.String("realname", "", "") realname := fs.String("realname", "", "")
admin := fs.Bool("admin", false, "") admin := fs.Bool("admin", false, "")
@ -832,6 +833,7 @@ func handleUserCreate(ctx context.Context, dc *downstreamConn, params []string)
user := &database.User{ user := &database.User{
Username: *username, Username: *username,
Nick: *nick,
Realname: *realname, Realname: *realname,
Admin: *admin, Admin: *admin,
} }
@ -854,10 +856,11 @@ func popArg(params []string) (string, []string) {
} }
func handleUserUpdate(ctx context.Context, dc *downstreamConn, params []string) error { func handleUserUpdate(ctx context.Context, dc *downstreamConn, params []string) error {
var password, realname *string var password, nick, realname *string
var admin *bool var admin *bool
fs := newFlagSet() fs := newFlagSet()
fs.Var(stringPtrFlag{&password}, "password", "") fs.Var(stringPtrFlag{&password}, "password", "")
fs.Var(stringPtrFlag{&nick}, "nick", "")
fs.Var(stringPtrFlag{&realname}, "realname", "") fs.Var(stringPtrFlag{&realname}, "realname", "")
fs.Var(boolPtrFlag{&admin}, "admin", "") fs.Var(boolPtrFlag{&admin}, "admin", "")
@ -873,6 +876,9 @@ func handleUserUpdate(ctx context.Context, dc *downstreamConn, params []string)
if !dc.user.Admin { if !dc.user.Admin {
return fmt.Errorf("you must be an admin to update other users") return fmt.Errorf("you must be an admin to update other users")
} }
if nick != nil {
return fmt.Errorf("cannot update -nick of other user")
}
if realname != nil { if realname != nil {
return fmt.Errorf("cannot update -realname of other user") return fmt.Errorf("cannot update -realname of other user")
} }
@ -918,6 +924,9 @@ func handleUserUpdate(ctx context.Context, dc *downstreamConn, params []string)
return err return err
} }
} }
if nick != nil {
record.Nick = *nick
}
if realname != nil { if realname != nil {
record.Realname = *realname record.Realname = *realname
} }

23
user.go
View File

@ -933,8 +933,11 @@ func (u *user) updateNetwork(ctx context.Context, record *database.Network) (*ne
panic("tried updating a new network") panic("tried updating a new network")
} }
// If the realname is reset to the default, just wipe the per-network // If the nickname/realname is reset to the default, just wipe the
// setting // per-network setting
if record.Nick == u.Nick {
record.Nick = ""
}
if record.Realname == u.Realname { if record.Realname == u.Realname {
record.Realname = "" record.Realname = ""
} }
@ -1030,12 +1033,28 @@ func (u *user) updateUser(ctx context.Context, record *database.User) error {
panic("ID mismatch when updating user") panic("ID mismatch when updating user")
} }
nickUpdated := u.Nick != record.Nick
realnameUpdated := u.Realname != record.Realname realnameUpdated := u.Realname != record.Realname
if err := u.srv.db.StoreUser(ctx, record); err != nil { if err := u.srv.db.StoreUser(ctx, record); err != nil {
return fmt.Errorf("failed to update user %q: %v", u.Username, err) return fmt.Errorf("failed to update user %q: %v", u.Username, err)
} }
u.User = *record u.User = *record
if nickUpdated {
for _, net := range u.networks {
if net.Nick != "" {
continue
}
if uc := net.conn; uc != nil {
uc.SendMessage(ctx, &irc.Message{
Command: "NICK",
Params: []string{database.GetNick(&u.User, &net.Network)},
})
}
}
}
if realnameUpdated { if realnameUpdated {
// Re-connect to networks which use the default realname // Re-connect to networks which use the default realname
var needUpdate []database.Network var needUpdate []database.Network