Add per-user realname setting

This allows users to set a default realname used if the per-network
realname isn't set.

A new "user update" command is introduced and can be extended to edit
other user properties and other users in the future.
This commit is contained in:
Simon Ser 2021-06-25 20:33:13 +02:00
parent 9a53d4cd08
commit a14f646135
7 changed files with 102 additions and 18 deletions

6
db.go
View File

@ -30,6 +30,7 @@ type User struct {
ID int64 ID int64
Username string Username string
Password string // hashed Password string // hashed
Realname string
Admin bool Admin bool
} }
@ -92,10 +93,13 @@ func (net *Network) GetUsername() string {
return net.Nick return net.Nick
} }
func (net *Network) GetRealname() string { func GetRealname(user *User, net *Network) string {
if net.Realname != "" { if net.Realname != "" {
return net.Realname return net.Realname
} }
if user.Realname != "" {
return user.Realname
}
return net.Nick return net.Nick
} }

View File

@ -16,7 +16,8 @@ CREATE TABLE User (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255), password VARCHAR(255),
admin INTEGER NOT NULL DEFAULT 0 admin INTEGER NOT NULL DEFAULT 0,
realname VARCHAR(255)
); );
CREATE TABLE Network ( CREATE TABLE Network (
@ -133,6 +134,7 @@ var sqliteMigrations = []string{
`, `,
"ALTER TABLE Channel ADD COLUMN detached_internal_msgid VARCHAR(255)", "ALTER TABLE Channel ADD COLUMN detached_internal_msgid VARCHAR(255)",
"ALTER TABLE Network ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1", "ALTER TABLE Network ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1",
"ALTER TABLE User ADD COLUMN realname VARCHAR(255)",
} }
type SqliteDB struct { type SqliteDB struct {
@ -242,12 +244,13 @@ func (db *SqliteDB) GetUser(username string) (*User, error) {
user := &User{Username: username} user := &User{Username: username}
var password sql.NullString var password, realname sql.NullString
row := db.db.QueryRow("SELECT id, password, admin FROM User WHERE username = ?", username) row := db.db.QueryRow("SELECT id, password, admin, realname FROM User WHERE username = ?", username)
if err := row.Scan(&user.ID, &password, &user.Admin); err != nil { if err := row.Scan(&user.ID, &password, &user.Admin, &realname); err != nil {
return nil, err return nil, err
} }
user.Password = password.String user.Password = password.String
user.Realname = realname.String
return user, nil return user, nil
} }
@ -256,15 +259,16 @@ func (db *SqliteDB) StoreUser(user *User) error {
defer db.lock.Unlock() defer db.lock.Unlock()
password := toNullString(user.Password) password := toNullString(user.Password)
realname := toNullString(user.Realname)
var err error var err error
if user.ID != 0 { if user.ID != 0 {
_, err = db.db.Exec("UPDATE User SET password = ?, admin = ? WHERE username = ?", _, err = db.db.Exec("UPDATE User SET password = ?, admin = ?, realname = ? WHERE username = ?",
password, user.Admin, user.Username) password, user.Admin, realname, user.Username)
} else { } else {
var res sql.Result var res sql.Result
res, err = db.db.Exec("INSERT INTO User(username, password, admin) VALUES (?, ?, ?)", res, err = db.db.Exec("INSERT INTO User(username, password, admin, realname) VALUES (?, ?, ?, ?)",
user.Username, password, user.Admin) user.Username, password, user.Admin, realname)
if err != nil { if err != nil {
return err return err
} }

View File

@ -160,7 +160,8 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just
Connect with the specified server password. Connect with the specified server password.
*-realname* <realname> *-realname* <realname>
Connect with the specified real name. By default, the nickname is used. Connect with the specified real name. By default, the account's realname
is used if set, otherwise the network's nickname is used.
*-nick* <nickname> *-nick* <nickname>
Connect with the specified nickname. By default, the account's username Connect with the specified nickname. By default, the account's username
@ -296,9 +297,21 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just
*sasl reset* <network name> *sasl reset* <network name>
Disable SASL authentication and remove stored credentials. Disable SASL authentication and remove stored credentials.
*user create* -username <username> -password <password> [-admin] *user create* -username <username> -password <password> [options...]
Create a new soju user. Only admin users can create new accounts. Create a new soju user. Only admin users can create new accounts.
Options:
*-admin*
Make the new user an administrator.
*-realname* <realname>
Set the user's realname. This is used as a fallback if there is no
realname set for a network.
*user update* [-realname <realname>]
Update the current user.
*user delete* <username> *user delete* <username>
Delete a soju user. Only admins can delete accounts. Delete a soju user. Only admins can delete accounts.

View File

@ -83,8 +83,8 @@ func getNetworkAttrs(network *network) irc.Tags {
if network.Username != "" { if network.Username != "" {
attrs["username"] = irc.TagValue(network.Username) attrs["username"] = irc.TagValue(network.Username)
} }
if network.Realname != "" { if realname := GetRealname(&network.user.User, &network.Network); realname != "" {
attrs["realname"] = irc.TagValue(network.Realname) attrs["realname"] = irc.TagValue(realname)
} }
if u, err := network.URL(); err == nil { if u, err := network.URL(); err == nil {
@ -1387,6 +1387,13 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
return err return err
} }
// If the client just resets to the default, just wipe the per-network
// preference
storeRealname := realname
if realname == dc.user.Realname {
storeRealname = ""
}
var storeErr error var storeErr error
var needUpdate []Network var needUpdate []Network
dc.forEachNetwork(func(n *network) { dc.forEachNetwork(func(n *network) {
@ -1398,7 +1405,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
Params: []string{realname}, Params: []string{realname},
}) })
n.Realname = realname n.Realname = storeRealname
if err := dc.srv.db.StoreNetwork(dc.user.ID, &n.Network); err != nil { if err := dc.srv.db.StoreNetwork(dc.user.ID, &n.Network); err != nil {
dc.logger.Printf("failed to store network realname: %v", err) dc.logger.Printf("failed to store network realname: %v", err)
storeErr = err storeErr = err
@ -1407,7 +1414,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
} }
record := n.Network // copy network record because we'll mutate it record := n.Network // copy network record because we'll mutate it
record.Realname = realname record.Realname = storeRealname
needUpdate = append(needUpdate, record) needUpdate = append(needUpdate, record)
}) })
@ -2223,6 +2230,10 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
realname, _ := attrs.GetTag("realname") realname, _ := attrs.GetTag("realname")
pass, _ := attrs.GetTag("pass") pass, _ := attrs.GetTag("pass")
if realname == dc.user.Realname {
realname = ""
}
// TODO: reject unknown attributes // TODO: reject unknown attributes
record := &Network{ record := &Network{

View File

@ -254,11 +254,16 @@ func init() {
"user": { "user": {
children: serviceCommandSet{ children: serviceCommandSet{
"create": { "create": {
usage: "-username <username> -password <password> [-admin]", usage: "-username <username> -password <password> [-realname <realname>] [-admin]",
desc: "create a new soju user", desc: "create a new soju user",
handle: handleUserCreate, handle: handleUserCreate,
admin: true, admin: true,
}, },
"update": {
usage: "[-realname <realname>]",
desc: "update the current user",
handle: handleUserUpdate,
},
"delete": { "delete": {
usage: "<username>", usage: "<username>",
desc: "delete a user", desc: "delete a user",
@ -266,7 +271,6 @@ func init() {
admin: true, admin: true,
}, },
}, },
admin: true,
}, },
"change-password": { "change-password": {
usage: "<new password>", usage: "<new password>",
@ -751,6 +755,7 @@ func handleUserCreate(dc *downstreamConn, params []string) error {
fs := newFlagSet() fs := newFlagSet()
username := fs.String("username", "", "") username := fs.String("username", "", "")
password := fs.String("password", "", "") password := fs.String("password", "", "")
realname := fs.String("realname", "", "")
admin := fs.Bool("admin", false, "") admin := fs.Bool("admin", false, "")
if err := fs.Parse(params); err != nil { if err := fs.Parse(params); err != nil {
@ -771,6 +776,7 @@ func handleUserCreate(dc *downstreamConn, params []string) error {
user := &User{ user := &User{
Username: *username, Username: *username,
Password: string(hashed), Password: string(hashed),
Realname: *realname,
Admin: *admin, Admin: *admin,
} }
if _, err := dc.srv.createUser(user); err != nil { if _, err := dc.srv.createUser(user); err != nil {
@ -781,6 +787,22 @@ func handleUserCreate(dc *downstreamConn, params []string) error {
return nil return nil
} }
func handleUserUpdate(dc *downstreamConn, params []string) error {
fs := newFlagSet()
realname := fs.String("realname", "", "")
if err := fs.Parse(params); err != nil {
return err
}
if err := dc.user.updateRealname(*realname); err != nil {
return err
}
sendServicePRIVMSG(dc, fmt.Sprintf("updated user %q", dc.user.Username))
return nil
}
func handleUserDelete(dc *downstreamConn, params []string) error { func handleUserDelete(dc *downstreamConn, params []string) error {
if len(params) != 1 { if len(params) != 1 {
return fmt.Errorf("expected exactly one argument") return fmt.Errorf("expected exactly one argument")

View File

@ -1671,7 +1671,7 @@ func (uc *upstreamConn) register() {
uc.nick = uc.network.Nick uc.nick = uc.network.Nick
uc.nickCM = uc.network.casemap(uc.nick) uc.nickCM = uc.network.casemap(uc.nick)
uc.username = uc.network.GetUsername() uc.username = uc.network.GetUsername()
uc.realname = uc.network.GetRealname() uc.realname = GetRealname(&uc.user.User, &uc.network.Network)
uc.SendMessage(&irc.Message{ uc.SendMessage(&irc.Message{
Command: "CAP", Command: "CAP",

30
user.go
View File

@ -763,6 +763,12 @@ func (u *user) updateNetwork(record *Network) (*network, error) {
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
// setting
if record.Realname == u.Realname {
record.Realname = ""
}
if err := u.checkNetwork(record); err != nil { if err := u.checkNetwork(record); err != nil {
return nil, err return nil, err
} }
@ -855,6 +861,30 @@ func (u *user) updatePassword(hashed string) error {
return u.srv.db.StoreUser(&u.User) return u.srv.db.StoreUser(&u.User)
} }
func (u *user) updateRealname(realname string) error {
u.User.Realname = realname
if err := u.srv.db.StoreUser(&u.User); err != nil {
return fmt.Errorf("failed to update user %q: %v", u.Username, err)
}
// Re-connect to networks which use the default realname
var needUpdate []Network
u.forEachNetwork(func(net *network) {
if net.Realname == "" {
needUpdate = append(needUpdate, net.Network)
}
})
var netErr error
for _, net := range needUpdate {
if _, err := u.updateNetwork(&net); err != nil {
netErr = err
}
}
return netErr
}
func (u *user) stop() { func (u *user) stop() {
u.events <- eventStop{} u.events <- eventStop{}
<-u.done <-u.done