Commands: /ban and /op
This commit is contained in:
parent
e0dcc33519
commit
974ee25b48
12
README.md
12
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
128
pkg/commands.go
128
pkg/commands.go
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user