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
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
}

View File

@ -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
}

View File

@ -160,7 +160,8 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just
Connect with the specified server password.
*-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>
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>
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.
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>
Delete a soju user. Only admins can delete accounts.

View File

@ -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{

View File

@ -254,11 +254,16 @@ func init() {
"user": {
children: serviceCommandSet{
"create": {
usage: "-username <username> -password <password> [-admin]",
usage: "-username <username> -password <password> [-realname <realname>] [-admin]",
desc: "create a new soju user",
handle: handleUserCreate,
admin: true,
},
"update": {
usage: "[-realname <realname>]",
desc: "update the current user",
handle: handleUserUpdate,
},
"delete": {
usage: "<username>",
desc: "delete a user",
@ -266,7 +271,6 @@ func init() {
admin: true,
},
},
admin: true,
},
"change-password": {
usage: "<new password>",
@ -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")

View File

@ -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",

30
user.go
View File

@ -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