From a14f646135c0f6a8d7ac389d8666cdd5c0f77018 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Fri, 25 Jun 2021 20:33:13 +0200 Subject: [PATCH] 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. --- db.go | 6 +++++- db_sqlite.go | 20 ++++++++++++-------- doc/soju.1.scd | 17 +++++++++++++++-- downstream.go | 19 +++++++++++++++---- service.go | 26 ++++++++++++++++++++++++-- upstream.go | 2 +- user.go | 30 ++++++++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 18 deletions(-) diff --git a/db.go b/db.go index 36ea462..b28827a 100644 --- a/db.go +++ b/db.go @@ -30,6 +30,7 @@ type User struct { ID int64 Username string Password string // hashed + Realname string Admin bool } @@ -92,10 +93,13 @@ func (net *Network) GetUsername() string { return net.Nick } -func (net *Network) GetRealname() string { +func GetRealname(user *User, net *Network) string { if net.Realname != "" { return net.Realname } + if user.Realname != "" { + return user.Realname + } return net.Nick } diff --git a/db_sqlite.go b/db_sqlite.go index 46c359a..58ea072 100644 --- a/db_sqlite.go +++ b/db_sqlite.go @@ -16,7 +16,8 @@ CREATE TABLE User ( id INTEGER PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255), - admin INTEGER NOT NULL DEFAULT 0 + admin INTEGER NOT NULL DEFAULT 0, + realname VARCHAR(255) ); CREATE TABLE Network ( @@ -133,6 +134,7 @@ var sqliteMigrations = []string{ `, "ALTER TABLE Channel ADD COLUMN detached_internal_msgid VARCHAR(255)", "ALTER TABLE Network ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1", + "ALTER TABLE User ADD COLUMN realname VARCHAR(255)", } type SqliteDB struct { @@ -242,12 +244,13 @@ func (db *SqliteDB) GetUser(username string) (*User, error) { user := &User{Username: username} - var password sql.NullString - row := db.db.QueryRow("SELECT id, password, admin FROM User WHERE username = ?", username) - if err := row.Scan(&user.ID, &password, &user.Admin); err != nil { + var password, realname sql.NullString + row := db.db.QueryRow("SELECT id, password, admin, realname FROM User WHERE username = ?", username) + if err := row.Scan(&user.ID, &password, &user.Admin, &realname); err != nil { return nil, err } user.Password = password.String + user.Realname = realname.String return user, nil } @@ -256,15 +259,16 @@ func (db *SqliteDB) StoreUser(user *User) error { defer db.lock.Unlock() password := toNullString(user.Password) + realname := toNullString(user.Realname) var err error if user.ID != 0 { - _, err = db.db.Exec("UPDATE User SET password = ?, admin = ? WHERE username = ?", - password, user.Admin, user.Username) + _, err = db.db.Exec("UPDATE User SET password = ?, admin = ?, realname = ? WHERE username = ?", + password, user.Admin, realname, user.Username) } else { var res sql.Result - res, err = db.db.Exec("INSERT INTO User(username, password, admin) VALUES (?, ?, ?)", - user.Username, password, user.Admin) + res, err = db.db.Exec("INSERT INTO User(username, password, admin, realname) VALUES (?, ?, ?, ?)", + user.Username, password, user.Admin, realname) if err != nil { return err } diff --git a/doc/soju.1.scd b/doc/soju.1.scd index ac65aa9..d941938 100644 --- a/doc/soju.1.scd +++ b/doc/soju.1.scd @@ -160,7 +160,8 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just Connect with the specified server password. *-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* 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* Disable SASL authentication and remove stored credentials. -*user create* -username -password [-admin] +*user create* -username -password [options...] Create a new soju user. Only admin users can create new accounts. + Options: + + *-admin* + Make the new user an administrator. + + *-realname* + Set the user's realname. This is used as a fallback if there is no + realname set for a network. + +*user update* [-realname ] + Update the current user. + *user delete* Delete a soju user. Only admins can delete accounts. diff --git a/downstream.go b/downstream.go index 4898d2b..fee5134 100644 --- a/downstream.go +++ b/downstream.go @@ -83,8 +83,8 @@ func getNetworkAttrs(network *network) irc.Tags { if network.Username != "" { attrs["username"] = irc.TagValue(network.Username) } - if network.Realname != "" { - attrs["realname"] = irc.TagValue(network.Realname) + if realname := GetRealname(&network.user.User, &network.Network); realname != "" { + attrs["realname"] = irc.TagValue(realname) } if u, err := network.URL(); err == nil { @@ -1387,6 +1387,13 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error { 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 needUpdate []Network dc.forEachNetwork(func(n *network) { @@ -1398,7 +1405,7 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error { Params: []string{realname}, }) - n.Realname = realname + n.Realname = storeRealname if err := dc.srv.db.StoreNetwork(dc.user.ID, &n.Network); err != nil { dc.logger.Printf("failed to store network realname: %v", 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.Realname = realname + record.Realname = storeRealname needUpdate = append(needUpdate, record) }) @@ -2223,6 +2230,10 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error { realname, _ := attrs.GetTag("realname") pass, _ := attrs.GetTag("pass") + if realname == dc.user.Realname { + realname = "" + } + // TODO: reject unknown attributes record := &Network{ diff --git a/service.go b/service.go index 88b75ae..00d65f7 100644 --- a/service.go +++ b/service.go @@ -254,11 +254,16 @@ func init() { "user": { children: serviceCommandSet{ "create": { - usage: "-username -password [-admin]", + usage: "-username -password [-realname ] [-admin]", desc: "create a new soju user", handle: handleUserCreate, admin: true, }, + "update": { + usage: "[-realname ]", + desc: "update the current user", + handle: handleUserUpdate, + }, "delete": { usage: "", desc: "delete a user", @@ -266,7 +271,6 @@ func init() { admin: true, }, }, - admin: true, }, "change-password": { usage: "", @@ -751,6 +755,7 @@ func handleUserCreate(dc *downstreamConn, params []string) error { fs := newFlagSet() username := fs.String("username", "", "") password := fs.String("password", "", "") + realname := fs.String("realname", "", "") admin := fs.Bool("admin", false, "") if err := fs.Parse(params); err != nil { @@ -771,6 +776,7 @@ func handleUserCreate(dc *downstreamConn, params []string) error { user := &User{ Username: *username, Password: string(hashed), + Realname: *realname, Admin: *admin, } if _, err := dc.srv.createUser(user); err != nil { @@ -781,6 +787,22 @@ func handleUserCreate(dc *downstreamConn, params []string) error { 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 { if len(params) != 1 { return fmt.Errorf("expected exactly one argument") diff --git a/upstream.go b/upstream.go index e033410..104f330 100644 --- a/upstream.go +++ b/upstream.go @@ -1671,7 +1671,7 @@ func (uc *upstreamConn) register() { uc.nick = uc.network.Nick uc.nickCM = uc.network.casemap(uc.nick) uc.username = uc.network.GetUsername() - uc.realname = uc.network.GetRealname() + uc.realname = GetRealname(&uc.user.User, &uc.network.Network) uc.SendMessage(&irc.Message{ Command: "CAP", diff --git a/user.go b/user.go index 1b5f4f2..46aa140 100644 --- a/user.go +++ b/user.go @@ -763,6 +763,12 @@ func (u *user) updateNetwork(record *Network) (*network, error) { 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 { return nil, err } @@ -855,6 +861,30 @@ func (u *user) updatePassword(hashed string) error { 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() { u.events <- eventStop{} <-u.done