Commands: /ban and /op

This commit is contained in:
Noah 2023-08-04 20:31:21 -07:00
parent e0dcc33519
commit 974ee25b48
4 changed files with 198 additions and 34 deletions

View File

@ -34,14 +34,14 @@ It is very much in the style of the old-school Flash based webcam chat rooms of
* WebRTC means peer-to-peer video streaming so cheap on hosting costs! * WebRTC means peer-to-peer video streaming so cheap on hosting costs!
* Simple integration with your existing userbase via signed JWT tokens. * Simple integration with your existing userbase via signed JWT tokens.
* User configurable sound effects to be notified of DMs or users entering/exiting the room. * User configurable sound effects to be notified of DMs or users entering/exiting the room.
Some important features still lacking:
* Operator commands * Operator commands
* [x] /kick users * [x] /kick users
* [x] /nsfw to mark someone's camera * [x] /ban users (and /unban, /bans to list)
* [ ] /ban users * [x] /nsfw to tag a user's camera as explicit
* [ ] /op users (give temporary mod control) * [x] /shutdown to gracefully reboot the server
* [x] /kickall to kick EVERYBODY off the server (e.g., for mandatory front-end reload for new features)
* [x] /op and /deop users (give temporary mod control)
* [x] /help to get in-chat help for moderator commands
# Configuration # Configuration

View File

@ -1,28 +1,73 @@
package barertc package barertc
import "time" import (
"fmt"
"strings"
"sync"
"time"
)
/* Functions to handle banned users */ /* Functions to handle banned users */
/*
BanList holds (in memory) knowledge of currently banned users.
All bans are reset if the chat server is rebooted. Otherwise each ban
comes with a duration - default is 24 hours by the operator can specify
a duration with a ban. If the server is not rebooted, bans will be lifted
after they expire.
Bans are against usernames and will also block a JWT token from
authenticating if they are currently banned.
*/
type BanList struct {
Active []Ban
}
// Ban is an entry on the ban list. // Ban is an entry on the ban list.
type Ban struct { type Ban struct {
Username string Username string
ExpiresAt time.Time ExpiresAt time.Time
} }
// // Global storage for banned users in memory.
var (
banList = map[string]Ban{}
banListMu sync.RWMutex
)
// BanUser adds a user to the ban list.
func BanUser(username string, duration time.Duration) {
banListMu.Lock()
defer banListMu.Unlock()
banList[username] = Ban{
Username: username,
ExpiresAt: time.Now().Add(duration),
}
}
// UnbanUser lifts the ban of a user early.
func UnbanUser(username string) bool {
banListMu.RLock()
defer banListMu.RUnlock()
_, ok := banList[username]
if ok {
delete(banList, username)
}
return ok
}
// StringifyBannedUsers returns a stringified list of all the current banned users.
func StringifyBannedUsers() string {
var lines = []string{}
banListMu.RLock()
defer banListMu.RUnlock()
for username, ban := range banList {
lines = append(lines, fmt.Sprintf(
"* `%s` banned until %s",
username,
ban.ExpiresAt.Format(time.RFC3339),
))
}
return strings.Join(lines, "\n")
}
// IsBanned returns whether the username is currently banned.
func IsBanned(username string) bool {
banListMu.Lock()
defer banListMu.Unlock()
ban, ok := banList[username]
if ok {
// Has the ban expired?
if time.Now().After(ban.ExpiresAt) {
delete(banList, username)
return false
}
}
return ok
}

View File

