dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

slashInterceptor.go (57802B)


      1 package interceptors
      2 
      3 import (
      4 	"dkforest/pkg/clockwork"
      5 	"dkforest/pkg/config"
      6 	"dkforest/pkg/database"
      7 	dutils "dkforest/pkg/database/utils"
      8 	"dkforest/pkg/levenshtein"
      9 	"dkforest/pkg/managers"
     10 	"dkforest/pkg/utils"
     11 	"dkforest/pkg/web/handlers/interceptors/command"
     12 	"dkforest/pkg/web/handlers/poker"
     13 	"dkforest/pkg/web/handlers/streamModals"
     14 	"errors"
     15 	"fmt"
     16 	"github.com/ProtonMail/go-crypto/openpgp/clearsign"
     17 	"github.com/ProtonMail/go-crypto/openpgp/packet"
     18 	"github.com/asaskevich/govalidator"
     19 	"github.com/dustin/go-humanize"
     20 	"github.com/sirupsen/logrus"
     21 	"html"
     22 	"os"
     23 	"path/filepath"
     24 	"sort"
     25 	"strconv"
     26 	"strings"
     27 	"time"
     28 )
     29 
     30 // SlashInterceptor handle all forward slash commands.
     31 //
     32 // If by the end of this function, the c.err is set, it will trigger
     33 // different behavior according to the type of error it holds.
     34 // if c.err is set to ErrRedirect, the chat-bar iframe will refresh completely.
     35 // if c.err is set to ErrStop, no further processing of the user input will be done,
     36 //
     37 //	and the chat iframe will be rendered instead of redirected.
     38 //	This is useful to keep a prefix in the text box (eg: /pm user )
     39 //
     40 // if c.err is set to an instance of ErrSuccess,
     41 //
     42 //	a green message will appear beside the text box.
     43 //
     44 // otherwise if c.err is set to a different error,
     45 //
     46 //	text box is retested to original message,
     47 //	and a red message will appear beside the text box.
     48 type SlashInterceptor struct{}
     49 
     50 type CmdHandler func(c *command.Command) (handled bool)
     51 
     52 var userCmdsMap = map[string]CmdHandler{
     53 	"/i":                   handleIgnoreCmd,
     54 	"/ignore":              handleIgnoreCmd,
     55 	"/ui":                  handleUnIgnoreCmd,
     56 	"/unignore":            handleUnIgnoreCmd,
     57 	"/toggle-autocomplete": handleToggleAutocomplete,
     58 	"/tuto":                handleTutorialCmd,
     59 	"/d":                   handleDeleteMsgCmd,
     60 	"/hide":                handleHideMsgCmd,
     61 	"/unhide":              handleUnHideMsgCmd,
     62 	"/pmwhitelist":         handleListPmWhitelistCmd,
     63 	"/setpmmode":           handleSetPmModeCmd,
     64 	"/pmb":                 handleTogglePmBlacklistedUser,
     65 	"/pmw":                 handleTogglePmWhitelistedUser,
     66 	"/g":                   handleGroupChatCmd,
     67 	"/me":                  handleMeCmd,
     68 	"/e":                   handleEditCmd,
     69 	"/pm":                  handlePMCmd,
     70 	"/subscribe":           handleSubscribeCmd,
     71 	"/unsubscribe":         handleUnsubscribeCmd,
     72 	"/p":                   handleProfileCmd,
     73 	"/inbox":               handleInboxCmd,
     74 	"/chess":               handleChessCmd,
     75 	"/hbm":                 handleHbmCmd,
     76 	"/hbmt":                handleHbmtCmd,
     77 	"/token":               handleTokenCmd,
     78 	"/md5":                 handleMd5Cmd,
     79 	"/sha1":                handleSha1Cmd,
     80 	"/sha256":              handleSha256Cmd,
     81 	"/sha512":              handleSha512Cmd,
     82 	"/dice":                handleDiceCmd,
     83 	"/rand":                handleRandCmd,
     84 	"/choice":              handleChoiceCmd,
     85 	"/memes":               handleListMemes,
     86 	"/success":             handleSuccessCmd,
     87 	"/afk":                 handleAfkCmd,
     88 	"/date":                handleDateCmd,
     89 	"/r":                   handleUpdateReadMarkerCmd,
     90 	"/code":                handleCodeCmd,
     91 	"/locate":              handleLocateCmd,
     92 	"/error":               handleErrorCmd,
     93 	"/chips":               handleChipsBalanceCmd,
     94 	"/chips-reset":         handleChipsResetCmd,
     95 	"/wizz":                handleWizzCmd,
     96 	"/itr":                 handleInThisRoomCmd,
     97 	"/check":               handleCheckCmd,
     98 	"/call":                handleCallCmd,
     99 	"/fold":                handleFoldCmd,
    100 	"/raise":               handleRaiseCmd,
    101 	"/allin":               handleAllInCmd,
    102 	"/bet":                 handleBetCmd,
    103 	"/deal":                handleDealCmd,
    104 	"/dist":                handleDistCmd,
    105 	//"/chips-send":          handleChipsSendCmd,
    106 }
    107 
    108 var privateRoomCmdsMap = map[string]CmdHandler{
    109 	"/mode":      handleGetModeCmd,
    110 	"/wl":        handleWhitelistCmd,
    111 	"/whitelist": handleWhitelistCmd,
    112 }
    113 
    114 var privateRoomOwnerCmdsMap = map[string]CmdHandler{
    115 	"/addgroup":  handleAddGroupCmd,
    116 	"/rmgroup":   handleRmGroupCmd,
    117 	"/glock":     handleLockGroupCmd,
    118 	"/gunlock":   handleUnlockGroupCmd,
    119 	"/gusers":    handleGroupUsersCmd,
    120 	"/groups":    handleListGroupsCmd,
    121 	"/gadduser":  handleGroupAddUserCmd,
    122 	"/grmuser":   handleGroupRmUserCmd,
    123 	"/mode":      handleSetModeCmd,
    124 	"/ro":        handleToggleReadOnlyCmd,
    125 	"/wl":        handleGetRoomWhitelistCmd,
    126 	"/whitelist": handleGetRoomWhitelistCmd,
    127 }
    128 
    129 var moderatorCmdsMap = map[string]CmdHandler{
    130 	"/m":          handleModeratorGroupCmd,
    131 	"/n":          handleModeratorGroupCmd,
    132 	"/moderators": handleListModeratorsCmd,
    133 	"/mods":       handleListModeratorsCmd,
    134 	"/k":          handleKickCmd,
    135 	"/kick":       handleKickCmd,
    136 	"/kk":         handleKickKeepCmd,
    137 	"/ks":         handleKickSilentCmd,
    138 	"/kks":        handleKickKeepSilentCmd,
    139 	"/uk":         handleUnkickCmd,
    140 	"/unkick":     handleUnkickCmd,
    141 	"/logout":     handleLogoutCmd,
    142 	"/captcha":    handleForceCaptchaCmd,
    143 	"/rtuto":      handleResetTutorialCmd,
    144 	"/hb":         handleHellbanCmd,
    145 	"/hellban":    handleHellbanCmd,
    146 	"/unhellban":  handleUnhellbanCmd,
    147 	"/uhb":        handleUnhellbanCmd,
    148 	"/invite":     handleInviteCmd,
    149 }
    150 
    151 var adminCmdsMap = map[string]CmdHandler{
    152 	"/sys":     handleSystemCmd,
    153 	"/system":  handleSystemCmd,
    154 	"/seturl":  handleSetChatRoomExternalLink,
    155 	"/purge":   handlePurge,
    156 	"/rename":  handleRename,
    157 	"/meme":    handleNewMeme,
    158 	"/memerm":  handleRemoveMeme,
    159 	"/refresh": handleRefreshCmd,
    160 	"/chips":   handleChipsCmd,
    161 	"/close":   handleCloseCmd,
    162 	"/closem":  handleCloseMenuCmd,
    163 }
    164 
    165 func (i SlashInterceptor) InterceptMsg(c *command.Command) {
    166 	if !strings.HasPrefix(c.Message, "/") ||
    167 		strings.HasPrefix(c.Message, "/u/") {
    168 		return
    169 	}
    170 	handled := handleUserCmd(c) ||
    171 		handlePrivateRoomCmd(c) ||
    172 		handlePrivateRoomOwnerCmd(c) ||
    173 		handleModeratorCmd(c) ||
    174 		handleAdminCmd(c)
    175 	if !handled {
    176 		c.Err = errors.New("invalid slash command")
    177 	}
    178 }
    179 
    180 func handleUserCmd(c *command.Command) (handled bool) {
    181 	cmd := strings.Fields(c.Message)[0]
    182 	if cmdFn, found := userCmdsMap[cmd]; found {
    183 		return cmdFn(c)
    184 	}
    185 	return
    186 }
    187 
    188 func handlePrivateRoomCmd(c *command.Command) (handled bool) {
    189 	cmd := strings.Fields(c.Message)[0]
    190 	if cmdFn, found := privateRoomCmdsMap[cmd]; found {
    191 		return cmdFn(c)
    192 	}
    193 	return
    194 }
    195 
    196 func handlePrivateRoomOwnerCmd(c *command.Command) (handled bool) {
    197 	if c.Room.IsRoomOwner(c.AuthUser.ID) || c.AuthUser.IsAdmin {
    198 		cmd := strings.Fields(c.Message)[0]
    199 		if cmdFn, found := privateRoomOwnerCmdsMap[cmd]; found {
    200 			return cmdFn(c)
    201 		}
    202 	}
    203 	return false
    204 }
    205 
    206 func handleModeratorCmd(c *command.Command) (handled bool) {
    207 	if c.AuthUser.IsModerator() {
    208 		cmd := strings.Fields(c.Message)[0]
    209 		if cmdFn, found := moderatorCmdsMap[cmd]; found {
    210 			return cmdFn(c)
    211 		}
    212 	}
    213 	return false
    214 }
    215 
    216 func handleAdminCmd(c *command.Command) (handled bool) {
    217 	if c.AuthUser.IsAdmin {
    218 		cmd := strings.Fields(c.Message)[0]
    219 		if cmdFn, found := adminCmdsMap[cmd]; found {
    220 			return cmdFn(c)
    221 		}
    222 	}
    223 	return false
    224 }
    225 
    226 func handleModeratorGroupCmd(c *command.Command) (handled bool) {
    227 	if strings.HasPrefix(c.Message, "/m ") || strings.HasPrefix(c.Message, "/n ") {
    228 		if strings.HasPrefix(c.Message, "/n ") {
    229 			c.Message = strings.Replace(c.Message, "/n ", "/m ", 1)
    230 		}
    231 		c.Message = strings.TrimPrefix(c.Message, "/m ")
    232 		c.RedirectQP.Set(command.RedirectModQP, "1")
    233 		c.ModMsg = true
    234 		if handleMeCmd(c) {
    235 			return true
    236 		} else if handleCodeCmd(c) {
    237 			return true
    238 		}
    239 		return true
    240 	}
    241 	return false
    242 }
    243 
    244 func handleListModeratorsCmd(c *command.Command) (handled bool) {
    245 	if c.Message == "/moderators" || c.Message == "/mods" {
    246 		mods, err := c.DB.GetModeratorsUsers()
    247 		if err != nil {
    248 			c.Err = err
    249 			return true
    250 		}
    251 		msg := "Moderators:\n"
    252 		if len(mods) > 0 {
    253 			msg += "\n"
    254 			for _, mod := range mods {
    255 				msg += mod.Username.AtStr() + "\n"
    256 			}
    257 		} else {
    258 			msg += "no moderators"
    259 		}
    260 		c.ZeroProcMsg(msg)
    261 		c.Err = command.ErrRedirect
    262 		return true
    263 	}
    264 	return false
    265 }
    266 
    267 func handleInThisRoomCmd(c *command.Command) (handled bool) {
    268 	if c.Message == "/itr" {
    269 		membersInRoom, _ := managers.ActiveUsers.GetRoomUsers(c.Room, managers.GetUserIgnoreSet(c.DB, c.AuthUser))
    270 
    271 		msg := "In this room:"
    272 		if len(membersInRoom) > 0 {
    273 			msg += " "
    274 			for _, mod := range membersInRoom {
    275 				msg += mod.Username.AtStr() + " "
    276 			}
    277 		} else {
    278 			msg += "no one"
    279 		}
    280 		c.ZeroProcMsg(msg)
    281 		c.Err = command.ErrRedirect
    282 		return true
    283 	}
    284 	return false
    285 }
    286 
    287 func handleKickCmd(c *command.Command) (handled bool) {
    288 	if m := kickRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    289 		username := database.Username(m[1])
    290 		if err := kickCmd(c, username, true, false); err != nil {
    291 			c.Err = err
    292 			return true
    293 		}
    294 		c.Err = command.ErrRedirect
    295 		return true
    296 	}
    297 	return
    298 }
    299 
    300 // Kick a user but keep the messages
    301 func handleKickKeepCmd(c *command.Command) (handled bool) {
    302 	if m := kickKeepRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    303 		username := database.Username(m[1])
    304 		if err := kickCmd(c, username, false, false); err != nil {
    305 			c.Err = err
    306 			return true
    307 		}
    308 		c.Err = command.ErrRedirect
    309 		return true
    310 	}
    311 	return
    312 }
    313 
    314 // Kick a user, no system message in chat
    315 func handleKickSilentCmd(c *command.Command) (handled bool) {
    316 	if m := kickSilentRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    317 		username := database.Username(m[1])
    318 		if err := kickCmd(c, username, true, true); err != nil {
    319 			c.Err = err
    320 			return true
    321 		}
    322 		c.Err = command.ErrRedirect
    323 		return true
    324 	}
    325 	return
    326 }
    327 
    328 // Kick a user, keep the messages, no system message in chat
    329 func handleKickKeepSilentCmd(c *command.Command) (handled bool) {
    330 	if m := kickKeepSilentRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    331 		username := database.Username(m[1])
    332 		if err := kickCmd(c, username, false, true); err != nil {
    333 			c.Err = err
    334 			return true
    335 		}
    336 		c.Err = command.ErrRedirect
    337 		return true
    338 	}
    339 	return
    340 }
    341 
    342 func kickCmd(c *command.Command, username database.Username, purge, silent bool) error {
    343 	user, err := c.DB.GetUserByUsername(username)
    344 	if err != nil {
    345 		return ErrUsernameNotFound
    346 	}
    347 	return dutils.Kick(c.DB, user, *c.AuthUser, purge, silent)
    348 }
    349 
    350 var ErrUsernameNotFound = errors.New("username not found")
    351 var ErrUnauthorized = errors.New("unauthorized")
    352 
    353 func handleUnkickCmd(c *command.Command) (handled bool) {
    354 	if m := unkickRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    355 		username := database.Username(m[1])
    356 		user, err := c.DB.GetUserByUsername(username)
    357 		if err != nil {
    358 			c.Err = ErrUsernameNotFound
    359 			return true
    360 		}
    361 		if user.Verified {
    362 			c.Err = errors.New("user already not kicked")
    363 			return true
    364 		}
    365 		c.DB.NewAudit(*c.AuthUser, fmt.Sprintf("unkick %s #%d", user.Username, user.ID))
    366 		user.SetVerified(c.DB, true)
    367 
    368 		// Display unkick message
    369 		c.DB.CreateUnkickMsg(user, *c.AuthUser)
    370 
    371 		c.Err = command.ErrRedirect
    372 		return true
    373 	}
    374 	return
    375 }
    376 
    377 func handleForceCaptchaCmd(c *command.Command) (handled bool) {
    378 	if m := forceCaptchaRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    379 		username := database.Username(m[1])
    380 		user, err := c.DB.GetUserByUsername(username)
    381 		if err != nil {
    382 			c.Err = ErrUsernameNotFound
    383 			return true
    384 		}
    385 		if c.AuthUser.IsAdmin || !user.IsModerator() || c.AuthUser.Username == username {
    386 			c.DB.NewAudit(*c.AuthUser, fmt.Sprintf("force captcha %s #%d", user.Username, user.ID))
    387 			user.SetCaptchaRequired(c.DB, true)
    388 			database.MsgPubSub.Pub("refresh_"+string(user.Username), database.ChatMessageType{Typ: database.ForceRefresh})
    389 		}
    390 		c.Err = command.ErrRedirect
    391 		return true
    392 	}
    393 	return
    394 }
    395 
    396 func handleLogoutCmd(c *command.Command) (handled bool) {
    397 	if m := logoutRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    398 		username := database.Username(m[1])
    399 		user, err := c.DB.GetUserByUsername(username)
    400 		if err != nil {
    401 			c.Err = ErrUsernameNotFound
    402 			return true
    403 		}
    404 		if !c.AuthUser.IsAdmin && user.Vetted {
    405 			c.Err = ErrUnauthorized
    406 			return true
    407 		}
    408 		if c.AuthUser.IsAdmin || !user.IsModerator() {
    409 			c.DB.NewAudit(*c.AuthUser, fmt.Sprintf("logout %s #%d", user.Username, user.ID))
    410 
    411 			_ = c.DB.DeleteUserSessions(user.ID)
    412 
    413 			// Remove user from the user cache
    414 			managers.ActiveUsers.RemoveUser(user.ID)
    415 			database.MsgPubSub.Pub("refresh_"+string(user.Username), database.ChatMessageType{Typ: database.ForceRefresh})
    416 		}
    417 
    418 		c.Err = command.ErrRedirect
    419 		return true
    420 	}
    421 	return
    422 }
    423 
    424 func handleResetTutorialCmd(c *command.Command) (handled bool) {
    425 	if m := rtutoRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    426 		username := database.Username(m[1])
    427 		user, err := c.DB.GetUserByUsername(username)
    428 		if err != nil {
    429 			c.Err = ErrUsernameNotFound
    430 			return true
    431 		}
    432 		if !c.AuthUser.IsAdmin && user.Vetted {
    433 			c.Err = ErrUnauthorized
    434 			return true
    435 		}
    436 		if c.AuthUser.IsAdmin || !user.IsModerator() {
    437 			c.DB.NewAudit(*c.AuthUser, fmt.Sprintf("rtuto %s #%d", user.Username, user.ID))
    438 			user.ResetTutorial(c.DB)
    439 		}
    440 		c.Err = command.ErrRedirect
    441 		return true
    442 	}
    443 	return
    444 }
    445 
    446 func handleHellbanCmd(c *command.Command) (handled bool) {
    447 	if m := hellbanRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    448 		username := database.Username(m[1])
    449 		user, err := c.DB.GetUserByUsername(username)
    450 		if err != nil {
    451 			c.Err = ErrUsernameNotFound
    452 			return true
    453 		}
    454 		if !c.AuthUser.IsAdmin && (user.Vetted || user.IsModerator()) {
    455 			c.Err = ErrUnauthorized
    456 			return true
    457 		}
    458 		c.DB.NewAudit(*c.AuthUser, fmt.Sprintf("hellban %s #%d", user.Username, user.ID))
    459 		user.HellBan(c.DB)
    460 		managers.ActiveUsers.UpdateUserHBInRooms(managers.NewUserInfo(&user))
    461 
    462 		c.Err = command.ErrRedirect
    463 		return true
    464 	}
    465 	return
    466 }
    467 
    468 func handleUnhellbanCmd(c *command.Command) (handled bool) {
    469 	if m := unhellbanRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    470 		username := database.Username(m[1])
    471 		user, err := c.DB.GetUserByUsername(username)
    472 		if err != nil {
    473 			c.Err = ErrUsernameNotFound
    474 			return true
    475 		}
    476 		if !c.AuthUser.IsAdmin && (user.Vetted || user.IsModerator()) {
    477 			c.Err = ErrUnauthorized
    478 			return true
    479 		}
    480 		c.DB.NewAudit(*c.AuthUser, fmt.Sprintf("unhellban %s #%d", user.Username, user.ID))
    481 		user.UnHellBan(c.DB)
    482 		managers.ActiveUsers.UpdateUserHBInRooms(managers.NewUserInfo(&user))
    483 
    484 		c.Err = command.ErrRedirect
    485 		return true
    486 	}
    487 	return false
    488 }
    489 
    490 func handleInviteCmd(c *command.Command) (handled bool) {
    491 	if m := inviteRgx.FindStringSubmatch(c.Message); len(m) == 3 {
    492 		username := database.Username(m[1])
    493 		nbInvites := utils.Clamp(utils.DoParseInt(m[2]), 1, 10)
    494 		if err := c.SetToUser(username); err != nil {
    495 			return true
    496 		}
    497 		c.RawMessage(buildInviteMessage(c, nbInvites))
    498 		c.RedirectQP.Set(command.RedirectPmQP, string(c.ToUser.Username))
    499 		return true
    500 	}
    501 	return
    502 }
    503 
    504 func buildInviteMessage(c *command.Command, nbInvites int) string {
    505 	tokens := make([]string, 0)
    506 	for i := 0; i < nbInvites; i++ {
    507 		if inviteToken, err := c.DB.CreateInvitation(c.AuthUser.ID); err == nil {
    508 			tokens = append(tokens, fmt.Sprintf(`<span style="color: Aqua; user-select: all; -webkit-user-select: all;">%s</span>`, inviteToken.Token))
    509 		}
    510 	}
    511 	return fmt.Sprintf(`invitation tokens:<br />%s`, strings.Join(tokens, "<br />"))
    512 }
    513 
    514 func handleHbmCmd(c *command.Command) (handled bool) {
    515 	if !c.AuthUser.CanSeeHB() {
    516 		return
    517 	}
    518 	if strings.HasPrefix(c.Message, "/hbm ") {
    519 		c.Message = strings.TrimPrefix(c.Message, "/hbm ")
    520 		c.HellbanMsg = true
    521 		c.RedirectQP.Set(command.RedirectHbmQP, "1")
    522 		return true
    523 	}
    524 	return
    525 }
    526 
    527 func handleHbmtCmd(c *command.Command) (handled bool) {
    528 	if !c.AuthUser.CanSeeHB() {
    529 		return
    530 	}
    531 	if m := hbmtRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    532 		date := m[1]
    533 		if dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock()); err == nil {
    534 			if msg, err := c.DB.GetRoomChatMessageByDate(c.Room.ID, c.AuthUser.ID, dt.UTC()); err == nil {
    535 				msg.IsHellbanned = !msg.IsHellbanned
    536 				msg.DoSave(c.DB)
    537 			} else {
    538 				c.Err = errors.New("no message found at this timestamp")
    539 				return true
    540 			}
    541 		}
    542 		c.Err = command.ErrRedirect
    543 		return true
    544 	}
    545 	return
    546 }
    547 
    548 func handleDiceCmd(c *command.Command) (handled bool) {
    549 	if strings.HasPrefix(c.Message, "/dice") {
    550 		dice := utils.RandInt(1, 6)
    551 		raw := fmt.Sprintf(`rolling dice for @%s ... "%d"`, c.AuthUser.Username, dice)
    552 		msg := fmt.Sprintf(`rolling dice for @%s ... "<span style="color: white;">%d</span>"`, c.AuthUser.Username, dice)
    553 		msg, _ = dutils.ColorifyTaggedUsers(msg, c.DB.GetUsersByUsername)
    554 		go func() {
    555 			time.Sleep(time.Second)
    556 			c.ZeroPublicMsg(raw, msg)
    557 		}()
    558 		return true
    559 	}
    560 	return
    561 }
    562 
    563 func handleRandCmd(c *command.Command) (handled bool) {
    564 	if strings.HasPrefix(c.Message, "/rand") {
    565 		minV := 1
    566 		maxV := 6
    567 		var dice int
    568 		if m := randRgx.FindStringSubmatch(c.Message); len(m) == 3 {
    569 			var err error
    570 			minV, err = strconv.Atoi(m[1])
    571 			if err != nil {
    572 				c.Err = err
    573 				return true
    574 			}
    575 			maxV, err = strconv.Atoi(m[2])
    576 			if err != nil {
    577 				c.Err = err
    578 				return true
    579 			}
    580 			if maxV <= minV {
    581 				c.Err = errors.New("max must be greater than min")
    582 				return true
    583 			}
    584 		} else if c.Message != "/rand" {
    585 			c.Err = errors.New("invalid /rand command")
    586 			return true
    587 		}
    588 		dice = utils.RandInt(minV, maxV)
    589 		raw := fmt.Sprintf(`rolling dice for @%s ... "%d"`, c.AuthUser.Username, dice)
    590 		msg := fmt.Sprintf(`rolling dice for @%s ... "<span style="color: white;">%d</span>"`, c.AuthUser.Username, dice)
    591 		msg, _ = dutils.ColorifyTaggedUsers(msg, c.DB.GetUsersByUsername)
    592 		go func() {
    593 			time.Sleep(time.Second)
    594 			c.ZeroPublicMsg(raw, msg)
    595 		}()
    596 		return true
    597 	}
    598 	return
    599 }
    600 
    601 func handleChoiceCmd(c *command.Command) (handled bool) {
    602 	if strings.HasPrefix(c.Message, "/choice ") {
    603 		tmp := html.EscapeString(strings.TrimPrefix(c.Message, "/choice "))
    604 		words := strings.Fields(tmp)
    605 		answer := utils.RandChoice(words)
    606 		raw := fmt.Sprintf(`@%s choice %s ... "%s"`, c.AuthUser.Username, words, answer)
    607 		msg := fmt.Sprintf(`@%s choice %s ... "<span style="color: white;">%s</span>"`, c.AuthUser.Username, words, answer)
    608 		msg, _ = dutils.ColorifyTaggedUsers(msg, c.DB.GetUsersByUsername)
    609 		go func() {
    610 			time.Sleep(time.Second)
    611 			c.ZeroPublicMsg(raw, msg)
    612 		}()
    613 		c.SkipInboxes = true
    614 		return true
    615 	}
    616 	return
    617 }
    618 
    619 func handleTokenCmd(c *command.Command) (handled bool) {
    620 	if c.Message == "/token" {
    621 		c.ZeroMsg(utils.GenerateToken10())
    622 		c.Err = command.ErrRedirect
    623 		return true
    624 	} else if m := tokenRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    625 		n, _ := strconv.Atoi(m[1])
    626 		if n < 1 || n > 32 {
    627 			c.Err = errors.New("value must be [1;32]")
    628 			return true
    629 		}
    630 		n = utils.Clamp(n, 1, 32)
    631 		c.ZeroMsg(utils.GenerateTokenN(n))
    632 		c.Err = command.ErrRedirect
    633 		return true
    634 	}
    635 	return
    636 }
    637 
    638 func handleMd5Cmd(c *command.Command) (handled bool) {
    639 	return handleHasherCmd(c, "/md5 ", utils.MD5)
    640 }
    641 
    642 func handleSha1Cmd(c *command.Command) (handled bool) {
    643 	return handleHasherCmd(c, "/sha1 ", utils.Sha1)
    644 }
    645 
    646 func handleSha256Cmd(c *command.Command) (handled bool) {
    647 	return handleHasherCmd(c, "/sha256 ", utils.Sha256)
    648 }
    649 
    650 func handleSha512Cmd(c *command.Command) (handled bool) {
    651 	return handleHasherCmd(c, "/sha512 ", utils.Sha512)
    652 }
    653 
    654 func handleHasherCmd(c *command.Command, prefix string, fn func([]byte) string) (handled bool) {
    655 	if strings.HasPrefix(c.Message, prefix) {
    656 		c.Message = strings.TrimPrefix(c.Message, prefix)
    657 		c.DataMessage = prefix
    658 		c.ZeroMsg(fn([]byte(c.Message)))
    659 		c.Err = command.ErrStop
    660 		return true
    661 	}
    662 	return
    663 }
    664 
    665 func handleRmGroupCmd(c *command.Command) (handled bool) {
    666 	if m := rmGroupRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    667 		groupName := m[1]
    668 		if err := c.DB.DeleteChatRoomGroup(c.Room.ID, groupName); err != nil {
    669 			c.Err = err
    670 			return true
    671 		}
    672 		c.Err = command.ErrRedirect
    673 		return true
    674 	}
    675 	return false
    676 }
    677 
    678 func handleLockGroupCmd(c *command.Command) (handled bool) {
    679 	if m := lockGroupRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    680 		groupName := m[1]
    681 		group, err := c.DB.GetRoomGroupByName(c.Room.ID, groupName)
    682 		if err != nil {
    683 			c.Err = err
    684 			return true
    685 		}
    686 		group.Locked = true
    687 		group.DoSave(c.DB)
    688 		c.Err = command.ErrRedirect
    689 		return true
    690 	}
    691 	return false
    692 }
    693 
    694 func handleUnlockGroupCmd(c *command.Command) (handled bool) {
    695 	if m := unlockGroupRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    696 		groupName := m[1]
    697 		group, err := c.DB.GetRoomGroupByName(c.Room.ID, groupName)
    698 		if err != nil {
    699 			c.Err = err
    700 			return true
    701 		}
    702 		group.Locked = false
    703 		group.DoSave(c.DB)
    704 		c.Err = command.ErrRedirect
    705 		return true
    706 	}
    707 	return false
    708 }
    709 
    710 func handleGroupUsersCmd(c *command.Command) (handled bool) {
    711 	if m := groupUsersRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    712 		groupName := m[1]
    713 		group, err := c.DB.GetRoomGroupByName(c.Room.ID, groupName)
    714 		if err != nil {
    715 			c.Err = err
    716 			return true
    717 		}
    718 		users, err := c.DB.GetRoomGroupUsers(c.Room.ID, group.ID)
    719 		sort.Slice(users, func(i, j int) bool {
    720 			return users[i].User.Username < users[j].User.Username
    721 		})
    722 		msg := ""
    723 		if len(users) > 0 {
    724 			msg += "\n"
    725 			for _, user := range users {
    726 				msg += user.User.Username.AtStr() + "\n"
    727 			}
    728 		} else {
    729 			msg += "no user in th group: " + groupName
    730 		}
    731 		c.ZeroProcMsg(msg)
    732 		c.Err = command.ErrRedirect
    733 		return true
    734 	}
    735 	return false
    736 }
    737 
    738 func handleListGroupsCmd(c *command.Command) (handled bool) {
    739 	if c.Message == "/groups" {
    740 		groups, err := c.DB.GetRoomGroups(c.Room.ID)
    741 		if err != nil {
    742 			c.Err = err
    743 			return true
    744 		}
    745 		msg := ""
    746 		if len(groups) > 0 {
    747 			msg += "\n"
    748 			for _, group := range groups {
    749 				msg += group.Name + " (" + group.Color + ")\n"
    750 			}
    751 		} else {
    752 			msg += "no groups"
    753 		}
    754 		c.ZeroProcMsg(msg)
    755 		c.Err = command.ErrRedirect
    756 		return true
    757 	}
    758 	return false
    759 }
    760 
    761 func handleGroupAddUserCmd(c *command.Command) (handled bool) {
    762 	if m := groupAddUserRgx.FindStringSubmatch(c.Message); len(m) == 3 {
    763 		groupName := m[1]
    764 		username := database.Username(m[2])
    765 		user, err := c.DB.GetUserByUsername(username)
    766 		if err != nil {
    767 			c.Err = err
    768 			return true
    769 		}
    770 		group, err := c.DB.GetRoomGroupByName(c.Room.ID, groupName)
    771 		if err != nil {
    772 			c.Err = err
    773 			return true
    774 		}
    775 		_, err = c.DB.AddUserToRoomGroup(c.Room.ID, group.ID, user.ID)
    776 		if err != nil {
    777 			c.Err = err
    778 			return true
    779 		}
    780 		c.Err = command.ErrRedirect
    781 		return true
    782 	} else if strings.HasPrefix(c.Message, "/gadduser ") {
    783 		c.Err = errors.New("invalid /gadduser command")
    784 	}
    785 	return false
    786 }
    787 
    788 func handleGroupRmUserCmd(c *command.Command) (handled bool) {
    789 	if m := groupRmUserRgx.FindStringSubmatch(c.Message); len(m) == 3 {
    790 		groupName := m[1]
    791 		username := database.Username(m[2])
    792 		user, err := c.DB.GetUserByUsername(username)
    793 		if err != nil {
    794 			c.Err = err
    795 			return true
    796 		}
    797 		group, err := c.DB.GetRoomGroupByName(c.Room.ID, groupName)
    798 		if err != nil {
    799 			c.Err = err
    800 			return true
    801 		}
    802 		err = c.DB.RmUserFromRoomGroup(c.Room.ID, group.ID, user.ID)
    803 		if err != nil {
    804 			c.Err = err
    805 			return true
    806 		}
    807 		c.Err = command.ErrRedirect
    808 		return true
    809 	} else if strings.HasPrefix(c.Message, "/grmuser ") {
    810 		c.Err = errors.New("invalid /grmuser command")
    811 	}
    812 	return false
    813 }
    814 
    815 func handleSetModeCmd(c *command.Command) (handled bool) {
    816 	if c.Message == "/mode user-whitelist" {
    817 		c.Room.Mode = database.UserWhitelistRoomMode
    818 		c.Room.DoSave(c.DB)
    819 		msg := `room mode set to "user whitelist"`
    820 		c.ZeroProcMsg(msg)
    821 		c.Err = command.ErrRedirect
    822 		return true
    823 
    824 	} else if c.Message == "/mode standard" {
    825 		c.Room.Mode = database.NormalRoomMode
    826 		c.Room.DoSave(c.DB)
    827 		msg := `room mode set to "standard"`
    828 		c.ZeroProcMsg(msg)
    829 		c.Err = command.ErrRedirect
    830 		return true
    831 	}
    832 	return false
    833 }
    834 
    835 func handleGetRoomWhitelistCmd(c *command.Command) (handled bool) {
    836 	if m := whitelistUserRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    837 		username := database.Username(m[1])
    838 		user, err := c.DB.GetUserByUsername(username)
    839 		var msg string
    840 		if err != nil {
    841 			msg = fmt.Sprintf(`username "%s" not found`, username)
    842 		} else {
    843 			if _, err := c.DB.WhitelistUser(c.Room.ID, user.ID); err != nil {
    844 				if err := c.DB.DeWhitelistUser(c.Room.ID, user.ID); err != nil {
    845 					msg = fmt.Sprintf("failed to toggle @%s in whitelist", user.Username)
    846 				} else {
    847 					msg = fmt.Sprintf("@%s removed from whitelist", user.Username)
    848 				}
    849 			} else {
    850 				msg = fmt.Sprintf("@%s added to whitelist", user.Username)
    851 			}
    852 		}
    853 		c.ZeroProcMsg(msg)
    854 		c.Err = command.ErrRedirect
    855 		return true
    856 	}
    857 	return false
    858 }
    859 
    860 func handleToggleReadOnlyCmd(c *command.Command) (handled bool) {
    861 	if c.Message == "/ro" {
    862 		c.Room.ReadOnly = !c.Room.ReadOnly
    863 		c.Room.DoSave(c.DB)
    864 		if c.Room.ReadOnly {
    865 			c.Err = command.NewErrSuccess("room is now read-only")
    866 		} else {
    867 			c.Err = command.NewErrSuccess("room is no longer read-only")
    868 		}
    869 		return true
    870 	}
    871 	return
    872 }
    873 
    874 func handleAddGroupCmd(c *command.Command) (handled bool) {
    875 	if m := addGroupRgx.FindStringSubmatch(c.Message); len(m) == 2 {
    876 		name := m[1]
    877 		_, err := c.DB.CreateChatRoomGroup(c.Room.ID, name, "#fff")
    878 		if err != nil {
    879 			c.Err = err
    880 			return true
    881 		}
    882 		c.Err = command.ErrRedirect
    883 		return true
    884 	}
    885 	return false
    886 }
    887 
    888 func handleWhitelistCmd(c *command.Command) (handled bool) {
    889 	if c.Message == "/whitelist" || c.Message == "/wl" {
    890 		usernames := make([]string, 0)
    891 		whitelistedUsers, _ := c.DB.GetWhitelistedUsers(c.Room.ID)
    892 		if c.Room.OwnerUserID != nil {
    893 			owner, _ := c.DB.GetUserByID(*c.Room.OwnerUserID)
    894 			usernames = append(usernames, owner.Username.AtStr())
    895 		}
    896 		for _, whitelistedUser := range whitelistedUsers {
    897 			usernames = append(usernames, whitelistedUser.User.Username.AtStr())
    898 		}
    899 		sort.Slice(usernames, func(i, j int) bool { return usernames[i] < usernames[j] })
    900 		var msg string
    901 		if len(whitelistedUsers) > 0 {
    902 			msg = "whitelisted users: " + strings.Join(usernames, ", ")
    903 		} else {
    904 			msg = "no whitelisted user"
    905 		}
    906 		c.ZeroProcMsg(msg)
    907 		c.Err = command.ErrRedirect
    908 		return true
    909 	}
    910 	return false
    911 }
    912 
    913 func handleGetModeCmd(c *command.Command) (handled bool) {
    914 	if c.Message == "/mode" {
    915 		var msg string
    916 		if c.Room.Mode == database.NormalRoomMode {
    917 			msg = `room is in "standard" mode`
    918 		} else if c.Room.Mode == database.UserWhitelistRoomMode {
    919 			msg = `room is in "user whitelist" mode`
    920 		}
    921 		c.ZeroProcMsg(msg)
    922 		c.Err = command.ErrRedirect
    923 		return true
    924 	}
    925 	return false
    926 }
    927 
    928 func handleMeCmd(c *command.Command) (handled bool) {
    929 	if c.Message == "/me " {
    930 		c.Err = errors.New("invalid /me command")
    931 		return true
    932 	}
    933 	if strings.HasPrefix(c.Message, "/me ") {
    934 		return true
    935 	}
    936 	return
    937 }
    938 
    939 func handleEditCmd(c *command.Command) (handled bool) {
    940 	if m := editRgx.FindStringSubmatch(c.Message); len(m) == 3 {
    941 		date := m[1]
    942 		newMsg := m[2]
    943 		dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
    944 		if err != nil {
    945 			c.Err = errors.New("failed to parse timestamp")
    946 			return true
    947 		}
    948 		if time.Since(dt) > config.EditMessageTimeLimit {
    949 			c.Err = errors.New("message too old to be edited")
    950 			return true
    951 		}
    952 		msg, err := c.DB.GetRoomChatMessageByDate(c.Room.ID, c.AuthUser.ID, dt.UTC())
    953 		if err != nil {
    954 			c.Err = fmt.Errorf("failed to get message at timestamp %s", date)
    955 			return true
    956 		}
    957 		c.EditMsg = &msg
    958 		c.OrigMessage = newMsg
    959 		c.Message = newMsg
    960 
    961 		// If we're editing a message which contains a link to an uploaded file,
    962 		// we need to re-add the link to the html.
    963 		if msg.UploadID != nil {
    964 			if newUpload, err := c.DB.GetUploadByID(*msg.UploadID); err == nil {
    965 				c.Upload = &newUpload
    966 			}
    967 		}
    968 
    969 		if pmRgx.MatchString(c.Message) {
    970 			handlePMCmd(c)
    971 		} else if c.AuthUser.IsModerator() && strings.HasPrefix(c.Message, "/m ") {
    972 			handleModeratorGroupCmd(c)
    973 		} else if strings.HasPrefix(c.Message, "/hbm ") {
    974 			handleHbmCmd(c)
    975 		} else if strings.HasPrefix(c.Message, "/g ") {
    976 			handleGroupChatCmd(c)
    977 		} else if strings.HasPrefix(c.Message, "/system ") || strings.HasPrefix(c.Message, "/sys ") {
    978 			handleSystemCmd(c)
    979 		}
    980 		return true
    981 
    982 	} else if c.Message == "/e" {
    983 		msg, err := c.DB.GetUserLastChatMessageInRoom(c.AuthUser.ID, c.Room.ID)
    984 		if err != nil {
    985 			return true
    986 		}
    987 		c.RedirectQP.Set(command.RedirectEditQP, msg.CreatedAt.Format("15:04:05"))
    988 		c.Err = command.ErrRedirect
    989 		return true
    990 	}
    991 	return
    992 }
    993 
    994 func canUserInboxOther(db *database.DkfDB, user, other database.User) error {
    995 	doesNotMatter := utils.False()
    996 	_, err := dutils.CanUserPmOther(db, user, other, doesNotMatter)
    997 	return err
    998 }
    999 
   1000 func handlePMCmd(c *command.Command) (handled bool) {
   1001 	if m := pmRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1002 		username := database.Username(m[1])
   1003 		newMsg := m[2]
   1004 		redirectPmQP := command.RedirectPmQP
   1005 
   1006 		// Chat helpers
   1007 		if username == config.NullUsername {
   1008 			c.RedirectQP.Set(redirectPmQP, config.NullUsername)
   1009 			return handlePm0(c, newMsg)
   1010 		}
   1011 
   1012 		// Hack to have 1 on 1 chat with the user
   1013 		if strings.TrimSpace(newMsg) == "" && c.Upload == nil {
   1014 			if _, err := c.DB.GetUserByUsername(username); err != nil {
   1015 				c.Err = errors.New("invalid username")
   1016 				return true
   1017 			}
   1018 			redirectPmUsernameQP := command.RedirectPmUsernameQP
   1019 			newURL := fmt.Sprintf("/api/v1/chat/messages/%s/stream?%s=%s", c.Room.Name, redirectPmUsernameQP, username)
   1020 			database.MsgPubSub.Pub("refresh_"+string(c.AuthUser.Username), database.ChatMessageType{Typ: database.Redirect, NewURL: newURL})
   1021 			c.RedirectQP.Set(redirectPmUsernameQP, username.String())
   1022 			c.Err = command.ErrRedirect
   1023 			return true
   1024 		}
   1025 
   1026 		if err := c.SetToUser(username); err != nil {
   1027 			return true
   1028 		}
   1029 		c.Message = newMsg
   1030 		c.RedirectQP.Set(redirectPmQP, string(c.ToUser.Username))
   1031 
   1032 		if newMsg == "/d" || strings.HasPrefix(newMsg, "/d ") {
   1033 			handled = handleDeleteMsgCmd(c)
   1034 			if c.Err != nil && !errors.Is(c.Err, command.ErrRedirect) {
   1035 				return handled
   1036 			}
   1037 			c.Err = command.ErrRedirect
   1038 			return handled
   1039 		}
   1040 
   1041 		if handleCodeCmd(c) {
   1042 			return true
   1043 		}
   1044 
   1045 		return true
   1046 	} else if strings.HasPrefix(c.Message, "/pm ") {
   1047 		c.Err = errors.New("invalid /pm command")
   1048 		return true
   1049 	}
   1050 	return false
   1051 }
   1052 
   1053 // Handle PMs sent to user 0 (/pm 0 msg)
   1054 func handlePm0(c *command.Command, msg string) (handled bool) {
   1055 	if msg == "ping" {
   1056 		c.ZeroMsg("pong")
   1057 		c.Err = command.ErrRedirect
   1058 		return true
   1059 
   1060 	} else if msg == "talk" {
   1061 		c.ZeroMsg("talking")
   1062 		c.Err = command.ErrRedirect
   1063 		return true
   1064 
   1065 	} else if msg == "pgp" || msg == "gpg" {
   1066 		pkey := c.AuthUser.GPGPublicKey
   1067 		if pkey == "" {
   1068 			c.Message = "I could not find a public pgp key in your profile."
   1069 		} else {
   1070 			msg := "This is a sample text " + utils.GenerateToken10()
   1071 			if encrypted, err := utils.GeneratePgpEncryptedMessage(pkey, msg); err != nil {
   1072 				c.Message = err.Error()
   1073 			} else {
   1074 				c.Message = strings.Join(strings.Split(encrypted, "\n"), " ")
   1075 			}
   1076 		}
   1077 		c.ZeroProcMsg(c.Message)
   1078 		c.Err = command.ErrRedirect
   1079 		return true
   1080 
   1081 	} else if pgpMsg, _, _ := dutils.ExtractPGPMessage(msg); pgpMsg != "" {
   1082 		decrypted, err := utils.PgpDecryptMessage(config.NullUserPrivateKey, pgpMsg)
   1083 		if err != nil {
   1084 			c.Message = err.Error()
   1085 		} else {
   1086 			c.Message = "Decrypted message: " + decrypted
   1087 		}
   1088 		c.ZeroProcMsg(c.Message)
   1089 		c.Err = command.ErrRedirect
   1090 		return true
   1091 
   1092 	} else if b, _ := clearsign.Decode([]byte(msg)); b != nil {
   1093 		if p, err := packet.Read(b.ArmoredSignature.Body); err == nil {
   1094 			if sig, ok := p.(*packet.Signature); ok {
   1095 				zero := c.GetZeroUser()
   1096 				msg := fmt.Sprintf("<br />"+
   1097 					"<table %s>"+
   1098 					"<tr><td align=\"right\">Signature made:&nbsp;&nbsp;</td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
   1099 					"<tr><td align=\"right\">Fingerprint:&nbsp;&nbsp;</td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
   1100 					"<tr><td align=\"right\">Issuer:&nbsp;&nbsp;</td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
   1101 					"</table>",
   1102 					zero.GenerateChatStyle(),
   1103 					sig.CreationTime.Format(time.RFC1123),
   1104 					utils.FormatPgPFingerprint(sig.IssuerFingerprint),
   1105 					utils.Ternary(sig.SignerUserId != nil, *sig.SignerUserId, "n/a"))
   1106 				c.ZeroMsg(msg)
   1107 				c.Err = command.ErrRedirect
   1108 				return true
   1109 			}
   1110 		}
   1111 
   1112 	} else if c.Upload != nil {
   1113 
   1114 		// If we sent a clearsign file to @0, the bot will reply with information about the signature
   1115 		if c.Upload.FileSize < config.MaxFileSizeBeforeDownload {
   1116 			if file, err := c.DB.GetUploadByFileName(c.Upload.FileName); err == nil {
   1117 				if _, by, err := file.GetContent(); err == nil {
   1118 					if b, _ := clearsign.Decode(by); b != nil {
   1119 						if p, err := packet.Read(b.ArmoredSignature.Body); err == nil {
   1120 							if sig, ok := p.(*packet.Signature); ok {
   1121 								zero := c.GetZeroUser()
   1122 								msg := fmt.Sprintf("<br />"+
   1123 									"<table %s>"+
   1124 									"<tr><td align=\"right\">File:&nbsp;&nbsp;</td><td><span style=\"color: #82e17f;\">%s</span> (<span style=\"color: #82e17f;\">%s</span>)</td></tr>"+
   1125 									"<tr><td align=\"right\">Signature made:&nbsp;&nbsp;</td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
   1126 									"<tr><td align=\"right\">Fingerprint:&nbsp;&nbsp;</td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
   1127 									"<tr><td align=\"right\">Issuer:&nbsp;&nbsp;</td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
   1128 									"</table>",
   1129 									zero.GenerateChatStyle(),
   1130 									c.Upload.OrigFileName,
   1131 									humanize.Bytes(uint64(c.Upload.FileSize)),
   1132 									sig.CreationTime.Format(time.RFC1123),
   1133 									utils.FormatPgPFingerprint(sig.IssuerFingerprint),
   1134 									utils.Ternary(sig.SignerUserId != nil, *sig.SignerUserId, "n/a"))
   1135 								c.ZeroMsg(msg)
   1136 								c.Err = command.ErrRedirect
   1137 								return true
   1138 							}
   1139 						}
   1140 					}
   1141 				}
   1142 			}
   1143 		}
   1144 	}
   1145 
   1146 	zeroUser := c.GetZeroUser()
   1147 	c.ToUser = &zeroUser
   1148 	c.Message = msg
   1149 
   1150 	return true
   1151 }
   1152 
   1153 func handleSubscribeCmd(c *command.Command) (handled bool) {
   1154 	if c.Message == "/subscribe" {
   1155 		_ = c.DB.SubscribeToRoom(c.AuthUser.ID, c.Room.ID)
   1156 		c.Err = command.ErrRedirect
   1157 		return true
   1158 	}
   1159 	return
   1160 }
   1161 
   1162 func handleUnsubscribeCmd(c *command.Command) (handled bool) {
   1163 	if m := unsubscribeRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1164 		room, err := c.DB.GetChatRoomByName(m[1])
   1165 		if err != nil {
   1166 			c.Err = err
   1167 			return true
   1168 		}
   1169 		_ = c.DB.UnsubscribeFromRoom(c.AuthUser.ID, room.ID)
   1170 		c.Err = command.ErrRedirect
   1171 		return true
   1172 
   1173 	} else if c.Message == "/unsubscribe" {
   1174 		_ = c.DB.UnsubscribeFromRoom(c.AuthUser.ID, c.Room.ID)
   1175 		c.Err = command.ErrRedirect
   1176 		return true
   1177 	}
   1178 	return
   1179 }
   1180 
   1181 func handleGroupChatCmd(c *command.Command) (handled bool) {
   1182 	if m := groupRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1183 		groupName := m[1]
   1184 		c.Message = m[2]
   1185 		group, err := c.DB.GetRoomGroupByName(c.Room.ID, groupName)
   1186 		if err != nil {
   1187 			c.Err = err
   1188 			return true
   1189 		}
   1190 		if group.Locked {
   1191 			c.Err = errors.New("group is locked")
   1192 			return true
   1193 		}
   1194 		c.RedirectQP.Set(command.RedirectGroupQP, group.Name)
   1195 		c.GroupID = &group.ID
   1196 		return true
   1197 	} else if strings.HasPrefix(c.Message, "/g ") {
   1198 		c.Err = errors.New("invalid /g command")
   1199 		return true
   1200 	}
   1201 	return false
   1202 }
   1203 
   1204 func handleListPmWhitelistCmd(c *command.Command) (handled bool) {
   1205 	if c.Message == "/pmwhitelist" {
   1206 		pmWhitelistUsers, _ := c.DB.GetPmWhitelistedUsers(c.AuthUser.ID)
   1207 		sort.Slice(pmWhitelistUsers, func(i, j int) bool {
   1208 			return pmWhitelistUsers[i].WhitelistedUser.Username < pmWhitelistUsers[j].WhitelistedUser.Username
   1209 		})
   1210 		msg := ""
   1211 		if len(pmWhitelistUsers) > 0 {
   1212 			msg += "\n"
   1213 			for _, ignoredUser := range pmWhitelistUsers {
   1214 				msg += ignoredUser.WhitelistedUser.Username.AtStr() + "\n"
   1215 			}
   1216 		} else {
   1217 			msg += "no PM whitelisted users"
   1218 		}
   1219 		c.ZeroProcMsg(msg)
   1220 		c.Err = command.ErrRedirect
   1221 		return true
   1222 	}
   1223 	return false
   1224 }
   1225 
   1226 func handleSetPmModeCmd(c *command.Command) (handled bool) {
   1227 	if c.Message == "/setpmmode whitelist" {
   1228 		c.AuthUser.SetPmMode(c.DB, database.PmModeWhitelist)
   1229 		msg := `pm mode set to "whitelist"`
   1230 		c.ZeroProcMsg(msg)
   1231 		c.Err = command.ErrRedirect
   1232 		return true
   1233 
   1234 	} else if c.Message == "/setpmmode standard" {
   1235 		c.AuthUser.SetPmMode(c.DB, database.PmModeStandard)
   1236 		msg := `pm mode set to "standard"`
   1237 		c.ZeroProcMsg(msg)
   1238 		c.Err = command.ErrRedirect
   1239 		return true
   1240 	}
   1241 	return false
   1242 }
   1243 
   1244 func handleTogglePmBlacklistedUser(c *command.Command) (handled bool) {
   1245 	if m := pmToggleBlacklistUserRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1246 		username := database.Username(m[1])
   1247 		user, err := c.DB.GetUserByUsername(username)
   1248 		if err != nil {
   1249 			c.Err = command.ErrRedirect
   1250 			return true
   1251 		}
   1252 		if c.DB.ToggleBlacklistedUser(c.AuthUser.ID, user.ID) {
   1253 			c.Err = command.NewErrSuccess("added to blacklist")
   1254 		} else {
   1255 			c.Err = command.NewErrSuccess("removed from blacklist")
   1256 		}
   1257 		return true
   1258 	}
   1259 	return false
   1260 }
   1261 
   1262 func handleTogglePmWhitelistedUser(c *command.Command) (handled bool) {
   1263 	if m := pmToggleWhitelistUserRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1264 		username := database.Username(m[1])
   1265 		user, err := c.DB.GetUserByUsername(username)
   1266 		if err != nil {
   1267 			c.Err = command.ErrRedirect
   1268 			return true
   1269 		}
   1270 		if c.DB.ToggleWhitelistedUser(c.AuthUser.ID, user.ID) {
   1271 			c.Err = command.NewErrSuccess("added to whitelist")
   1272 		} else {
   1273 			c.Err = command.NewErrSuccess("removed from whitelist")
   1274 		}
   1275 		return true
   1276 	}
   1277 	return false
   1278 }
   1279 
   1280 func handleChessCmd(c *command.Command) (handled bool) {
   1281 	if m := chessRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1282 		username := database.Username(m[1])
   1283 		color := m[2]
   1284 		player1 := *c.AuthUser
   1285 		player2, err := c.DB.GetUserByUsername(username)
   1286 		if err != nil {
   1287 			c.Err = errors.New("invalid username")
   1288 			return true
   1289 		}
   1290 		if _, err := ChessInstance.NewGame1(c.RoomKey, c.Room.ID, player1, player2, color); err != nil {
   1291 			c.Err = err
   1292 			return true
   1293 		}
   1294 		c.Err = command.NewErrSuccess("chess game created")
   1295 		return true
   1296 	}
   1297 	return
   1298 }
   1299 
   1300 func handleInboxCmd(c *command.Command) (handled bool) {
   1301 	if m := inboxRgx.FindStringSubmatch(c.Message); len(m) == 4 {
   1302 		username := database.Username(m[1])
   1303 		encryptRaw := m[2]
   1304 		message := m[3]
   1305 		tryEncrypt := false
   1306 		if encryptRaw == " -e" {
   1307 			tryEncrypt = true
   1308 		}
   1309 		toUser, err := c.DB.GetUserByUsername(username)
   1310 		if err != nil {
   1311 			c.Err = errors.New("invalid username")
   1312 			return true
   1313 		}
   1314 
   1315 		if err := canUserInboxOther(c.DB, *c.AuthUser, toUser); err != nil {
   1316 			c.Err = err
   1317 			return true
   1318 		}
   1319 
   1320 		inboxHTML := message
   1321 		if tryEncrypt {
   1322 			if toUser.GPGPublicKey == "" {
   1323 				c.Err = errors.New("user has no pgp public key")
   1324 				return true
   1325 			}
   1326 			inboxHTML, err = utils.GeneratePgpEncryptedMessage(toUser.GPGPublicKey, message)
   1327 			if err != nil {
   1328 				c.Err = errors.New("failed to encrypt")
   1329 				return true
   1330 			}
   1331 			inboxHTML = strings.Join(strings.Split(inboxHTML, "\n"), " ")
   1332 		}
   1333 
   1334 		inboxHTML, _, _ = dutils.ProcessRawMessage(c.DB, inboxHTML, c.RoomKey, c.AuthUser.ID, c.Room.ID, nil, c.AuthUser.IsModerator(), c.AuthUser.CanUseMultiline, c.AuthUser.ManualMultiline)
   1335 		c.DB.CreateInboxMessage(inboxHTML, c.Room.ID, c.AuthUser.ID, toUser.ID, true, false, nil)
   1336 
   1337 		c.DataMessage = "/inbox " + string(username) + " "
   1338 		c.Err = command.NewErrSuccess("inbox sent")
   1339 		return true
   1340 
   1341 	} else if strings.HasPrefix(c.Message, "/inbox ") {
   1342 		c.Err = errors.New("invalid /inbox command")
   1343 		return true
   1344 	}
   1345 	return
   1346 }
   1347 
   1348 func handleProfileCmd(c *command.Command) (handled bool) {
   1349 	if m := profileRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1350 		username := database.Username(m[1])
   1351 		user, err := c.DB.GetUserByUsername(username)
   1352 		if err != nil {
   1353 			c.Err = ErrUsernameNotFound
   1354 			return true
   1355 		}
   1356 		profile := `/u/` + user.Username
   1357 		c.ZeroMsg(fmt.Sprintf(`[<a href="%s" rel="noopener noreferrer" target="_blank">profile of %s</a>]`, profile, user.Username))
   1358 		c.Err = command.ErrRedirect
   1359 		return true
   1360 	} else if strings.HasPrefix(c.Message, "/p ") {
   1361 		c.Err = errors.New("invalid profile command")
   1362 		return true
   1363 	}
   1364 	return
   1365 }
   1366 
   1367 func handleChipsSendCmd(c *command.Command) (handled bool) {
   1368 	if m := chipsSendRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1369 		username := database.Username(m[1])
   1370 		chips := database.PokerChip(utils.DoParseInt(m[2]))
   1371 		if chips <= 0 {
   1372 			c.Err = errors.New("must send at least 1 chip")
   1373 			return true
   1374 		}
   1375 		if chips > 1000000 {
   1376 			c.Err = errors.New("cannot send more than 1000000 chips")
   1377 			return true
   1378 		}
   1379 		if c.AuthUser.ChipsTest < chips {
   1380 			c.Err = errors.New("you do not have enough chips")
   1381 			return true
   1382 		}
   1383 		user, err := c.DB.GetUserByUsername(username)
   1384 		if err != nil {
   1385 			c.Err = errors.New("username does not exists")
   1386 			return true
   1387 		}
   1388 		user.ChipsTest += chips
   1389 		user.DoSave(c.DB)
   1390 		c.DataMessage = "/chips-send " + username.String() + " "
   1391 		c.Err = command.NewErrSuccess("chips sent")
   1392 		return true
   1393 	}
   1394 	return
   1395 }
   1396 
   1397 func handleChipsResetCmd(c *command.Command) (handled bool) {
   1398 	if c.Message == "/chips-reset" {
   1399 		c.AuthUser.ResetChipsTest(c.DB)
   1400 		c.Err = command.ErrRedirect
   1401 		return true
   1402 	}
   1403 	return
   1404 }
   1405 
   1406 func handleChipsBalanceCmd(c *command.Command) (handled bool) {
   1407 	if c.Message == "/chips" {
   1408 		c.ZeroMsg(fmt.Sprintf(`Balance: %d`, c.AuthUser.ChipsTest))
   1409 		c.Err = command.ErrRedirect
   1410 		return true
   1411 	}
   1412 	return
   1413 }
   1414 
   1415 func handleTutorialCmd(c *command.Command) (handled bool) {
   1416 	if c.Message == "/tuto" && utils.False() {
   1417 		name := "tuto_" + utils.GenerateToken10()
   1418 		room, _ := c.DB.CreateRoom(name, "", c.AuthUser.ID, false)
   1419 		c.Err = command.ErrRedirect
   1420 		c.ZeroProcMsg("Tutorial here -> #" + room.Name)
   1421 		c.ZeroPublicProcMsgRoom("Welcome to the tutorial", "", room.ID)
   1422 		return true
   1423 	}
   1424 	return
   1425 }
   1426 
   1427 func handleDeleteMsgCmd(c *command.Command) (handled bool) {
   1428 	getMsgForUsername := func(msgs []database.ChatMessage, username database.Username) (database.ChatMessage, error) {
   1429 		var msg database.ChatMessage
   1430 		for _, msgTmp := range msgs {
   1431 			if msgTmp.User.Username == username {
   1432 				msg = msgTmp
   1433 				return msg, nil
   1434 			}
   1435 		}
   1436 		return msg, errors.New("failed to find msg")
   1437 	}
   1438 	delMsgFn := func(msgs []database.ChatMessage) error {
   1439 		msg, err := getMsgForUsername(msgs, c.AuthUser.Username)
   1440 		if err != nil {
   1441 			return err
   1442 		}
   1443 		if err := msg.UserCanDeleteErr(c.AuthUser); err != nil {
   1444 			return err
   1445 		}
   1446 		if msg.RoomID == config.GeneralRoomID && !msg.IsPm() {
   1447 			msg.User.DecrGeneralMessagesCount(c.DB)
   1448 		}
   1449 		_ = msg.Delete(c.DB)
   1450 		return command.ErrRedirect
   1451 	}
   1452 	if c.Message == "/d" {
   1453 		lastMsg, err := c.DB.GetUserLastChatMessageInRoom(c.AuthUser.ID, c.Room.ID)
   1454 		if err != nil {
   1455 			c.Err = errors.New("unable to find last message")
   1456 			return true
   1457 		}
   1458 		msgs := []database.ChatMessage{lastMsg}
   1459 		c.Err = delMsgFn(msgs)
   1460 		return true
   1461 
   1462 	} else if m := deleteMsgRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1463 		date := m[1]
   1464 		matchUsername := m[2]
   1465 		dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
   1466 		if err != nil {
   1467 			logrus.Error(err)
   1468 			c.Err = err
   1469 			return true
   1470 		}
   1471 		msgs, err := c.DB.GetRoomChatMessagesByDate(c.Room.ID, dt.UTC())
   1472 		if err != nil {
   1473 			c.Err = err
   1474 			return true
   1475 		}
   1476 		if len(msgs) == 0 {
   1477 			c.Err = errors.New("failed to find msg")
   1478 			return true
   1479 		}
   1480 
   1481 		if !c.AuthUser.IsModerator() {
   1482 			c.Err = delMsgFn(msgs)
   1483 			return true
   1484 		}
   1485 
   1486 		// Moderator
   1487 		var msg database.ChatMessage
   1488 		if len(msgs) == 1 {
   1489 			msg = msgs[0]
   1490 		} else if len(msgs) > 1 {
   1491 			if matchUsername == "" {
   1492 				c.Err = errors.New("more the 1 msg with this timestamp")
   1493 				return true
   1494 			}
   1495 			msg, err = getMsgForUsername(msgs, database.Username(matchUsername))
   1496 			if err != nil {
   1497 				c.Err = err
   1498 				return true
   1499 			}
   1500 		}
   1501 		if err := msg.UserCanDeleteErr(c.AuthUser); err != nil {
   1502 			c.Err = err
   1503 			return true
   1504 		}
   1505 		_ = msg.Delete(c.DB)
   1506 		c.Err = command.ErrRedirect
   1507 		return true
   1508 
   1509 	} else if strings.HasPrefix(c.Message, "/d ") {
   1510 		c.Err = errors.New("invalid /d command")
   1511 		return true
   1512 	}
   1513 	return
   1514 }
   1515 
   1516 func handleHideMsgCmd(c *command.Command) (handled bool) {
   1517 	if m := hideRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1518 		date := m[1]
   1519 		dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
   1520 		if err != nil {
   1521 			logrus.Error(err)
   1522 			c.Err = err
   1523 			return true
   1524 		}
   1525 		msgs, err := c.DB.GetRoomChatMessagesByDate(c.Room.ID, dt.UTC())
   1526 		if err != nil {
   1527 			c.Err = err
   1528 			return true
   1529 		}
   1530 		if len(msgs) == 1 {
   1531 			c.DB.IgnoreMessage(c.AuthUser.ID, msgs[0].ID)
   1532 			c.Err = command.ErrRedirect
   1533 		} else {
   1534 			c.Err = errors.New("more than 1 message")
   1535 		}
   1536 		return true
   1537 	}
   1538 	return
   1539 }
   1540 
   1541 func handleUnHideMsgCmd(c *command.Command) (handled bool) {
   1542 	if m := unhideRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1543 		date := m[1]
   1544 		dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
   1545 		if err != nil {
   1546 			logrus.Error(err)
   1547 			c.Err = err
   1548 			return true
   1549 		}
   1550 		msgs, err := c.DB.GetRoomChatMessagesByDate(c.Room.ID, dt.UTC())
   1551 		if err != nil {
   1552 			c.Err = err
   1553 			return true
   1554 		}
   1555 		if len(msgs) == 1 {
   1556 			c.DB.UnIgnoreMessage(c.AuthUser.ID, msgs[0].ID)
   1557 			c.Err = command.ErrRedirect
   1558 		} else {
   1559 			c.Err = errors.New("more than 1 message")
   1560 		}
   1561 		return true
   1562 	}
   1563 	return
   1564 }
   1565 
   1566 func handleIgnoreCmd(c *command.Command) (handled bool) {
   1567 	if m := ignoreRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1568 		username := database.Username(m[1])
   1569 		user, err := c.DB.GetUserByUsername(username)
   1570 		if err != nil {
   1571 			c.Err = command.ErrRedirect
   1572 			return true
   1573 		}
   1574 		c.DB.IgnoreUser(c.AuthUser.ID, user.ID)
   1575 		database.MsgPubSub.Pub("refresh_"+string(c.AuthUser.Username), database.ChatMessageType{Typ: database.ForceRefresh})
   1576 		c.Err = command.ErrRedirect
   1577 		return true
   1578 
   1579 	} else if c.Message == "/i" || c.Message == "/ignore" {
   1580 		ignoredUsers, _ := c.DB.GetIgnoredUsers(c.AuthUser.ID)
   1581 		sort.Slice(ignoredUsers, func(i, j int) bool {
   1582 			return ignoredUsers[i].IgnoredUser.Username < ignoredUsers[j].IgnoredUser.Username
   1583 		})
   1584 		msg := ""
   1585 		if len(ignoredUsers) > 0 {
   1586 			msg += "\n"
   1587 			for _, ignoredUser := range ignoredUsers {
   1588 				msg += ignoredUser.IgnoredUser.Username.AtStr() + "\n"
   1589 			}
   1590 		} else {
   1591 			msg += "no ignored users"
   1592 		}
   1593 		c.ZeroProcMsg(msg)
   1594 		c.Err = command.ErrRedirect
   1595 		return true
   1596 
   1597 	} else if strings.HasPrefix(c.Message, "/ignore ") || strings.HasPrefix(c.Message, "/i ") {
   1598 		c.Err = errors.New("invalid ignore command")
   1599 		return true
   1600 	}
   1601 	return
   1602 }
   1603 
   1604 func handleUnIgnoreCmd(c *command.Command) (handled bool) {
   1605 	if m := unIgnoreRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1606 		username := database.Username(m[1])
   1607 		user, err := c.DB.GetUserByUsername(username)
   1608 		if err != nil {
   1609 			c.Err = command.ErrRedirect
   1610 			return true
   1611 		}
   1612 		c.DB.UnIgnoreUser(c.AuthUser.ID, user.ID)
   1613 		database.MsgPubSub.Pub("refresh_"+string(c.AuthUser.Username), database.ChatMessageType{Typ: database.ForceRefresh})
   1614 		c.Err = command.ErrRedirect
   1615 		return true
   1616 	} else if strings.HasPrefix(c.Message, "/unignore ") || strings.HasPrefix(c.Message, "/ui ") {
   1617 		c.Err = errors.New("invalid unignore command")
   1618 		return true
   1619 	}
   1620 	return
   1621 }
   1622 
   1623 func handleToggleAutocomplete(c *command.Command) (handled bool) {
   1624 	if c.Message == "/toggle-autocomplete" {
   1625 		c.AuthUser.ToggleAutocompleteCommandsEnabled(c.DB)
   1626 		c.Err = command.ErrRedirect
   1627 		return true
   1628 	}
   1629 	return
   1630 }
   1631 
   1632 func handleAfkCmd(c *command.Command) (handled bool) {
   1633 	if c.Message == "/afk" {
   1634 		c.AuthUser.ToggleAFK(c.DB)
   1635 		c.Err = command.ErrRedirect
   1636 		return true
   1637 	}
   1638 	return
   1639 }
   1640 
   1641 func handleDateCmd(c *command.Command) (handled bool) {
   1642 	if c.Message == "/date" {
   1643 		c.ZeroMsg(time.Now().Format(time.RFC1123))
   1644 		c.Err = command.ErrRedirect
   1645 		return true
   1646 	}
   1647 	return
   1648 }
   1649 
   1650 func handleSuccessCmd(c *command.Command) (handled bool) {
   1651 	if c.Message == "/success" {
   1652 		c.Err = command.NewErrSuccess("success message")
   1653 		return true
   1654 	}
   1655 	return
   1656 }
   1657 
   1658 func handleErrorCmd(c *command.Command) (handled bool) {
   1659 	if c.Message == "/error" {
   1660 		c.Err = errors.New("error message")
   1661 		return true
   1662 	}
   1663 	return
   1664 }
   1665 
   1666 func handleSystemCmd(c *command.Command) (handled bool) {
   1667 	if strings.HasPrefix(c.Message, "/sys ") {
   1668 		c.Message = strings.Replace(c.Message, "/sys ", "/system ", 1)
   1669 	}
   1670 	if strings.HasPrefix(c.Message, "/system ") {
   1671 		c.Message = strings.TrimPrefix(c.Message, "/system ")
   1672 		c.SystemMsg = true
   1673 		return true
   1674 	}
   1675 	return false
   1676 }
   1677 
   1678 func handleSetChatRoomExternalLink(c *command.Command) (handled bool) {
   1679 	if m := setUrlRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1680 		externalURL := m[1]
   1681 		if !govalidator.IsURL(externalURL) {
   1682 			externalURL = ""
   1683 		}
   1684 		room, err := c.DB.GetChatRoomByID(c.Room.ID)
   1685 		if err != nil {
   1686 			c.Err = err
   1687 			return true
   1688 		}
   1689 		room.ExternalLink = externalURL
   1690 		room.DoSave(c.DB)
   1691 		c.Err = command.ErrRedirect
   1692 		return true
   1693 	}
   1694 	return
   1695 }
   1696 
   1697 func handlePurge(c *command.Command) (handled bool) {
   1698 	if m := purgeRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1699 		isHB := m[1] == " -hb"
   1700 		username := database.Username(m[2])
   1701 		user, err := c.DB.GetUserByUsername(username)
   1702 		if err != nil {
   1703 			c.Err = err
   1704 			return true
   1705 		}
   1706 		c.DB.NewAudit(*c.AuthUser, fmt.Sprintf("purge %s #%d", user.Username, user.ID))
   1707 		if isHB {
   1708 			_ = c.DB.DeleteUserHbChatMessages(user.ID)
   1709 		} else {
   1710 			_ = c.DB.DeleteUserChatMessages(user.ID)
   1711 		}
   1712 		database.MsgPubSub.Pub(database.RefreshTopic, database.ChatMessageType{Typ: database.ForceRefresh})
   1713 		c.Err = command.ErrRedirect
   1714 		return true
   1715 
   1716 	} else if c.Message == "/purge" {
   1717 		c.Err = command.ErrRedirect
   1718 		if !c.AuthUser.UseStream {
   1719 			c.Err = errors.New("only work on stream version of this chat")
   1720 			return true
   1721 		}
   1722 		payload := database.ChatMessageType{}
   1723 		streamModals.PurgeModal{}.Show(c.AuthUser.ID, c.Room.ID, payload)
   1724 		return true
   1725 	}
   1726 	return
   1727 }
   1728 
   1729 func handleRename(c *command.Command) (handled bool) {
   1730 	if m := renameRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1731 		oldUsername := database.Username(m[1])
   1732 		newUsername := database.Username(m[2])
   1733 		user, err := c.DB.GetUserByUsername(oldUsername)
   1734 		if err != nil {
   1735 			c.Err = err
   1736 			return true
   1737 		}
   1738 		c.DB.NewAudit(*c.AuthUser, fmt.Sprintf("rename %s -> %s #%d", user.Username, newUsername, user.ID))
   1739 
   1740 		if err := c.DB.CanRenameTo(oldUsername, newUsername); err != nil {
   1741 			c.Err = err
   1742 			return true
   1743 		}
   1744 
   1745 		managers.ActiveUsers.RemoveUser(user.ID)
   1746 		user.Username = newUsername
   1747 		user.DoSave(c.DB)
   1748 
   1749 		c.Err = command.ErrRedirect
   1750 		return true
   1751 	}
   1752 	return
   1753 }
   1754 
   1755 func handleNewMeme(c *command.Command) (handled bool) {
   1756 	if m := memeRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1757 		if c.Upload == nil {
   1758 			c.Err = errors.New("no file uploaded")
   1759 			return true
   1760 		}
   1761 		slug := m[1]
   1762 		oldPath := filepath.Join(config.Global.ProjectUploadsPath.Get(), c.Upload.FileName)
   1763 		newPath := filepath.Join(config.Global.ProjectMemesPath.Get(), c.Upload.FileName)
   1764 		_ = os.Rename(oldPath, newPath)
   1765 
   1766 		if err := c.DB.DB().Delete(&c.Upload).Error; err != nil {
   1767 			logrus.Error(err)
   1768 		}
   1769 
   1770 		meme := database.Meme{
   1771 			Slug:         slug,
   1772 			FileName:     c.Upload.FileName,
   1773 			OrigFileName: c.Upload.OrigFileName,
   1774 			FileSize:     c.Upload.FileSize,
   1775 		}
   1776 		if err := c.DB.DB().Create(&meme).Error; err != nil {
   1777 			_ = os.Remove(newPath)
   1778 			logrus.Error(err)
   1779 		}
   1780 
   1781 		c.Err = command.ErrRedirect
   1782 		return true
   1783 
   1784 	} else if m := memeRenameRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1785 		slug := m[1]
   1786 		newSlug := m[2]
   1787 		meme, err := c.DB.GetMemeBySlug(slug)
   1788 		if err != nil {
   1789 			c.Err = errors.New("meme not found")
   1790 			return true
   1791 		}
   1792 		meme.Slug = newSlug
   1793 		meme.DoSave(c.DB)
   1794 		c.Err = command.NewErrSuccess("meme renamed")
   1795 		return true
   1796 	}
   1797 	return
   1798 }
   1799 
   1800 func handleRemoveMeme(c *command.Command) (handled bool) {
   1801 	if m := memeRemoveRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1802 		slug := m[1]
   1803 		meme, err := c.DB.GetMemeBySlug(slug)
   1804 		if err != nil {
   1805 			c.Err = errors.New("meme not found")
   1806 			return true
   1807 		}
   1808 		if err := meme.Delete(c.DB); err != nil {
   1809 			c.Err = err
   1810 			return true
   1811 		}
   1812 		c.Err = command.NewErrSuccess("meme removed")
   1813 		return true
   1814 	}
   1815 	return
   1816 }
   1817 
   1818 func handleListMemes(c *command.Command) (handled bool) {
   1819 	if m := memesRgx.FindStringSubmatch(c.Message); len(m) == 1 {
   1820 		memes, _ := c.DB.GetMemes()
   1821 		msg := ""
   1822 		for _, m := range memes {
   1823 			msg += fmt.Sprintf(`<a href="/memes/%s" rel="noopener noreferrer" target="_blank">%s</a>`, m.Slug, m.Slug)
   1824 			if c.AuthUser.IsAdmin {
   1825 				msg += fmt.Sprintf(` (%s)`, humanize.Bytes(uint64(m.FileSize)))
   1826 			}
   1827 			msg += "<br />"
   1828 		}
   1829 		c.ZeroMsg(msg)
   1830 		c.Err = command.ErrRedirect
   1831 		return true
   1832 	}
   1833 	return
   1834 }
   1835 
   1836 func handleRefreshCmd(c *command.Command) (handled bool) {
   1837 	if c.Message == "/refresh" {
   1838 		c.Err = command.ErrRedirect
   1839 		database.MsgPubSub.Pub(database.RefreshTopic, database.ChatMessageType{Typ: database.ForceRefresh})
   1840 		return true
   1841 	}
   1842 	return
   1843 }
   1844 
   1845 func handleWizzCmd(c *command.Command) (handled bool) {
   1846 	m := wizzRgx.FindStringSubmatch(c.Message)
   1847 	if c.Message == "/wizz" || len(m) == 2 {
   1848 		var wizzedUser *database.User
   1849 		wizzedUser = c.AuthUser
   1850 
   1851 		if len(m) == 2 {
   1852 			username := database.Username(m[1])
   1853 			if username != c.AuthUser.Username {
   1854 				user, err := c.DB.GetUserByUsername(username)
   1855 				if err != nil {
   1856 					c.Err = ErrUsernameNotFound
   1857 					return true
   1858 				}
   1859 				wizzedUser = &user
   1860 			}
   1861 			c.ZeroSysMsgToSkipNotify(c.AuthUser, "you wizzed "+wizzedUser.Username.String())
   1862 		}
   1863 
   1864 		c.ZeroSysMsgTo(wizzedUser, "wizzed by "+c.AuthUser.Username.String())
   1865 		database.MsgPubSub.Pub("wizz_"+wizzedUser.Username.String(), database.ChatMessageType{Typ: database.Wizz})
   1866 		c.Err = command.ErrRedirect
   1867 		return true
   1868 	}
   1869 	return
   1870 }
   1871 
   1872 func handleChipsCmd(c *command.Command) (handled bool) {
   1873 	if m := chipsRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   1874 		username := database.Username(m[1])
   1875 		chips := utils.DoParseInt64(m[2])
   1876 
   1877 		if c.DB.DB().Model(&database.User{}).
   1878 			Where("username = ?", username).
   1879 			Select("ChipsTest").
   1880 			Updates(database.User{ChipsTest: database.PokerChip(chips)}).RowsAffected == 0 {
   1881 			c.Err = errors.New("username does not exists")
   1882 			return true
   1883 		}
   1884 		c.Err = command.NewErrSuccess("chips set")
   1885 		return true
   1886 	}
   1887 	return
   1888 }
   1889 
   1890 func handleLocateCmd(c *command.Command) (handled bool) {
   1891 	if m := locateRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   1892 		username := database.Username(m[1])
   1893 		user, err := c.DB.GetUserByUsername(username)
   1894 		if err != nil {
   1895 			c.Err = errors.New("username does not exists")
   1896 			return true
   1897 		}
   1898 		roomIDs := managers.ActiveUsers.LocateUser(user.Username)
   1899 		rooms, _ := c.DB.GetChatRoomsByID(roomIDs)
   1900 		var msg string
   1901 		if len(rooms) > 0 {
   1902 			roomLinks := make([]string, len(rooms))
   1903 			for idx, room := range rooms {
   1904 				roomLinks[idx] = "#" + room.Name
   1905 			}
   1906 			msg = username.AtStr() + " is in " + strings.Join(roomLinks, " ")
   1907 		} else {
   1908 			msg = username.AtStr() + " could not be located in a public room"
   1909 		}
   1910 		c.ZeroProcMsg(msg)
   1911 		c.DataMessage = "/locate "
   1912 		c.Err = command.ErrStop
   1913 		return true
   1914 	}
   1915 	return
   1916 }
   1917 
   1918 func handleCodeCmd(c *command.Command) (handled bool) {
   1919 	if c.Message == "/code" {
   1920 		c.Err = command.ErrRedirect
   1921 		if !c.AuthUser.CanUseMultiline {
   1922 			c.Err = errors.New("multiline is disabled for your account")
   1923 			return true
   1924 		} else if !c.AuthUser.UseStream {
   1925 			c.Err = errors.New("only work on stream version of this chat")
   1926 			return true
   1927 		}
   1928 		payload := database.ChatMessageType{}
   1929 		if c.ModMsg {
   1930 			payload.IsMod = true
   1931 		}
   1932 		if c.ToUser != nil {
   1933 			toUserUsername := c.ToUser.Username
   1934 			payload.ToUserUsername = &toUserUsername
   1935 		}
   1936 		streamModals.CodeModal{}.Show(c.AuthUser.ID, c.Room.ID, payload)
   1937 		return true
   1938 	}
   1939 	return
   1940 }
   1941 
   1942 func handleUpdateReadMarkerCmd(c *command.Command) (handled bool) {
   1943 	if c.Message == "/r" {
   1944 		c.DB.UpdateChatReadMarker(c.AuthUser.ID, c.Room.ID)
   1945 		c.Err = command.ErrRedirect
   1946 		return true
   1947 	}
   1948 	return
   1949 }
   1950 
   1951 func handleCheckCmd(c *command.Command) (handled bool) {
   1952 	if c.Message == "/check" {
   1953 		roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-"))
   1954 		if g := poker.PokerInstance.GetGame(roomID); g != nil {
   1955 			g.Check(c.AuthUser.ID)
   1956 		}
   1957 		c.Err = command.ErrRedirect
   1958 		return true
   1959 	}
   1960 	return false
   1961 }
   1962 
   1963 func handleCallCmd(c *command.Command) (handled bool) {
   1964 	if c.Message == "/call" {
   1965 		roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-"))
   1966 		if g := poker.PokerInstance.GetGame(roomID); g != nil {
   1967 			g.Call(c.AuthUser.ID)
   1968 		}
   1969 		c.Err = command.ErrRedirect
   1970 		return true
   1971 	}
   1972 	return false
   1973 }
   1974 
   1975 func handleFoldCmd(c *command.Command) (handled bool) {
   1976 	if c.Message == "/fold" {
   1977 		roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-"))
   1978 		if g := poker.PokerInstance.GetGame(roomID); g != nil {
   1979 			g.Fold(c.AuthUser.ID)
   1980 		}
   1981 		c.Err = command.ErrRedirect
   1982 		return true
   1983 	}
   1984 	return false
   1985 }
   1986 
   1987 func handleRaiseCmd(c *command.Command) (handled bool) {
   1988 	if c.Message == "/raise" {
   1989 		roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-"))
   1990 		if g := poker.PokerInstance.GetGame(roomID); g != nil {
   1991 			g.Raise(c.AuthUser.ID)
   1992 		}
   1993 		c.Err = command.ErrRedirect
   1994 		return true
   1995 	}
   1996 	return false
   1997 }
   1998 
   1999 func handleBetCmd(c *command.Command) (handled bool) {
   2000 	if m := betRgx.FindStringSubmatch(c.Message); len(m) == 2 {
   2001 		roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-"))
   2002 		if g := poker.PokerInstance.GetGame(roomID); g != nil {
   2003 			bet := database.PokerChip(utils.DoParseUint64(m[1]))
   2004 			g.Bet(c.AuthUser.ID, bet)
   2005 		}
   2006 		c.Err = command.ErrRedirect
   2007 		return true
   2008 	}
   2009 	return false
   2010 }
   2011 
   2012 func handleAllInCmd(c *command.Command) (handled bool) {
   2013 	if c.Message == "/allin" {
   2014 		roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-"))
   2015 		if g := poker.PokerInstance.GetGame(roomID); g != nil {
   2016 			g.AllIn(c.AuthUser.ID)
   2017 		}
   2018 		c.Err = command.ErrRedirect
   2019 		return true
   2020 	}
   2021 	return false
   2022 }
   2023 
   2024 func handleDealCmd(c *command.Command) (handled bool) {
   2025 	if c.Message == "/deal" {
   2026 		roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-"))
   2027 		if g := poker.PokerInstance.GetGame(roomID); g != nil {
   2028 			g.Deal(c.AuthUser.ID)
   2029 		}
   2030 		c.Err = command.ErrRedirect
   2031 		return true
   2032 	}
   2033 	return false
   2034 }
   2035 
   2036 func handleDistCmd(c *command.Command) (handled bool) {
   2037 	if m := distRgx.FindStringSubmatch(c.Message); len(m) == 3 {
   2038 		u1 := strings.ToLower(m[1])
   2039 		u2 := strings.ToLower(m[2])
   2040 		dist := levenshtein.ComputeDistance(u1, u2)
   2041 		c.ZeroProcMsg(fmt.Sprintf("levenshtein distance is %d", dist))
   2042 		c.Err = command.ErrRedirect
   2043 		return true
   2044 	}
   2045 	return false
   2046 }
   2047 
   2048 func handleCloseCmd(c *command.Command) (handled bool) {
   2049 	if c.Message == "/close" {
   2050 		database.MsgPubSub.Pub("refresh_"+string(c.AuthUser.Username), database.ChatMessageType{Typ: database.Close})
   2051 		c.Err = command.ErrRedirect
   2052 		return true
   2053 	}
   2054 	return false
   2055 }
   2056 
   2057 func handleCloseMenuCmd(c *command.Command) (handled bool) {
   2058 	if c.Message == "/closem" {
   2059 		database.MsgPubSub.Pub("refresh_loading_icon_"+string(c.AuthUser.Username), database.ChatMessageType{Typ: database.CloseMenu})
   2060 		c.Err = command.ErrRedirect
   2061 		return true
   2062 	}
   2063 	return false
   2064 }