Add command to manage power levels

This commit is contained in:
Tulir Asokan 2023-04-04 22:25:36 +03:00
parent 7e738485ee
commit 22900d8f8a
3 changed files with 185 additions and 0 deletions

View File

@ -69,3 +69,20 @@ func autocompleteToggle(cmd *CommandAutocomplete) (completions []string, newText
} }
return return
} }
var staticPowerLevelKeys = []string{"ban", "kick", "redact", "invite", "state_default", "events_default", "users_default"}
func autocompletePowerLevel(cmd *CommandAutocomplete) (completions []string, newText string) {
if len(cmd.Args) > 1 {
return
}
for _, staticKey := range staticPowerLevelKeys {
if strings.HasPrefix(staticKey, cmd.RawArgs) {
completions = append(completions, staticKey)
}
}
for _, cpl := range cmd.Room.AutocompleteUser(cmd.RawArgs) {
completions = append(completions, cpl.id)
}
return
}

View File

@ -111,6 +111,8 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"4s": {"ssss"}, "4s": {"ssss"},
"s4": {"ssss"}, "s4": {"ssss"},
"cs": {"cross-signing"}, "cs": {"cross-signing"},
"power": {"powerlevel"},
"pl": {"powerlevel"},
}, },
autocompleters: map[string]CommandAutocompleter{ autocompleters: map[string]CommandAutocompleter{
"devices": autocompleteUser, "devices": autocompleteUser,
@ -126,6 +128,7 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"export": autocompleteFile, "export": autocompleteFile,
"export-room": autocompleteFile, "export-room": autocompleteFile,
"toggle": autocompleteToggle, "toggle": autocompleteToggle,
"powerlevel": autocompletePowerLevel,
}, },
commands: map[string]CommandHandler{ commands: map[string]CommandHandler{
"unknown-command": cmdUnknownCommand, "unknown-command": cmdUnknownCommand,
@ -142,6 +145,7 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"kick": cmdKick, "kick": cmdKick,
"ban": cmdBan, "ban": cmdBan,
"unban": cmdUnban, "unban": cmdUnban,
"powerlevel": cmdPowerLevel,
"toggle": cmdToggle, "toggle": cmdToggle,
"logout": cmdLogout, "logout": cmdLogout,
"accept": cmdAccept, "accept": cmdAccept,

View File

@ -613,6 +613,170 @@ func cmdKick(cmd *Command) {
} }
} }
func formatPowerLevels(pl *event.PowerLevelsEventContent) string {
var buf strings.Builder
buf.WriteString("Membership actions:\n")
_, _ = fmt.Fprintf(&buf, " Invite: %d\n", pl.Invite())
_, _ = fmt.Fprintf(&buf, " Kick: %d\n", pl.Kick())
_, _ = fmt.Fprintf(&buf, " Ban: %d\n", pl.Ban())
buf.WriteString("Events:\n")
_, _ = fmt.Fprintf(&buf, " Redact: %d\n", pl.Redact())
_, _ = fmt.Fprintf(&buf, " State default: %d\n", pl.StateDefault())
_, _ = fmt.Fprintf(&buf, " Event default: %d\n", pl.EventsDefault)
for evtType, level := range pl.Events {
_, _ = fmt.Fprintf(&buf, " %s: %d\n", evtType, level)
}
buf.WriteString("Users:\n")
_, _ = fmt.Fprintf(&buf, " Default: %d\n", pl.UsersDefault)
for userID, level := range pl.Users {
_, _ = fmt.Fprintf(&buf, " %s: %d\n", userID, level)
}
return strings.TrimSpace(buf.String())
}
func copyPtr(ptr *int) *int {
if ptr == nil {
return nil
}
val := *ptr
return &val
}
func copyMap[Key comparable](m map[Key]int) map[Key]int {
if m == nil {
return nil
}
copied := make(map[Key]int, len(m))
for k, v := range m {
copied[k] = v
}
return copied
}
func copyPowerLevels(pl *event.PowerLevelsEventContent) *event.PowerLevelsEventContent {
return &event.PowerLevelsEventContent{
Users: copyMap(pl.Users),
Events: copyMap(pl.Events),
InvitePtr: copyPtr(pl.InvitePtr),
KickPtr: copyPtr(pl.KickPtr),
BanPtr: copyPtr(pl.BanPtr),
RedactPtr: copyPtr(pl.RedactPtr),
StateDefaultPtr: copyPtr(pl.StateDefaultPtr),
EventsDefault: pl.EventsDefault,
UsersDefault: pl.UsersDefault,
}
}
var things = `
[thing] can be one of the following
Literals:
* invite, kick, ban, redact - special moderation action levels
* state_default, events_default - default level for state and non-state events
* users_default - default level for users
Patterns:
* user ID - specific user level
* event type - specific event type level
The default levels are 0 for users, 50 for moderators and 100 for admins.`
func cmdPowerLevel(cmd *Command) {
evt := cmd.Room.MxRoom().GetStateEvent(event.StatePowerLevels, "")
pl := copyPowerLevels(evt.Content.AsPowerLevels())
if len(cmd.Args) == 0 {
// TODO open in modal?
cmd.Reply(formatPowerLevels(pl))
return
} else if len(cmd.Args) < 2 {
cmd.Reply("Usage: /%s [thing] [level]\n%s", cmd.Command, things)
return
}
value, err := strconv.Atoi(cmd.Args[1])
if err != nil {
cmd.Reply("Invalid power level %q: %v", cmd.Args[1], err)
return
}
ownLevel := pl.GetUserLevel(cmd.Matrix.Client().UserID)
plChangeLevel := pl.GetEventLevel(event.StatePowerLevels)
if ownLevel < plChangeLevel {
cmd.Reply("Can't modify power levels (own level is %d, modifying requires %d)", ownLevel, plChangeLevel)
return
} else if value > ownLevel {
cmd.Reply("Can't set level to be higher than own level (%d > %d)", value, ownLevel)
return
}
var oldValue int
var thing string
switch cmd.Args[0] {
case "invite":
oldValue = pl.Invite()
pl.InvitePtr = &value
thing = "invite level"
case "kick":
oldValue = pl.Kick()
pl.KickPtr = &value
thing = "kick level"
case "ban":
oldValue = pl.Ban()
pl.BanPtr = &value
thing = "ban level"
case "redact":
oldValue = pl.Redact()
pl.RedactPtr = &value
thing = "level for redacting other users' events"
case "state_default":
oldValue = pl.StateDefault()
pl.StateDefaultPtr = &value
thing = "default level for state events"
case "events_default":
oldValue = pl.EventsDefault
pl.EventsDefault = value
thing = "default level for normal events"
case "users_default":
oldValue = pl.UsersDefault
pl.UsersDefault = value
thing = "default level for users"
default:
userID := id.UserID(cmd.Args[0])
if _, _, err = userID.Parse(); err == nil {
if pl.Users == nil {
pl.Users = make(map[id.UserID]int)
}
oldValue = pl.Users[userID]
if oldValue == ownLevel && userID != cmd.Matrix.Client().UserID {
cmd.Reply("Can't change level of another user which is equal to own level (%d)", ownLevel)
return
}
pl.Users[userID] = value
thing = fmt.Sprintf("level of user %s", userID)
} else {
if pl.Events == nil {
pl.Events = make(map[string]int)
}
oldValue = pl.Events[cmd.Args[0]]
pl.Events[cmd.Args[0]] = value
thing = fmt.Sprintf("level for event %s", cmd.Args[0])
}
}
if oldValue == value {
cmd.Reply("%s is already %d", strings.ToUpper(thing[0:1])+thing[1:], value)
} else if oldValue > ownLevel {
cmd.Reply("Can't change level which is higher than own level (%d > %d)", oldValue, ownLevel)
} else if resp, err := cmd.Matrix.Client().SendStateEvent(cmd.Room.MxRoom().ID, event.StatePowerLevels, "", pl); err != nil {
if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil {
err = httpErr.RespError
}
cmd.Reply("Failed to set %s to %d: %v", thing, value, err)
} else {
cmd.Reply("Successfully set %s to %d\n(event ID: %s)", thing, value, resp.EventID)
}
}
func cmdCreateRoom(cmd *Command) { func cmdCreateRoom(cmd *Command) {
req := &mautrix.ReqCreateRoom{} req := &mautrix.ReqCreateRoom{}
if len(cmd.Args) > 0 { if len(cmd.Args) > 0 {