@ -7,7 +7,9 @@ import (
"time" "time"
"git.kirsle.net/apps/barertc/pkg/config" "git.kirsle.net/apps/barertc/pkg/config"
ourjwt "git.kirsle.net/apps/barertc/pkg/jwt"
"git.kirsle.net/apps/barertc/pkg/log" "git.kirsle.net/apps/barertc/pkg/log"
"github.com/golang-jwt/jwt/v4"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
) )
@ -35,6 +37,12 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
case "/ban": case "/ban":
s.BanCommand(words, sub) s.BanCommand(words, sub)
return true return true
case "/unban":
s.UnbanCommand(words, sub)
return true
case "/bans":
s.BansCommand(words, sub)
return true
case "/nsfw": case "/nsfw":
if len(words) == 1 { if len(words) == 1 {
sub.ChatServer("Usage: `/nsfw username` to add the NSFW flag to their camera.") sub.ChatServer("Usage: `/nsfw username` to add the NSFW flag to their camera.")
@ -58,7 +66,12 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
case "/help": case "/help":
sub.ChatServer(RenderMarkdown("Moderator commands are:\n\n" + sub.ChatServer(RenderMarkdown("Moderator commands are:\n\n" +
"* `/kick <username>` to kick from chat\n" + "* `/kick <username>` to kick from chat\n" +
"* `/ban <username> <duration>` to ban from chat (default duration is 24 (hours))\n" +
"* `/unban <username>` to list the ban on a user\n" +
"* `/bans` to list current banned users and their expiration date\n" +
"* `/nsfw <username>` to mark their camera NSFW\n" + "* `/nsfw <username>` to mark their camera NSFW\n" +
"* `/op <username>` to grant operator rights to a user\n" +
"* `/deop <username>` to remove operator rights from a user\n" +
"* `/shutdown` to gracefully shut down (reboot) the chat server\n" + "* `/shutdown` to gracefully shut down (reboot) the chat server\n" +
"* `/kickall` to kick EVERYBODY off and force them to log back in\n" + "* `/kickall` to kick EVERYBODY off and force them to log back in\n" +
"* `/help` to show this message\n\n" + "* `/help` to show this message\n\n" +
@ -72,8 +85,16 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
Message: "The chat server is going down for a reboot NOW!", Message: "The chat server is going down for a reboot NOW!",
}) })
os.Exit(1) os.Exit(1)
return true
case "/kickall": case "/kickall":
s.KickAllCommand() s.KickAllCommand()
return true
case "/op":
s.OpCommand(words, sub)
return true
case "/deop":
s.DeopCommand(words, sub)
return true
} }
} }
@ -151,7 +172,7 @@ func (s *Server) BanCommand(words []string, sub *Subscriber) {
if len(words) == 1 { if len(words) == 1 {
sub.ChatServer(RenderMarkdown( sub.ChatServer(RenderMarkdown(
"Usage: `/ban username` to remove the user from the chat room for 24 hours (default).\n\n" + "Usage: `/ban username` to remove the user from the chat room for 24 hours (default).\n\n" +
"Set another duration (in hours, fractions supported) like: `/ban username 0.5` for a 30-minute ban.", "Set another duration (in hours) like: `/ban username 2` for a 2-hour ban.",
)) ))
return return
} }
@ -162,27 +183,112 @@ func (s *Server) BanCommand(words []string, sub *Subscriber) {
duration = 24 * time.Hour duration = 24 * time.Hour
) )
if len(words) >= 3 { if len(words) >= 3 {
if dur, err := strconv.ParseFloat(words[2], 64); err == nil { if dur, err := strconv.Atoi(words[2]); err == nil {
if dur < 1 { duration = time.Duration(dur) * time.Hour
duration = time.Duration(dur*60) * time.Second
} else {
duration = time.Duration(dur) * time.Hour
}
} }
} }
// TODO: banning, for now it just kicks. log.Info("Operator %s bans %s for %d hours", sub.Username, username, duration/time.Hour)
_ = duration
other, err := s.GetSubscriber(username) other, err := s.GetSubscriber(username)
if err != nil { if err != nil {
sub.ChatServer("/ban: username not found: %s", username) sub.ChatServer("/ban: username not found: %s", username)
} else { } else {
other.ChatServer("You have been kicked from the chat room by %s", sub.Username) // Ban them.
BanUser(username, duration)
other.ChatServer("You have been banned from the chat room by %s. You may come back after %d hours.", sub.Username, duration/time.Hour)
other.SendJSON(Message{ other.SendJSON(Message{
Action: ActionKick, Action: ActionKick,
}) })
s.DeleteSubscriber(other) s.DeleteSubscriber(other)
sub.ChatServer("%s has been kicked from the room", username) sub.ChatServer("%s has been banned from the room for %d hours.", username, duration/time.Hour)
}
}
// UnbanCommand handles the `/unban` operator command.
func (s *Server) UnbanCommand(words []string, sub *Subscriber) {
if len(words) == 1 {
sub.ChatServer(RenderMarkdown(
"Usage: `/unban username` to lift the ban on a user and allow them back into the chat room.",
))
return
}
// Parse the command.
var username = words[1]
if UnbanUser(username) {
sub.ChatServer("The ban on %s has been lifted.", username)
} else {
sub.ChatServer("/unban: user %s was not found to be banned. Try `/bans` to see current banned users.", username)
}
}
// BansCommand handles the `/bans` operator command.
func (s *Server) BansCommand(words []string, sub *Subscriber) {
result := StringifyBannedUsers()
sub.ChatServer(
RenderMarkdown("The listing of banned users currently includes:\n\n" + result),
)
}
// OpCommand handles the `/op` operator command.
func (s *Server) OpCommand(words []string, sub *Subscriber) {
if len(words) == 1 {
sub.ChatServer(RenderMarkdown(
"Usage: `/op username` to grant temporary operator rights to a user.",
))
return
}
// Parse the command.
var username = words[1]
if other, err := s.GetSubscriber(username); err != nil {
sub.ChatServer("/op: user %s was not found.", username)
} else {
if other.JWTClaims == nil {
other.JWTClaims = &ourjwt.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: username,
},
}
}
other.JWTClaims.IsAdmin = true
// Send everyone the Who List.
s.SendWhoList()
sub.ChatServer("Operator rights have been granted to %s", username)
}
}
// DeopCommand handles the `/deop` operator command.
func (s *Server) DeopCommand(words []string, sub *Subscriber) {
if len(words) == 1 {
sub.ChatServer(RenderMarkdown(
"Usage: `/deop username` to remove operator rights from a user.",
))
return
}
// Parse the command.
var username = words[1]
if other, err := s.GetSubscriber(username); err != nil {
sub.ChatServer("/deop: user %s was not found.", username)
} else {
if other.JWTClaims == nil {
other.JWTClaims = &ourjwt.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: username,
},
}
}
other.JWTClaims.IsAdmin = false
// Send everyone the Who List.
s.SendWhoList()
sub.ChatServer("Operator rights have been taken from %s", username)
} }
} }

View File

@ -71,6 +71,19 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
} }
msg.Username = username msg.Username = username
// Is the username currently banned?
if IsBanned(msg.Username) {
sub.ChatServer(
"You are currently banned from entering the chat room. Chat room bans are temporarily and usually last for " +
"24 hours. Please try coming back later.",
)
sub.SendJSON(Message{
Action: ActionKick,
})
s.DeleteSubscriber(sub)
return
}
// Use their username. // Use their username.
sub.Username = msg.Username sub.Username = msg.Username
sub.authenticated = true sub.authenticated = true