commit 6e04d49934d8683ff468676ec97eaa0ae377fc4e
parent 96745a8e63122a5445b4be746379a014f5187868
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Thu, 8 Jun 2023 01:05:11 -0700
separate interceptors in own package
Diffstat:
29 files changed, 5573 insertions(+), 5546 deletions(-)
diff --git a/pkg/actions/actions.go b/pkg/actions/actions.go
@@ -10,7 +10,7 @@ import (
"dkforest/pkg/managers"
"dkforest/pkg/utils"
"dkforest/pkg/web"
- "dkforest/pkg/web/handlers/api/v1"
+ "dkforest/pkg/web/handlers/api/v1/interceptors"
"errors"
"fmt"
"github.com/mattn/go-colorable"
@@ -82,10 +82,10 @@ func Start(c *cli.Context) error {
utils.SGo(func() { xmrWatch(db) })
utils.SGo(func() { openBrowser(noBrowser, int64(port)) })
- v1.LoadFilters(db)
- v1.ChessInstance = v1.NewChess(db)
- v1.BattleshipInstance = v1.NewBattleship(db)
- v1.WWInstance = v1.NewWerewolf(db)
+ interceptors.LoadFilters(db)
+ interceptors.ChessInstance = interceptors.NewChess(db)
+ interceptors.BattleshipInstance = interceptors.NewBattleship(db)
+ interceptors.WWInstance = interceptors.NewWerewolf(db)
web.Start(db, host, port)
return nil
diff --git a/pkg/web/handlers/admin.go b/pkg/web/handlers/admin.go
@@ -3,7 +3,7 @@ package handlers
import (
dutils "dkforest/pkg/database/utils"
"dkforest/pkg/managers"
- v1 "dkforest/pkg/web/handlers/api/v1"
+ "dkforest/pkg/web/handlers/api/v1/interceptors"
"github.com/jinzhu/gorm"
"net/http"
"regexp"
@@ -40,12 +40,12 @@ func AdminSpamFiltersHandler(c echo.Context) error {
if _, err := db.CreateOrEditSpamFilter(data.ID, data.Filter, data.IsRegex, data.Action); err != nil {
logrus.Error(err)
}
- v1.LoadFilters(db)
+ interceptors.LoadFilters(db)
} else if btnSubmit == "delete" {
if err := db.DeleteSpamFilterByID(data.ID); err != nil {
logrus.Error(err)
}
- v1.LoadFilters(db)
+ interceptors.LoadFilters(db)
}
return c.Redirect(http.StatusFound, "/admin/spam-filters")
}
diff --git a/pkg/web/handlers/api/v1/bangInterceptor.go b/pkg/web/handlers/api/v1/bangInterceptor.go
@@ -1,32 +0,0 @@
-package v1
-
-import "dkforest/pkg/config"
-
-type BangInterceptor struct{}
-
-func (i BangInterceptor) InterceptMsg(cmd *Command) {
- switch cmd.message {
- case "!links":
- handleLinksBangCmd(cmd)
- case "!rtuto":
- handleRtutoBangCmd(cmd)
- }
- return
-}
-
-func handleLinksBangCmd(cmd *Command) {
- message := `
-Chats:
-Black Hat Chat: ` + config.BhcOnion + `
-Forums:
-CryptBB: ` + config.CryptbbOnion
- msg, _, _ := ProcessRawMessage(cmd.db, message, "", cmd.authUser.ID, cmd.room.ID, nil, true)
- cmd.zeroMsg(msg)
- cmd.err = ErrRedirect
-}
-
-func handleRtutoBangCmd(cmd *Command) {
- cmd.authUser.ChatTutorial = 0
- cmd.authUser.DoSave(cmd.db)
- cmd.err = ErrRedirect
-}
diff --git a/pkg/web/handlers/api/v1/battleship.go b/pkg/web/handlers/api/v1/battleship.go
@@ -1,607 +0,0 @@
-package v1
-
-import (
- "bytes"
- "dkforest/pkg/config"
- "dkforest/pkg/database"
- "dkforest/pkg/utils"
- "encoding/base64"
- "errors"
- "fmt"
- "github.com/fogleman/gg"
- "github.com/sirupsen/logrus"
- "html/template"
- "image/color"
- "strconv"
- "sync"
- "time"
-)
-
-// Carrier 5
-// Battleship 4
-// Cruiser 3
-// Submarine 3
-// Destroyer 2
-
-/**
-◀■■▶
-▲
-█
-▼
-●
-*/
-
-var BattleshipInstance *Battleship
-
-type BSCoordinate struct {
- x, y int
-}
-
-type BSPlayer struct {
- id database.UserID
- username database.Username
- userStyle string
- card *BSCard
- shots map[int]struct{}
-}
-
-func newPlayer(player database.User) *BSPlayer {
- p := new(BSPlayer)
- p.id = player.ID
- p.username = player.Username
- p.userStyle = player.GenerateChatStyle()
- p.card = generateCard()
- p.shots = make(map[int]struct{})
- return p
-}
-
-type Direction int
-
-const (
- vertical Direction = iota + 1
- horizontal
-)
-
-type BSShip struct {
- name string
- x, y, size int
- direction Direction
- reversed bool
- health int
-}
-
-func newShip(name string, x, y, size int, dir Direction, reversed bool) BSShip {
- return BSShip{name: name, x: x, y: y, size: size, direction: dir, health: size, reversed: reversed}
-}
-
-func (s BSShip) contains(pos int) bool {
- for _, el := range s.getPos() {
- if pos == el {
- return true
- }
- }
- return false
-}
-
-func (s BSShip) getPos() (out []int) {
- for i := 0; i < s.size; i++ {
- incr := i
- if s.direction == vertical {
- incr *= 10
- }
- out = append(out, s.y*10+s.x+incr)
- }
- return
-}
-
-type BSCard struct {
- carrier BSShip
- battleShip BSShip
- cruiser BSShip
- submarine BSShip
- destroyer BSShip
-}
-
-func (c *BSCard) collide(newShip BSShip) bool {
- for _, p := range newShip.getPos() {
- if c.carrier.contains(p) ||
- c.battleShip.contains(p) ||
- c.cruiser.contains(p) ||
- c.submarine.contains(p) ||
- c.destroyer.contains(p) {
- return true
- }
- }
- return false
-}
-
-func (c *BSCard) shot(pos int) {
- if c.carrier.contains(pos) {
- c.carrier.health -= 1
- } else if c.battleShip.contains(pos) {
- c.battleShip.health -= 1
- } else if c.cruiser.contains(pos) {
- c.cruiser.health -= 1
- } else if c.submarine.contains(pos) {
- c.submarine.health -= 1
- } else if c.destroyer.contains(pos) {
- c.destroyer.health -= 1
- }
-}
-
-func (c BSCard) allShipsDead() bool {
- return c.carrier.health == 0 &&
- c.battleShip.health == 0 &&
- c.cruiser.health == 0 &&
- c.submarine.health == 0 &&
- c.destroyer.health == 0
-}
-
-func (c BSCard) shipAt(pos int) (string, bool) {
- if c.carrier.contains(pos) {
- return "carrier", c.carrier.health == 0
- } else if c.battleShip.contains(pos) {
- return "battleShip", c.battleShip.health == 0
- } else if c.cruiser.contains(pos) {
- return "cruiser", c.cruiser.health == 0
- } else if c.submarine.contains(pos) {
- return "submarine", c.submarine.health == 0
- } else if c.destroyer.contains(pos) {
- return "destroyer", c.destroyer.health == 0
- }
- return "", false
-}
-
-func (c BSCard) hasShipAt(pos int) bool {
- var allPos []int
- allPos = append(allPos, c.carrier.getPos()...)
- allPos = append(allPos, c.battleShip.getPos()...)
- allPos = append(allPos, c.cruiser.getPos()...)
- allPos = append(allPos, c.submarine.getPos()...)
- allPos = append(allPos, c.destroyer.getPos()...)
- for _, el := range allPos {
- if pos == el {
- return true
- }
- }
- return false
-}
-
-type BSGame struct {
- lastUpdated time.Time
- turn int
- player1 *BSPlayer
- player2 *BSPlayer
-}
-
-func newGame(player1, player2 database.User) *BSGame {
- g := new(BSGame)
- g.lastUpdated = time.Now()
- g.player1 = newPlayer(player1)
- g.player2 = newPlayer(player2)
- return g
-}
-
-func (g BSGame) IsPlayerTurn(playerID database.UserID) bool {
- return g.turn == 0 && g.player1.id == playerID ||
- g.turn == 1 && g.player2.id == playerID
-}
-
-func (g *BSGame) Shot(pos string) (shipStr string, shipDead, gameEnded bool, err error) {
- g.lastUpdated = time.Now()
- rowStr := pos[0]
- row := int(rowStr - 'A')
- col, _ := strconv.Atoi(string(pos[1]))
- p := row*10 + col
-
- ent1 := g.player1
- ent2 := g.player2
- if g.turn == 1 {
- ent1, ent2 = ent2, ent1
- }
- if _, ok := ent1.shots[p]; ok {
- return "", false, false, errors.New("position already hit")
- }
- ent1.shots[p] = struct{}{}
- ent2.card.shot(p)
- shipStr, shipDead = ent2.card.shipAt(p)
- gameEnded = ent2.card.allShipsDead()
-
- g.turn = (g.turn + 1) % 2
- return
-}
-
-type Battleship struct {
- sync.Mutex
- db *database.DkfDB
- zeroID database.UserID
- games map[string]*BSGame
-}
-
-func NewBattleship(db *database.DkfDB) *Battleship {
- zeroUser, _ := db.GetUserByUsername(config.NullUsername)
- b := &Battleship{db: db, zeroID: zeroUser.ID}
- b.games = make(map[string]*BSGame)
-
- // Thread that cleanup inactive games
- go func() {
- for {
- time.Sleep(time.Minute)
- b.Lock()
- for k, g := range b.games {
- if time.Since(g.lastUpdated) > 5*time.Minute {
- delete(b.games, k)
- }
- }
- b.Unlock()
- }
- }()
-
- return b
-}
-
-func generateCard() *BSCard {
- c := new(BSCard)
- genTmpShip := func(name string, size int) (out BSShip) {
- reversed := utils.RandBool()
- dir := utils.RandChoice([]Direction{horizontal, vertical})
- val1 := utils.RandInt(0, 9)
- val2 := utils.RandInt(0, 9-size)
- if dir == horizontal {
- val1, val2 = val2, val1
- }
- out = newShip(name, val1, val2, size, dir, reversed)
- return
- }
- for _, i := range []int{0, 1, 2, 3, 4} { // iterate 5 times (for each boat)
- names := []string{"carrier", "battleship", "cruiser", "submarine", "destroyer"}
- sizes := []int{5, 4, 3, 3, 2} // respective boat size
- for {
- tmpShip := genTmpShip(names[i], sizes[i])
- // If boat collide with another boat, we need to generate a new position for that boat
- if c.collide(tmpShip) {
- continue
- }
- // boat position is valid, assign it
- switch i {
- case 0:
- c.carrier = tmpShip
- case 1:
- c.battleShip = tmpShip
- case 2:
- c.cruiser = tmpShip
- case 3:
- c.submarine = tmpShip
- case 4:
- c.destroyer = tmpShip
- }
- break
- }
- }
- return c
-}
-
-func (g *BSGame) drawCardFor(tmp int, roomName string, isNewGame, shipDead, gameEnded bool, shipStr, pos string) (out string) {
- you := g.player1
- enemy := g.player2
- if tmp == 1 {
- you = g.player2
- enemy = g.player1
- }
-
- imgB64Fn := func(myCard bool) string {
- ent1 := enemy
- ent2 := you
- if myCard {
- ent1 = you
- ent2 = enemy
- }
-
- c := gg.NewContext(177, 177)
-
- c.Push()
- c.SetColor(color.White)
- c.DrawRectangle(0, 0, 177, 177)
- c.Fill()
- c.Pop()
-
- c.Push()
- c.SetColor(color.Black)
- x := 22.0
- y := 13.0
- c.DrawString("0", x, y)
- c.DrawString("1", x+16, y)
- c.DrawString("2", x+16+16, y)
- c.DrawString("3", x+16+16+16, y)
- c.DrawString("4", x+16+16+16+16, y)
- c.DrawString("5", x+16+16+16+16+16, y)
- c.DrawString("6", x+16+16+16+16+16+16, y)
- c.DrawString("7", x+16+16+16+16+16+16+16, y)
- c.DrawString("8", x+16+16+16+16+16+16+16+16, y)
- c.DrawString("9", x+16+16+16+16+16+16+16+16+16, y)
- x = 6
- y = 29.0
- c.DrawString("A", x, y)
- c.DrawString("B", x, y+16)
- c.DrawString("C", x, y+16+16)
- c.DrawString("D", x, y+16+16+16)
- c.DrawString("E", x, y+16+16+16+16)
- c.DrawString("F", x, y+16+16+16+16+16)
- c.DrawString("G", x, y+16+16+16+16+16+16)
- c.DrawString("H", x, y+16+16+16+16+16+16+16)
- c.DrawString("I", x, y+16+16+16+16+16+16+16+16)
- c.DrawString("J", x, y+16+16+16+16+16+16+16+16+16)
- c.Pop()
-
- c.Push()
- c.SetLineWidth(1)
- c.SetColor(color.RGBA{R: 90, G: 90, B: 90, A: 255})
- for col := 0.0; col < 12; col++ {
- c.MoveTo(0.5+col*16, 0)
- c.LineTo(0.5+col*16, 176)
- c.Stroke()
- }
- for row := 0.0; row < 12; row++ {
- c.MoveTo(0, 0.5+row*16)
- c.LineTo(176, 0.5+row*16)
- c.Stroke()
- }
- c.Pop()
-
- drawShip := func(s BSShip) {
- if !myCard && s.health != 0 && !gameEnded {
- return
- }
- //fmt.Println(s.name, s.x, s.y, s.direction, s.reversed)
- c.Push()
- c.Translate(0.5, 0.5)
- c.Translate(16, 16)
- c.Translate(float64(s.x)*16, float64(s.y)*16)
- if s.direction == horizontal {
- if s.reversed {
- c.Translate(float64(s.size)*16, 0)
- c.Rotate(gg.Radians(90))
- } else {
- c.Translate(0, 16)
- c.Rotate(gg.Radians(-90))
- }
- } else {
- if s.reversed {
- c.Translate(16, float64(s.size)*16)
- c.Rotate(gg.Radians(180))
- }
- }
- // Front of the ship
- c.MoveTo(1, 11)
- c.QuadraticTo(8, -10, 15, 11)
- // Length of the ship
- c.Translate(0, float64(s.size-1)*16)
- // back of the ship
- c.LineTo(15, 11)
- c.QuadraticTo(8, 17, 1, 11)
- c.ClosePath()
- if s.health == 0 {
- c.SetColor(color.RGBA{R: 100, G: 100, B: 100, A: 200})
- c.Fill()
- } else if !myCard && gameEnded {
- c.SetColor(color.RGBA{R: 100, G: 130, B: 100, A: 200})
- c.Fill()
- } else {
- c.SetColor(color.RGBA{R: 100, G: 100, B: 100, A: 255})
- c.Fill()
- }
- c.Pop()
- }
- drawShip(ent1.card.carrier)
- drawShip(ent1.card.battleShip)
- drawShip(ent1.card.cruiser)
- drawShip(ent1.card.submarine)
- drawShip(ent1.card.destroyer)
-
- c.Push()
- c.Translate(0.5, 0.5)
- c.Translate(16, 16)
- for shot := range ent2.shots {
- shotRow := shot / 10
- shotCol := shot % 10
- c.Push()
- c.Translate(float64(shotCol)*16, float64(shotRow)*16)
-
- if ent1.card.hasShipAt(shot) {
- c.DrawCircle(8, 8, 4)
- c.SetColor(color.RGBA{R: 255, G: 200, B: 0, A: 255})
- c.Fill()
-
- c.DrawCircle(8, 8, 3)
- c.SetColor(color.RGBA{R: 255, G: 0, B: 0, A: 255})
- c.Fill()
- } else {
- c.DrawCircle(8, 8, 3)
- c.SetColor(color.RGBA{R: 60, G: 200, B: 0, A: 255})
- c.Fill()
-
- c.DrawCircle(8, 8, 2)
- c.SetColor(color.RGBA{R: 100, G: 0, B: 0, A: 255})
- c.Fill()
- }
-
- c.Pop()
- }
- c.Pop()
-
- var buf bytes.Buffer
- _ = c.EncodePNG(&buf)
- imgB64 := base64.StdEncoding.EncodeToString(buf.Bytes())
- return imgB64
- }
-
- imgB64 := imgB64Fn(true)
- img1B64 := imgB64Fn(false)
-
- htmlTmpl := `
-Against <span {{ .EnemyUserStyle | HTMLAttr }}>@{{ .EnemyUsername }}</span><br />
-{{ if not .IsNewGame }}
- {{ if .YourTurn }}
- <span {{ .EnemyUserStyle | HTMLAttr }}>@{{ .EnemyUsername }}</span> played {{ .Pos }}
- {{ else }}
- you played {{ .Pos }}
- {{ end }}
- ;
- {{ if .ShipStr }}
- {{ .ShipStr }} hit
- {{ if .ShipDead }}
- and sunk
- {{ end }}
- {{ else }}
- miss
- {{ end }}
- ;
-{{ end }}
-{{ if .GameEnded }}
- {{ if .YourTurn }}
- You lost!<br />
- {{ else }}
- You win!<br />
- {{ end }}
-{{ else }}
- {{ if .YourTurn }}
- now is your turn<br />
- {{ else }}
- waiting for opponent<br />
- {{ end }}
-{{ end }}
-<table>
- <tr>
- <td><img src="data:image/png;base64,{{ .ImgB64 }}" alt="" /></td>
- <td style="vertical-align: top;">
- <form method="post" style="margin-left: 10px;" action="/api/v1/battleship">
- <input type="hidden" name="room" value="{{ .RoomName }}" />
- <input type="hidden" name="enemyUsername" value="{{ .EnemyUsername }}" />
- <table style="width: 177px; height: 177px; background-image: url(data:image/png;base64,{{ .Img1B64 }})">
- <tr style="height: 16px;"><td colspan="11"> </td></tr>
- {{- range $row := .Rows -}}
- <tr style="height: 16px;">
- <td style="width: 16px;"></td>
- {{- range $col := $.Cols -}}
- {{- if NotShot $row $col -}}
- {{- if and $.YourTurn (not $.GameEnded) -}}
- <td style="width: 16px;">
- <button style="height: 15px; width: 15px;" name="move" value="{{ GetRune $row }}{{ $col }}"></button>
- </td>
- {{- else -}}
- <td style="width: 16px;"></td>
- {{- end -}}
- {{- else -}}
- <td style="width: 16px;"></td>
- {{- end -}}
- {{- end -}}
- </tr>
- {{- end -}}
- </table>
- </form>
- </td>
- </tr>
-</table>
-`
- data := map[string]any{
- "RoomName": roomName,
- "EnemyUserStyle": enemy.userStyle,
- "EnemyUsername": enemy.username,
- "IsNewGame": isNewGame,
- "YourTurn": g.turn == tmp,
- "Pos": pos,
- "ShipStr": shipStr,
- "ShipDead": shipDead,
- "GameEnded": gameEnded,
- "ImgB64": imgB64,
- "Img1B64": img1B64,
- "Rows": []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
- "Cols": []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
- }
- fns := template.FuncMap{
- "GetRune": func(i int) string {
- return string(rune('A' + i))
- },
- "NotShot": func(i, j int) bool {
- _, ok := you.shots[i*10+j]
- return !ok
- },
- "HTMLAttr": func(in string) template.HTMLAttr {
- return template.HTMLAttr(in)
- },
- }
- var buf bytes.Buffer
- _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf, data)
- return buf.String()
-}
-
-func (b *Battleship) InterceptMsg(cmd *Command) {
- m := bsRgx.FindStringSubmatch(cmd.message)
- if len(m) != 3 {
- return
- }
- enemyUsername := database.Username(m[1])
- pos := m[2]
- if err := b.playMove(cmd.room.Name, cmd.room.ID, cmd.roomKey, *cmd.authUser, enemyUsername, pos); err != nil {
- cmd.err = err
- return
- }
- cmd.err = ErrStop
- return
-}
-
-func (b *Battleship) playMove(roomName string, roomID database.RoomID, roomKey string, authUser database.User, enemyUsername database.Username, pos string) error {
- b.Lock()
- defer b.Unlock()
-
- user, err := b.db.GetUserByUsername(enemyUsername)
- if err != nil {
- return errors.New("invalid username")
- }
-
- var gameKey string
- if authUser.ID < user.ID {
- gameKey = fmt.Sprintf("%d_%d", authUser.ID, user.ID)
- } else {
- gameKey = fmt.Sprintf("%d_%d", user.ID, authUser.ID)
- }
-
- var shipStr string
- var isNewGame, shipDead, gameEnded bool
- g, ok := b.games[gameKey]
- if ok {
- if !g.IsPlayerTurn(authUser.ID) {
- return errors.New("not your turn")
- }
- shipStr, shipDead, gameEnded, err = g.Shot(pos)
- if err != nil {
- return err
- }
- } else {
- if pos != "" {
- return errors.New("no Game ongoing")
- }
- g = newGame(user, authUser)
- b.games[gameKey] = g
- isNewGame = true
- }
-
- // Delete old messages sent by "0" to the players
- if err := b.db.DB().
- Where("room_id = ? AND user_id = ? AND (to_user_id = ? OR to_user_id = ?)", roomID, b.zeroID, g.player1.id, g.player2.id).
- Delete(&database.ChatMessage{}).Error; err != nil {
- logrus.Error(err)
- }
-
- card1 := g.drawCardFor(0, roomName, isNewGame, shipDead, gameEnded, shipStr, pos)
- _, _ = b.db.CreateMsg(card1, card1, roomKey, roomID, b.zeroID, &g.player1.id)
-
- card2 := g.drawCardFor(1, roomName, isNewGame, shipDead, gameEnded, shipStr, pos)
- _, _ = b.db.CreateMsg(card2, card2, roomKey, roomID, b.zeroID, &g.player2.id)
-
- if gameEnded {
- delete(b.games, gameKey)
- }
-
- return nil
-}
diff --git a/pkg/web/handlers/api/v1/chess.go b/pkg/web/handlers/api/v1/chess.go
@@ -1,578 +0,0 @@
-package v1
-
-import (
- "bytes"
- "dkforest/bindata"
- "dkforest/pkg/config"
- "dkforest/pkg/database"
- dutils "dkforest/pkg/database/utils"
- "dkforest/pkg/pubsub"
- "dkforest/pkg/utils"
- "encoding/base64"
- "errors"
- "fmt"
- "github.com/fogleman/gg"
- "github.com/google/uuid"
- "github.com/labstack/echo"
- "github.com/notnil/chess"
- "github.com/sirupsen/logrus"
- "html/template"
- "image"
- "image/color"
- "image/png"
- "sort"
- "strconv"
- "sync"
- "time"
-)
-
-type ChessPlayer struct {
- ID database.UserID
- Username database.Username
- UserStyle string
- NotifyChessMove bool
-}
-
-type ChessGame struct {
- Key string
- Game *chess.Game
- lastUpdated time.Time
- Player1 *ChessPlayer
- Player2 *ChessPlayer
- CreatedAt time.Time
-}
-
-func newChessPlayer(player database.User) *ChessPlayer {
- p := new(ChessPlayer)
- p.ID = player.ID
- p.Username = player.Username
- p.UserStyle = player.GenerateChatStyle()
- p.NotifyChessMove = player.NotifyChessMove
- return p
-}
-
-func newChessGame(gameKey string, player1, player2 database.User) *ChessGame {
- g := new(ChessGame)
- g.CreatedAt = time.Now()
- g.Key = gameKey
- g.Game = chess.NewGame()
- g.lastUpdated = time.Now()
- g.Player1 = newChessPlayer(player1)
- g.Player2 = newChessPlayer(player2)
- return g
-}
-
-type Chess struct {
- sync.Mutex
- db *database.DkfDB
- zeroID database.UserID
- games map[string]*ChessGame
-}
-
-func NewChess(db *database.DkfDB) *Chess {
- zeroUser, _ := db.GetUserByUsername(config.NullUsername)
- c := &Chess{db: db, zeroID: zeroUser.ID}
- c.games = make(map[string]*ChessGame)
-
- // Thread that cleanup inactive games
- go func() {
- for {
- time.Sleep(15 * time.Minute)
- c.Lock()
- for k, g := range c.games {
- if time.Since(g.lastUpdated) > 3*time.Hour {
- delete(c.games, k)
- }
- }
- c.Unlock()
- }
- }()
-
- return c
-}
-
-var ChessInstance *Chess
-
-const (
- sqSize = 45
- boardSize = 8 * sqSize
-)
-
-func renderBoardPng(last *chess.Move, position *chess.Position, isFlipped bool) image.Image {
- boardMap := position.Board().SquareMap()
- ctx := gg.NewContext(boardSize, boardSize)
- for i := 0; i < 64; i++ {
- sq := chess.Square(i)
- sqPiece := boardMap[sq]
- renderSquare(ctx, sq, last, position.Turn(), sqPiece, isFlipped)
- }
- return ctx.Image()
-}
-
-func XyForSquare(isFlipped bool, sq chess.Square) (x, y int) {
- fileIndex := int(sq.File())
- rankIndex := 7 - int(sq.Rank())
- x = fileIndex * sqSize
- y = rankIndex * sqSize
- if isFlipped {
- x = boardSize - x - sqSize
- y = boardSize - y - sqSize
- }
- return
-}
-
-func colorForSquare(sq chess.Square) color.RGBA {
- sqSum := int(sq.File()) + int(sq.Rank())
- if sqSum%2 == 0 {
- return color.RGBA{R: 165, G: 117, B: 81, A: 255}
- }
- return color.RGBA{R: 235, G: 209, B: 166, A: 255}
-}
-
-func renderSquare(ctx *gg.Context, sq chess.Square, last *chess.Move, turn chess.Color, sqPiece chess.Piece, isFlipped bool) {
- x, y := XyForSquare(isFlipped, sq)
- // draw square
- ctx.Push()
- ctx.SetColor(colorForSquare(sq))
- ctx.DrawRectangle(float64(x), float64(y), sqSize, sqSize)
- ctx.Fill()
- ctx.Pop()
- // Draw previous move
- if last != nil {
- if last.S1() == sq || last.S2() == sq {
- ctx.Push()
- ctx.SetRGBA(0, 1, 0, 0.1)
- ctx.DrawRectangle(float64(x), float64(y), sqSize, sqSize)
- ctx.Fill()
- ctx.Pop()
- }
- // Draw check
- p := sqPiece
- if p != chess.NoPiece {
- if p.Type() == chess.King && p.Color() == turn && last.HasTag(chess.Check) {
- ctx.Push()
- ctx.SetRGBA(1, 0, 0, 0.4)
- ctx.DrawRectangle(float64(x), float64(y), sqSize, sqSize)
- ctx.Fill()
- ctx.Pop()
- }
- }
- }
-
- ctx.Push()
- ctx.SetColor(color.RGBA{R: 0, G: 0, B: 0, A: 180})
- if (!isFlipped && sq.Rank() == chess.Rank1) || (isFlipped && sq.Rank() == chess.Rank8) {
- ctx.DrawString(sq.File().String(), float64(x+sqSize-7), float64(y+sqSize-1))
- }
- if (!isFlipped && sq.File() == chess.FileA) || (isFlipped && sq.File() == chess.FileH) {
- ctx.DrawString(sq.Rank().String(), float64(x+1), float64(y+11))
- }
- ctx.Pop()
-
- // draw piece
- p := sqPiece
- if p != chess.NoPiece {
- img := getFile("img/chess/" + p.Color().String() + pieceTypeMap[p.Type()] + ".png")
- ctx.Push()
- ctx.DrawImage(img, x, y)
- ctx.Pop()
- }
-}
-
-var pieceTypeMap = map[chess.PieceType]string{
- chess.King: "K",
- chess.Queen: "Q",
- chess.Rook: "R",
- chess.Bishop: "B",
- chess.Knight: "N",
- chess.Pawn: "P",
-}
-
-var cache = make(map[string]image.Image)
-
-func getFile(fileName string) image.Image {
- if img, ok := cache[fileName]; ok {
- return img
- }
- fileBy := bindata.MustAsset(fileName)
- img, _ := png.Decode(bytes.NewReader(fileBy))
- cache[fileName] = img
- return img
-}
-
-func renderTable(imgB64 string, isBlack bool) string {
- htmlTmpl := `
-<style>
-input[type=checkbox] {
- display:none;
-}
-input[type=checkbox] + label {
- display: inline-block;
- padding: 0 0 0 0;
- margin: 0 0 0 0;
- height: 39px;
- width: 39px;
- background-size: 100%;
- border: 3px solid transparent;
-}
-input[type=checkbox]:checked + label {
- display: inline-block;
- background-size: 100%;
- border: 3px solid red;
-}
-</style>
-
-<table style="width: 360px; height: 360px; background-image: url(data:image/png;base64,{{ .ImgB64 }})">
- {{ range $row := .Rows }}
- <tr>
- {{ range $col := $.Cols }}
- {{ $id := GetID $row $col }}
- <td>
- <input name="sq_{{ $id }}" ID="sq_{{ $id }}" type="checkbox" value="1" />
- <label for="sq_{{ $id }}"></label>
- </td>
- {{ end }}
- </tr>
- {{ end }}
-</table>
-`
- data := map[string]any{
- "ImgB64": imgB64,
- "Rows": []int{0, 1, 2, 3, 4, 5, 6, 7},
- "Cols": []int{0, 1, 2, 3, 4, 5, 6, 7},
- }
-
- fns := template.FuncMap{
- "GetID": func(row, col int) int {
- var id int
- if isBlack {
- id = row*8 + (7 - col)
- } else {
- id = (7-row)*8 + col
- }
- return id
- },
- }
-
- var buf bytes.Buffer
- _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf, data)
- return buf.String()
-}
-
-func (g *ChessGame) renderBoardB64(isFlipped bool) string {
- position := g.Game.Position()
- moves := g.Game.Moves()
- var last *chess.Move
- if len(moves) > 0 {
- last = moves[len(moves)-1]
- }
- var buf bytes.Buffer
- img := renderBoardPng(last, position, isFlipped)
- _ = png.Encode(&buf, img)
- imgB64 := base64.StdEncoding.EncodeToString(buf.Bytes())
- return imgB64
-}
-
-func (g *ChessGame) DrawPlayerCard(isBlack, isYourTurn bool) string {
- return g.drawPlayerCard("", isBlack, isYourTurn)
-}
-
-func (g *ChessGame) drawPlayerCard(roomName string, isBlack, isYourTurn bool) string {
- enemy := utils.Ternary(isBlack, g.Player1, g.Player2)
-
- imgB64 := g.renderBoardB64(isBlack)
-
- htmlTmpl := `
-<div style="margin: 0 auto; width: 360px;">
- <div style="color: #eee;">
- <span {{ .White.UserStyle | attr }}>@{{ .White.Username }}</span> (white) VS
- <span {{ .Black.UserStyle | attr }}>@{{ .Black.Username }}</span> (black)
- </div>
-
- {{ if .GameOver }}
- <div style="width: 360px; height: 360px; background-image: url(data:image/png;base64,{{ .ImgB64 }})"></div>
- {{ else }}
- <form method="post">
- <input type="hidden" name="message" value="resign" />
- <button type="submit" style="background-color: #aaa; margin: 5px 0;">Resign</button>
- </form>
- {{ if .IsYourTurn }}
- <form method="post"{{ if .InChat }} action="/api/v1/chess"{{ end }}>
- {{ .Table }}
- {{ if .InChat }}
- <input type="hidden" name="room" value="{{ .RoomName }}" />
- <input type="hidden" name="enemyUsername" value="{{ .Username }}" />
- <input type="hidden" name="move" value="move" />
- {{ else }}
- <input type="hidden" name="message" value="/pm {{ .Username }} /c move" />
- {{ end }}
- <div style="width: 100%; display: flex; margin: 5px 0;">
- <div>
- <button type="submit" style="background-color: #aaa;">Move</button>
- </div>
- <div style="margin-left: auto;">
- <span style="color: #aaa; margin-left: 20px;">Promo:</span>
- <select name="promotion" style="background-color: #aaa;">
- <option value="queen">Queen</option>
- <option value="rook">Rook</option>
- <option value="knight">Knight</option>
- <option value="bishop">Bishop</option>
- </select>
- </div>
- </div>
- </form>
- {{ else }}
- <div style="width: 360px; height: 360px; background-image: url(data:image/png;base64,{{ .ImgB64 }})"></div>
- {{ end }}
- {{ end }}
- <div style="color: #eee;">Outcome: {{ .Outcome }}</div>
-
- {{ if .GameOver }}
- <div><textarea>{{ .PGN }}</textarea></div>
- {{ end }}
-</div>
-`
-
- data := map[string]any{
- "RoomName": roomName,
- "IsYourTurn": isYourTurn,
- "InChat": roomName != "",
- "White": g.Player1,
- "Black": g.Player2,
- "Username": enemy.Username,
- "Table": template.HTML(renderTable(imgB64, isBlack)),
- "ImgB64": imgB64,
- "Outcome": g.Game.Outcome().String(),
- "GameOver": g.Game.Outcome() != chess.NoOutcome,
- "PGN": g.Game.String(),
- }
-
- fns := template.FuncMap{
- "attr": func(s string) template.HTMLAttr {
- return template.HTMLAttr(s)
- },
- }
-
- var buf1 bytes.Buffer
- _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf1, data)
- return buf1.String()
-}
-
-func (g *ChessGame) DrawSpectatorCard(isFlipped bool) string {
- imgB64 := g.renderBoardB64(isFlipped)
-
- htmlTmpl := `
-<div style="margin: 0 auto; width: 360px;">
- <div style="color: #eee;">
- <span {{ .White.UserStyle | attr }}>@{{ .White.Username }}</span> (white) VS
- <span {{ .Black.UserStyle | attr }}>@{{ .Black.Username }}</span> (black)
- </div>
- <div style="width: 360px; height: 360px; background-image: url(data:image/png;base64,{{ .ImgB64 }})"></div>
- <div style="color: #eee;">Outcome: {{ .Outcome }}</div>
- {{ if .GameOver }}
- <div><textarea>{{ .PGN }}</textarea></div>
- {{ end }}
-</div>
-`
-
- data := map[string]any{
- "White": g.Player1,
- "Black": g.Player2,
- "ImgB64": imgB64,
- "Outcome": g.Game.Outcome().String(),
- "GameOver": g.Game.Outcome() != chess.NoOutcome,
- "PGN": g.Game.String(),
- }
-
- fns := template.FuncMap{
- "attr": func(s string) template.HTMLAttr {
- return template.HTMLAttr(s)
- },
- }
-
- var buf1 bytes.Buffer
- _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf1, data)
- return buf1.String()
-}
-
-func (b *Chess) GetGame(key string) *ChessGame {
- b.Lock()
- defer b.Unlock()
- if g, ok := b.games[key]; ok {
- return g
- }
- return nil
-}
-
-func (b *Chess) GetGames() (out []ChessGame) {
- b.Lock()
- defer b.Unlock()
- for _, v := range b.games {
- out = append(out, *v)
- }
- sort.Slice(out, func(i, j int) bool {
- return out[i].CreatedAt.After(out[j].CreatedAt)
- })
- return
-}
-
-func (b *Chess) NewGame1(roomKey string, roomID database.RoomID, player1, player2 database.User) (*ChessGame, error) {
- if player1.ID == player2.ID {
- return nil, errors.New("can't play yourself")
- }
-
- key := uuid.New().String()
- g := b.NewGame(key, player1, player2)
-
- zeroUser := dutils.GetZeroUser(b.db)
- dutils.SendNewChessGameMessages(b.db, key, roomKey, roomID, zeroUser, player1, player2)
- return g, nil
-}
-
-func (b *Chess) NewGame(gameKey string, user1, user2 database.User) *ChessGame {
- g := newChessGame(gameKey, user1, user2)
- b.Lock()
- b.games[gameKey] = g
- b.Unlock()
- return g
-}
-
-func (b *Chess) newGame(gameKey string, user1, user2 database.User) *ChessGame {
- g := newChessGame(gameKey, user1, user2)
- b.games[gameKey] = g
- return g
-}
-
-func (b *Chess) SendMove(gameKey string, userID database.UserID, g *ChessGame, c echo.Context) error {
- if (g.Game.Position().Turn() == chess.White && userID != g.Player1.ID) ||
- (g.Game.Position().Turn() == chess.Black && userID != g.Player2.ID) {
- return errors.New("not your turn")
- }
-
- you := g.Player2
- opponent := g.Player1
- if g.Game.Position().Turn() == chess.White {
- you = g.Player1
- opponent = g.Player2
- }
-
- selectedSquares := make([]chess.Square, 0)
- for i := 0; i < 64; i++ {
- if utils.DoParseBool(c.Request().PostFormValue("sq_" + strconv.Itoa(i))) {
- selectedSquares = append(selectedSquares, chess.Square(i))
- }
- }
-
- if len(selectedSquares) != 2 {
- return errors.New("must select 2 squares")
- }
-
- promo := chess.Queen
- switch c.Request().PostFormValue("promotion") {
- case "queen":
- promo = chess.Queen
- case "rook":
- promo = chess.Rook
- case "knight":
- promo = chess.Knight
- case "bishop":
- promo = chess.Bishop
- }
-
- var moveStr string
- validMoves := g.Game.Position().ValidMoves()
- var found bool
- for _, move := range validMoves {
- if (move.S1() == selectedSquares[0] && move.S2() == selectedSquares[1] && (move.Promo() == chess.NoPieceType || move.Promo() == promo)) ||
- (move.S1() == selectedSquares[1] && move.S2() == selectedSquares[0] && (move.Promo() == chess.NoPieceType || move.Promo() == promo)) {
- moveStr = chess.AlgebraicNotation{}.Encode(g.Game.Position(), move)
- found = true
- break
- }
- }
-
- if !found {
- return fmt.Errorf("invalid move %s %s", selectedSquares[0], selectedSquares[1])
- }
-
- _ = g.Game.MoveStr(moveStr)
- g.lastUpdated = time.Now()
- if g.Game.Outcome() != chess.NoOutcome {
- //delete(b.games, gameKey)
- }
-
- ChessPubSub.Pub(gameKey, true)
-
- // Notify (pm) the opponent that you made a move
- if opponent.NotifyChessMove {
- msg := fmt.Sprintf("@%s played %s", you.Username, moveStr)
- msg, _ = colorifyTaggedUsers(msg, b.db.GetUsersByUsername)
- chatMsg, _ := b.db.CreateMsg(msg, msg, "", config.GeneralRoomID, b.zeroID, &opponent.ID)
- go func() {
- time.Sleep(30 * time.Second)
- _ = chatMsg.Delete(b.db)
- }()
- }
-
- return nil
-}
-
-var ChessPubSub = pubsub.NewPubSub[bool]()
-
-func (b *Chess) InterceptMsg(cmd *Command) {
- m := cRgx.FindStringSubmatch(cmd.message)
- if len(m) != 3 {
- return
- }
- enemyUsername := database.Username(m[1])
- pos := m[2]
- if err := b.playMove(enemyUsername, pos, *cmd.authUser, cmd.c, cmd.room.Name, cmd.roomKey, cmd.room.ID); err != nil {
- cmd.err = err
- return
- }
- cmd.err = ErrStop
-}
-
-func (b *Chess) playMove(enemyUsername database.Username, pos string, authUser database.User, c echo.Context, roomName, roomKey string, roomID database.RoomID) error {
- b.Lock()
- defer b.Unlock()
-
- user, err := b.db.GetUserByUsername(enemyUsername)
- if err != nil {
- return errors.New("invalid username")
- }
-
- var gameKey string
- if authUser.ID < user.ID {
- gameKey = fmt.Sprintf("%d_%d", authUser.ID, user.ID)
- } else {
- gameKey = fmt.Sprintf("%d_%d", user.ID, authUser.ID)
- }
-
- g, ok := b.games[gameKey]
- if ok {
- if err := b.SendMove(gameKey, authUser.ID, g, c); err != nil {
- return err
- }
- } else {
- if pos != "" {
- return errors.New("no Game ongoing")
- }
- g = b.newGame(gameKey, user, authUser)
- }
-
- // Delete old messages sent by "0" to the players
- if err := b.db.DB().
- Where("room_id = ? AND user_id = ? AND (to_user_id = ? OR to_user_id = ?)", roomID, b.zeroID, g.Player1.ID, g.Player2.ID).
- Delete(&database.ChatMessage{}).Error; err != nil {
- logrus.Error(err)
- }
-
- card1 := g.drawPlayerCard(roomName, false, true)
- _, _ = b.db.CreateMsg(card1, card1, roomKey, roomID, b.zeroID, &g.Player1.ID)
-
- card1 = g.drawPlayerCard(roomName, true, true)
- _, _ = b.db.CreateMsg(card1, card1, roomKey, roomID, b.zeroID, &g.Player2.ID)
-
- return nil
-}
diff --git a/pkg/web/handlers/api/v1/codeModalInterceptor.go b/pkg/web/handlers/api/v1/codeModalInterceptor.go
@@ -1,51 +0,0 @@
-package v1
-
-import (
- "dkforest/pkg/database"
- "dkforest/pkg/utils"
-)
-
-type CodeModalInterceptor struct{}
-
-func (i CodeModalInterceptor) InterceptMsg(cmd *Command) {
- sender := cmd.c.Request().PostFormValue("sender")
- lang := cmd.c.Request().PostFormValue("lang")
- isMod := utils.DoParseBool(cmd.c.Request().PostFormValue("isMod"))
- pm := cmd.c.Request().PostFormValue("pm")
-
- if !cmd.authUser.CanUseMultiline || sender != "codeModal" {
- return
- }
-
- database.MsgPubSub.Pub("modal_code_hide_"+cmd.authUser.ID.String()+"_"+cmd.room.ID.String(), database.ChatMessageType{})
-
- cmd.modMsg = isMod
- if pm != "" {
- toUser, err := cmd.db.GetUserByUsername(database.Username(pm))
- if err != nil {
- cmd.err = ErrRedirect
- return
- }
- cmd.skipInboxes, cmd.err = canUserPmOther(cmd.db, *cmd.authUser, toUser, cmd.room.IsOwned())
- if cmd.err != nil {
- return
- }
- cmd.toUser = &toUser
- cmd.redirectQP.Set(redirectPmQP, string(toUser.Username))
- }
-
- if cmd.origMessage == "" {
- cmd.err = ErrRedirect
- return
- }
-
- if !utils.InArr(lang, []string{"go", "c", "cpp", "py", "js", "php", "css", "sql", "rs", "c#", "rb", "html", "bash"}) {
- lang = ""
- }
- cmd.origMessage = codeFenceWrap(lang, cmd.origMessage)
- cmd.message = codeFenceWrap(lang, cmd.message)
-}
-
-func codeFenceWrap(lang, msg string) string {
- return "\n```" + lang + "\n" + msg + "\n```\n"
-}
diff --git a/pkg/web/handlers/api/v1/handlers.go b/pkg/web/handlers/api/v1/handlers.go
@@ -10,6 +10,7 @@ import (
"dkforest/pkg/hashset"
"dkforest/pkg/managers"
"dkforest/pkg/utils"
+ "dkforest/pkg/web/handlers/api/v1/interceptors"
hutils "dkforest/pkg/web/handlers/utils"
"encoding/json"
"errors"
@@ -20,73 +21,10 @@ import (
"golang.org/x/net/websocket"
"io"
"net/http"
- "regexp"
"strings"
"time"
)
-var usernameF = `\w{3,20}` // username (regex Fragment)
-var userOr0 = usernameF + `|0`
-var groupName = `\w{3,20}`
-var roomNameF = `\w{3,50}`
-var chatTs = `\d{2}:\d{2}:\d{2}`
-var optAtGUser = `@?(` + usernameF + `)` // Optional @, Grouped, Username
-var optAtGUserOr0 = `@?(` + userOr0 + `)` // Optional @, Grouped, Username or 0
-var onionV2Rgx = regexp.MustCompile(`[a-z2-7]{16}\.onion`)
-var onionV3Rgx = regexp.MustCompile(`[a-z2-7]{56}\.onion`)
-var deleteMsgRgx = regexp.MustCompile(`^/d (\d{2}:\d{2}:\d{2})(?:\s` + optAtGUserOr0 + `)?$`)
-var ignoreRgx = regexp.MustCompile(`^/(?:ignore|i) ` + optAtGUser)
-var pmToggleWhitelistUserRgx = regexp.MustCompile(`^/pmw ` + optAtGUser)
-var pmToggleBlacklistUserRgx = regexp.MustCompile(`^/pmb ` + optAtGUser)
-var whitelistUserRgx = regexp.MustCompile(`^/(?:whitelist|wl) ` + optAtGUser)
-var unIgnoreRgx = regexp.MustCompile(`^/(?:unignore|ui) ` + optAtGUser)
-var groupRgx = regexp.MustCompile(`^/g (` + groupName + `)\s(?s:(.*))`)
-var pmRgx = regexp.MustCompile(`^/pm ` + optAtGUserOr0 + `(?:\s(?s:(.*)))?`)
-var editRgx = regexp.MustCompile(`^/e (` + chatTs + `)\s(?s:(.*))`)
-var hbmtRgx = regexp.MustCompile(`^/hbmt (` + chatTs + `)$`)
-var chessRgx = regexp.MustCompile(`^/chess ` + optAtGUser + `(?:\s(w|b|r))?`)
-var inboxRgx = regexp.MustCompile(`^/inbox ` + optAtGUser + `(\s-e)?\s(?s:(.*))`)
-var purgeRgx = regexp.MustCompile(`^/purge(\s-hb)? ` + optAtGUserOr0)
-var renameRgx = regexp.MustCompile(`^/rename ` + optAtGUser + ` ` + optAtGUser)
-var profileRgx = regexp.MustCompile(`^/p ` + optAtGUserOr0)
-var kickRgx = regexp.MustCompile(`^/(?:kick|k) ` + optAtGUser)
-var setUrlRgx = regexp.MustCompile(`^/seturl (.+)`)
-var kickKeepRgx = regexp.MustCompile(`^/(?:kk) ` + optAtGUser)
-var kickSilentRgx = regexp.MustCompile(`^/(?:ks) ` + optAtGUser)
-var kickKeepSilentRgx = regexp.MustCompile(`^/(?:kks) ` + optAtGUser)
-var rtutoRgx = regexp.MustCompile(`^/(?:rtuto) ` + optAtGUser)
-var logoutRgx = regexp.MustCompile(`^/(?:logout) ` + optAtGUser)
-var forceCaptchaRgx = regexp.MustCompile(`^/(?:captcha) ` + optAtGUser)
-var unkickRgx = regexp.MustCompile(`^/(?:unkick|uk) ` + optAtGUser)
-var hellbanRgx = regexp.MustCompile(`^/(?:hellban|hb) ` + optAtGUser)
-var unhellbanRgx = regexp.MustCompile(`^/(?:unhellban|uhb) ` + optAtGUser)
-var randRgx = regexp.MustCompile(`^/rand (-?\d+) (-?\d+)$`)
-var tokenRgx = regexp.MustCompile(`^/token (\d{1,2})$`)
-var snippetRgx = regexp.MustCompile(`!\w{1,20}`)
-var tagRgx = regexp.MustCompile(`@(` + userOr0 + `)`)
-var autoTagRgx = regexp.MustCompile(`@(\w+)\*`)
-var roomTagRgx = regexp.MustCompile(`#(` + roomNameF + `)`)
-var tzRgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} at \d{1,2}\.\d{1,2}\.\d{1,2} (?i)[A|P]M)`) // Screen Shot 2022-02-04 at 11.58.58 PM
-var tz1Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} \d{1,2}-\d{1,2}-\d{1,2})`) // Screenshot from 2022-02-04 11-58-58.png
-var tz3Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} \d{1,6})`) // Screenshot 2023-05-20 202351.png
-var tz4Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2}_\d{1,2}_\d{1,2}_\d{1,2})`) // Screenshot_2023-05-20_11_13_14.png
-var addGroupRgx = regexp.MustCompile(`^/addgroup (` + groupName + `)$`)
-var rmGroupRgx = regexp.MustCompile(`^/rmgroup (` + groupName + `)$`)
-var lockGroupRgx = regexp.MustCompile(`^/glock (` + groupName + `)$`)
-var unlockGroupRgx = regexp.MustCompile(`^/gunlock (` + groupName + `)$`)
-var groupUsersRgx = regexp.MustCompile(`^/gusers (` + groupName + `)$`)
-var groupAddUserRgx = regexp.MustCompile(`^/gadduser (` + groupName + `) ` + optAtGUser + `$`)
-var groupRmUserRgx = regexp.MustCompile(`^/grmuser (` + groupName + `) ` + optAtGUser + `$`)
-var unsubscribeRgx = regexp.MustCompile(`^/unsubscribe (` + roomNameF + `)$`)
-var bsRgx = regexp.MustCompile(`^/pm ` + optAtGUser + ` /bs\s?([A-J]\d)?$`)
-var cRgx = regexp.MustCompile(`^/pm ` + optAtGUser + ` /c\s?(move)?$`)
-var hideRgx = regexp.MustCompile(`^/hide (?:“\[)?(\d{2}:\d{2}:\d{2})`)
-var unhideRgx = regexp.MustCompile(`^/unhide (\d{2}:\d{2}:\d{2})$`)
-var memeRgx = regexp.MustCompile(`^/meme ([a-zA-Z0-9_-]{3,50})$`)
-var memeRenameRgx = regexp.MustCompile(`^/meme ([a-zA-Z0-9_-]{3,50}) ([a-zA-Z0-9_-]{3,50})$`)
-var memeRemoveRgx = regexp.MustCompile(`^/memerm ([a-zA-Z0-9_-]{3,50})$`)
-var memesRgx = regexp.MustCompile(`^/memes$`)
-
// GetChatMenuData gets the data needed to render the "right-menu" in a chat room.
// We have it separate because we have one endpoint that only render the right menu (for "stream" chat).
// and one endpoint to render both messages and menu ("non-stream" chat).
@@ -613,7 +551,7 @@ func ChessHandler(c echo.Context) error {
if err != nil {
return c.Redirect(http.StatusFound, redirectURL+"?error="+err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix()))
}
- if err = ChessInstance.playMove(enemyUsername, pos, *authUser, c, roomName, roomKey, room.ID); err != nil {
+ if err = interceptors.ChessInstance.PlayMove(enemyUsername, pos, *authUser, c, roomName, roomKey, room.ID); err != nil {
return c.Redirect(http.StatusFound, redirectURL+"?error="+err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix()))
}
return c.Redirect(http.StatusFound, redirectURL)
@@ -628,10 +566,10 @@ func WerewolfHandler(c echo.Context) error {
if err != nil {
return c.Redirect(http.StatusFound, redirectURL+"?error="+err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix()))
}
- cmd := NewCommand(c, origMessage, room, roomKey)
- WWInstance.InterceptMsg(cmd)
- if cmd.err != nil {
- return c.Redirect(http.StatusFound, redirectURL+"?error="+cmd.err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix()))
+ cmd := interceptors.NewCommand(c, origMessage, room, roomKey)
+ interceptors.WWInstance.InterceptMsg(cmd)
+ if cmd.Err() != nil {
+ return c.Redirect(http.StatusFound, redirectURL+"?error="+cmd.Err().Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix()))
}
return c.Redirect(http.StatusFound, redirectURL)
}
@@ -647,7 +585,7 @@ func BattleshipHandler(c echo.Context) error {
if err != nil {
return c.Redirect(http.StatusFound, redirectURL+"?error="+err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix()))
}
- if err = BattleshipInstance.playMove(roomName, room.ID, roomKey, *authUser, enemyUsername, pos); err != nil {
+ if err = interceptors.BattleshipInstance.PlayMove(roomName, room.ID, roomKey, *authUser, enemyUsername, pos); err != nil {
return c.Redirect(http.StatusFound, redirectURL+"?error="+err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix()))
}
return c.Redirect(http.StatusFound, redirectURL)
diff --git a/pkg/web/handlers/api/v1/interceptors/bangInterceptor.go b/pkg/web/handlers/api/v1/interceptors/bangInterceptor.go
@@ -0,0 +1,32 @@
+package interceptors
+
+import "dkforest/pkg/config"
+
+type BangInterceptor struct{}
+
+func (i BangInterceptor) InterceptMsg(cmd *Command) {
+ switch cmd.message {
+ case "!links":
+ handleLinksBangCmd(cmd)
+ case "!rtuto":
+ handleRtutoBangCmd(cmd)
+ }
+ return
+}
+
+func handleLinksBangCmd(cmd *Command) {
+ message := `
+Chats:
+Black Hat Chat: ` + config.BhcOnion + `
+Forums:
+CryptBB: ` + config.CryptbbOnion
+ msg, _, _ := ProcessRawMessage(cmd.db, message, "", cmd.authUser.ID, cmd.room.ID, nil, true)
+ cmd.zeroMsg(msg)
+ cmd.err = ErrRedirect
+}
+
+func handleRtutoBangCmd(cmd *Command) {
+ cmd.authUser.ChatTutorial = 0
+ cmd.authUser.DoSave(cmd.db)
+ cmd.err = ErrRedirect
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/battleship.go b/pkg/web/handlers/api/v1/interceptors/battleship.go
@@ -0,0 +1,607 @@
+package interceptors
+
+import (
+ "bytes"
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ "dkforest/pkg/utils"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "github.com/fogleman/gg"
+ "github.com/sirupsen/logrus"
+ "html/template"
+ "image/color"
+ "strconv"
+ "sync"
+ "time"
+)
+
+// Carrier 5
+// Battleship 4
+// Cruiser 3
+// Submarine 3
+// Destroyer 2
+
+/**
+◀■■▶
+▲
+█
+▼
+●
+*/
+
+var BattleshipInstance *Battleship
+
+type BSCoordinate struct {
+ x, y int
+}
+
+type BSPlayer struct {
+ id database.UserID
+ username database.Username
+ userStyle string
+ card *BSCard
+ shots map[int]struct{}
+}
+
+func newPlayer(player database.User) *BSPlayer {
+ p := new(BSPlayer)
+ p.id = player.ID
+ p.username = player.Username
+ p.userStyle = player.GenerateChatStyle()
+ p.card = generateCard()
+ p.shots = make(map[int]struct{})
+ return p
+}
+
+type Direction int
+
+const (
+ vertical Direction = iota + 1
+ horizontal
+)
+
+type BSShip struct {
+ name string
+ x, y, size int
+ direction Direction
+ reversed bool
+ health int
+}
+
+func newShip(name string, x, y, size int, dir Direction, reversed bool) BSShip {
+ return BSShip{name: name, x: x, y: y, size: size, direction: dir, health: size, reversed: reversed}
+}
+
+func (s BSShip) contains(pos int) bool {
+ for _, el := range s.getPos() {
+ if pos == el {
+ return true
+ }
+ }
+ return false
+}
+
+func (s BSShip) getPos() (out []int) {
+ for i := 0; i < s.size; i++ {
+ incr := i
+ if s.direction == vertical {
+ incr *= 10
+ }
+ out = append(out, s.y*10+s.x+incr)
+ }
+ return
+}
+
+type BSCard struct {
+ carrier BSShip
+ battleShip BSShip
+ cruiser BSShip
+ submarine BSShip
+ destroyer BSShip
+}
+
+func (c *BSCard) collide(newShip BSShip) bool {
+ for _, p := range newShip.getPos() {
+ if c.carrier.contains(p) ||
+ c.battleShip.contains(p) ||
+ c.cruiser.contains(p) ||
+ c.submarine.contains(p) ||
+ c.destroyer.contains(p) {
+ return true
+ }
+ }
+ return false
+}
+
+func (c *BSCard) shot(pos int) {
+ if c.carrier.contains(pos) {
+ c.carrier.health -= 1
+ } else if c.battleShip.contains(pos) {
+ c.battleShip.health -= 1
+ } else if c.cruiser.contains(pos) {
+ c.cruiser.health -= 1
+ } else if c.submarine.contains(pos) {
+ c.submarine.health -= 1
+ } else if c.destroyer.contains(pos) {
+ c.destroyer.health -= 1
+ }
+}
+
+func (c BSCard) allShipsDead() bool {
+ return c.carrier.health == 0 &&
+ c.battleShip.health == 0 &&
+ c.cruiser.health == 0 &&
+ c.submarine.health == 0 &&
+ c.destroyer.health == 0
+}
+
+func (c BSCard) shipAt(pos int) (string, bool) {
+ if c.carrier.contains(pos) {
+ return "carrier", c.carrier.health == 0
+ } else if c.battleShip.contains(pos) {
+ return "battleShip", c.battleShip.health == 0
+ } else if c.cruiser.contains(pos) {
+ return "cruiser", c.cruiser.health == 0
+ } else if c.submarine.contains(pos) {
+ return "submarine", c.submarine.health == 0
+ } else if c.destroyer.contains(pos) {
+ return "destroyer", c.destroyer.health == 0
+ }
+ return "", false
+}
+
+func (c BSCard) hasShipAt(pos int) bool {
+ var allPos []int
+ allPos = append(allPos, c.carrier.getPos()...)
+ allPos = append(allPos, c.battleShip.getPos()...)
+ allPos = append(allPos, c.cruiser.getPos()...)
+ allPos = append(allPos, c.submarine.getPos()...)
+ allPos = append(allPos, c.destroyer.getPos()...)
+ for _, el := range allPos {
+ if pos == el {
+ return true
+ }
+ }
+ return false
+}
+
+type BSGame struct {
+ lastUpdated time.Time
+ turn int
+ player1 *BSPlayer
+ player2 *BSPlayer
+}
+
+func newGame(player1, player2 database.User) *BSGame {
+ g := new(BSGame)
+ g.lastUpdated = time.Now()
+ g.player1 = newPlayer(player1)
+ g.player2 = newPlayer(player2)
+ return g
+}
+
+func (g BSGame) IsPlayerTurn(playerID database.UserID) bool {
+ return g.turn == 0 && g.player1.id == playerID ||
+ g.turn == 1 && g.player2.id == playerID
+}
+
+func (g *BSGame) Shot(pos string) (shipStr string, shipDead, gameEnded bool, err error) {
+ g.lastUpdated = time.Now()
+ rowStr := pos[0]
+ row := int(rowStr - 'A')
+ col, _ := strconv.Atoi(string(pos[1]))
+ p := row*10 + col
+
+ ent1 := g.player1
+ ent2 := g.player2
+ if g.turn == 1 {
+ ent1, ent2 = ent2, ent1
+ }
+ if _, ok := ent1.shots[p]; ok {
+ return "", false, false, errors.New("position already hit")
+ }
+ ent1.shots[p] = struct{}{}
+ ent2.card.shot(p)
+ shipStr, shipDead = ent2.card.shipAt(p)
+ gameEnded = ent2.card.allShipsDead()
+
+ g.turn = (g.turn + 1) % 2
+ return
+}
+
+type Battleship struct {
+ sync.Mutex
+ db *database.DkfDB
+ zeroID database.UserID
+ games map[string]*BSGame
+}
+
+func NewBattleship(db *database.DkfDB) *Battleship {
+ zeroUser, _ := db.GetUserByUsername(config.NullUsername)
+ b := &Battleship{db: db, zeroID: zeroUser.ID}
+ b.games = make(map[string]*BSGame)
+
+ // Thread that cleanup inactive games
+ go func() {
+ for {
+ time.Sleep(time.Minute)
+ b.Lock()
+ for k, g := range b.games {
+ if time.Since(g.lastUpdated) > 5*time.Minute {
+ delete(b.games, k)
+ }
+ }
+ b.Unlock()
+ }
+ }()
+
+ return b
+}
+
+func generateCard() *BSCard {
+ c := new(BSCard)
+ genTmpShip := func(name string, size int) (out BSShip) {
+ reversed := utils.RandBool()
+ dir := utils.RandChoice([]Direction{horizontal, vertical})
+ val1 := utils.RandInt(0, 9)
+ val2 := utils.RandInt(0, 9-size)
+ if dir == horizontal {
+ val1, val2 = val2, val1
+ }
+ out = newShip(name, val1, val2, size, dir, reversed)
+ return
+ }
+ for _, i := range []int{0, 1, 2, 3, 4} { // iterate 5 times (for each boat)
+ names := []string{"carrier", "battleship", "cruiser", "submarine", "destroyer"}
+ sizes := []int{5, 4, 3, 3, 2} // respective boat size
+ for {
+ tmpShip := genTmpShip(names[i], sizes[i])
+ // If boat collide with another boat, we need to generate a new position for that boat
+ if c.collide(tmpShip) {
+ continue
+ }
+ // boat position is valid, assign it
+ switch i {
+ case 0:
+ c.carrier = tmpShip
+ case 1:
+ c.battleShip = tmpShip
+ case 2:
+ c.cruiser = tmpShip
+ case 3:
+ c.submarine = tmpShip
+ case 4:
+ c.destroyer = tmpShip
+ }
+ break
+ }
+ }
+ return c
+}
+
+func (g *BSGame) drawCardFor(tmp int, roomName string, isNewGame, shipDead, gameEnded bool, shipStr, pos string) (out string) {
+ you := g.player1
+ enemy := g.player2
+ if tmp == 1 {
+ you = g.player2
+ enemy = g.player1
+ }
+
+ imgB64Fn := func(myCard bool) string {
+ ent1 := enemy
+ ent2 := you
+ if myCard {
+ ent1 = you
+ ent2 = enemy
+ }
+
+ c := gg.NewContext(177, 177)
+
+ c.Push()
+ c.SetColor(color.White)
+ c.DrawRectangle(0, 0, 177, 177)
+ c.Fill()
+ c.Pop()
+
+ c.Push()
+ c.SetColor(color.Black)
+ x := 22.0
+ y := 13.0
+ c.DrawString("0", x, y)
+ c.DrawString("1", x+16, y)
+ c.DrawString("2", x+16+16, y)
+ c.DrawString("3", x+16+16+16, y)
+ c.DrawString("4", x+16+16+16+16, y)
+ c.DrawString("5", x+16+16+16+16+16, y)
+ c.DrawString("6", x+16+16+16+16+16+16, y)
+ c.DrawString("7", x+16+16+16+16+16+16+16, y)
+ c.DrawString("8", x+16+16+16+16+16+16+16+16, y)
+ c.DrawString("9", x+16+16+16+16+16+16+16+16+16, y)
+ x = 6
+ y = 29.0
+ c.DrawString("A", x, y)
+ c.DrawString("B", x, y+16)
+ c.DrawString("C", x, y+16+16)
+ c.DrawString("D", x, y+16+16+16)
+ c.DrawString("E", x, y+16+16+16+16)
+ c.DrawString("F", x, y+16+16+16+16+16)
+ c.DrawString("G", x, y+16+16+16+16+16+16)
+ c.DrawString("H", x, y+16+16+16+16+16+16+16)
+ c.DrawString("I", x, y+16+16+16+16+16+16+16+16)
+ c.DrawString("J", x, y+16+16+16+16+16+16+16+16+16)
+ c.Pop()
+
+ c.Push()
+ c.SetLineWidth(1)
+ c.SetColor(color.RGBA{R: 90, G: 90, B: 90, A: 255})
+ for col := 0.0; col < 12; col++ {
+ c.MoveTo(0.5+col*16, 0)
+ c.LineTo(0.5+col*16, 176)
+ c.Stroke()
+ }
+ for row := 0.0; row < 12; row++ {
+ c.MoveTo(0, 0.5+row*16)
+ c.LineTo(176, 0.5+row*16)
+ c.Stroke()
+ }
+ c.Pop()
+
+ drawShip := func(s BSShip) {
+ if !myCard && s.health != 0 && !gameEnded {
+ return
+ }
+ //fmt.Println(s.name, s.x, s.y, s.direction, s.reversed)
+ c.Push()
+ c.Translate(0.5, 0.5)
+ c.Translate(16, 16)
+ c.Translate(float64(s.x)*16, float64(s.y)*16)
+ if s.direction == horizontal {
+ if s.reversed {
+ c.Translate(float64(s.size)*16, 0)
+ c.Rotate(gg.Radians(90))
+ } else {
+ c.Translate(0, 16)
+ c.Rotate(gg.Radians(-90))
+ }
+ } else {
+ if s.reversed {
+ c.Translate(16, float64(s.size)*16)
+ c.Rotate(gg.Radians(180))
+ }
+ }
+ // Front of the ship
+ c.MoveTo(1, 11)
+ c.QuadraticTo(8, -10, 15, 11)
+ // Length of the ship
+ c.Translate(0, float64(s.size-1)*16)
+ // back of the ship
+ c.LineTo(15, 11)
+ c.QuadraticTo(8, 17, 1, 11)
+ c.ClosePath()
+ if s.health == 0 {
+ c.SetColor(color.RGBA{R: 100, G: 100, B: 100, A: 200})
+ c.Fill()
+ } else if !myCard && gameEnded {
+ c.SetColor(color.RGBA{R: 100, G: 130, B: 100, A: 200})
+ c.Fill()
+ } else {
+ c.SetColor(color.RGBA{R: 100, G: 100, B: 100, A: 255})
+ c.Fill()
+ }
+ c.Pop()
+ }
+ drawShip(ent1.card.carrier)
+ drawShip(ent1.card.battleShip)
+ drawShip(ent1.card.cruiser)
+ drawShip(ent1.card.submarine)
+ drawShip(ent1.card.destroyer)
+
+ c.Push()
+ c.Translate(0.5, 0.5)
+ c.Translate(16, 16)
+ for shot := range ent2.shots {
+ shotRow := shot / 10
+ shotCol := shot % 10
+ c.Push()
+ c.Translate(float64(shotCol)*16, float64(shotRow)*16)
+
+ if ent1.card.hasShipAt(shot) {
+ c.DrawCircle(8, 8, 4)
+ c.SetColor(color.RGBA{R: 255, G: 200, B: 0, A: 255})
+ c.Fill()
+
+ c.DrawCircle(8, 8, 3)
+ c.SetColor(color.RGBA{R: 255, G: 0, B: 0, A: 255})
+ c.Fill()
+ } else {
+ c.DrawCircle(8, 8, 3)
+ c.SetColor(color.RGBA{R: 60, G: 200, B: 0, A: 255})
+ c.Fill()
+
+ c.DrawCircle(8, 8, 2)
+ c.SetColor(color.RGBA{R: 100, G: 0, B: 0, A: 255})
+ c.Fill()
+ }
+
+ c.Pop()
+ }
+ c.Pop()
+
+ var buf bytes.Buffer
+ _ = c.EncodePNG(&buf)
+ imgB64 := base64.StdEncoding.EncodeToString(buf.Bytes())
+ return imgB64
+ }
+
+ imgB64 := imgB64Fn(true)
+ img1B64 := imgB64Fn(false)
+
+ htmlTmpl := `
+Against <span {{ .EnemyUserStyle | HTMLAttr }}>@{{ .EnemyUsername }}</span><br />
+{{ if not .IsNewGame }}
+ {{ if .YourTurn }}
+ <span {{ .EnemyUserStyle | HTMLAttr }}>@{{ .EnemyUsername }}</span> played {{ .Pos }}
+ {{ else }}
+ you played {{ .Pos }}
+ {{ end }}
+ ;
+ {{ if .ShipStr }}
+ {{ .ShipStr }} hit
+ {{ if .ShipDead }}
+ and sunk
+ {{ end }}
+ {{ else }}
+ miss
+ {{ end }}
+ ;
+{{ end }}
+{{ if .GameEnded }}
+ {{ if .YourTurn }}
+ You lost!<br />
+ {{ else }}
+ You win!<br />
+ {{ end }}
+{{ else }}
+ {{ if .YourTurn }}
+ now is your turn<br />
+ {{ else }}
+ waiting for opponent<br />
+ {{ end }}
+{{ end }}
+<table>
+ <tr>
+ <td><img src="data:image/png;base64,{{ .ImgB64 }}" alt="" /></td>
+ <td style="vertical-align: top;">
+ <form method="post" style="margin-left: 10px;" action="/api/v1/battleship">
+ <input type="hidden" name="room" value="{{ .RoomName }}" />
+ <input type="hidden" name="enemyUsername" value="{{ .EnemyUsername }}" />
+ <table style="width: 177px; height: 177px; background-image: url(data:image/png;base64,{{ .Img1B64 }})">
+ <tr style="height: 16px;"><td colspan="11"> </td></tr>
+ {{- range $row := .Rows -}}
+ <tr style="height: 16px;">
+ <td style="width: 16px;"></td>
+ {{- range $col := $.Cols -}}
+ {{- if NotShot $row $col -}}
+ {{- if and $.YourTurn (not $.GameEnded) -}}
+ <td style="width: 16px;">
+ <button style="height: 15px; width: 15px;" name="move" value="{{ GetRune $row }}{{ $col }}"></button>
+ </td>
+ {{- else -}}
+ <td style="width: 16px;"></td>
+ {{- end -}}
+ {{- else -}}
+ <td style="width: 16px;"></td>
+ {{- end -}}
+ {{- end -}}
+ </tr>
+ {{- end -}}
+ </table>
+ </form>
+ </td>
+ </tr>
+</table>
+`
+ data := map[string]any{
+ "RoomName": roomName,
+ "EnemyUserStyle": enemy.userStyle,
+ "EnemyUsername": enemy.username,
+ "IsNewGame": isNewGame,
+ "YourTurn": g.turn == tmp,
+ "Pos": pos,
+ "ShipStr": shipStr,
+ "ShipDead": shipDead,
+ "GameEnded": gameEnded,
+ "ImgB64": imgB64,
+ "Img1B64": img1B64,
+ "Rows": []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+ "Cols": []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
+ }
+ fns := template.FuncMap{
+ "GetRune": func(i int) string {
+ return string(rune('A' + i))
+ },
+ "NotShot": func(i, j int) bool {
+ _, ok := you.shots[i*10+j]
+ return !ok
+ },
+ "HTMLAttr": func(in string) template.HTMLAttr {
+ return template.HTMLAttr(in)
+ },
+ }
+ var buf bytes.Buffer
+ _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf, data)
+ return buf.String()
+}
+
+func (b *Battleship) InterceptMsg(cmd *Command) {
+ m := bsRgx.FindStringSubmatch(cmd.message)
+ if len(m) != 3 {
+ return
+ }
+ enemyUsername := database.Username(m[1])
+ pos := m[2]
+ if err := b.PlayMove(cmd.room.Name, cmd.room.ID, cmd.roomKey, *cmd.authUser, enemyUsername, pos); err != nil {
+ cmd.err = err
+ return
+ }
+ cmd.err = ErrStop
+ return
+}
+
+func (b *Battleship) PlayMove(roomName string, roomID database.RoomID, roomKey string, authUser database.User, enemyUsername database.Username, pos string) error {
+ b.Lock()
+ defer b.Unlock()
+
+ user, err := b.db.GetUserByUsername(enemyUsername)
+ if err != nil {
+ return errors.New("invalid username")
+ }
+
+ var gameKey string
+ if authUser.ID < user.ID {
+ gameKey = fmt.Sprintf("%d_%d", authUser.ID, user.ID)
+ } else {
+ gameKey = fmt.Sprintf("%d_%d", user.ID, authUser.ID)
+ }
+
+ var shipStr string
+ var isNewGame, shipDead, gameEnded bool
+ g, ok := b.games[gameKey]
+ if ok {
+ if !g.IsPlayerTurn(authUser.ID) {
+ return errors.New("not your turn")
+ }
+ shipStr, shipDead, gameEnded, err = g.Shot(pos)
+ if err != nil {
+ return err
+ }
+ } else {
+ if pos != "" {
+ return errors.New("no Game ongoing")
+ }
+ g = newGame(user, authUser)
+ b.games[gameKey] = g
+ isNewGame = true
+ }
+
+ // Delete old messages sent by "0" to the players
+ if err := b.db.DB().
+ Where("room_id = ? AND user_id = ? AND (to_user_id = ? OR to_user_id = ?)", roomID, b.zeroID, g.player1.id, g.player2.id).
+ Delete(&database.ChatMessage{}).Error; err != nil {
+ logrus.Error(err)
+ }
+
+ card1 := g.drawCardFor(0, roomName, isNewGame, shipDead, gameEnded, shipStr, pos)
+ _, _ = b.db.CreateMsg(card1, card1, roomKey, roomID, b.zeroID, &g.player1.id)
+
+ card2 := g.drawCardFor(1, roomName, isNewGame, shipDead, gameEnded, shipStr, pos)
+ _, _ = b.db.CreateMsg(card2, card2, roomKey, roomID, b.zeroID, &g.player2.id)
+
+ if gameEnded {
+ delete(b.games, gameKey)
+ }
+
+ return nil
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/chess.go b/pkg/web/handlers/api/v1/interceptors/chess.go
@@ -0,0 +1,578 @@
+package interceptors
+
+import (
+ "bytes"
+ "dkforest/bindata"
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ dutils "dkforest/pkg/database/utils"
+ "dkforest/pkg/pubsub"
+ "dkforest/pkg/utils"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "github.com/fogleman/gg"
+ "github.com/google/uuid"
+ "github.com/labstack/echo"
+ "github.com/notnil/chess"
+ "github.com/sirupsen/logrus"
+ "html/template"
+ "image"
+ "image/color"
+ "image/png"
+ "sort"
+ "strconv"
+ "sync"
+ "time"
+)
+
+type ChessPlayer struct {
+ ID database.UserID
+ Username database.Username
+ UserStyle string
+ NotifyChessMove bool
+}
+
+type ChessGame struct {
+ Key string
+ Game *chess.Game
+ lastUpdated time.Time
+ Player1 *ChessPlayer
+ Player2 *ChessPlayer
+ CreatedAt time.Time
+}
+
+func newChessPlayer(player database.User) *ChessPlayer {
+ p := new(ChessPlayer)
+ p.ID = player.ID
+ p.Username = player.Username
+ p.UserStyle = player.GenerateChatStyle()
+ p.NotifyChessMove = player.NotifyChessMove
+ return p
+}
+
+func newChessGame(gameKey string, player1, player2 database.User) *ChessGame {
+ g := new(ChessGame)
+ g.CreatedAt = time.Now()
+ g.Key = gameKey
+ g.Game = chess.NewGame()
+ g.lastUpdated = time.Now()
+ g.Player1 = newChessPlayer(player1)
+ g.Player2 = newChessPlayer(player2)
+ return g
+}
+
+type Chess struct {
+ sync.Mutex
+ db *database.DkfDB
+ zeroID database.UserID
+ games map[string]*ChessGame
+}
+
+func NewChess(db *database.DkfDB) *Chess {
+ zeroUser, _ := db.GetUserByUsername(config.NullUsername)
+ c := &Chess{db: db, zeroID: zeroUser.ID}
+ c.games = make(map[string]*ChessGame)
+
+ // Thread that cleanup inactive games
+ go func() {
+ for {
+ time.Sleep(15 * time.Minute)
+ c.Lock()
+ for k, g := range c.games {
+ if time.Since(g.lastUpdated) > 3*time.Hour {
+ delete(c.games, k)
+ }
+ }
+ c.Unlock()
+ }
+ }()
+
+ return c
+}
+
+var ChessInstance *Chess
+
+const (
+ sqSize = 45
+ boardSize = 8 * sqSize
+)
+
+func renderBoardPng(last *chess.Move, position *chess.Position, isFlipped bool) image.Image {
+ boardMap := position.Board().SquareMap()
+ ctx := gg.NewContext(boardSize, boardSize)
+ for i := 0; i < 64; i++ {
+ sq := chess.Square(i)
+ sqPiece := boardMap[sq]
+ renderSquare(ctx, sq, last, position.Turn(), sqPiece, isFlipped)
+ }
+ return ctx.Image()
+}
+
+func XyForSquare(isFlipped bool, sq chess.Square) (x, y int) {
+ fileIndex := int(sq.File())
+ rankIndex := 7 - int(sq.Rank())
+ x = fileIndex * sqSize
+ y = rankIndex * sqSize
+ if isFlipped {
+ x = boardSize - x - sqSize
+ y = boardSize - y - sqSize
+ }
+ return
+}
+
+func colorForSquare(sq chess.Square) color.RGBA {
+ sqSum := int(sq.File()) + int(sq.Rank())
+ if sqSum%2 == 0 {
+ return color.RGBA{R: 165, G: 117, B: 81, A: 255}
+ }
+ return color.RGBA{R: 235, G: 209, B: 166, A: 255}
+}
+
+func renderSquare(ctx *gg.Context, sq chess.Square, last *chess.Move, turn chess.Color, sqPiece chess.Piece, isFlipped bool) {
+ x, y := XyForSquare(isFlipped, sq)
+ // draw square
+ ctx.Push()
+ ctx.SetColor(colorForSquare(sq))
+ ctx.DrawRectangle(float64(x), float64(y), sqSize, sqSize)
+ ctx.Fill()
+ ctx.Pop()
+ // Draw previous move
+ if last != nil {
+ if last.S1() == sq || last.S2() == sq {
+ ctx.Push()
+ ctx.SetRGBA(0, 1, 0, 0.1)
+ ctx.DrawRectangle(float64(x), float64(y), sqSize, sqSize)
+ ctx.Fill()
+ ctx.Pop()
+ }
+ // Draw check
+ p := sqPiece
+ if p != chess.NoPiece {
+ if p.Type() == chess.King && p.Color() == turn && last.HasTag(chess.Check) {
+ ctx.Push()
+ ctx.SetRGBA(1, 0, 0, 0.4)
+ ctx.DrawRectangle(float64(x), float64(y), sqSize, sqSize)
+ ctx.Fill()
+ ctx.Pop()
+ }
+ }
+ }
+
+ ctx.Push()
+ ctx.SetColor(color.RGBA{R: 0, G: 0, B: 0, A: 180})
+ if (!isFlipped && sq.Rank() == chess.Rank1) || (isFlipped && sq.Rank() == chess.Rank8) {
+ ctx.DrawString(sq.File().String(), float64(x+sqSize-7), float64(y+sqSize-1))
+ }
+ if (!isFlipped && sq.File() == chess.FileA) || (isFlipped && sq.File() == chess.FileH) {
+ ctx.DrawString(sq.Rank().String(), float64(x+1), float64(y+11))
+ }
+ ctx.Pop()
+
+ // draw piece
+ p := sqPiece
+ if p != chess.NoPiece {
+ img := getFile("img/chess/" + p.Color().String() + pieceTypeMap[p.Type()] + ".png")
+ ctx.Push()
+ ctx.DrawImage(img, x, y)
+ ctx.Pop()
+ }
+}
+
+var pieceTypeMap = map[chess.PieceType]string{
+ chess.King: "K",
+ chess.Queen: "Q",
+ chess.Rook: "R",
+ chess.Bishop: "B",
+ chess.Knight: "N",
+ chess.Pawn: "P",
+}
+
+var cache = make(map[string]image.Image)
+
+func getFile(fileName string) image.Image {
+ if img, ok := cache[fileName]; ok {
+ return img
+ }
+ fileBy := bindata.MustAsset(fileName)
+ img, _ := png.Decode(bytes.NewReader(fileBy))
+ cache[fileName] = img
+ return img
+}
+
+func renderTable(imgB64 string, isBlack bool) string {
+ htmlTmpl := `
+<style>
+input[type=checkbox] {
+ display:none;
+}
+input[type=checkbox] + label {
+ display: inline-block;
+ padding: 0 0 0 0;
+ margin: 0 0 0 0;
+ height: 39px;
+ width: 39px;
+ background-size: 100%;
+ border: 3px solid transparent;
+}
+input[type=checkbox]:checked + label {
+ display: inline-block;
+ background-size: 100%;
+ border: 3px solid red;
+}
+</style>
+
+<table style="width: 360px; height: 360px; background-image: url(data:image/png;base64,{{ .ImgB64 }})">
+ {{ range $row := .Rows }}
+ <tr>
+ {{ range $col := $.Cols }}
+ {{ $id := GetID $row $col }}
+ <td>
+ <input name="sq_{{ $id }}" ID="sq_{{ $id }}" type="checkbox" value="1" />
+ <label for="sq_{{ $id }}"></label>
+ </td>
+ {{ end }}
+ </tr>
+ {{ end }}
+</table>
+`
+ data := map[string]any{
+ "ImgB64": imgB64,
+ "Rows": []int{0, 1, 2, 3, 4, 5, 6, 7},
+ "Cols": []int{0, 1, 2, 3, 4, 5, 6, 7},
+ }
+
+ fns := template.FuncMap{
+ "GetID": func(row, col int) int {
+ var id int
+ if isBlack {
+ id = row*8 + (7 - col)
+ } else {
+ id = (7-row)*8 + col
+ }
+ return id
+ },
+ }
+
+ var buf bytes.Buffer
+ _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf, data)
+ return buf.String()
+}
+
+func (g *ChessGame) renderBoardB64(isFlipped bool) string {
+ position := g.Game.Position()
+ moves := g.Game.Moves()
+ var last *chess.Move
+ if len(moves) > 0 {
+ last = moves[len(moves)-1]
+ }
+ var buf bytes.Buffer
+ img := renderBoardPng(last, position, isFlipped)
+ _ = png.Encode(&buf, img)
+ imgB64 := base64.StdEncoding.EncodeToString(buf.Bytes())
+ return imgB64
+}
+
+func (g *ChessGame) DrawPlayerCard(isBlack, isYourTurn bool) string {
+ return g.drawPlayerCard("", isBlack, isYourTurn)
+}
+
+func (g *ChessGame) drawPlayerCard(roomName string, isBlack, isYourTurn bool) string {
+ enemy := utils.Ternary(isBlack, g.Player1, g.Player2)
+
+ imgB64 := g.renderBoardB64(isBlack)
+
+ htmlTmpl := `
+<div style="margin: 0 auto; width: 360px;">
+ <div style="color: #eee;">
+ <span {{ .White.UserStyle | attr }}>@{{ .White.Username }}</span> (white) VS
+ <span {{ .Black.UserStyle | attr }}>@{{ .Black.Username }}</span> (black)
+ </div>
+
+ {{ if .GameOver }}
+ <div style="width: 360px; height: 360px; background-image: url(data:image/png;base64,{{ .ImgB64 }})"></div>
+ {{ else }}
+ <form method="post">
+ <input type="hidden" name="message" value="resign" />
+ <button type="submit" style="background-color: #aaa; margin: 5px 0;">Resign</button>
+ </form>
+ {{ if .IsYourTurn }}
+ <form method="post"{{ if .InChat }} action="/api/v1/chess"{{ end }}>
+ {{ .Table }}
+ {{ if .InChat }}
+ <input type="hidden" name="room" value="{{ .RoomName }}" />
+ <input type="hidden" name="enemyUsername" value="{{ .Username }}" />
+ <input type="hidden" name="move" value="move" />
+ {{ else }}
+ <input type="hidden" name="message" value="/pm {{ .Username }} /c move" />
+ {{ end }}
+ <div style="width: 100%; display: flex; margin: 5px 0;">
+ <div>
+ <button type="submit" style="background-color: #aaa;">Move</button>
+ </div>
+ <div style="margin-left: auto;">
+ <span style="color: #aaa; margin-left: 20px;">Promo:</span>
+ <select name="promotion" style="background-color: #aaa;">
+ <option value="queen">Queen</option>
+ <option value="rook">Rook</option>
+ <option value="knight">Knight</option>
+ <option value="bishop">Bishop</option>
+ </select>
+ </div>
+ </div>
+ </form>
+ {{ else }}
+ <div style="width: 360px; height: 360px; background-image: url(data:image/png;base64,{{ .ImgB64 }})"></div>
+ {{ end }}
+ {{ end }}
+ <div style="color: #eee;">Outcome: {{ .Outcome }}</div>
+
+ {{ if .GameOver }}
+ <div><textarea>{{ .PGN }}</textarea></div>
+ {{ end }}
+</div>
+`
+
+ data := map[string]any{
+ "RoomName": roomName,
+ "IsYourTurn": isYourTurn,
+ "InChat": roomName != "",
+ "White": g.Player1,
+ "Black": g.Player2,
+ "Username": enemy.Username,
+ "Table": template.HTML(renderTable(imgB64, isBlack)),
+ "ImgB64": imgB64,
+ "Outcome": g.Game.Outcome().String(),
+ "GameOver": g.Game.Outcome() != chess.NoOutcome,
+ "PGN": g.Game.String(),
+ }
+
+ fns := template.FuncMap{
+ "attr": func(s string) template.HTMLAttr {
+ return template.HTMLAttr(s)
+ },
+ }
+
+ var buf1 bytes.Buffer
+ _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf1, data)
+ return buf1.String()
+}
+
+func (g *ChessGame) DrawSpectatorCard(isFlipped bool) string {
+ imgB64 := g.renderBoardB64(isFlipped)
+
+ htmlTmpl := `
+<div style="margin: 0 auto; width: 360px;">
+ <div style="color: #eee;">
+ <span {{ .White.UserStyle | attr }}>@{{ .White.Username }}</span> (white) VS
+ <span {{ .Black.UserStyle | attr }}>@{{ .Black.Username }}</span> (black)
+ </div>
+ <div style="width: 360px; height: 360px; background-image: url(data:image/png;base64,{{ .ImgB64 }})"></div>
+ <div style="color: #eee;">Outcome: {{ .Outcome }}</div>
+ {{ if .GameOver }}
+ <div><textarea>{{ .PGN }}</textarea></div>
+ {{ end }}
+</div>
+`
+
+ data := map[string]any{
+ "White": g.Player1,
+ "Black": g.Player2,
+ "ImgB64": imgB64,
+ "Outcome": g.Game.Outcome().String(),
+ "GameOver": g.Game.Outcome() != chess.NoOutcome,
+ "PGN": g.Game.String(),
+ }
+
+ fns := template.FuncMap{
+ "attr": func(s string) template.HTMLAttr {
+ return template.HTMLAttr(s)
+ },
+ }
+
+ var buf1 bytes.Buffer
+ _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf1, data)
+ return buf1.String()
+}
+
+func (b *Chess) GetGame(key string) *ChessGame {
+ b.Lock()
+ defer b.Unlock()
+ if g, ok := b.games[key]; ok {
+ return g
+ }
+ return nil
+}
+
+func (b *Chess) GetGames() (out []ChessGame) {
+ b.Lock()
+ defer b.Unlock()
+ for _, v := range b.games {
+ out = append(out, *v)
+ }
+ sort.Slice(out, func(i, j int) bool {
+ return out[i].CreatedAt.After(out[j].CreatedAt)
+ })
+ return
+}
+
+func (b *Chess) NewGame1(roomKey string, roomID database.RoomID, player1, player2 database.User) (*ChessGame, error) {
+ if player1.ID == player2.ID {
+ return nil, errors.New("can't play yourself")
+ }
+
+ key := uuid.New().String()
+ g := b.NewGame(key, player1, player2)
+
+ zeroUser := dutils.GetZeroUser(b.db)
+ dutils.SendNewChessGameMessages(b.db, key, roomKey, roomID, zeroUser, player1, player2)
+ return g, nil
+}
+
+func (b *Chess) NewGame(gameKey string, user1, user2 database.User) *ChessGame {
+ g := newChessGame(gameKey, user1, user2)
+ b.Lock()
+ b.games[gameKey] = g
+ b.Unlock()
+ return g
+}
+
+func (b *Chess) newGame(gameKey string, user1, user2 database.User) *ChessGame {
+ g := newChessGame(gameKey, user1, user2)
+ b.games[gameKey] = g
+ return g
+}
+
+func (b *Chess) SendMove(gameKey string, userID database.UserID, g *ChessGame, c echo.Context) error {
+ if (g.Game.Position().Turn() == chess.White && userID != g.Player1.ID) ||
+ (g.Game.Position().Turn() == chess.Black && userID != g.Player2.ID) {
+ return errors.New("not your turn")
+ }
+
+ you := g.Player2
+ opponent := g.Player1
+ if g.Game.Position().Turn() == chess.White {
+ you = g.Player1
+ opponent = g.Player2
+ }
+
+ selectedSquares := make([]chess.Square, 0)
+ for i := 0; i < 64; i++ {
+ if utils.DoParseBool(c.Request().PostFormValue("sq_" + strconv.Itoa(i))) {
+ selectedSquares = append(selectedSquares, chess.Square(i))
+ }
+ }
+
+ if len(selectedSquares) != 2 {
+ return errors.New("must select 2 squares")
+ }
+
+ promo := chess.Queen
+ switch c.Request().PostFormValue("promotion") {
+ case "queen":
+ promo = chess.Queen
+ case "rook":
+ promo = chess.Rook
+ case "knight":
+ promo = chess.Knight
+ case "bishop":
+ promo = chess.Bishop
+ }
+
+ var moveStr string
+ validMoves := g.Game.Position().ValidMoves()
+ var found bool
+ for _, move := range validMoves {
+ if (move.S1() == selectedSquares[0] && move.S2() == selectedSquares[1] && (move.Promo() == chess.NoPieceType || move.Promo() == promo)) ||
+ (move.S1() == selectedSquares[1] && move.S2() == selectedSquares[0] && (move.Promo() == chess.NoPieceType || move.Promo() == promo)) {
+ moveStr = chess.AlgebraicNotation{}.Encode(g.Game.Position(), move)
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ return fmt.Errorf("invalid move %s %s", selectedSquares[0], selectedSquares[1])
+ }
+
+ _ = g.Game.MoveStr(moveStr)
+ g.lastUpdated = time.Now()
+ if g.Game.Outcome() != chess.NoOutcome {
+ //delete(b.games, gameKey)
+ }
+
+ ChessPubSub.Pub(gameKey, true)
+
+ // Notify (pm) the opponent that you made a move
+ if opponent.NotifyChessMove {
+ msg := fmt.Sprintf("@%s played %s", you.Username, moveStr)
+ msg, _ = colorifyTaggedUsers(msg, b.db.GetUsersByUsername)
+ chatMsg, _ := b.db.CreateMsg(msg, msg, "", config.GeneralRoomID, b.zeroID, &opponent.ID)
+ go func() {
+ time.Sleep(30 * time.Second)
+ _ = chatMsg.Delete(b.db)
+ }()
+ }
+
+ return nil
+}
+
+var ChessPubSub = pubsub.NewPubSub[bool]()
+
+func (b *Chess) InterceptMsg(cmd *Command) {
+ m := cRgx.FindStringSubmatch(cmd.message)
+ if len(m) != 3 {
+ return
+ }
+ enemyUsername := database.Username(m[1])
+ pos := m[2]
+ if err := b.PlayMove(enemyUsername, pos, *cmd.authUser, cmd.c, cmd.room.Name, cmd.roomKey, cmd.room.ID); err != nil {
+ cmd.err = err
+ return
+ }
+ cmd.err = ErrStop
+}
+
+func (b *Chess) PlayMove(enemyUsername database.Username, pos string, authUser database.User, c echo.Context, roomName, roomKey string, roomID database.RoomID) error {
+ b.Lock()
+ defer b.Unlock()
+
+ user, err := b.db.GetUserByUsername(enemyUsername)
+ if err != nil {
+ return errors.New("invalid username")
+ }
+
+ var gameKey string
+ if authUser.ID < user.ID {
+ gameKey = fmt.Sprintf("%d_%d", authUser.ID, user.ID)
+ } else {
+ gameKey = fmt.Sprintf("%d_%d", user.ID, authUser.ID)
+ }
+
+ g, ok := b.games[gameKey]
+ if ok {
+ if err := b.SendMove(gameKey, authUser.ID, g, c); err != nil {
+ return err
+ }
+ } else {
+ if pos != "" {
+ return errors.New("no Game ongoing")
+ }
+ g = b.newGame(gameKey, user, authUser)
+ }
+
+ // Delete old messages sent by "0" to the players
+ if err := b.db.DB().
+ Where("room_id = ? AND user_id = ? AND (to_user_id = ? OR to_user_id = ?)", roomID, b.zeroID, g.Player1.ID, g.Player2.ID).
+ Delete(&database.ChatMessage{}).Error; err != nil {
+ logrus.Error(err)
+ }
+
+ card1 := g.drawPlayerCard(roomName, false, true)
+ _, _ = b.db.CreateMsg(card1, card1, roomKey, roomID, b.zeroID, &g.Player1.ID)
+
+ card1 = g.drawPlayerCard(roomName, true, true)
+ _, _ = b.db.CreateMsg(card1, card1, roomKey, roomID, b.zeroID, &g.Player2.ID)
+
+ return nil
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/codeModalInterceptor.go b/pkg/web/handlers/api/v1/interceptors/codeModalInterceptor.go
@@ -0,0 +1,51 @@
+package interceptors
+
+import (
+ "dkforest/pkg/database"
+ "dkforest/pkg/utils"
+)
+
+type CodeModalInterceptor struct{}
+
+func (i CodeModalInterceptor) InterceptMsg(cmd *Command) {
+ sender := cmd.c.Request().PostFormValue("sender")
+ lang := cmd.c.Request().PostFormValue("lang")
+ isMod := utils.DoParseBool(cmd.c.Request().PostFormValue("isMod"))
+ pm := cmd.c.Request().PostFormValue("pm")
+
+ if !cmd.authUser.CanUseMultiline || sender != "codeModal" {
+ return
+ }
+
+ database.MsgPubSub.Pub("modal_code_hide_"+cmd.authUser.ID.String()+"_"+cmd.room.ID.String(), database.ChatMessageType{})
+
+ cmd.modMsg = isMod
+ if pm != "" {
+ toUser, err := cmd.db.GetUserByUsername(database.Username(pm))
+ if err != nil {
+ cmd.err = ErrRedirect
+ return
+ }
+ cmd.skipInboxes, cmd.err = canUserPmOther(cmd.db, *cmd.authUser, toUser, cmd.room.IsOwned())
+ if cmd.err != nil {
+ return
+ }
+ cmd.toUser = &toUser
+ cmd.redirectQP.Set(RedirectPmQP, string(toUser.Username))
+ }
+
+ if cmd.origMessage == "" {
+ cmd.err = ErrRedirect
+ return
+ }
+
+ if !utils.InArr(lang, []string{"go", "c", "cpp", "py", "js", "php", "css", "sql", "rs", "c#", "rb", "html", "bash"}) {
+ lang = ""
+ }
+ cmd.origMessage = codeFenceWrap(lang, cmd.origMessage)
+ cmd.message = codeFenceWrap(lang, cmd.message)
+}
+
+func codeFenceWrap(lang, msg string) string {
+ return "\n```" + lang + "\n" + msg + "\n```\n"
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/interceptor.go b/pkg/web/handlers/api/v1/interceptors/interceptor.go
@@ -0,0 +1,150 @@
+package interceptors
+
+import (
+ "dkforest/pkg/database"
+ dutils "dkforest/pkg/database/utils"
+ "errors"
+ "fmt"
+ "github.com/labstack/echo"
+ "net/url"
+)
+
+var ErrRedirect = errors.New("redirect")
+var ErrStop = errors.New("stop")
+
+const (
+ RedirectPmQP = "pm"
+ RedirectEditQP = "e"
+ RedirectGroupQP = "g"
+ RedirectModQP = "m"
+ RedirectHbmQP = "hbm"
+ RedirectTagQP = "tag"
+ RedirectHTagQP = "htag"
+ RedirectMTagQP = "mtag"
+ RedirectQuoteQP = "quote"
+ RedirectMultilineQP = "ml"
+)
+
+type Interceptor interface {
+ InterceptMsg(*Command)
+}
+
+type Command struct {
+ err error
+
+ // Data that can be mutated
+ redirectQP url.Values // RedirectURL Query Parameters
+ origMessage string // This is the original text that the user input (can be changed by /e)
+ dataMessage string // This is what the user will have in his input box
+ message string // Un-sanitized message received from the user
+ room database.ChatRoom // Room the user is in
+ roomKey string // Room password (if any)
+ authUser *database.User // Authenticated user (sender of the message)
+ db *database.DkfDB // Database instance
+ toUser *database.User // If not nil, will be a PM
+ upload *database.Upload // If the message contains an uploaded file
+ editMsg *database.ChatMessage // If we're editing a message
+ groupID *database.GroupID // If the message is for a subgroup
+ hellbanMsg bool // Is the message will be marked HB
+ systemMsg bool // Is the message system
+ modMsg bool // Is the message part of the "moderators" group
+ c echo.Context
+ zeroUser *database.User // Cache the zero (@0) user
+ skipInboxes bool
+}
+
+func (c *Command) Err() error {
+ return c.err
+}
+
+func (c *Command) OrigMessage() string {
+ return c.origMessage
+}
+
+func (c *Command) DataMessage() string {
+ return c.dataMessage
+}
+
+func (c *Command) SetRedirectQP(v url.Values) {
+ c.redirectQP = v
+}
+
+func NewCommand(c echo.Context, origMessage string, room database.ChatRoom, roomKey string) *Command {
+ authUser := c.Get("authUser").(*database.User)
+ db := c.Get("database").(*database.DkfDB)
+ return &Command{
+ c: c,
+ authUser: authUser,
+ db: db,
+ hellbanMsg: authUser.IsHellbanned,
+ redirectQP: url.Values{},
+ origMessage: origMessage,
+ message: origMessage,
+ room: room,
+ roomKey: roomKey,
+ }
+}
+
+func (c *Command) RedirectURL() string {
+ return fmt.Sprintf("/api/v1/chat/top-bar/%s?%s", c.room.Name, c.redirectQP.Encode())
+}
+
+// Lazy loading and cache of the zero user
+func (c *Command) getZeroUser() database.User {
+ if c.zeroUser == nil {
+ zeroUser := dutils.GetZeroUser(c.db)
+ c.zeroUser = &zeroUser
+ }
+ return *c.zeroUser
+}
+
+// Have the "zero user" send a processed message to the authUser
+func (c *Command) zeroProcMsg(rawMsg string) {
+ c.zeroProcMsgRoom(rawMsg, c.roomKey, c.room.ID)
+}
+
+// Have the "zero user" send a processed message in the specified room
+func (c *Command) zeroPublicProcMsgRoom(rawMsg, roomKey string, roomID database.RoomID) {
+ c.zeroProcMsgRoomToUser(rawMsg, roomKey, roomID, nil)
+}
+
+// Have the "zero user" send a processed message to the authUser in the specified room
+func (c *Command) zeroProcMsgRoom(rawMsg, roomKey string, roomID database.RoomID) {
+ c.zeroProcMsgRoomToUser(rawMsg, roomKey, roomID, c.authUser)
+}
+
+// Have the "zero user" send a "processed message" PM to a user in a specific room.
+func (c *Command) zeroProcMsgRoomToUser(rawMsg, roomKey string, roomID database.RoomID, toUser *database.User) {
+ procMsg, _, _ := ProcessRawMessage(c.db, rawMsg, roomKey, c.authUser.ID, roomID, nil, true)
+ c.zeroRawMsg(toUser, rawMsg, procMsg)
+}
+
+// Have the "zero usser" send an unprocessed private message to the authUser
+func (c *Command) zeroMsg(msg string) {
+ c.zeroRawMsg(c.authUser, msg, msg)
+}
+
+// Have the "zero usser" send an unprocessed message in the current room
+func (c *Command) zeroPublicMsg(raw, msg string) {
+ c.zeroRawMsg(nil, raw, msg)
+}
+
+func (c *Command) zeroRawMsg(user2 *database.User, raw, msg string) {
+ zeroUser := c.getZeroUser()
+ c.rawMsg(zeroUser, user2, raw, msg)
+}
+
+func (c *Command) rawMsg(user1 database.User, user2 *database.User, raw, msg string) {
+ if c.room.ReadOnly {
+ return
+ }
+ rawMsgRoom(c.db, user1, user2, raw, msg, c.roomKey, c.room.ID)
+}
+
+func rawMsgRoom(db *database.DkfDB, user1 database.User, user2 *database.User, raw, msg, roomKey string, roomID database.RoomID) {
+ var toUserID *database.UserID
+ if user2 != nil {
+ toUserID = &user2.ID
+ }
+ _, _ = db.CreateMsg(raw, msg, roomKey, roomID, user1.ID, toUserID)
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/msgInterceptor.go b/pkg/web/handlers/api/v1/interceptors/msgInterceptor.go
@@ -0,0 +1,1109 @@
+package interceptors
+
+import (
+ bf "dkforest/pkg/blackfriday/v2"
+ "dkforest/pkg/clockwork"
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ "dkforest/pkg/hashset"
+ "dkforest/pkg/levenshtein"
+ "dkforest/pkg/managers"
+ "dkforest/pkg/utils"
+ "errors"
+ "fmt"
+ "github.com/ProtonMail/go-crypto/openpgp/clearsign"
+ "github.com/microcosm-cc/bluemonday"
+ html2 "html"
+ "math"
+ "regexp"
+ "strings"
+ "time"
+)
+
+const minMsgLen = 1
+const maxMsgLen = 10000
+
+const (
+ agePrefix = "-----BEGIN AGE ENCRYPTED FILE-----"
+ ageSuffix = "-----END AGE ENCRYPTED FILE-----"
+ pgpPrefix = "-----BEGIN PGP MESSAGE-----"
+ pgpSuffix = "-----END PGP MESSAGE-----"
+ pgpPKeyPrefix = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
+ pgpPKeySuffix = "-----END PGP PUBLIC KEY BLOCK-----"
+ pgpSignedPrefix = "-----BEGIN PGP SIGNED MESSAGE-----"
+ pgpSignedSuffix = "-----END PGP SIGNATURE-----"
+)
+
+var emojiReplacer = strings.NewReplacer(
+ ":):", `<span class="emoji" title=":):">☺</span>`,
+ ":smile:", `<span class="emoji" title=":smile:">☺</span>`,
+ ":happy:", `<span class="emoji" title=":happy:">😃</span>`,
+ ":see-no-evil:", `<span class="emoji" title=":see-no-evil:">🙈</span>`,
+ ":hear-no-evil:", `<span class="emoji" title=":hear-no-evil:">🙉</span>`,
+ ":speak-no-evil:", `<span class="emoji" title=":speak-no-evil:">🙊</span>`,
+ ":poop:", `<span class="emoji" title=":poop:">💩</span>`,
+ ":+1:", `<span class="emoji" title=":+1:">👍</span>`,
+ ":evil:", `<span class="emoji" title=":evil:">😈</span>`,
+ ":cat-happy:", `<span class="emoji" title=":cat-happy:">😸</span>`,
+ ":eyes:", `<span class="emoji" title=":eyes:">👀</span>`,
+ ":wave:", `<span class="emoji" title=":wave:">👋</span>`,
+ ":clap:", `<span class="emoji" title=":clap:">👏</span>`,
+ ":fire:", `<span class="emoji" title=":fire:">🔥</span>`,
+ ":sparkles:", `<span class="emoji" title=":sparkles:">✨</span>`,
+ ":sweat:", `<span class="emoji" title=":sweat:">💦</span>`,
+ ":heart:", `<span class="emoji" title=":heart:">❤</span>`,
+ ":broken-heart:", `<span class="emoji" title=":broken-heart:">💔</span>`,
+ ":zzz:", `<span class="emoji" title=":zzz:">💤</span>`,
+ ":praise:", `<span class="emoji" title=":praise:">🙌</span>`,
+ ":joy:", `<span class="emoji" title=":joy:">😂</span>`,
+ ":sob:", `<span class="emoji" title=":sob:">😭</span>`,
+ ":scream:", `<span class="emoji" title=":scream:">😱</span>`,
+ ":heart-eyes:", `<span class="emoji" title=":heart-eyes:">😍</span>`,
+ ":blush:", `<span class="emoji" title=":blush:">☺</span>`,
+ ":crazy:", `<span class="emoji" title=":crazy:">😜</span>`,
+ ":angry:", `<span class="emoji" title=":angry:">😡</span>`,
+ ":triumph:", `<span class="emoji" title=":triumph:">😤</span>`,
+ ":vomit:", `<span class="emoji" title=":vomit:">🤮</span>`,
+ ":skull:", `<span class="emoji" title=":skull:">💀</span>`,
+ ":alien:", `<span class="emoji" title=":alien:">👽</span>`,
+ ":sleeping:", `<span class="emoji" title=":sleeping:">😴</span>`,
+ ":tongue:", `<span class="emoji" title=":tongue:">😛</span>`,
+ ":cool:", `<span class="emoji" title=":cool:">😎</span>`,
+ ":wink:", `<span class="emoji" title=":wink:">😉</span>`,
+ ":thinking:", `<span class="emoji" title=":thinking:">🤔</span>`,
+ ":happy-sweat:", `<span class="emoji" title=":happy-sweat:">😅</span>`,
+ ":nerd:", `<span class="emoji" title=":nerd:">🤓</span>`,
+ ":fox:", `<span class="emoji" title=":fox:">🦊</span>`,
+ ":popcorn:", `<span class="emoji" title=":popcorn:">🍿</span>`,
+ ":shrug:", `¯\_(ツ)_/¯`,
+ ":flip:", `(╯°□°)╯︵ ┻━┻`,
+ ":flip-all:", `┻━┻︵ \(°□°)/ ︵ ┻━┻`,
+ ":fix-table:", `(ヘ・_・)ヘ┳━┳`,
+ ":disap:", `ಠ_ಠ`,
+)
+
+var usernameF = `\w{3,20}` // username (regex Fragment)
+var userOr0 = usernameF + `|0`
+var groupName = `\w{3,20}`
+var roomNameF = `\w{3,50}`
+var chatTs = `\d{2}:\d{2}:\d{2}`
+var optAtGUser = `@?(` + usernameF + `)` // Optional @, Grouped, Username
+var optAtGUserOr0 = `@?(` + userOr0 + `)` // Optional @, Grouped, Username or 0
+var onionV2Rgx = regexp.MustCompile(`[a-z2-7]{16}\.onion`)
+var onionV3Rgx = regexp.MustCompile(`[a-z2-7]{56}\.onion`)
+var deleteMsgRgx = regexp.MustCompile(`^/d (\d{2}:\d{2}:\d{2})(?:\s` + optAtGUserOr0 + `)?$`)
+var ignoreRgx = regexp.MustCompile(`^/(?:ignore|i) ` + optAtGUser)
+var pmToggleWhitelistUserRgx = regexp.MustCompile(`^/pmw ` + optAtGUser)
+var pmToggleBlacklistUserRgx = regexp.MustCompile(`^/pmb ` + optAtGUser)
+var whitelistUserRgx = regexp.MustCompile(`^/(?:whitelist|wl) ` + optAtGUser)
+var unIgnoreRgx = regexp.MustCompile(`^/(?:unignore|ui) ` + optAtGUser)
+var groupRgx = regexp.MustCompile(`^/g (` + groupName + `)\s(?s:(.*))`)
+var pmRgx = regexp.MustCompile(`^/pm ` + optAtGUserOr0 + `(?:\s(?s:(.*)))?`)
+var editRgx = regexp.MustCompile(`^/e (` + chatTs + `)\s(?s:(.*))`)
+var hbmtRgx = regexp.MustCompile(`^/hbmt (` + chatTs + `)$`)
+var chessRgx = regexp.MustCompile(`^/chess ` + optAtGUser + `(?:\s(w|b|r))?`)
+var inboxRgx = regexp.MustCompile(`^/inbox ` + optAtGUser + `(\s-e)?\s(?s:(.*))`)
+var purgeRgx = regexp.MustCompile(`^/purge(\s-hb)? ` + optAtGUserOr0)
+var renameRgx = regexp.MustCompile(`^/rename ` + optAtGUser + ` ` + optAtGUser)
+var profileRgx = regexp.MustCompile(`^/p ` + optAtGUserOr0)
+var kickRgx = regexp.MustCompile(`^/(?:kick|k) ` + optAtGUser)
+var setUrlRgx = regexp.MustCompile(`^/seturl (.+)`)
+var kickKeepRgx = regexp.MustCompile(`^/(?:kk) ` + optAtGUser)
+var kickSilentRgx = regexp.MustCompile(`^/(?:ks) ` + optAtGUser)
+var kickKeepSilentRgx = regexp.MustCompile(`^/(?:kks) ` + optAtGUser)
+var rtutoRgx = regexp.MustCompile(`^/(?:rtuto) ` + optAtGUser)
+var logoutRgx = regexp.MustCompile(`^/(?:logout) ` + optAtGUser)
+var forceCaptchaRgx = regexp.MustCompile(`^/(?:captcha) ` + optAtGUser)
+var unkickRgx = regexp.MustCompile(`^/(?:unkick|uk) ` + optAtGUser)
+var hellbanRgx = regexp.MustCompile(`^/(?:hellban|hb) ` + optAtGUser)
+var unhellbanRgx = regexp.MustCompile(`^/(?:unhellban|uhb) ` + optAtGUser)
+var randRgx = regexp.MustCompile(`^/rand (-?\d+) (-?\d+)$`)
+var tokenRgx = regexp.MustCompile(`^/token (\d{1,2})$`)
+var snippetRgx = regexp.MustCompile(`!\w{1,20}`)
+var tagRgx = regexp.MustCompile(`@(` + userOr0 + `)`)
+var autoTagRgx = regexp.MustCompile(`@(\w+)\*`)
+var roomTagRgx = regexp.MustCompile(`#(` + roomNameF + `)`)
+var tzRgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} at \d{1,2}\.\d{1,2}\.\d{1,2} (?i)[A|P]M)`) // Screen Shot 2022-02-04 at 11.58.58 PM
+var tz1Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} \d{1,2}-\d{1,2}-\d{1,2})`) // Screenshot from 2022-02-04 11-58-58.png
+var tz3Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} \d{1,6})`) // Screenshot 2023-05-20 202351.png
+var tz4Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2}_\d{1,2}_\d{1,2}_\d{1,2})`) // Screenshot_2023-05-20_11_13_14.png
+var addGroupRgx = regexp.MustCompile(`^/addgroup (` + groupName + `)$`)
+var rmGroupRgx = regexp.MustCompile(`^/rmgroup (` + groupName + `)$`)
+var lockGroupRgx = regexp.MustCompile(`^/glock (` + groupName + `)$`)
+var unlockGroupRgx = regexp.MustCompile(`^/gunlock (` + groupName + `)$`)
+var groupUsersRgx = regexp.MustCompile(`^/gusers (` + groupName + `)$`)
+var groupAddUserRgx = regexp.MustCompile(`^/gadduser (` + groupName + `) ` + optAtGUser + `$`)
+var groupRmUserRgx = regexp.MustCompile(`^/grmuser (` + groupName + `) ` + optAtGUser + `$`)
+var unsubscribeRgx = regexp.MustCompile(`^/unsubscribe (` + roomNameF + `)$`)
+var bsRgx = regexp.MustCompile(`^/pm ` + optAtGUser + ` /bs\s?([A-J]\d)?$`)
+var cRgx = regexp.MustCompile(`^/pm ` + optAtGUser + ` /c\s?(move)?$`)
+var hideRgx = regexp.MustCompile(`^/hide (?:“\[)?(\d{2}:\d{2}:\d{2})`)
+var unhideRgx = regexp.MustCompile(`^/unhide (\d{2}:\d{2}:\d{2})$`)
+var memeRgx = regexp.MustCompile(`^/meme ([a-zA-Z0-9_-]{3,50})$`)
+var memeRenameRgx = regexp.MustCompile(`^/meme ([a-zA-Z0-9_-]{3,50}) ([a-zA-Z0-9_-]{3,50})$`)
+var memeRemoveRgx = regexp.MustCompile(`^/memerm ([a-zA-Z0-9_-]{3,50})$`)
+var memesRgx = regexp.MustCompile(`^/memes$`)
+
+type MsgInterceptor struct{}
+
+func (i MsgInterceptor) InterceptMsg(cmd *Command) {
+ if cmd.room.ReadOnly {
+ if cmd.room.OwnerUserID != nil && *cmd.room.OwnerUserID != cmd.authUser.ID {
+ cmd.err = fmt.Errorf("room is read-only")
+ return
+ }
+ }
+
+ // Only check maximum length of message if we are uploading a file
+ // Trim whitespaces and ensure minimum length
+ minLen := utils.Ternary(cmd.upload != nil, 0, minMsgLen)
+ if !utils.ValidateRuneLength(strings.TrimSpace(cmd.message), minLen, maxMsgLen) {
+ cmd.dataMessage = cmd.origMessage
+ cmd.err = fmt.Errorf("%d - %d characters", minLen, maxMsgLen)
+ return
+ }
+
+ html, taggedUsersIDsMap, err := ProcessRawMessage(cmd.db, cmd.message, cmd.roomKey, cmd.authUser.ID, cmd.room.ID, cmd.upload, cmd.authUser.CanUseMultiline)
+ if err != nil {
+ cmd.dataMessage = cmd.origMessage
+ cmd.err = err
+ return
+ }
+
+ if len(strings.TrimSpace(html)) <= len("<p></p>") {
+ cmd.dataMessage = cmd.origMessage
+ cmd.err = errors.New("empty message")
+ return
+ }
+
+ toUserID := database.UserPtrID(cmd.toUser)
+
+ msgID, _ := cmd.db.CreateOrEditMessage(cmd.editMsg, html, cmd.origMessage, cmd.roomKey, cmd.room.ID, cmd.authUser.ID, toUserID, cmd.upload, cmd.groupID, cmd.hellbanMsg, cmd.modMsg, cmd.systemMsg)
+
+ if !cmd.skipInboxes {
+ sendInboxes(cmd.db, cmd.room, cmd.authUser, cmd.toUser, msgID, cmd.groupID, html, cmd.modMsg, taggedUsersIDsMap)
+ }
+
+ // Count public messages in #general room
+ if cmd.room.ID == config.GeneralRoomID && cmd.toUser == nil {
+ cmd.authUser.GeneralMessagesCount++
+ generalRoomKarma(cmd.db, cmd.authUser)
+ cmd.authUser.DoSave(cmd.db)
+ }
+
+ // Update chat read marker
+ cmd.db.UpdateChatReadMarker(cmd.authUser.ID, cmd.room.ID)
+
+ // Update user activity
+ isPM := cmd.toUser != nil
+ updateUserActivity(isPM, cmd.modMsg, cmd.room, cmd.authUser)
+}
+
+func generalRoomKarma(db *database.DkfDB, authUser *database.User) {
+ // Hellban users ain't getting karma
+ if authUser.IsHellbanned {
+ return
+ }
+ messagesCount := authUser.GeneralMessagesCount
+ if messagesCount%100 == 0 {
+ description := fmt.Sprintf("sent %d messages", messagesCount)
+ authUser.IncrKarma(db, 1, description)
+ } else if messagesCount == 20 {
+ authUser.IncrKarma(db, 1, "first 20 messages sent")
+ }
+}
+
+var msgPolicy = bluemonday.NewPolicy().
+ AllowElements("a", "p", "span", "strong", "del", "code", "pre", "em", "ul", "li", "br", "small", "i").
+ AllowAttrs("href", "rel", "target").OnElements("a").
+ AllowAttrs("tabindex", "style").OnElements("pre").
+ AllowAttrs("style", "class", "title").OnElements("span").
+ AllowAttrs("style").OnElements("small")
+
+// ProcessRawMessage return the new html, and a map of tagged users used for notifications
+// This function takes an "unsafe" user input "in", and return html which will be safe to render.
+func ProcessRawMessage(db *database.DkfDB, in, roomKey string, authUserID database.UserID, roomID database.RoomID,
+ upload *database.Upload, canUseMultiline bool) (string, map[database.UserID]database.User, error) {
+ html, quoted := convertQuote(db, in, roomKey, roomID) // Get raw quote text which is not safe to render
+ html = convertNewLines(html, canUseMultiline)
+ html = html2.EscapeString(html) // Makes user input safe to render
+ // All html generated from this point on shall be safe to render.
+ html = convertPGPClearsignToFile(db, html, authUserID)
+ html = convertPGPMessageToFile(db, html, authUserID)
+ html = convertPGPPublicKeyToFile(db, html, authUserID)
+ html = convertAgeMessageToFile(db, html, authUserID)
+ html = convertLinksWithoutScheme(html)
+ html = convertMarkdown(html)
+ html = convertBangShortcuts(html)
+ html = convertArchiveLinks(db, html, roomID, authUserID)
+ html = convertLinks(html, roomID, db.GetUserByUsername, db.GetLinkByShorthand, db.GetChatMessageByUUID)
+ html = linkDefaultRooms(html)
+ html, taggedUsersIDsMap := colorifyTaggedUsers(html, db.GetUsersByUsername)
+ html = linkRoomTags(db, html)
+ html = emojiReplacer.Replace(html)
+ html = styleQuote(html, quoted)
+ html = appendUploadLink(html, upload)
+ if quoted != nil { // Add quoted message owner for inboxes
+ taggedUsersIDsMap[quoted.UserID] = quoted.User
+ }
+ html = msgPolicy.Sanitize(html)
+ return html, taggedUsersIDsMap, nil
+}
+
+func sendInboxes(db *database.DkfDB, room database.ChatRoom, authUser, toUser *database.User, msgID int64, groupID *database.GroupID, html string, modMsg bool,
+ taggedUsersIDsMap map[database.UserID]database.User) {
+ // Only have chat inbox for unencrypted messages
+ if room.IsProtected() {
+ return
+ }
+ // If user is hellbanned, do not send inboxes
+ if authUser.IsHellbanned {
+ return
+ }
+ // Early return if we don't need to send inboxes
+ if toUser == nil && len(taggedUsersIDsMap) == 0 {
+ return
+ }
+
+ blacklistedBy, _ := db.GetPmBlacklistedByUsers(authUser.ID)
+ blacklistedByMap := make(map[database.UserID]struct{})
+ for _, b := range blacklistedBy {
+ blacklistedByMap[b.UserID] = struct{}{}
+ }
+
+ ignoredBy, _ := db.GetIgnoredByUsers(authUser.ID)
+ ignoredByMap := make(map[database.UserID]struct{})
+ for _, b := range ignoredBy {
+ ignoredByMap[b.UserID] = struct{}{}
+ }
+
+ sendInbox := func(user database.User, isPM, modCh bool) {
+ if !managers.ActiveUsers.IsUserActiveInRoom(user.ID, room) || user.AFK {
+ // Do not send notification if receiver is blacklisting you
+ if _, ok := blacklistedByMap[user.ID]; ok {
+ return
+ }
+ // Do not send notification if receiver is ignoring you
+ if _, ok := ignoredByMap[user.ID]; ok {
+ return
+ }
+ db.CreateInboxMessage(html, room.ID, authUser.ID, user.ID, isPM, modCh, &msgID)
+ }
+ }
+
+ // If the message is a PM, only notify the receiver, not the tagged people in it.
+ if toUser != nil {
+ sendInbox(*toUser, true, false)
+ } else if room.Name == "moderators" { // Only tags other moderators on "moderators" room
+ for _, user := range taggedUsersIDsMap {
+ if user.IsModerator() {
+ sendInbox(user, false, false)
+ }
+ }
+ } else if modMsg { // Only tags other moderators on /m messages
+ for _, user := range taggedUsersIDsMap {
+ if user.IsModerator() {
+ sendInbox(user, false, true)
+ }
+ }
+ } else if groupID != nil { // Only tags other people in the group
+ for _, user := range taggedUsersIDsMap {
+ if db.IsUserInGroupByID(user.ID, *groupID) {
+ sendInbox(user, false, false)
+ }
+ }
+ } else { // Otherwise, notify tagged people
+ for _, user := range taggedUsersIDsMap {
+ sendInbox(user, false, false)
+ }
+ }
+}
+
+func updateUserActivity(isPM, modMsg bool, room database.ChatRoom, authUser *database.User) {
+ // We do not update user presence when they send private messages or moderators group message
+ if isPM || modMsg {
+ return
+ }
+ managers.ActiveUsers.UpdateUserInRoom(room, managers.NewUserInfoUpdateActivity(authUser))
+}
+
+func appendUploadLink(html string, upload *database.Upload) string {
+ if upload != nil {
+ if html != "" {
+ html += " "
+ }
+ html += `[` + upload.GetHTMLLink() + `]`
+ }
+ return html
+}
+
+func checkCPLinks(db *database.DkfDB, html string) bool {
+ m1 := onionV3Rgx.FindAllStringSubmatch(html, -1)
+ m2 := onionV2Rgx.FindAllStringSubmatch(html, -1)
+ for _, m := range append(m1, m2...) {
+ hash := utils.MD5([]byte(m[0]))
+ if _, err := db.GetOnionBlacklist(hash); err == nil {
+ return true
+ }
+ }
+ return false
+}
+
+type getUsersByUsernameFn func(usernames []string) ([]database.User, error)
+
+// Update the given html to add user style for tags.
+// Return the new html, and a map[userID]User of tagged users.
+func colorifyTaggedUsers(html string, getUsersByUsername getUsersByUsernameFn) (string, map[database.UserID]database.User) {
+ usernameMatches := tagRgx.FindAllStringSubmatch(html, -1)
+ usernames := hashset.New[string]()
+ for _, usernameMatch := range usernameMatches {
+ usernames.Insert(usernameMatch[1])
+ }
+ taggedUsers, _ := getUsersByUsername(usernames.ToArray())
+
+ taggedUsersMap := make(map[string]database.User)
+ taggedUsersIDsMap := make(map[database.UserID]database.User)
+ for _, taggedUser := range taggedUsers {
+ taggedUsersMap[strings.ToLower(taggedUser.Username.AtStr())] = taggedUser
+ if taggedUser.Username != config.NullUsername {
+ taggedUsersIDsMap[taggedUser.ID] = taggedUser
+ }
+ }
+
+ if len(usernameMatches) > 0 {
+ html = tagRgx.ReplaceAllStringFunc(html, func(s string) string {
+ lowerS := strings.ToLower(s)
+ if user, ok := taggedUsersMap[lowerS]; ok {
+ return fmt.Sprintf("<span %s>@%s</span>", user.GenerateChatStyle1(), user.Username)
+ }
+
+ // Not found, try to fix typos using levenshtein
+ activeUsers := managers.ActiveUsers.GetActiveUsers()
+ if len(activeUsers) > 0 {
+ minDist := math.MaxInt
+ minAu := activeUsers[0]
+ for _, au := range activeUsers {
+ lowerAu := strings.ToLower(string(au.Username))
+ d := levenshtein.ComputeDistance(lowerS, lowerAu)
+ if d < minDist {
+ minDist = d
+ minAu = au
+ }
+ }
+ if minDist <= 3 {
+ if users, _ := getUsersByUsername([]string{minAu.Username.String()}); len(users) > 0 {
+ user := users[0]
+ return fmt.Sprintf("<span %s>@%s</span>", user.GenerateChatStyle1(), user.Username)
+ }
+ }
+ }
+
+ return s
+ })
+ }
+ return html, taggedUsersIDsMap
+}
+
+func linkRoomTags(db *database.DkfDB, html string) string {
+ if roomTagRgx.MatchString(html) {
+ html = roomTagRgx.ReplaceAllStringFunc(html, func(s string) string {
+ if room, err := db.GetChatRoomByName(strings.TrimPrefix(s, "#")); err == nil {
+ return `<a href="/chat/` + room.Name + `" target="_top">` + s + `</a>`
+ }
+ return s
+ })
+ }
+ return html
+}
+
+func linkDefaultRooms(html string) string {
+ r := strings.NewReplacer(
+ "#general", `<a href="/chat/general" target="_top">#general</a>`,
+ "#programming", `<a href="/chat/programming" target="_top">#programming</a>`,
+ "#hacking", `<a href="/chat/hacking" target="_top">#hacking</a>`,
+ "#suggestions", `<a href="/chat/suggestions" target="_top">#suggestions</a>`,
+ "#announcements", `<a href="/chat/announcements" target="_top">#announcements</a>`,
+ )
+ return r.Replace(html)
+}
+
+// Convert timestamps such as 01:23:45 to an archive link if a message with that timestamp exists.
+// eg: "Some text 14:31:46 some more text"
+func convertArchiveLinks(db *database.DkfDB, html string, roomID database.RoomID, authUserID database.UserID) string {
+ start, rest := "", html
+
+ // Do not replace timestamps that are inside a quote text
+ const quoteSuffix = `”`
+ endOfQuoteIdx := strings.LastIndex(html, quoteSuffix)
+ if endOfQuoteIdx != -1 {
+ start, rest = html[:endOfQuoteIdx], html[endOfQuoteIdx:]
+ }
+
+ archiveRgx := regexp.MustCompile(`(\d{2}-\d{2} )?\d{2}:\d{2}:\d{2}`)
+ if archiveRgx.MatchString(rest) {
+ rest = archiveRgx.ReplaceAllStringFunc(rest, func(s string) string {
+ var dt time.Time
+ var err error
+ if len(s) == 8 { // HH:MM:SS
+ dt, err = utils.ParsePrevDatetimeAt(s, clockwork.NewRealClock())
+ } else if len(s) == 14 { // mm-dd HH:MM:SS
+ dt, err = utils.ParsePrevDatetimeAt2(s, clockwork.NewRealClock())
+ }
+ if err != nil {
+ return s
+ }
+ if msgs, err := db.GetRoomChatMessagesByDate(roomID, dt.UTC()); err == nil && len(msgs) > 0 {
+ msg := msgs[0]
+ if len(msgs) > 1 {
+ for _, msgTmp := range msgs {
+ if msgTmp.User.ID == authUserID || (msgTmp.ToUserID != nil && *msgTmp.ToUserID == authUserID) {
+ msg = msgTmp
+ break
+ }
+ }
+ }
+ return fmt.Sprintf(`<a href="/chat/%s/archive#%s" target="_blank" rel="noopener noreferrer">%s</a>`, msg.Room.Name, msg.UUID, s)
+ }
+ return s
+ })
+ }
+ return start + rest
+}
+
+func convertBangShortcuts(html string) string {
+ r := strings.NewReplacer(
+ "!bhc", config.BhcOnion,
+ "!cryptbb", config.CryptbbOnion,
+ "!dread", config.DreadOnion,
+ "!dkf", config.DkfOnion,
+ "!rroom", config.DkfOnion+`/red-room`,
+ "!dnmx", config.DnmxOnion,
+ "!whonix", config.WhonixOnion,
+ "!age", config.AgeUrl,
+ "!chattor", config.ChattorOnion,
+ "!lulbins", config.LulbinsOnion,
+ )
+ return r.Replace(html)
+}
+
+func convertMarkdown(in string) string {
+ out := strings.Replace(in, "\r", "", -1)
+ resBytes := bf.Run([]byte(out), bf.WithRenderer(utils.MyRenderer(false, false)), bf.WithExtensions(
+ bf.NoIntraEmphasis|bf.Tables|bf.FencedCode|
+ bf.Strikethrough|bf.SpaceHeadings|
+ bf.DefinitionLists|bf.HardLineBreak|bf.NoLink))
+ out = string(resBytes)
+ return out
+}
+
+// This function will get the raw user input message which is not safe to directly render.
+//
+// To prevent people from altering the text of the quote,
+// we retrieve the original quoted message using the timestamp and username,
+// and we use the original message text.
+//
+// eg: we received altered quote, and return original quote ->
+// “[01:23:45] username - Some maliciously altered quote” Some text
+// “[01:23:45] username - The original text” Some text
+func convertQuote(db *database.DkfDB, origHtml string, roomKey string, roomID database.RoomID) (html string, quoted *database.ChatMessage) {
+ const quotePrefix = `“[`
+ const quoteSuffix = `”`
+ html = origHtml
+ idx := strings.Index(origHtml, quoteSuffix)
+ if strings.HasPrefix(origHtml, quotePrefix) && idx > -1 {
+ prefixLen := len(quotePrefix)
+ suffixLen := len(quoteSuffix)
+ if len(origHtml) > prefixLen+9 {
+ hourMinSec := origHtml[prefixLen : prefixLen+8]
+ username := database.Username(origHtml[prefixLen+10 : strings.Index(origHtml[prefixLen+10:], " ")+prefixLen+10])
+ if quoted = getQuotedChatMessage(db, hourMinSec, username, roomID); quoted != nil {
+ html = GetQuoteTxt(db, roomKey, *quoted)
+ html += origHtml[idx+suffixLen:]
+ }
+ }
+ }
+ return html, quoted
+}
+
+func styleQuote(origHtml string, quoted *database.ChatMessage) (html string) {
+ const quoteSuffix = `”`
+ html = origHtml
+ if quoted != nil {
+ idx := strings.Index(origHtml, quoteSuffix)
+ prefixLen := len(`<p>“[`)
+ suffixLen := len(quoteSuffix)
+ dateLen := 8 // 01:23:45 --> 8
+
+ // <p>“[01:23:45] username - quoted text” user text</p>
+ date := origHtml[prefixLen : prefixLen+dateLen] // `01:23:45`
+ quoteTxt := origHtml[prefixLen+dateLen+1 : idx] // ` username - quoted text`
+ userTxt := origHtml[idx+suffixLen:] // ` user text</p>`
+
+ sb := strings.Builder{}
+ sb.WriteString(`<p>“<small style="opacity: 0.8;"><i>[`)
+
+ // Date link
+ sb.WriteString(`<a href="/chat/`)
+ sb.WriteString(quoted.Room.Name)
+ sb.WriteString(`/archive#`)
+ sb.WriteString(quoted.UUID)
+ sb.WriteString(`" target="_blank" rel="noopener noreferrer">`)
+ sb.WriteString(date)
+ sb.WriteString(`</a>`)
+
+ sb.WriteString(`]<span `)
+ sb.WriteString(quoted.User.GenerateChatStyle1())
+ sb.WriteString(`>`)
+ sb.WriteString(quoteTxt)
+ sb.WriteString(`</span></i></small>”`)
+ sb.WriteString(userTxt)
+ html = sb.String()
+ }
+ return html
+}
+
+// Given a roomID and hourMinSec (01:23:45) and a username, retrieve the message from database that fits the predicates.
+func getQuotedChatMessage(db *database.DkfDB, hourMinSec string, username database.Username, roomID database.RoomID) (quoted *database.ChatMessage) {
+ if dt, err := utils.ParsePrevDatetimeAt(hourMinSec, clockwork.NewRealClock()); err == nil {
+ if msgs, err := db.GetRoomChatMessagesByDate(roomID, dt.UTC()); err == nil && len(msgs) > 0 {
+ msg := msgs[0]
+ if len(msgs) > 1 {
+ for _, msgTmp := range msgs {
+ if msgTmp.User.Username == username {
+ msg = msgTmp
+ break
+ }
+ }
+ }
+ quoted = &msg
+ }
+ }
+ return
+}
+
+// GetQuoteTxt given a chat message, return the text to be used as a quote.
+func GetQuoteTxt(db *database.DkfDB, roomKey string, quoted database.ChatMessage) (out string) {
+ var err error
+ decrypted, err := quoted.GetRawMessage(roomKey)
+ if err != nil {
+ return
+ }
+ if quoted.ToUserID != nil {
+ if m := pmRgx.FindStringSubmatch(decrypted); len(m) == 3 {
+ decrypted = m[2]
+ }
+ } else if quoted.Moderators {
+ decrypted = strings.TrimPrefix(decrypted, "/m ")
+ } else if quoted.IsHellbanned {
+ decrypted = strings.TrimPrefix(decrypted, "/hbm ")
+ }
+ isMe := false
+ if strings.HasPrefix(decrypted, "/me") {
+ isMe = true
+ decrypted = strings.TrimPrefix(decrypted, "/me ")
+ }
+
+ startIdx := 0
+ if strings.HasPrefix(decrypted, `“[`) {
+ startIdx = strings.Index(decrypted, `” `)
+ if startIdx == -1 {
+ startIdx = 0
+ } else {
+ startIdx += len(`” `)
+ }
+ }
+
+ decrypted = replTextPrefixSuffix(decrypted, agePrefix, ageSuffix, "[age.txt]")
+ decrypted = replTextPrefixSuffix(decrypted, pgpPrefix, pgpSuffix, "[pgp.txt]")
+ decrypted = replTextPrefixSuffix(decrypted, pgpPKeyPrefix, pgpPKeySuffix, "[pgp_pkey.txt]")
+
+ remaining := " "
+ if !quoted.System {
+ remaining += fmt.Sprintf(`%s `, quoted.User.Username)
+ }
+ if quoted.UploadID != nil {
+ if upload, err := db.GetUploadByID(*quoted.UploadID); err == nil {
+ if decrypted != "" {
+ decrypted += " "
+ }
+ decrypted += `[` + upload.OrigFileName + `]`
+ }
+ }
+ if !isMe {
+ remaining += "- "
+ }
+
+ toBeQuoted := decrypted[startIdx:]
+ toBeQuoted = strings.ReplaceAll(toBeQuoted, "\n", ` `)
+ toBeQuoted = strings.ReplaceAll(toBeQuoted, `“`, `"`)
+ toBeQuoted = strings.ReplaceAll(toBeQuoted, `”`, `"`)
+
+ remaining += utils.TruncStr2(toBeQuoted, 70, "…")
+ return `“[` + quoted.CreatedAt.Format("15:04:05") + "]" + remaining + `”`
+}
+
+func replTextPrefixSuffix(msg, prefix, suffix, repl string) (out string) {
+ out = msg
+ pgpPIdx := strings.Index(msg, prefix)
+ pgpSIdx := strings.Index(msg, suffix)
+ if pgpPIdx != -1 && pgpSIdx != -1 {
+ newMsg := msg[:pgpPIdx]
+ newMsg += repl
+ newMsg += msg[pgpSIdx+len(suffix):]
+ out = newMsg
+ }
+ return
+}
+
+var noSchemeOnionLinkRgx = regexp.MustCompile(`\s[a-z2-7]{56}\.onion`)
+
+// Fix up onion links that are missing the http scheme. This often happen when copy/pasting a link.
+func convertLinksWithoutScheme(in string) string {
+ html := noSchemeOnionLinkRgx.ReplaceAllStringFunc(in, func(s string) string {
+ return " http://" + strings.TrimSpace(s)
+ })
+ return html
+}
+
+var linkRgxStr = `(http|ftp|https):\/\/([\w\-_]+(?:(?:\.[\w\-_]+)+))([\w\-\.,@?^=%&:/~\+#\(\)]*[\w\-\@?^=%&/~\+#\(\)])?`
+var profileRgxStr = `/u/\w{3,20}`
+var linkShorthandRgxStr = `/l/\w{3,20}`
+var dkfArchiveRgx = regexp.MustCompile(`/chat/([\w_]{3,50})/archive\?uuid=([\w-]{36})#[\w-]{36}`)
+var linkOrProfileRgx = regexp.MustCompile(`(` + linkRgxStr + `|` + profileRgxStr + `|` + linkShorthandRgxStr + `)`)
+var userProfileLinkRgx = regexp.MustCompile(`^` + profileRgxStr + `$`)
+var linkShorthandPageLinkRgx = regexp.MustCompile(`^` + linkShorthandRgxStr + `$`)
+var youtubeComIDRgx = regexp.MustCompile(`watch\?v=([\w-]+)`)
+var youtubeComShortsIDRgx = regexp.MustCompile(`/shorts/([\w-]+)`)
+var youtuBeIDRgx = regexp.MustCompile(`https://youtu\.be/([\w-]+)`)
+var yewtubeBeIDRgx = youtubeComIDRgx
+var invidiousIDRgx = youtubeComIDRgx
+
+func makeHtmlLink(label, link string) string {
+ // We replace @ to prevent colorifyTaggedUsers from trying to generate html inside the links.
+ r := strings.NewReplacer("@", "@", "#", "#")
+ label = r.Replace(label)
+ link = r.Replace(link)
+ return fmt.Sprintf(`<a href="%s" rel="noopener noreferrer" target="_blank">%s</a>`, link, label)
+}
+
+func splitQuote(in string) (string, string) {
+ const quotePrefix = `<p>“[`
+ const quoteSuffix = `”`
+ idx := strings.Index(in, quoteSuffix)
+ if idx == -1 || !strings.HasPrefix(in, quotePrefix) {
+ return "", in
+ }
+ return in[:idx], in[idx:]
+}
+
+func convertLinks(in string,
+ roomID database.RoomID,
+ getUserByUsername func(database.Username) (database.User, error),
+ getLinkByShorthand func(string) (database.Link, error),
+ getChatMessageByUUID func(string) (database.ChatMessage, error)) string {
+ quote, rest := splitQuote(in)
+
+ libredditURLs := []string{
+ "http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion",
+ "http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion",
+ "http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion",
+ "http://inytumdgnri7xsqtvpntjevaelxtgbjqkuqhtf6txxhwbll2fwqtakqd.onion",
+ "http://liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion",
+ "http://kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion",
+ "http://ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion",
+ "http://ledditqo2mxfvlgobxnlhrkq4dh34jss6evfkdkb2thlvy6dn4f4gpyd.onion",
+ "http://ol5begilptoou34emq2sshf3may3hlblvipdjtybbovpb7c7zodxmtqd.onion",
+ "http://lbrdtjaj7567ptdd4rv74lv27qhxfkraabnyphgcvptl64ijx2tijwid.onion",
+ }
+
+ invidiousURLs := []string{
+ "http://c7hqkpkpemu6e7emz5b4vyz7idjgdvgaaa3dyimmeojqbgpea3xqjoid.onion",
+ "http://kbjggqkzv65ivcqj6bumvp337z6264huv5kpkwuv6gu5yjiskvan7fad.onion",
+ "http://grwp24hodrefzvjjuccrkw3mjq4tzhaaq32amf33dzpmuxe7ilepcmad.onion"}
+
+ wikilessURLs := []string{
+ "http://c2pesewpalbi6lbfc5hf53q4g3ovnxe4s7tfa6k2aqkf7jd7a7dlz5ad.onion",
+ "http://dj2tbh2nqfxyfmvq33cjmhuw7nb6am7thzd3zsjvizeqf374fixbrxyd.onion"}
+
+ nitterURLs := []string{
+ "http://nitraeju2mipeziu2wtcrqsxg7h62v5y4eqgwi75uprynkj74gevvuqd.onion"}
+
+ rimgoURLs := []string{
+ "http://be7udfhmnzqyt7cxysg6c4pbawarvaofjjywp35nhd5qamewdfxl6sid.onion"}
+
+ knownOnions := [][]string{
+ {"http://dkfgit.onion", config.DkfGitOnion},
+ {"http://dread.onion", config.DreadOnion},
+ {"http://cryptbb.onion", config.CryptbbOnion},
+ {"http://blkhat.onion", config.BhcOnion},
+ {"http://dnmx.onion", config.DnmxOnion},
+ {"http://whonix.onion", config.WhonixOnion},
+ {"http://chattor.onion", config.ChattorOnion},
+ {"http://lulbins.onion", config.LulbinsOnion},
+ }
+
+ newRest := linkOrProfileRgx.ReplaceAllStringFunc(rest, func(link string) string {
+ // Convert all occurrences of "/u/username" to a link to user profile page if the user exists
+ if userProfileLinkRgx.MatchString(link) {
+ user, err := getUserByUsername(database.Username(strings.TrimPrefix(link, "/u/")))
+ if err != nil {
+ return link
+ }
+ href := "/u/" + string(user.Username)
+ return makeHtmlLink(href, href)
+ }
+
+ // Convert all occurrences of "/l/shorthand" to a link to link page if the shorthand exists
+ if linkShorthandPageLinkRgx.MatchString(link) {
+ l, err := getLinkByShorthand(strings.TrimPrefix(link, "/l/"))
+ if err != nil {
+ return link
+ }
+ href := "/l/" + *l.Shorthand
+ return makeHtmlLink(href, href)
+ }
+
+ // Handle reddit links
+ if strings.HasPrefix(link, "https://www.reddit.com/") {
+ old := strings.Replace(link, "https://www.reddit.com/", "https://old.reddit.com/", 1)
+ libredditLink := utils.RandChoice(libredditURLs)
+ libredditLink = strings.Replace(link, "https://www.reddit.com", libredditLink, 1)
+ oldHtmlLink := makeHtmlLink("old", old)
+ libredditHtmlLink := makeHtmlLink("libredditLink", libredditLink)
+ htmlLink := makeHtmlLink(link, link)
+ return htmlLink + ` (` + oldHtmlLink + ` | ` + libredditHtmlLink + `)`
+ } else if strings.HasPrefix(link, "https://old.reddit.com/") {
+ libredditLink := utils.RandChoice(libredditURLs)
+ libredditLink = strings.Replace(link, "https://old.reddit.com", libredditLink, 1)
+ libredditHtmlLink := makeHtmlLink("libredditLink", libredditLink)
+ htmlLink := makeHtmlLink(link, link)
+ return htmlLink + ` (` + libredditHtmlLink + `)`
+ }
+ for _, libredditURL := range libredditURLs {
+ if strings.HasPrefix(link, libredditURL) {
+ newPrefix := strings.Replace(link, libredditURL, "http://reddit.onion", 1)
+ old := strings.Replace(link, libredditURL, "https://old.reddit.com", 1)
+ oldHtmlLink := makeHtmlLink("old", old)
+ htmlLink := makeHtmlLink(newPrefix, link)
+ return htmlLink + ` (` + oldHtmlLink + `)`
+ }
+ }
+
+ // Append YouTube link to invidious link
+ for _, invidiousURL := range invidiousURLs {
+ if strings.HasPrefix(link, invidiousURL) {
+ if strings.Contains(link, ".onion/watch?v=") {
+ newPrefix := strings.Replace(link, invidiousURL, "http://invidious.onion", 1)
+ m := invidiousIDRgx.FindStringSubmatch(link)
+ if len(m) == 2 {
+ videoID := m[1]
+ youtubeLink := "https://www.youtube.com/watch?v=" + videoID
+ youtubeHtmlLink := makeHtmlLink("Youtube", youtubeLink)
+ htmlLink := makeHtmlLink(newPrefix, link)
+ return htmlLink + ` (` + youtubeHtmlLink + `)`
+ }
+ }
+ }
+ }
+ // Unknown invidious links
+ if strings.Contains(link, ".onion/watch?v=") {
+ m := invidiousIDRgx.FindStringSubmatch(link)
+ if len(m) == 2 {
+ videoID := m[1]
+ youtubeLink := "https://www.youtube.com/watch?v=" + videoID
+ youtubeHtmlLink := makeHtmlLink("Youtube", youtubeLink)
+ htmlLink := makeHtmlLink(link, link)
+ return htmlLink + ` (` + youtubeHtmlLink + `)`
+ }
+ }
+
+ // Append wikiless link to wikipedia link
+ if strings.HasPrefix(link, "https://en.wikipedia.org/") {
+ wikilessLink := utils.RandChoice(wikilessURLs)
+ wikilessLink = strings.Replace(link, "https://en.wikipedia.org", wikilessLink, 1)
+ wikilessHtmlLink := makeHtmlLink("Wikiless", wikilessLink)
+ htmlLink := makeHtmlLink(link, link)
+ return htmlLink + ` (` + wikilessHtmlLink + `)`
+ }
+ for _, wikilessURL := range wikilessURLs {
+ if strings.HasPrefix(link, wikilessURL) {
+ newPrefix := strings.Replace(link, wikilessURL, "http://wikiless.onion", 1)
+ wikipediaPrefix := strings.Replace(link, wikilessURL, "https://en.wikipedia.org", 1)
+ wikipediaHtmlLink := makeHtmlLink("Wikipedia", wikipediaPrefix)
+ htmlLink := makeHtmlLink(newPrefix, link)
+ return htmlLink + ` (` + wikipediaHtmlLink + `)`
+ }
+ }
+
+ // Append nitter link to twitter link
+ if strings.HasPrefix(link, "https://twitter.com/") {
+ nitterLink := utils.RandChoice(nitterURLs)
+ nitterLink = strings.Replace(link, "https://twitter.com", nitterLink, 1)
+ nitterHtmlLink := makeHtmlLink("Nitter", nitterLink)
+ htmlLink := makeHtmlLink(link, link)
+ return htmlLink + ` (` + nitterHtmlLink + `)`
+ }
+ for _, nitterURL := range nitterURLs {
+ if strings.HasPrefix(link, nitterURL) {
+ newPrefix := strings.Replace(link, nitterURL, "http://nitter.onion", 1)
+ twitterPrefix := strings.Replace(link, nitterURL, "https://twitter.com", 1)
+ twitterHtmlLink := makeHtmlLink("Twitter", twitterPrefix)
+ htmlLink := makeHtmlLink(newPrefix, link)
+ return htmlLink + ` (` + twitterHtmlLink + `)`
+ }
+ }
+
+ // Append rimgo link to imgur link
+ if strings.HasPrefix(link, "https://imgur.com/") {
+ rimgoLink := utils.RandChoice(rimgoURLs)
+ rimgoLink = strings.Replace(link, "https://imgur.com", rimgoLink, 1)
+ rimgoHtmlLink := makeHtmlLink("Rimgo", rimgoLink)
+ htmlLink := makeHtmlLink(link, link)
+ return htmlLink + ` (` + rimgoHtmlLink + `)`
+ }
+ for _, rimgoURL := range rimgoURLs {
+ if strings.HasPrefix(link, rimgoURL) {
+ newPrefix := strings.Replace(link, rimgoURL, "http://rimgo.onion", 1)
+ imgurPrefix := strings.Replace(link, rimgoURL, "https://imgur.com", 1)
+ imgurHtmlLink := makeHtmlLink("Imgur", imgurPrefix)
+ htmlLink := makeHtmlLink(newPrefix, link)
+ return htmlLink + ` (` + imgurHtmlLink + `)`
+ }
+ }
+
+ // Append invidious link to YouTube/yewtube link
+ var videoID string
+ var m []string
+ var isShortUrl, isYewtube bool
+ if strings.HasPrefix(link, "https://youtu.be/") {
+ m = youtuBeIDRgx.FindStringSubmatch(link)
+ } else if strings.HasPrefix(link, "https://www.youtube.com/watch?v=") {
+ m = youtubeComIDRgx.FindStringSubmatch(link)
+ } else if strings.HasPrefix(link, "https://yewtu.be/") || strings.HasPrefix(link, "https://www.yewtu.be/") {
+ m = yewtubeBeIDRgx.FindStringSubmatch(link)
+ isYewtube = true
+ } else if strings.HasPrefix(link, "https://www.youtube.com/shorts/") {
+ m = youtubeComShortsIDRgx.FindStringSubmatch(link)
+ isShortUrl = true
+ }
+ if len(m) == 2 {
+ videoID = m[1]
+ }
+ if videoID != "" {
+ invidiousLink := utils.RandChoice(invidiousURLs) + "/watch?v=" + videoID + "&local=true"
+ invidiousHtmlLink := makeHtmlLink("Invidious", invidiousLink)
+ htmlLink := makeHtmlLink(link, link)
+ youtubeLink := "https://www.youtube.com/watch?v=" + videoID
+ youtubeHtmlLink := makeHtmlLink("YT", youtubeLink)
+ out := htmlLink + ` (` + invidiousHtmlLink + `)`
+ if isShortUrl || isYewtube {
+ out = htmlLink + ` (` + youtubeHtmlLink + ` | ` + invidiousHtmlLink + `)`
+ }
+ return out
+ }
+
+ // Special case for dkf links.
+ {
+ dkfLocalPrefix := "http://127.0.0.1:8080"
+ dkfShortPrefix := "http://dkf.onion"
+ dkfLongPrefix := config.DkfOnion
+ hasLocalPrefix := strings.HasPrefix(link, dkfLocalPrefix)
+ hasDkfShortPrefix := strings.HasPrefix(link, dkfShortPrefix)
+ hasDkfLongPrefix := strings.HasPrefix(link, dkfLongPrefix)
+ if hasLocalPrefix || hasDkfLongPrefix || hasDkfShortPrefix {
+ var trimmed string
+ if hasLocalPrefix {
+ trimmed = strings.TrimPrefix(link, dkfLocalPrefix)
+ } else if hasDkfLongPrefix {
+ trimmed = strings.TrimPrefix(link, dkfLongPrefix)
+ } else if hasDkfShortPrefix {
+ trimmed = strings.TrimPrefix(link, dkfShortPrefix)
+ }
+ label := dkfShortPrefix + trimmed
+ href := trimmed
+ // Shorten archive links
+ if m := dkfArchiveRgx.FindStringSubmatch(label); len(m) == 3 {
+ if msg, err := getChatMessageByUUID(m[2]); err == nil {
+ if roomID == msg.RoomID {
+ label = msg.CreatedAt.Format("[Jan 02 03:04:05]")
+ } else {
+ label = msg.CreatedAt.Format("[#" + m[1] + " Jan 02 03:04:05]")
+ }
+ }
+ }
+ // Allows to have messages such as: "my profile is /u/username :)"
+ if userProfileLinkRgx.MatchString(trimmed) {
+ if user, err := getUserByUsername(database.Username(strings.TrimPrefix(trimmed, "/u/"))); err == nil {
+ label = "/u/" + string(user.Username)
+ href = "/u/" + string(user.Username)
+ }
+ } else if linkShorthandPageLinkRgx.MatchString(trimmed) {
+ // Convert all occurrences of "/l/shorthand" to a link to link page if the shorthand exists
+ if l, err := getLinkByShorthand(strings.TrimPrefix(trimmed, "/l/")); err == nil {
+ label = "/l/" + *l.Shorthand
+ href = "/l/" + *l.Shorthand
+ }
+ }
+ return makeHtmlLink(label, href)
+ }
+ }
+
+ for _, el := range knownOnions {
+ shortPrefix := el[0]
+ longPrefix := el[1]
+ if strings.HasPrefix(link, longPrefix) {
+ return makeHtmlLink(shortPrefix+strings.TrimPrefix(link, longPrefix), link)
+ } else if strings.HasPrefix(link, shortPrefix) {
+ return makeHtmlLink(link, longPrefix+strings.TrimPrefix(link, shortPrefix))
+ }
+ }
+ return makeHtmlLink(link, link)
+ })
+
+ return quote + newRest
+}
+
+func convertNewLines(html string, canUseMultiline bool) string {
+ if !canUseMultiline {
+ html = strings.ReplaceAll(html, "\n", "")
+ }
+ return html
+}
+
+func extractPGPMessage(html string) (out string) {
+ pgpPrefixL := pgpPrefix
+ pgpSuffixL := pgpSuffix
+ startIdx := strings.Index(html, pgpPrefixL)
+ endIdx := strings.Index(html, pgpSuffixL)
+ if startIdx != -1 && endIdx != -1 {
+ out = html[startIdx : endIdx+len(pgpSuffixL)]
+ out = strings.TrimSpace(out)
+ out = strings.TrimPrefix(out, pgpPrefixL)
+ out = strings.TrimSuffix(out, pgpSuffixL)
+ out = strings.Join(strings.Split(out, " "), "\n")
+ out = pgpPrefixL + out
+ out += pgpSuffixL
+ }
+ return out
+}
+
+// Auto convert pasted pgp public key into uploaded file
+func convertPGPPublicKeyToFile(db *database.DkfDB, html string, authUserID database.UserID) string {
+ pgpPKeyPrefixL := pgpPKeyPrefix
+ pgpPKeySuffixL := pgpPKeySuffix
+ startIdx := strings.Index(html, pgpPKeyPrefixL)
+ endIdx := strings.Index(html, pgpPKeySuffixL)
+ if startIdx != -1 && endIdx != -1 {
+ pkeySubSlice := html[startIdx : endIdx+len(pgpPKeySuffixL)]
+ unescapedPkey := html2.UnescapeString(pkeySubSlice)
+ tmp := convertInlinePGPPublicKey(unescapedPkey)
+ upload, _ := db.CreateUpload("pgp_pkey.txt", []byte(tmp), authUserID)
+ msgBefore := html[0:startIdx]
+ msgAfter := html[endIdx+len(pgpPKeySuffixL):]
+ html = msgBefore + ` [` + upload.GetHTMLLink() + `] ` + msgAfter
+ html = strings.TrimSpace(html)
+ }
+ return html
+}
+
+func convertPGPClearsignToFile(db *database.DkfDB, html string, authUserID database.UserID) string {
+ if b, _ := clearsign.Decode([]byte(html)); b != nil {
+ pgpSignedPrefixL := pgpSignedPrefix
+ pgpSignedSuffixL := pgpSignedSuffix
+ startIdx := strings.Index(html, pgpSignedPrefixL)
+ endIdx := strings.Index(html, pgpSignedSuffixL)
+ tmp := html[startIdx : endIdx+len(pgpSignedSuffixL)]
+ upload, _ := db.CreateUpload("pgp_clearsign.txt", []byte(tmp), authUserID)
+ msgBefore := html[0:startIdx]
+ msgAfter := html[endIdx+len(pgpSignedSuffixL):]
+ html = msgBefore + ` [` + upload.GetHTMLLink() + `] ` + msgAfter
+ html = strings.TrimSpace(html)
+ }
+ return html
+}
+
+// Auto convert pasted pgp message into uploaded file
+func convertPGPMessageToFile(db *database.DkfDB, html string, authUserID database.UserID) string {
+ pgpPrefixL := pgpPrefix
+ pgpSuffixL := pgpSuffix
+ startIdx := strings.Index(html, pgpPrefixL)
+ endIdx := strings.Index(html, pgpSuffixL)
+ if startIdx != -1 && endIdx != -1 {
+ tmp := html[startIdx : endIdx+len(pgpSuffixL)]
+ tmp = strings.TrimSpace(tmp)
+ tmp = strings.TrimPrefix(tmp, pgpPrefixL)
+ tmp = strings.TrimSuffix(tmp, pgpSuffixL)
+ tmp = strings.Join(strings.Split(tmp, " "), "\n")
+ tmp = pgpPrefixL + tmp
+ tmp += pgpSuffixL
+ upload, _ := db.CreateUpload("pgp.txt", []byte(tmp), authUserID)
+ msgBefore := html[0:startIdx]
+ msgAfter := html[endIdx+len(pgpSuffixL):]
+ html = msgBefore + ` [` + upload.GetHTMLLink() + `] ` + msgAfter
+ html = strings.TrimSpace(html)
+ }
+ return html
+}
+
+// Auto convert pasted age message into uploaded file
+func convertAgeMessageToFile(db *database.DkfDB, html string, authUserID database.UserID) string {
+ agePrefixL := agePrefix
+ ageSuffixL := ageSuffix
+ startIdx := strings.Index(html, agePrefixL)
+ endIdx := strings.Index(html, ageSuffixL)
+ if startIdx != -1 && endIdx != -1 {
+ tmp := html[startIdx : endIdx+len(ageSuffixL)]
+ tmp = strings.TrimSpace(tmp)
+ tmp = strings.TrimPrefix(tmp, agePrefixL)
+ tmp = strings.TrimSuffix(tmp, ageSuffixL)
+ tmp = strings.Join(strings.Split(tmp, " "), "\n")
+ tmp = agePrefixL + tmp
+ tmp += ageSuffixL
+ upload, _ := db.CreateUpload("age.txt", []byte(tmp), authUserID)
+ msgBefore := html[0:startIdx]
+ msgAfter := html[endIdx+len(ageSuffixL):]
+ html = msgBefore + ` [` + upload.GetHTMLLink() + `] ` + msgAfter
+ html = strings.TrimSpace(html)
+ }
+ return html
+}
+
+func convertInlinePGPPublicKey(inlinePKey string) string {
+ pgpPKeyPrefixL := pgpPKeyPrefix
+ pgpPKeySuffixL := pgpPKeySuffix
+ // If it contains new lines, it was probably pasted using multi-line text box
+ if strings.Contains(inlinePKey, "\n") {
+ return inlinePKey
+ }
+ inlinePKey = strings.TrimSpace(inlinePKey)
+ inlinePKey = strings.TrimPrefix(inlinePKey, pgpPKeyPrefixL)
+ inlinePKey = strings.TrimSuffix(inlinePKey, pgpPKeySuffixL)
+ inlinePKey = strings.TrimSpace(inlinePKey)
+ commentsParts := strings.Split(inlinePKey, "Comment: ")
+ commentsParts, lastCommentPart := commentsParts[:len(commentsParts)-1], commentsParts[len(commentsParts)-1]
+ newCommentsParts := make([]string, 0)
+ for idx := range commentsParts {
+ if commentsParts[idx] != "" {
+ commentsParts[idx] = "Comment: " + commentsParts[idx]
+ commentsParts[idx] = strings.TrimSpace(commentsParts[idx])
+ newCommentsParts = append(newCommentsParts, commentsParts[idx])
+ }
+ }
+
+ rgx := regexp.MustCompile(`\s\s(\w|\+|/){64}`)
+ m := rgx.FindStringIndex(lastCommentPart)
+ commentsStr := ""
+ key := ""
+ if len(m) == 2 {
+ idx := m[0]
+ lastCommentP1 := lastCommentPart[:idx]
+ lastCommentP2 := lastCommentPart[idx+2:]
+ key = strings.Join(strings.Split(lastCommentP2, " "), "\n")
+ commentsStr = strings.Join(newCommentsParts, "\n")
+ commentsStr += "\nComment: " + lastCommentP1 + "\n\n"
+ } else {
+ key = "\n" + strings.Join(strings.Split(lastCommentPart, " "), "\n")
+ }
+ inlinePKey = pgpPKeyPrefixL + "\n" + commentsStr + key + "\n" + pgpPKeySuffixL
+ return inlinePKey
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/slashInterceptor.go b/pkg/web/handlers/api/v1/interceptors/slashInterceptor.go
@@ -0,0 +1,1817 @@
+package interceptors
+
+import (
+ "dkforest/pkg/clockwork"
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ dutils "dkforest/pkg/database/utils"
+ "dkforest/pkg/managers"
+ "dkforest/pkg/utils"
+ "errors"
+ "fmt"
+ "github.com/ProtonMail/go-crypto/openpgp/clearsign"
+ "github.com/ProtonMail/go-crypto/openpgp/packet"
+ "github.com/asaskevich/govalidator"
+ "github.com/dustin/go-humanize"
+ "github.com/sirupsen/logrus"
+ "html"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type ErrSuccess struct {
+ msg string
+}
+
+func NewErrSuccess(msg string) *ErrSuccess {
+ return &ErrSuccess{msg: msg}
+}
+
+func (e ErrSuccess) Error() string {
+ return e.msg
+}
+
+// SlashInterceptor handle all forward slash commands.
+//
+// If by the end of this function, the c.err is set, it will trigger
+// different behavior according to the type of error it holds.
+// if c.err is set to ErrRedirect, the chat-bar iframe will refresh completely.
+// if c.err is set to ErrStop, no further processing of the user input will be done,
+// and the chat iframe will be rendered instead of redirected.
+// This is useful to keep a prefix in the text box (eg: /pm user )
+// if c.err is set to an instance of ErrSuccess,
+// a green message will appear beside the text box.
+// otherwise if c.err is set to a different error,
+// text box is retested to original message,
+// and a red message will appear beside the text box.
+type SlashInterceptor struct{}
+
+func (i SlashInterceptor) InterceptMsg(c *Command) {
+ if !strings.HasPrefix(c.message, "/") {
+ return
+ }
+ handled := handleUserCmd(c) ||
+ handlePrivateRoomCmd(c) ||
+ handlePrivateRoomOwnerCmd(c) ||
+ handleModeratorCmd(c) ||
+ handleAdminCmd(c)
+ if !handled {
+ c.err = errors.New("invalid slash command")
+ }
+}
+
+func handleUserCmd(c *Command) (handled bool) {
+ return handleIgnoreCmd(c) ||
+ handleUnIgnoreCmd(c) ||
+ handleToggleAutocomplete(c) ||
+ handleTutorialCmd(c) ||
+ handleDeleteMsgCmd(c) ||
+ handleHideMsgCmd(c) ||
+ handleUnHideMsgCmd(c) ||
+ handleListIgnoredCmd(c) ||
+ handleListPmWhitelistCmd(c) ||
+ handleSetPmModeWhitelistCmd(c) ||
+ handleSetPmModeStandardCmd(c) ||
+ handleTogglePmBlacklistedUser(c) ||
+ handleTogglePmWhitelistedUser(c) ||
+ handleGroupChatCmd(c) ||
+ handleMeCmd(c) ||
+ handleEditCmd(c) ||
+ handlePMCmd(c) ||
+ handleEditLastCmd(c) ||
+ handleSubscribeCmd(c) ||
+ handleUnsubscribeCmd(c) ||
+ handleProfileCmd(c) ||
+ handleInboxCmd(c) ||
+ handleChessCmd(c) ||
+ handleHbmCmd(c) ||
+ handleHbmtCmd(c) ||
+ handleTokenCmd(c) ||
+ handleMd5Cmd(c) ||
+ handleSha1Cmd(c) ||
+ handleSha256Cmd(c) ||
+ handleSha512Cmd(c) ||
+ handleDiceCmd(c) ||
+ handleRandCmd(c) ||
+ handleChoiceCmd(c) ||
+ handleListMemes(c) ||
+ handleSuccessCmd(c) ||
+ handleAfkCmd(c) ||
+ handleDateCmd(c) ||
+ handleUpdateReadMarkerCmd(c) ||
+ handleCodeCmd(c) ||
+ handleErrorCmd(c)
+}
+
+func handlePrivateRoomCmd(c *Command) (handled bool) {
+ return handleGetModeCmd(c) ||
+ handleWhitelistCmd(c)
+}
+
+func handlePrivateRoomOwnerCmd(c *Command) (handled bool) {
+ if (c.room.OwnerUserID != nil && *c.room.OwnerUserID == c.authUser.ID) || c.authUser.IsAdmin {
+ return handleAddGroupCmd(c) ||
+ handleRmGroupCmd(c) ||
+ handleLockGroupCmd(c) ||
+ handleUnlockGroupCmd(c) ||
+ handleGroupUsersCmd(c) ||
+ handleListGroupsCmd(c) ||
+ handleGroupAddUserCmd(c) ||
+ handleGroupRmUserCmd(c) ||
+ handleSetModeWhitelistCmd(c) ||
+ handleSetModeStandardCmd(c) ||
+ handleGetRoomWhitelistCmd(c) ||
+ handleToggleReadOnlyCmd(c)
+ }
+ return false
+}
+
+func handleModeratorCmd(c *Command) (handled bool) {
+ if c.authUser.IsModerator() {
+ return handleModeratorGroupCmd(c) ||
+ handleListModeratorsCmd(c) ||
+ handleKickCmd(c) ||
+ handleKickKeepCmd(c) ||
+ handleKickSilentCmd(c) ||
+ handleKickKeepSilentCmd(c) ||
+ handleUnkickCmd(c) ||
+ handleLogoutCmd(c) ||
+ handleForceCaptchaCmd(c) ||
+ handleResetTutorialCmd(c) ||
+ handleHellbanCmd(c) ||
+ handleUnhellbanCmd(c)
+ }
+ return false
+}
+
+func handleAdminCmd(c *Command) (handled bool) {
+ if c.authUser.IsAdmin {
+ return handleSystemCmd(c) ||
+ handleSetChatRoomExternalLink(c) ||
+ handlePurge(c) ||
+ handleRename(c) ||
+ handleNewMeme(c) ||
+ handleRenameMeme(c) ||
+ handleRemoveMeme(c) ||
+ handleRefreshCmd(c)
+ }
+ return false
+}
+
+func handleModeratorGroupCmd(c *Command) (handled bool) {
+ if strings.HasPrefix(c.message, "/m ") || strings.HasPrefix(c.message, "/n ") {
+ if strings.HasPrefix(c.message, "/n ") {
+ c.message = strings.Replace(c.message, "/n ", "/m ", 1)
+ }
+ c.message = strings.TrimPrefix(c.message, "/m ")
+ c.redirectQP.Set(RedirectModQP, "1")
+ c.modMsg = true
+ if handleMeCmd(c) {
+ return true
+ } else if handleCodeCmd(c) {
+ return true
+ }
+ return true
+ }
+ return false
+}
+
+func handleListModeratorsCmd(c *Command) (handled bool) {
+ if c.message == "/moderators" || c.message == "/mods" {
+ mods, err := c.db.GetModeratorsUsers()
+ if err != nil {
+ c.err = err
+ return true
+ }
+ msg := "Moderators:\n"
+ if len(mods) > 0 {
+ msg += "\n"
+ for _, mod := range mods {
+ msg += mod.Username.AtStr() + "\n"
+ }
+ } else {
+ msg += "no moderators"
+ }
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleKickCmd(c *Command) (handled bool) {
+ if m := kickRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ if err := kickCmd(c, username, true, false); err != nil {
+ c.err = err
+ return true
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+// Kick a user but keep the messages
+func handleKickKeepCmd(c *Command) (handled bool) {
+ if m := kickKeepRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ if err := kickCmd(c, username, false, false); err != nil {
+ c.err = err
+ return true
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+// Kick a user, no system message in chat
+func handleKickSilentCmd(c *Command) (handled bool) {
+ if m := kickSilentRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ if err := kickCmd(c, username, true, true); err != nil {
+ c.err = err
+ return true
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+// Kick a user, keep the messages, no system message in chat
+func handleKickKeepSilentCmd(c *Command) (handled bool) {
+ if m := kickKeepSilentRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ if err := kickCmd(c, username, false, true); err != nil {
+ c.err = err
+ return true
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func kickCmd(c *Command, username database.Username, purge, silent bool) error {
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ return ErrUsernameNotFound
+ }
+ return dutils.Kick(c.db, user, *c.authUser, purge, silent)
+}
+
+var ErrUsernameNotFound = errors.New("username not found")
+var ErrUnauthorized = errors.New("unauthorized")
+
+func handleUnkickCmd(c *Command) (handled bool) {
+ if m := unkickRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrUsernameNotFound
+ return true
+ }
+ if user.Verified {
+ c.err = errors.New("user already not kicked")
+ return true
+ }
+ c.db.NewAudit(*c.authUser, fmt.Sprintf("unkick %s #%d", user.Username, user.ID))
+ user.Verified = true
+ user.DoSave(c.db)
+
+ // Display unkick message
+ c.db.CreateUnkickMsg(user, *c.authUser)
+
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleForceCaptchaCmd(c *Command) (handled bool) {
+ if m := forceCaptchaRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrUsernameNotFound
+ return true
+ }
+ if c.authUser.IsAdmin || !user.IsModerator() || c.authUser.Username == username {
+ c.db.NewAudit(*c.authUser, fmt.Sprintf("force captcha %s #%d", user.Username, user.ID))
+ user.CaptchaRequired = true
+ user.DoSave(c.db)
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleLogoutCmd(c *Command) (handled bool) {
+ if m := logoutRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrUsernameNotFound
+ return true
+ }
+ if !c.authUser.IsAdmin && user.Vetted {
+ c.err = ErrUnauthorized
+ return true
+ }
+ if c.authUser.IsAdmin || !user.IsModerator() {
+ c.db.NewAudit(*c.authUser, fmt.Sprintf("logout %s #%d", user.Username, user.ID))
+
+ _ = c.db.DeleteUserSessions(user.ID)
+
+ // Remove user from the user cache
+ managers.ActiveUsers.RemoveUser(user.ID)
+ }
+
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleResetTutorialCmd(c *Command) (handled bool) {
+ if m := rtutoRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrUsernameNotFound
+ return true
+ }
+ if !c.authUser.IsAdmin && user.Vetted {
+ c.err = ErrUnauthorized
+ return true
+ }
+ if c.authUser.IsAdmin || !user.IsModerator() {
+ c.db.NewAudit(*c.authUser, fmt.Sprintf("rtuto %s #%d", user.Username, user.ID))
+ user.ChatTutorial = 0
+ user.DoSave(c.db)
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleHellbanCmd(c *Command) (handled bool) {
+ if m := hellbanRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrUsernameNotFound
+ return true
+ }
+ if !c.authUser.IsAdmin && (user.Vetted || user.IsModerator()) {
+ c.err = ErrUnauthorized
+ return true
+ }
+ c.db.NewAudit(*c.authUser, fmt.Sprintf("hellban %s #%d", user.Username, user.ID))
+ user.HellBan(c.db)
+ managers.ActiveUsers.UpdateUserHBInRooms(managers.NewUserInfo(&user))
+
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleUnhellbanCmd(c *Command) (handled bool) {
+ if m := unhellbanRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrUsernameNotFound
+ return true
+ }
+ if !c.authUser.IsAdmin && (user.Vetted || user.IsModerator()) {
+ c.err = ErrUnauthorized
+ return true
+ }
+ c.db.NewAudit(*c.authUser, fmt.Sprintf("unhellban %s #%d", user.Username, user.ID))
+ user.UnHellBan(c.db)
+ managers.ActiveUsers.UpdateUserHBInRooms(managers.NewUserInfo(&user))
+
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleHbmCmd(c *Command) (handled bool) {
+ if !c.authUser.CanSeeHB() {
+ return
+ }
+ if strings.HasPrefix(c.message, "/hbm ") {
+ c.message = strings.TrimPrefix(c.message, "/hbm ")
+ c.hellbanMsg = true
+ c.redirectQP.Set(RedirectHbmQP, "1")
+ return true
+ }
+ return
+}
+
+func handleHbmtCmd(c *Command) (handled bool) {
+ if !c.authUser.CanSeeHB() {
+ return
+ }
+ if m := hbmtRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ date := m[1]
+ if dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock()); err == nil {
+ if msg, err := c.db.GetRoomChatMessageByDate(c.room.ID, c.authUser.ID, dt.UTC()); err == nil {
+ msg.IsHellbanned = !msg.IsHellbanned
+ msg.DoSave(c.db)
+ } else {
+ c.err = errors.New("no message found at this timestamp")
+ return true
+ }
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleDiceCmd(c *Command) (handled bool) {
+ if strings.HasPrefix(c.message, "/dice") {
+ dice := utils.RandInt(1, 6)
+ raw := fmt.Sprintf(`rolling dice for @%s ... "%d"`, c.authUser.Username, dice)
+ msg := fmt.Sprintf(`rolling dice for @%s ... "<span style="color: white;">%d</span>"`, c.authUser.Username, dice)
+ msg, _ = colorifyTaggedUsers(msg, c.db.GetUsersByUsername)
+ go func() {
+ time.Sleep(time.Second)
+ c.zeroPublicMsg(raw, msg)
+ }()
+ return true
+ }
+ return
+}
+
+func handleRandCmd(c *Command) (handled bool) {
+ if strings.HasPrefix(c.message, "/rand") {
+ min := 1
+ max := 6
+ var dice int
+ if m := randRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ var err error
+ min, err = strconv.Atoi(m[1])
+ if err != nil {
+ c.err = err
+ return true
+ }
+ max, err = strconv.Atoi(m[2])
+ if err != nil {
+ c.err = err
+ return true
+ }
+ if max <= min {
+ c.err = errors.New("max must be greater than min")
+ return true
+ }
+ } else if c.message != "/rand" {
+ c.err = errors.New("invalid /rand command")
+ return true
+ }
+ dice = utils.RandInt(min, max)
+ raw := fmt.Sprintf(`rolling dice for @%s ... "%d"`, c.authUser.Username, dice)
+ msg := fmt.Sprintf(`rolling dice for @%s ... "<span style="color: white;">%d</span>"`, c.authUser.Username, dice)
+ msg, _ = colorifyTaggedUsers(msg, c.db.GetUsersByUsername)
+ go func() {
+ time.Sleep(time.Second)
+ c.zeroPublicMsg(raw, msg)
+ }()
+ return true
+ }
+ return
+}
+
+func handleChoiceCmd(c *Command) (handled bool) {
+ if strings.HasPrefix(c.message, "/choice ") {
+ tmp := html.EscapeString(strings.TrimPrefix(c.message, "/choice "))
+ words := strings.Fields(tmp)
+ answer := utils.RandChoice(words)
+ raw := fmt.Sprintf(`@%s choice %s ... "%s"`, c.authUser.Username, words, answer)
+ msg := fmt.Sprintf(`@%s choice %s ... "<span style="color: white;">%s</span>"`, c.authUser.Username, words, answer)
+ msg, _ = colorifyTaggedUsers(msg, c.db.GetUsersByUsername)
+ go func() {
+ time.Sleep(time.Second)
+ c.zeroPublicMsg(raw, msg)
+ }()
+ c.skipInboxes = true
+ return true
+ }
+ return
+}
+
+func handleTokenCmd(c *Command) (handled bool) {
+ if c.message == "/token" {
+ c.zeroMsg(utils.GenerateToken10())
+ c.err = ErrRedirect
+ return true
+ } else if m := tokenRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ n, _ := strconv.Atoi(m[1])
+ if n < 1 || n > 32 {
+ c.err = errors.New("value must be [1;32]")
+ return true
+ }
+ n = utils.Clamp(n, 1, 32)
+ c.zeroMsg(utils.GenerateTokenN(n))
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleMd5Cmd(c *Command) (handled bool) {
+ return handleHasherCmd(c, "/md5 ", utils.MD5)
+}
+
+func handleSha1Cmd(c *Command) (handled bool) {
+ return handleHasherCmd(c, "/sha1 ", utils.Sha1)
+}
+
+func handleSha256Cmd(c *Command) (handled bool) {
+ return handleHasherCmd(c, "/sha256 ", utils.Sha256)
+}
+
+func handleSha512Cmd(c *Command) (handled bool) {
+ return handleHasherCmd(c, "/sha512 ", utils.Sha512)
+}
+
+func handleHasherCmd(c *Command, prefix string, fn func([]byte) string) (handled bool) {
+ if strings.HasPrefix(c.message, prefix) {
+ c.message = strings.TrimPrefix(c.message, prefix)
+ c.dataMessage = prefix
+ c.zeroMsg(fn([]byte(c.message)))
+ c.err = ErrStop
+ return true
+ }
+ return
+}
+
+func handleRmGroupCmd(c *Command) (handled bool) {
+ if m := rmGroupRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ groupName := m[1]
+ if err := c.db.DeleteChatRoomGroup(c.room.ID, groupName); err != nil {
+ c.err = err
+ return true
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleLockGroupCmd(c *Command) (handled bool) {
+ if m := lockGroupRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ groupName := m[1]
+ group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ group.Locked = true
+ group.DoSave(c.db)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleUnlockGroupCmd(c *Command) (handled bool) {
+ if m := unlockGroupRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ groupName := m[1]
+ group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ group.Locked = false
+ group.DoSave(c.db)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleGroupUsersCmd(c *Command) (handled bool) {
+ if m := groupUsersRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ groupName := m[1]
+ group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ users, err := c.db.GetRoomGroupUsers(c.room.ID, group.ID)
+ sort.Slice(users, func(i, j int) bool {
+ return users[i].User.Username < users[j].User.Username
+ })
+ msg := ""
+ if len(users) > 0 {
+ msg += "\n"
+ for _, user := range users {
+ msg += user.User.Username.AtStr() + "\n"
+ }
+ } else {
+ msg += "no user in th group: " + groupName
+ }
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleListGroupsCmd(c *Command) (handled bool) {
+ if c.message == "/groups" {
+ groups, err := c.db.GetRoomGroups(c.room.ID)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ msg := ""
+ if len(groups) > 0 {
+ msg += "\n"
+ for _, group := range groups {
+ msg += group.Name + " (" + group.Color + ")\n"
+ }
+ } else {
+ msg += "no groups"
+ }
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleGroupAddUserCmd(c *Command) (handled bool) {
+ if m := groupAddUserRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ groupName := m[1]
+ username := database.Username(m[2])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ _, err = c.db.AddUserToRoomGroup(c.room.ID, group.ID, user.ID)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ c.err = ErrRedirect
+ return true
+ } else if strings.HasPrefix(c.message, "/gadduser ") {
+ c.err = errors.New("invalid /gadduser command")
+ }
+ return false
+}
+
+func handleGroupRmUserCmd(c *Command) (handled bool) {
+ if m := groupRmUserRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ groupName := m[1]
+ username := database.Username(m[2])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ err = c.db.RmUserFromRoomGroup(c.room.ID, group.ID, user.ID)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ c.err = ErrRedirect
+ return true
+ } else if strings.HasPrefix(c.message, "/grmuser ") {
+ c.err = errors.New("invalid /grmuser command")
+ }
+ return false
+}
+
+func handleSetModeWhitelistCmd(c *Command) (handled bool) {
+ if c.message == "/mode user-whitelist" {
+ c.room.Mode = database.UserWhitelistRoomMode
+ c.room.DoSave(c.db)
+ msg := `room mode set to "user whitelist"`
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleSetModeStandardCmd(c *Command) (handled bool) {
+ if c.message == "/mode standard" {
+ c.room.Mode = database.NormalRoomMode
+ c.room.DoSave(c.db)
+ msg := `room mode set to "standard"`
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleGetRoomWhitelistCmd(c *Command) (handled bool) {
+ if m := whitelistUserRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ var msg string
+ if err != nil {
+ msg = fmt.Sprintf(`username "%s" not found`, username)
+ } else {
+ if _, err := c.db.WhitelistUser(c.room.ID, user.ID); err != nil {
+ if err := c.db.DeWhitelistUser(c.room.ID, user.ID); err != nil {
+ msg = fmt.Sprintf("failed to toggle @%s in whitelist", user.Username)
+ } else {
+ msg = fmt.Sprintf("@%s removed from whitelist", user.Username)
+ }
+ } else {
+ msg = fmt.Sprintf("@%s added to whitelist", user.Username)
+ }
+ }
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleToggleReadOnlyCmd(c *Command) (handled bool) {
+ if c.message == "/ro" {
+ c.room.ReadOnly = !c.room.ReadOnly
+ c.room.DoSave(c.db)
+ if c.room.ReadOnly {
+ c.err = NewErrSuccess("room is now read-only")
+ } else {
+ c.err = NewErrSuccess("room is no longer read-only")
+ }
+ return true
+ }
+ return
+}
+
+func handleAddGroupCmd(c *Command) (handled bool) {
+ if m := addGroupRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ name := m[1]
+ _, err := c.db.CreateChatRoomGroup(c.room.ID, name, "#fff")
+ if err != nil {
+ c.err = err
+ return true
+ }
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleWhitelistCmd(c *Command) (handled bool) {
+ if c.message == "/whitelist" || c.message == "/wl" {
+ whitelistedUsers, _ := c.db.GetWhitelistedUsers(c.room.ID)
+ var msg string
+ if len(whitelistedUsers) > 0 {
+ usernames := make([]string, 0)
+ for _, whitelistedUser := range whitelistedUsers {
+ usernames = append(usernames, whitelistedUser.User.Username.AtStr())
+ }
+ sort.Slice(usernames, func(i, j int) bool { return usernames[i] < usernames[j] })
+ msg = "whitelisted users: " + strings.Join(usernames, ", ")
+ } else {
+ msg = "no whitelisted user"
+ }
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleGetModeCmd(c *Command) (handled bool) {
+ if c.message == "/mode" {
+ var msg string
+ if c.room.Mode == database.NormalRoomMode {
+ msg = `room is in "standard" mode`
+ } else if c.room.Mode == database.UserWhitelistRoomMode {
+ msg = `room is in "user whitelist" mode`
+ }
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleMeCmd(c *Command) (handled bool) {
+ if c.message == "/me " {
+ c.err = errors.New("invalid /me command")
+ return true
+ }
+ if strings.HasPrefix(c.message, "/me ") {
+ return true
+ }
+ return
+}
+
+func handleEditCmd(c *Command) (handled bool) {
+ if m := editRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ date := m[1]
+ newMsg := m[2]
+ if dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock()); err == nil {
+ if time.Since(dt) <= config.EditMessageTimeLimit {
+ if msg, err := c.db.GetRoomChatMessageByDate(c.room.ID, c.authUser.ID, dt.UTC()); err == nil {
+ c.editMsg = &msg
+ c.origMessage = newMsg
+ c.message = newMsg
+
+ // If we're editing a message which contains a link to an uploaded file,
+ // we need to re-add the link to the html.
+ if msg.UploadID != nil {
+ if newUpload, err := c.db.GetUploadByID(*msg.UploadID); err == nil {
+ c.upload = &newUpload
+ }
+ }
+
+ if pmRgx.MatchString(c.message) {
+ handlePMCmd(c)
+ } else if c.authUser.IsModerator() && strings.HasPrefix(c.message, "/m ") {
+ handleModeratorGroupCmd(c)
+ } else if strings.HasPrefix(c.message, "/hbm ") {
+ handleHbmCmd(c)
+ } else if strings.HasPrefix(c.message, "/g ") {
+ handleGroupChatCmd(c)
+ } else if strings.HasPrefix(c.message, "/system ") || strings.HasPrefix(c.message, "/sys ") {
+ handleSystemCmd(c)
+ }
+ }
+ }
+ }
+ return true
+ }
+ return
+}
+
+func handleEditLastCmd(c *Command) (handled bool) {
+ if c.message == "/e" {
+ msg, err := c.db.GetUserLastChatMessageInRoom(c.authUser.ID, c.room.ID)
+ if err != nil {
+ return true
+ }
+ c.redirectQP.Set(RedirectEditQP, msg.CreatedAt.Format("15:04:05"))
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+var ErrPMDenied = errors.New("you cannot pm/inbox this user")
+var Err20Msgs = errors.New("you need 20 public messages to unlock PMs/Inbox; or be whitelisted")
+var ErrOther20Msgs = errors.New("dest user must be whitelisted or have 20 public messages")
+
+func canUserInboxOther(db *database.DkfDB, user, other database.User) error {
+ doesNotMatter := false
+ _, err := canUserPmOther(db, user, other, doesNotMatter)
+ return err
+}
+
+func canUserPmOther(db *database.DkfDB, user, other database.User, roomIsPrivate bool) (skipInbox bool, err error) {
+ errPMDenied := ErrPMDenied
+
+ if user.ID == other.ID {
+ return false, errors.New("cannot /pm yourself")
+ }
+
+ if db.IsUserPmWhitelisted(user.ID, other.ID) {
+ return false, nil
+ }
+
+ switch other.PmMode {
+ case database.PmModeWhitelist:
+ // We are in whitelist mode, and user is not whitelisted
+ return false, errPMDenied
+
+ case database.PmModeStandard:
+ if !user.CanSendPM() {
+ // In private rooms, can send PM but inboxes will be skipped if not enough public messages
+ if roomIsPrivate {
+ return true, nil
+ }
+ // Need at least 20 public messages to send PM in a public room
+ return false, Err20Msgs
+ }
+
+ // User on blacklist cannot PM/Inbox
+ if db.IsUserPmBlacklisted(user.ID, other.ID) {
+ return false, errPMDenied
+ }
+ // Other doesn't want PM from new users
+ if !user.AccountOldEnough() && other.BlockNewUsersPm {
+ return false, errPMDenied
+ }
+
+ if !other.CanSendPM() {
+ if db.IsUserPmWhitelisted(other.ID, user.ID) {
+ return true, nil
+ }
+ // In private rooms, can send PM but inboxes will be skipped if not enough public messages
+ if roomIsPrivate {
+ return true, nil
+ }
+ return false, ErrOther20Msgs
+ }
+
+ return false, nil
+ }
+
+ // Should never go here
+ return false, nil
+}
+
+func handlePMCmd(c *Command) (handled bool) {
+ if m := pmRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ username := database.Username(m[1])
+ newMsg := m[2]
+
+ // Chat helpers
+ if username == config.NullUsername {
+ return handlePm0(c, newMsg)
+ }
+
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = errors.New("invalid username")
+ return true
+ }
+
+ c.skipInboxes, c.err = canUserPmOther(c.db, *c.authUser, user, c.room.IsOwned())
+ if c.err != nil {
+ return true
+ }
+
+ c.toUser = &user
+ c.message = newMsg
+ c.redirectQP.Set(RedirectPmQP, string(user.Username))
+
+ if newMsg == "/d" || strings.HasPrefix(newMsg, "/d ") {
+ handled = handleDeleteMsgCmd(c)
+ if c.err != nil && c.err != ErrRedirect {
+ return handled
+ }
+ c.err = ErrRedirect
+ return handled
+ }
+
+ if handleCodeCmd(c) {
+ return true
+ }
+
+ return true
+ } else if strings.HasPrefix(c.message, "/pm ") {
+ c.err = errors.New("invalid /pm command")
+ return true
+ }
+ return false
+}
+
+// Handle PMs sent to user 0 (/pm 0 msg)
+func handlePm0(c *Command, msg string) (handled bool) {
+ c.redirectQP.Set(RedirectPmQP, "0")
+ if msg == "ping" {
+ c.zeroMsg("pong")
+ c.err = ErrRedirect
+ return true
+
+ } else if msg == "talk" {
+ c.zeroMsg("talking")
+ c.err = ErrRedirect
+ return true
+
+ } else if msg == "pgp" || msg == "gpg" {
+ pkey := c.authUser.GPGPublicKey
+ if pkey == "" {
+ c.message = "I could not find a public pgp key in your profile."
+ } else {
+ msg := "This is a sample text"
+ if encrypted, err := utils.GeneratePgpEncryptedMessage(pkey, msg); err != nil {
+ c.message = err.Error()
+ } else {
+ c.message = strings.Join(strings.Split(encrypted, "\n"), " ")
+ }
+ }
+ c.zeroProcMsg(c.message)
+ c.err = ErrRedirect
+ return true
+
+ } else if pgpMsg := extractPGPMessage(msg); pgpMsg != "" {
+ decrypted, err := utils.PgpDecryptMessage(config.NullUserPrivateKey, pgpMsg)
+ if err != nil {
+ c.message = err.Error()
+ } else {
+ c.message = "Decrypted message: " + decrypted
+ }
+ c.zeroProcMsg(c.message)
+ c.err = ErrRedirect
+ return true
+
+ } else if b, _ := clearsign.Decode([]byte(msg)); b != nil {
+ if p, err := packet.Read(b.ArmoredSignature.Body); err == nil {
+ if sig, ok := p.(*packet.Signature); ok {
+ zero := c.getZeroUser()
+ msg := fmt.Sprintf("<br />"+
+ "<table %s>"+
+ "<tr><td align=\"right\">Signature made: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
+ "<tr><td align=\"right\">Fingerprint: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
+ "<tr><td align=\"right\">Issuer: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
+ "</table>",
+ zero.GenerateChatStyle(),
+ sig.CreationTime.Format(time.RFC1123),
+ utils.FormatPgPFingerprint(sig.IssuerFingerprint),
+ utils.Ternary(sig.SignerUserId != nil, *sig.SignerUserId, "n/a"))
+ c.zeroMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ }
+
+ } else if c.upload != nil {
+
+ // If we sent a clearsign file to @0, the bot will reply with information about the signature
+ if c.upload.FileSize < config.MaxFileSizeBeforeDownload {
+ if file, err := c.db.GetUploadByFileName(c.upload.FileName); err == nil {
+ if _, by, err := file.GetContent(); err == nil {
+ if b, _ := clearsign.Decode(by); b != nil {
+ if p, err := packet.Read(b.ArmoredSignature.Body); err == nil {
+ if sig, ok := p.(*packet.Signature); ok {
+ zero := c.getZeroUser()
+ msg := fmt.Sprintf("<br />"+
+ "<table %s>"+
+ "<tr><td align=\"right\">File: </td><td><span style=\"color: #82e17f;\">%s</span> (<span style=\"color: #82e17f;\">%s</span>)</td></tr>"+
+ "<tr><td align=\"right\">Signature made: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
+ "<tr><td align=\"right\">Fingerprint: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
+ "<tr><td align=\"right\">Issuer: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
+ "</table>",
+ zero.GenerateChatStyle(),
+ c.upload.OrigFileName,
+ humanize.Bytes(uint64(c.upload.FileSize)),
+ sig.CreationTime.Format(time.RFC1123),
+ utils.FormatPgPFingerprint(sig.IssuerFingerprint),
+ utils.Ternary(sig.SignerUserId != nil, *sig.SignerUserId, "n/a"))
+ c.zeroMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ zeroUser := c.getZeroUser()
+ c.toUser = &zeroUser
+ c.message = msg
+
+ return true
+}
+
+func handleSubscribeCmd(c *Command) (handled bool) {
+ if c.message == "/subscribe" {
+ _ = c.db.SubscribeToRoom(c.authUser.ID, c.room.ID)
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleUnsubscribeCmd(c *Command) (handled bool) {
+ if m := unsubscribeRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ room, err := c.db.GetChatRoomByName(m[1])
+ if err != nil {
+ c.err = err
+ return true
+ }
+ _ = c.db.UnsubscribeFromRoom(c.authUser.ID, room.ID)
+ c.err = ErrRedirect
+ return true
+
+ } else if c.message == "/unsubscribe" {
+ _ = c.db.UnsubscribeFromRoom(c.authUser.ID, c.room.ID)
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleGroupChatCmd(c *Command) (handled bool) {
+ if m := groupRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ groupName := m[1]
+ c.message = m[2]
+ group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ if group.Locked {
+ c.err = errors.New("group is locked")
+ return true
+ }
+ c.redirectQP.Set(RedirectGroupQP, group.Name)
+ c.groupID = &group.ID
+ return true
+ } else if strings.HasPrefix(c.message, "/g ") {
+ c.err = errors.New("invalid /g command")
+ return true
+ }
+ return false
+}
+
+func handleListIgnoredCmd(c *Command) (handled bool) {
+ if c.message == "/i" || c.message == "/ignore" {
+ ignoredUsers, _ := c.db.GetIgnoredUsers(c.authUser.ID)
+ sort.Slice(ignoredUsers, func(i, j int) bool {
+ return ignoredUsers[i].IgnoredUser.Username < ignoredUsers[j].IgnoredUser.Username
+ })
+ msg := ""
+ if len(ignoredUsers) > 0 {
+ msg += "\n"
+ for _, ignoredUser := range ignoredUsers {
+ msg += ignoredUser.IgnoredUser.Username.AtStr() + "\n"
+ }
+ } else {
+ msg += "no ignored users"
+ }
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleListPmWhitelistCmd(c *Command) (handled bool) {
+ if c.message == "/pmwhitelist" {
+ pmWhitelistUsers, _ := c.db.GetPmWhitelistedUsers(c.authUser.ID)
+ sort.Slice(pmWhitelistUsers, func(i, j int) bool {
+ return pmWhitelistUsers[i].WhitelistedUser.Username < pmWhitelistUsers[j].WhitelistedUser.Username
+ })
+ msg := ""
+ if len(pmWhitelistUsers) > 0 {
+ msg += "\n"
+ for _, ignoredUser := range pmWhitelistUsers {
+ msg += ignoredUser.WhitelistedUser.Username.AtStr() + "\n"
+ }
+ } else {
+ msg += "no PM whitelisted users"
+ }
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleSetPmModeWhitelistCmd(c *Command) (handled bool) {
+ if c.message == "/setpmmode whitelist" {
+ c.authUser.PmMode = database.PmModeWhitelist
+ c.authUser.DoSave(c.db)
+ msg := `pm mode set to "whitelist"`
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleSetPmModeStandardCmd(c *Command) (handled bool) {
+ if c.message == "/setpmmode standard" {
+ c.authUser.PmMode = database.PmModeStandard
+ c.authUser.DoSave(c.db)
+ msg := `pm mode set to "standard"`
+ c.zeroProcMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return false
+}
+
+func handleTogglePmBlacklistedUser(c *Command) (handled bool) {
+ if m := pmToggleBlacklistUserRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrRedirect
+ return true
+ }
+ if c.db.ToggleBlacklistedUser(c.authUser.ID, user.ID) {
+ c.err = NewErrSuccess("added to blacklist")
+ } else {
+ c.err = NewErrSuccess("removed from blacklist")
+ }
+ return true
+ }
+ return false
+}
+
+func handleTogglePmWhitelistedUser(c *Command) (handled bool) {
+ if m := pmToggleWhitelistUserRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrRedirect
+ return true
+ }
+ if c.db.ToggleWhitelistedUser(c.authUser.ID, user.ID) {
+ c.err = NewErrSuccess("added to whitelist")
+ } else {
+ c.err = NewErrSuccess("removed from whitelist")
+ }
+ return true
+ }
+ return false
+}
+
+func handleChessCmd(c *Command) (handled bool) {
+ if m := chessRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ username := database.Username(m[1])
+ color := m[2]
+ player1 := *c.authUser
+ player2, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = errors.New("invalid username")
+ return true
+ }
+ if color == "r" {
+ color = utils.RandChoice([]string{"w", "b"})
+ }
+ if color == "b" {
+ player1, player2 = player2, player1
+ }
+ if _, err := ChessInstance.NewGame1(c.roomKey, c.room.ID, player1, player2); err != nil {
+ c.err = err
+ return true
+ }
+ c.err = NewErrSuccess("chess game created")
+ return true
+ }
+ return
+}
+
+func handleInboxCmd(c *Command) (handled bool) {
+ if m := inboxRgx.FindStringSubmatch(c.message); len(m) == 4 {
+ username := database.Username(m[1])
+ encryptRaw := m[2]
+ message := m[3]
+ tryEncrypt := false
+ if encryptRaw == " -e" {
+ tryEncrypt = true
+ }
+ toUser, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = errors.New("invalid username")
+ return true
+ }
+
+ if err := canUserInboxOther(c.db, *c.authUser, toUser); err != nil {
+ c.err = err
+ return true
+ }
+
+ html := message
+ if tryEncrypt {
+ if toUser.GPGPublicKey == "" {
+ c.err = errors.New("user has no pgp public key")
+ return true
+ }
+ html, err = utils.GeneratePgpEncryptedMessage(toUser.GPGPublicKey, message)
+ if err != nil {
+ c.err = errors.New("failed to encrypt")
+ return true
+ }
+ html = strings.Join(strings.Split(html, "\n"), " ")
+ }
+
+ html, _, _ = ProcessRawMessage(c.db, html, c.roomKey, c.authUser.ID, c.room.ID, nil, c.authUser.CanUseMultiline)
+ c.db.CreateInboxMessage(html, c.room.ID, c.authUser.ID, toUser.ID, true, false, nil)
+
+ c.dataMessage = "/inbox " + string(username) + " "
+ c.err = NewErrSuccess("inbox sent")
+ return true
+
+ } else if strings.HasPrefix(c.message, "/inbox ") {
+ c.err = errors.New("invalid /inbox command")
+ return true
+ }
+ return
+}
+
+func handleProfileCmd(c *Command) (handled bool) {
+ if m := profileRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrUsernameNotFound
+ return true
+ }
+ profile := `/u/` + user.Username
+ c.zeroMsg(fmt.Sprintf(`[<a href="%s" rel="noopener noreferrer" target="_blank">profile of %s</a>]`, profile, user.Username))
+ c.err = ErrRedirect
+ return true
+ } else if strings.HasPrefix(c.message, "/p ") {
+ c.err = errors.New("invalid profile command")
+ return true
+ }
+ return
+}
+
+type tutorialSteps struct {
+ SendMessage bool
+ EditMessage bool
+ SendPM bool
+ EditPM bool
+ SendQuote bool
+ TagSomeone bool
+ VisitProfile bool
+}
+
+func handleTutorialCmd(c *Command) (handled bool) {
+ if c.message == "/tuto" && false {
+ name := "tuto_" + utils.GenerateToken10()
+ room, _ := c.db.CreateRoom(name, "", c.authUser.ID, false)
+ c.err = ErrRedirect
+ c.zeroProcMsg("Tutorial here -> #" + room.Name)
+ c.zeroPublicProcMsgRoom("Welcome to the tutorial", "", room.ID)
+ return true
+ }
+ return
+}
+
+func handleDeleteMsgCmd(c *Command) (handled bool) {
+ delMsgFn := func(msg database.ChatMessage) {
+ if msg.RoomID == config.GeneralRoomID && msg.ToUserID == nil {
+ msg.User.GeneralMessagesCount--
+ msg.User.DoSave(c.db)
+ }
+ _ = msg.Delete(c.db)
+ }
+ if c.message == "/d" {
+ if msg, err := c.db.GetUserLastChatMessageInRoom(c.authUser.ID, c.room.ID); err != nil {
+ c.err = errors.New("unable to find last message")
+ return true
+ } else if msg.TooOldToDelete() {
+ c.err = errors.New("message is to old to be deleted")
+ return true
+ } else {
+ delMsgFn(msg)
+ }
+ c.err = ErrRedirect
+ return true
+
+ } else if m := deleteMsgRgx.FindStringSubmatch(c.message); len(m) >= 3 {
+ if len(m) == 3 {
+ date := m[1]
+ matchUsername := m[2]
+ dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
+ if err != nil {
+ logrus.Error(err)
+ c.err = err
+ return true
+ }
+ msgs, err := c.db.GetRoomChatMessagesByDate(c.room.ID, dt.UTC())
+ if err != nil {
+ c.err = err
+ return true
+ }
+ if len(msgs) == 0 {
+ c.err = errors.New("failed to find msg")
+ return true
+
+ } else if len(msgs) == 1 {
+ msg := msgs[0]
+ if !c.authUser.IsModerator() {
+ if msg.User.Username != c.authUser.Username {
+ c.err = errors.New("failed to find msg")
+ return true
+ }
+ if msg.TooOldToDelete() {
+ c.err = errors.New("message is to old to be deleted")
+ return true
+ }
+ delMsgFn(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ // Moderator
+ _ = msg.Delete(c.db)
+ c.err = ErrRedirect
+ return true
+
+ } else if len(msgs) > 1 {
+ if !c.authUser.IsModerator() {
+ var msg database.ChatMessage
+ for _, msgTmp := range msgs {
+ if msgTmp.User.Username == c.authUser.Username {
+ msg = msgTmp
+ break
+ }
+ }
+ if msg.UUID == "" {
+ c.err = errors.New("failed to find msg")
+ return true
+ }
+ if msg.TooOldToDelete() {
+ c.err = errors.New("message is to old to be deleted")
+ return true
+ }
+ delMsgFn(msg)
+ c.err = ErrRedirect
+ return true
+
+ }
+
+ // Moderator
+ if matchUsername == "" {
+ c.err = errors.New("more the 1 msg with this timestamp")
+ return true
+ }
+ var msg database.ChatMessage
+ for _, msgTmp := range msgs {
+ if string(msgTmp.User.Username) == matchUsername {
+ msg = msgTmp
+ break
+ }
+ }
+ if msg.UUID == "" {
+ c.err = errors.New("failed to find msg")
+ return true
+ }
+ _ = msg.Delete(c.db)
+ c.err = ErrRedirect
+ return true
+ }
+ }
+ return true
+
+ } else if strings.HasPrefix(c.message, "/d ") {
+ c.err = errors.New("invalid /d command")
+ return true
+ }
+ return
+}
+
+func handleHideMsgCmd(c *Command) (handled bool) {
+ if m := hideRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ date := m[1]
+ dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
+ if err != nil {
+ logrus.Error(err)
+ c.err = err
+ return true
+ }
+ msgs, err := c.db.GetRoomChatMessagesByDate(c.room.ID, dt.UTC())
+ if err != nil {
+ c.err = err
+ return true
+ }
+ if len(msgs) == 1 {
+ c.db.IgnoreMessage(c.authUser.ID, msgs[0].ID)
+ c.err = ErrRedirect
+ } else {
+ c.err = errors.New("more than 1 message")
+ }
+ return true
+ }
+ return
+}
+
+func handleUnHideMsgCmd(c *Command) (handled bool) {
+ if m := unhideRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ date := m[1]
+ dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
+ if err != nil {
+ logrus.Error(err)
+ c.err = err
+ return true
+ }
+ msgs, err := c.db.GetRoomChatMessagesByDate(c.room.ID, dt.UTC())
+ if err != nil {
+ c.err = err
+ return true
+ }
+ if len(msgs) == 1 {
+ c.db.UnIgnoreMessage(c.authUser.ID, msgs[0].ID)
+ c.err = ErrRedirect
+ } else {
+ c.err = errors.New("more than 1 message")
+ }
+ return true
+ }
+ return
+}
+
+func handleIgnoreCmd(c *Command) (handled bool) {
+ if m := ignoreRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrRedirect
+ return true
+ }
+ c.db.IgnoreUser(c.authUser.ID, user.ID)
+ database.MsgPubSub.Pub("refresh_"+string(c.authUser.Username), database.ChatMessageType{Typ: database.ForceRefresh})
+ c.err = ErrRedirect
+ return true
+ } else if strings.HasPrefix(c.message, "/ignore ") || strings.HasPrefix(c.message, "/i ") {
+ c.err = errors.New("invalid ignore command")
+ return true
+ }
+ return
+}
+
+func handleUnIgnoreCmd(c *Command) (handled bool) {
+ if m := unIgnoreRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ username := database.Username(m[1])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = ErrRedirect
+ return true
+ }
+ c.db.UnIgnoreUser(c.authUser.ID, user.ID)
+ database.MsgPubSub.Pub("refresh_"+string(c.authUser.Username), database.ChatMessageType{Typ: database.ForceRefresh})
+ c.err = ErrRedirect
+ return true
+ } else if strings.HasPrefix(c.message, "/unignore ") || strings.HasPrefix(c.message, "/ui ") {
+ c.err = errors.New("invalid unignore command")
+ return true
+ }
+ return
+}
+
+func handleToggleAutocomplete(c *Command) (handled bool) {
+ if c.message == "/toggle-autocomplete" {
+ c.authUser.AutocompleteCommandsEnabled = !c.authUser.AutocompleteCommandsEnabled
+ c.authUser.DoSave(c.db)
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleAfkCmd(c *Command) (handled bool) {
+ if c.message == "/afk" {
+ c.authUser.AFK = !c.authUser.AFK
+ c.authUser.DoSave(c.db)
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleDateCmd(c *Command) (handled bool) {
+ if c.message == "/date" {
+ c.zeroMsg(time.Now().Format(time.RFC1123))
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleSuccessCmd(c *Command) (handled bool) {
+ if c.message == "/success" {
+ c.err = NewErrSuccess("success message")
+ return true
+ }
+ return
+}
+
+func handleErrorCmd(c *Command) (handled bool) {
+ if c.message == "/error" {
+ c.err = errors.New("error message")
+ return true
+ }
+ return
+}
+
+func handleSystemCmd(c *Command) (handled bool) {
+ if strings.HasPrefix(c.message, "/sys ") {
+ c.message = strings.Replace(c.message, "/sys ", "/system ", 1)
+ }
+ if strings.HasPrefix(c.message, "/system ") {
+ c.message = strings.TrimPrefix(c.message, "/system ")
+ c.systemMsg = true
+ return true
+ }
+ return false
+}
+
+func handleSetChatRoomExternalLink(c *Command) (handled bool) {
+ if m := setUrlRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ externalURL := m[1]
+ if !govalidator.IsURL(externalURL) {
+ externalURL = ""
+ }
+ room, err := c.db.GetChatRoomByID(c.room.ID)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ room.ExternalLink = externalURL
+ room.DoSave(c.db)
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handlePurge(c *Command) (handled bool) {
+ if m := purgeRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ isHB := m[1] == " -hb"
+ username := database.Username(m[2])
+ user, err := c.db.GetUserByUsername(username)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ c.db.NewAudit(*c.authUser, fmt.Sprintf("purge %s #%d", user.Username, user.ID))
+ if isHB {
+ _ = c.db.DeleteUserHbChatMessages(user.ID)
+ } else {
+ _ = c.db.DeleteUserChatMessages(user.ID)
+ }
+ database.MsgPubSub.Pub(database.RefreshTopic, database.ChatMessageType{Typ: database.ForceRefresh})
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleRename(c *Command) (handled bool) {
+ if m := renameRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ oldUsername := database.Username(m[1])
+ newUsername := database.Username(m[2])
+ user, err := c.db.GetUserByUsername(oldUsername)
+ if err != nil {
+ c.err = err
+ return true
+ }
+ c.db.NewAudit(*c.authUser, fmt.Sprintf("rename %s -> %s #%d", user.Username, newUsername, user.ID))
+
+ if err := c.db.CanRenameTo(oldUsername, newUsername); err != nil {
+ c.err = err
+ return true
+ }
+
+ managers.ActiveUsers.RemoveUser(user.ID)
+ user.Username = newUsername
+ user.DoSave(c.db)
+
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleNewMeme(c *Command) (handled bool) {
+ if m := memeRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ if c.upload == nil {
+ c.err = errors.New("no file uploaded")
+ return true
+ }
+ slug := m[1]
+ oldPath := filepath.Join(config.Global.ProjectUploadsPath(), c.upload.FileName)
+ newPath := filepath.Join(config.Global.ProjectMemesPath(), c.upload.FileName)
+ _ = os.Rename(oldPath, newPath)
+
+ if err := c.db.DB().Delete(&c.upload).Error; err != nil {
+ logrus.Error(err)
+ }
+
+ meme := database.Meme{
+ Slug: slug,
+ FileName: c.upload.FileName,
+ OrigFileName: c.upload.OrigFileName,
+ FileSize: c.upload.FileSize,
+ }
+ if err := c.db.DB().Create(&meme).Error; err != nil {
+ _ = os.Remove(newPath)
+ logrus.Error(err)
+ }
+
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleRemoveMeme(c *Command) (handled bool) {
+ if m := memeRemoveRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ slug := m[1]
+ meme, err := c.db.GetMemeBySlug(slug)
+ if err != nil {
+ c.err = errors.New("meme not found")
+ return true
+ }
+ if err := meme.Delete(c.db); err != nil {
+ c.err = err
+ return true
+ }
+ c.err = NewErrSuccess("meme removed")
+ return true
+ }
+ return
+}
+
+func handleRenameMeme(c *Command) (handled bool) {
+ if m := memeRenameRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ slug := m[1]
+ newSlug := m[2]
+ meme, err := c.db.GetMemeBySlug(slug)
+ if err != nil {
+ c.err = errors.New("meme not found")
+ return true
+ }
+ meme.Slug = newSlug
+ meme.DoSave(c.db)
+ c.err = NewErrSuccess("meme renamed")
+ return true
+ }
+ return
+}
+
+func handleListMemes(c *Command) (handled bool) {
+ if m := memesRgx.FindStringSubmatch(c.message); len(m) == 1 {
+ memes, _ := c.db.GetMemes()
+ msg := ""
+ for _, m := range memes {
+ msg += fmt.Sprintf(`<a href="/memes/%s" rel="noopener noreferrer" target="_blank">%s</a>`, m.Slug, m.Slug)
+ if c.authUser.IsAdmin {
+ msg += fmt.Sprintf(` (%s)`, humanize.Bytes(uint64(m.FileSize)))
+ }
+ msg += "<br />"
+ }
+ c.zeroMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleRefreshCmd(c *Command) (handled bool) {
+ if c.message == "/refresh" {
+ c.err = ErrRedirect
+ database.MsgPubSub.Pub(database.RefreshTopic, database.ChatMessageType{Typ: database.ForceRefresh})
+ return true
+ }
+ return
+}
+
+func handleCodeCmd(c *Command) (handled bool) {
+ if c.message == "/code" {
+ c.err = ErrRedirect
+ if !c.authUser.CanUseMultiline {
+ c.err = errors.New("multiline is disabled for your account")
+ return true
+ } else if !c.authUser.UseStream {
+ c.err = errors.New("only work on stream version of this chat")
+ return true
+ }
+ payload := database.ChatMessageType{}
+ if c.modMsg {
+ payload.IsMod = true
+ }
+ if c.toUser != nil {
+ toUserUsername := c.toUser.Username
+ payload.ToUserUsername = &toUserUsername
+ }
+ // ModalManager.Pub("code", payload)
+ database.MsgPubSub.Pub("modal_code_show_"+c.authUser.ID.String()+"_"+c.room.ID.String(), payload)
+ return true
+ }
+ return
+}
+
+func handleUpdateReadMarkerCmd(c *Command) (handled bool) {
+ if c.message == "/r" {
+ c.db.UpdateChatReadMarker(c.authUser.ID, c.room.ID)
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/snippetInterceptor.go b/pkg/web/handlers/api/v1/interceptors/snippetInterceptor.go
@@ -0,0 +1,57 @@
+package interceptors
+
+import (
+ "dkforest/pkg/database"
+ "dkforest/pkg/managers"
+ "strings"
+)
+
+type SnippetInterceptor struct{}
+
+func (i SnippetInterceptor) InterceptMsg(cmd *Command) {
+ // Snippets actually mutate the original message,
+ // to simulate that the user actually typed the text
+ cmd.origMessage = snippets(cmd.db, cmd.authUser.ID, cmd.origMessage)
+
+ cmd.origMessage = autocompleteTags(cmd.origMessage)
+
+ cmd.message = cmd.origMessage
+}
+
+func snippets(db *database.DkfDB, authUserID database.UserID, html string) string {
+ if snippetRgx.MatchString(html) {
+ userSnippets, _ := db.GetUserSnippets(authUserID)
+ if len(userSnippets) > 0 {
+ // Build hashmap for fast lookup
+ m := make(map[string]string)
+ for _, snippet := range userSnippets {
+ m["!"+snippet.Name] = snippet.Text
+ }
+ html = snippetRgx.ReplaceAllStringFunc(html, func(s string) string {
+ // If snippet name exists, use the mapped value
+ if v, ok := m[s]; ok {
+ return v
+ }
+ return s
+ })
+ }
+ }
+ return html
+}
+
+func autocompleteTags(html string) string {
+ activeUsers := managers.ActiveUsers.GetActiveUsers()
+ html = autoTagRgx.ReplaceAllStringFunc(html, func(s string) string {
+ s1 := strings.TrimPrefix(s, "@")
+ s1 = strings.TrimSuffix(s1, "*")
+ s1 = strings.ToLower(s1)
+ for _, au := range activeUsers {
+ l := strings.ToLower(string(au.Username))
+ if strings.HasPrefix(l, s1) {
+ return au.Username.AtStr()
+ }
+ }
+ return s
+ })
+ return html
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/spamInterceptor.go b/pkg/web/handlers/api/v1/interceptors/spamInterceptor.go
@@ -0,0 +1,265 @@
+package interceptors
+
+import (
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ dutils "dkforest/pkg/database/utils"
+ "dkforest/pkg/utils"
+ "errors"
+ "github.com/sirupsen/logrus"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+)
+
+type SpamInterceptor struct{}
+
+type Filter struct {
+ IsRegex bool
+ Term string
+ Rgx *regexp.Regexp
+ Kick bool
+ Hb bool
+}
+
+var filters []Filter
+var filtersMtx sync.RWMutex
+
+func LoadFilters(db *database.DkfDB) {
+ filtersMtx.Lock()
+ defer filtersMtx.Unlock()
+ filters = make([]Filter, 0)
+ dbFilters, _ := db.GetSpamFilters()
+ for _, dbFilter := range dbFilters {
+ f := Filter{IsRegex: dbFilter.IsRegex}
+ if dbFilter.Action == 1 {
+ f.Kick = true
+ } else if dbFilter.Action == 2 {
+ f.Hb = true
+ }
+ if dbFilter.IsRegex {
+ f.Rgx = regexp.MustCompile(dbFilter.Filter)
+ } else {
+ f.Term = dbFilter.Filter
+ }
+ filters = append(filters, f)
+ }
+}
+
+// Check the filters that we have in the database.
+func checkDynamicFilters(c *Command, lowerCaseMessage string, silentSelfKick bool) error {
+ filtersMtx.RLock()
+ defer filtersMtx.RUnlock()
+ for _, f := range filters {
+ isMatch := (f.IsRegex && f.Rgx.MatchString(c.message)) ||
+ (!f.IsRegex && strings.Contains(lowerCaseMessage, f.Term))
+ if isMatch {
+ if f.Hb {
+ dutils.SelfHellBan(c.db, c.authUser)
+ return ErrSilent
+ }
+ if f.Kick {
+ _ = dutils.SelfKick(c.db, *c.authUser, silentSelfKick)
+ }
+ return ErrSpamFilterTriggered
+ }
+ }
+ return nil
+}
+
+func (i SpamInterceptor) InterceptMsg(c *Command) {
+ lowerCaseMessage := strings.ToLower(c.message)
+ silentSelfKick := config.SilentSelfKick.Load()
+
+ if err := checkDynamicFilters(c, lowerCaseMessage, silentSelfKick); err != nil {
+ if !errors.Is(err, ErrSilent) {
+ c.err = err
+ }
+ return
+ }
+
+ if c.room.IsOfficialRoom() {
+ if err := checkSpam(c.db, c.origMessage, lowerCaseMessage, c.authUser); err != nil {
+ c.err = err
+ return
+ }
+ }
+
+ // Check CP links
+ if checkCPLinks(c.db, c.message) {
+ c.err = errors.New("forbidden url")
+ return
+ }
+
+ if !c.authUser.CanUseUppercase {
+ c.message = strings.ToLower(c.message)
+ }
+}
+
+var ErrSilent = errors.New("")
+var ErrSpamFilterTriggered = errors.New("spam filter triggered")
+
+func checkSpam(db *database.DkfDB, origMessage, lowerCaseMessage string, authUser *database.User) error {
+ silentSelfKick := config.SilentSelfKick.Load()
+
+ // Kick retard new users
+ if time.Since(authUser.CreatedAt) < 5*time.Hour {
+ if strings.Contains(lowerCaseMessage, "fucked up links") ||
+ strings.Contains(lowerCaseMessage, "i wanna see gore") ||
+ strings.Contains(lowerCaseMessage, "how can i make money") ||
+ strings.Contains(lowerCaseMessage, "any links for scary stuff") {
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+ }
+ if authUser.GeneralMessagesCount < 20 || time.Since(authUser.CreatedAt) < 5*time.Hour {
+ if strings.Contains(lowerCaseMessage, "cp link") {
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+ }
+
+ if strings.Contains(lowerCaseMessage, "#dorkforest") {
+ if authUser.IsModerator() {
+ return ErrSpamFilterTriggered
+ }
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+
+ // Auto kick upper case typing retards
+ if authUser.GeneralMessagesCount <= 5 {
+ count, total := utils.CountUppercase(origMessage)
+ pct := float64(count) / float64(total)
+ if total > 5 && pct > 0.8 {
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+ }
+
+ // Auto HB "new here"/"legit market" retards
+ if autoHellbanCheck(authUser, lowerCaseMessage) {
+ dutils.SelfHellBan(db, authUser)
+ return nil
+ }
+
+ if autoKickSpammers(authUser, lowerCaseMessage) {
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+
+ tot, wordsMap := utils.WordCount(lowerCaseMessage)
+ if tot >= 5 {
+ totalUniqueWords := len(wordsMap)
+ uniqueRatio := float64(totalUniqueWords) / float64(tot)
+ repeatedWordsCount := 0
+ for word, count := range wordsMap {
+ if len(word) >= 5 && count > 10 {
+ repeatedWordsCount++
+ }
+ }
+ retardRatio := float64(repeatedWordsCount) / float64(totalUniqueWords)
+ //fmt.Println(tot, totalUniqueWords, uniqueRatio, repeatedWordsCount, retardRatio, wordsMap)
+ if uniqueRatio < 0.2 {
+ logrus.Error("failed unique ratio: " + origMessage)
+ return errors.New("failed unique ratio")
+ }
+ if retardRatio > 0.1 {
+ logrus.Error("failed retard ratio: " + origMessage)
+ return errors.New("failed retard ratio")
+ }
+ }
+
+ if authUser.GeneralMessagesCount < 10 {
+ if autoKickProfanity(tot, wordsMap) {
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+ }
+
+ if authUser.GeneralMessagesCount < 4 {
+ if (wordsMap["need"] > 0 && wordsMap["help"] > 0) ||
+ (wordsMap["help"] > 0 && wordsMap["me"] > 0) ||
+ (wordsMap["make"] > 0 && wordsMap["money"] > 0) ||
+ wordsMap["porn"] > 0 ||
+ wordsMap["murder"] > 0 {
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+ }
+
+ if authUser.GeneralMessagesCount < 10 {
+ if ((wordsMap["learn"] > 0 || wordsMap["teach"] > 0) && (wordsMap["hacking"] > 0 || wordsMap["hack"] > 0)) ||
+ (wordsMap["cook"] > 0 && wordsMap["meth"] > 0) ||
+ (wordsMap["creepy"] > 0 && (wordsMap["site"] > 0 || wordsMap["sites"] > 0)) ||
+ (wordsMap["porn"] > 0 && (wordsMap["link"] > 0 || wordsMap["links"] > 0)) ||
+ (wordsMap["topic"] > 0 && wordsMap["link"] > 0) {
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+ }
+
+ if authUser.GeneralMessagesCount < 20 || time.Since(authUser.CreatedAt) < 5*time.Hour {
+ if wordsMap["cp"] > 0 && (wordsMap["link"] > 0 || wordsMap["links"] > 0) {
+ _ = dutils.SelfKick(db, *authUser, silentSelfKick)
+ return ErrSpamFilterTriggered
+ }
+ }
+
+ return nil
+}
+
+func autoKickProfanityTmp(orig string) bool {
+ tot, m := utils.WordCount(strings.ToLower(orig))
+ return autoKickProfanity(tot, m)
+}
+
+func autoKickProfanity(tot int, wordsMap map[string]int) bool {
+ if tot > 4 && countProfanity(wordsMap) >= 4 {
+ return true
+ }
+ return false
+}
+
+func countProfanity(wordsMap map[string]int) int {
+ profanityWords := []string{"anus", "asshole", "cock", "dick", "nigger", "niggers", "nigga", "niggas", "sex", "rape", "porn",
+ "cunt", "murder", "fuck", "blood", "corpse", "hole", "slut", "bitch", "shit", "poop", "butt", "faggot",
+ "submissive", "slurping", "suck", "nuts", "gore", "stupid", "dumb", "jerking", "rotten", "rotted", "stinky"}
+ profanity := 0
+ for _, w := range profanityWords {
+ if n, ok := wordsMap[w]; ok {
+ profanity += n
+ }
+ }
+ return profanity
+}
+
+var spamCharsRgx = regexp.MustCompile("[^a-z0-9]+")
+
+func autoKickSpammers(authUser *database.User, lowerCaseMessage string) bool {
+ if authUser.GeneralMessagesCount <= 10 {
+ processedString := spamCharsRgx.ReplaceAllString(lowerCaseMessage, "")
+ return strings.Contains(processedString, "lemybeauty") ||
+ strings.Contains(processedString, "blacktorcc") ||
+ strings.Contains(processedString, "profjerry") ||
+ strings.Contains(processedString, "shopdarkse")
+ }
+ return false
+}
+
+func autoHellbanCheck(authUser *database.User, lowerCaseMessage string) bool {
+ checks := []string{
+ "new here",
+ "legit market",
+ "help me",
+ }
+ if authUser.GeneralMessagesCount <= 5 {
+ for _, check := range checks {
+ if strings.Contains(lowerCaseMessage, check) {
+ return true
+ }
+ }
+ }
+ return false
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/spamInterceptor_test.go b/pkg/web/handlers/api/v1/interceptors/spamInterceptor_test.go
@@ -0,0 +1,69 @@
+package interceptors
+
+import (
+ "dkforest/pkg/database"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func Test_autoHellbanCheck(t *testing.T) {
+ type args struct {
+ authUser *database.User
+ lowerCaseMessage string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {name: "", args: args{authUser: &database.User{GeneralMessagesCount: 2}, lowerCaseMessage: "hi new here"}, want: true},
+ {name: "", args: args{authUser: &database.User{GeneralMessagesCount: 2}, lowerCaseMessage: "hello anybody know of any legit market places ? its getting tough on here to find any that actually do what they supposed to "}, want: true},
+ {name: "", args: args{authUser: &database.User{GeneralMessagesCount: 2}, lowerCaseMessage: "Hello Guys and Ladys someone can help me? I Have a Little problem.."}, want: true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equalf(t, tt.want, autoHellbanCheck(tt.args.authUser, tt.args.lowerCaseMessage), "autoHellbanCheck(%v, %v)", tt.args.authUser, tt.args.lowerCaseMessage)
+ })
+ }
+}
+
+func Test_autoKickSpammers(t *testing.T) {
+ type args struct {
+ authUser *database.User
+ lowerCaseMessage string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {name: "", args: args{authUser: &database.User{GeneralMessagesCount: 2}, lowerCaseMessage: "blablabla l e m y _ b e a u t y on "}, want: true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equalf(t, tt.want, autoKickSpammers(tt.args.authUser, tt.args.lowerCaseMessage), "autoKickSpammers(%v, %v)", tt.args.authUser, tt.args.lowerCaseMessage)
+ })
+ }
+}
+
+func Test_autoKickProfanityTmp(t *testing.T) {
+ type args struct {
+ orig string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {"", args{orig: "biden is dumb fuck can suck my dick stupid nigger"}, true},
+ {"", args{orig: "u can suck his nuts like the submissive faggot u are. slurping eye contact for deep man love with dirty butthole sniffing."}, true},
+ {"", args{orig: "how to tear a human slut bitch from the cunt to the part in her hairline then shit into the chest cavity for happy dumpling poop soup"}, true},
+ {"", args{orig: "lets murder a nun and fuck the blood scabs into her corpse pussy hole"}, true},
+ {"", args{orig: "quick question, whats the best method to plant a grenaed in old ladys stinky rotted cunt hole to blast her bloods on a hotel walls"}, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equalf(t, tt.want, autoKickProfanityTmp(tt.args.orig), "autoKickProfanityTmp(%v)", tt.args.orig)
+ })
+ }
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/uploadInterceptor.go b/pkg/web/handlers/api/v1/interceptors/uploadInterceptor.go
@@ -0,0 +1,77 @@
+package interceptors
+
+import (
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ "dkforest/pkg/utils"
+ hutils "dkforest/pkg/web/handlers/utils"
+ "errors"
+ "fmt"
+ "github.com/asaskevich/govalidator"
+ "github.com/dustin/go-humanize"
+ "github.com/sirupsen/logrus"
+ "io/ioutil"
+ "mime/multipart"
+)
+
+type UploadInterceptor struct{}
+
+func (i UploadInterceptor) InterceptMsg(cmd *Command) {
+ if file, handler, uploadErr := cmd.c.Request().FormFile("file"); uploadErr == nil {
+ // Save file on disk & database & append file link to html
+ var err error
+ cmd.upload, err = handleUploadedFile(cmd.db, file, handler, cmd.authUser)
+ if err != nil {
+ cmd.err = err
+ return
+ }
+ }
+}
+
+func handleUploadedFile(db *database.DkfDB, file multipart.File, handler *multipart.FileHeader, authUser *database.User) (*database.Upload, error) {
+ defer file.Close()
+ if !authUser.CanUpload() {
+ return nil, hutils.AccountTooYoungErr
+ }
+ userSizeUploaded := db.GetUserTotalUploadSize(authUser.ID)
+ if handler.Size+userSizeUploaded > config.MaxUserTotalUploadSize {
+ return nil, fmt.Errorf("user upload limit reached (%s)", humanize.Bytes(config.MaxUserTotalUploadSize))
+ }
+ origFileName := handler.Filename
+ if handler.Size > config.MaxUserFileUploadSize {
+ return nil, fmt.Errorf("the maximum file size is %s", humanize.Bytes(config.MaxUserFileUploadSize))
+ }
+ if !govalidator.StringLength(origFileName, "3", "50") {
+ return nil, errors.New("invalid file name, 3-50 characters")
+ }
+ if !govalidator.IsPrintableASCII(origFileName) {
+ return nil, errors.New("file name must be ascii printable only")
+ }
+ origFileName = tzRgx.ReplaceAllString(origFileName, "xxxx-xx-xx at xx.xx.xx XX")
+ origFileName = tz1Rgx.ReplaceAllString(origFileName, "xxxx-xx-xx xx-xx-xx")
+ origFileName = tz3Rgx.ReplaceAllString(origFileName, "xxxx-xx-xx xxxxxx")
+ origFileName = tz4Rgx.ReplaceAllString(origFileName, "xxxx-xx-xx_xx_xx_xx")
+ fileBytes, err := ioutil.ReadAll(file)
+ if err != nil {
+ return nil, err
+ }
+
+ // Validate image type and determine extension
+ mimeType := handler.Header.Get("Content-Type")
+ if mimeType == "image/jpeg" {
+ fileBytes, err = utils.ReencodeJpg(fileBytes)
+ } else if mimeType == "image/png" {
+ fileBytes, err = utils.ReencodePng(fileBytes)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ // Uploaded files are encrypted on disk
+ upload, err := db.CreateEncryptedUploadWithSize(origFileName, fileBytes, authUser.ID, handler.Size)
+ if err != nil {
+ logrus.Error(err)
+ return nil, err
+ }
+ return upload, nil
+}
diff --git a/pkg/web/handlers/api/v1/interceptors/werewolf.go b/pkg/web/handlers/api/v1/interceptors/werewolf.go
@@ -0,0 +1,701 @@
+package interceptors
+
+import (
+ "bytes"
+ "context"
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ "dkforest/pkg/hashset"
+ "dkforest/pkg/utils"
+ "errors"
+ "fmt"
+ "github.com/sirupsen/logrus"
+ "html/template"
+ "math/rand"
+ "sort"
+ "strings"
+ "time"
+)
+
+var WWInstance *Werewolf
+
+const (
+ PreGameState = iota + 1
+ DayState
+ NightState
+ VoteState
+ EndGameState
+)
+
+const (
+ TownspeopleRole = "townspeople"
+ WerewolfRole = "werewolf"
+ SeerRole = "seer"
+ HealerRole = "healer"
+)
+
+var ErrInvalidPlayerName = errors.New("unknown player name, please send a valid name")
+
+type Werewolf struct {
+ db *database.DkfDB
+ ctx context.Context
+ cancel context.CancelFunc
+ readyCh chan bool
+ narratorID database.UserID
+ roomID database.RoomID
+ werewolfGroupID database.GroupID
+ spectatorGroupID database.GroupID
+ deadGroupID database.GroupID
+ players map[database.Username]*Player
+ playersAlive map[database.Username]*Player
+ state int64
+ werewolfSet *hashset.HashSet[database.UserID]
+ spectatorSet *hashset.HashSet[database.UserID]
+ townspersonSet *hashset.HashSet[database.UserID]
+ healerID *database.UserID
+ seerID *database.UserID
+ werewolfCh chan string
+ seerCh chan string
+ healerCh chan string
+ votesCh chan string
+ voted *hashset.HashSet[database.UserID] // Keep track of which user voted already
+}
+
+// Return either or not the userID is an active player (alive)
+func (b *Werewolf) isAlivePlayer(userID database.UserID) bool {
+ for _, player := range b.playersAlive {
+ if player.UserID == userID {
+ return true
+ }
+ }
+ return false
+}
+
+func (b *Werewolf) InterceptPreGameMsg(cmd *Command) {
+ if cmd.message == "/players" {
+ b.Narrate("Registered players: "+b.alivePlayersStr(), nil, nil)
+ cmd.err = ErrRedirect
+ return
+
+ } else if cmd.message == "/join" {
+ if cmd.authUser.IsHellbanned {
+ cmd.err = ErrRedirect
+ return
+ }
+ if _, found := b.players[cmd.authUser.Username]; found {
+ cmd.err = ErrRedirect
+ return
+ }
+ player := &Player{
+ UserID: cmd.authUser.ID,
+ Username: cmd.authUser.Username,
+ }
+ b.players[cmd.authUser.Username] = player
+ b.playersAlive[cmd.authUser.Username] = player
+ b.Narrate(cmd.authUser.Username.AtStr()+" joined the Game", nil, nil)
+ cmd.err = ErrRedirect
+ return
+
+ } else if cmd.message == "/spectate" {
+ b.spectatorSet.Insert(cmd.authUser.ID)
+ b.Narrate(cmd.authUser.Username.AtStr()+" spectate the Game", nil, nil)
+ cmd.err = ErrRedirect
+ return
+
+ } else if cmd.message == "/start" {
+ b.cancel()
+ time.Sleep(time.Second)
+ utils.SGo(func() {
+ b.StartGame(cmd.db)
+ })
+ cmd.err = ErrRedirect
+ return
+ }
+}
+
+func (b *Werewolf) InterceptNightMsg(cmd *Command) {
+ if cmd.groupID != nil && *cmd.groupID == b.werewolfGroupID {
+ select {
+ case b.werewolfCh <- cmd.message:
+ cmd.err = ErrRedirect
+ default:
+ cmd.err = errors.New("narrator doesn't need your input")
+ }
+ return
+ } else if b.isForNarrator(cmd) && b.seerID != nil && cmd.authUser.ID == *b.seerID {
+ select {
+ case b.seerCh <- cmd.message:
+ cmd.err = ErrRedirect
+ default:
+ cmd.err = errors.New("narrator doesn't need your input")
+ }
+ return
+ } else if b.isForNarrator(cmd) && b.healerID != nil && cmd.authUser.ID == *b.healerID {
+ select {
+ case b.healerCh <- cmd.message:
+ cmd.err = ErrRedirect
+ default:
+ cmd.err = errors.New("narrator doesn't need your input")
+ }
+ return
+ }
+ cmd.err = errors.New("chat disabled")
+ return
+}
+
+// Return either or not the message is a PM for the narrator
+func (b *Werewolf) isForNarrator(cmd *Command) bool {
+ return cmd.toUser != nil && cmd.toUser.ID == b.narratorID
+}
+
+func (b *Werewolf) InterceptVoteMsg(cmd *Command) {
+ if !b.isAlivePlayer(cmd.authUser.ID) || !b.isForNarrator(cmd) {
+ cmd.err = errors.New("chat disabled")
+ return
+ }
+ if b.isForNarrator(cmd) {
+ if !b.voted.Contains(cmd.authUser.ID) {
+ name := cmd.message
+ if b.isValidPlayerName(name) {
+ b.votesCh <- name
+ } else {
+ b.Narrate(ErrInvalidPlayerName.Error(), &cmd.authUser.ID, nil)
+ }
+ } else {
+ b.Narrate("You have already voted", &cmd.authUser.ID, nil)
+ }
+ }
+}
+
+var tuto = `Tutorial:
+"/join" to join the Game
+"/players" list the players that have joined the Game
+"/start" to start the Game
+"/stop" to stop the Game
+"/ready" will skip the 5min conversation
+"/tuto" will display this tutorial
+"/clear" will reset the room and display this tutorial
+
+Werewolf: To kill someone during the night, you have to reply in the "werewolf" group with the name of the person to kill (no @)
+Seer/Healer: You have reply to the narrator with the name (eg: "/pm 0 n0tr1v")
+Townspeople: To vote, you have to pm the narrator with a name (eg: "/pm 0 n0tr1v")`
+
+func (b *Werewolf) InterceptMsg(cmd *Command) {
+ if cmd.room.ID != b.roomID {
+ return
+ }
+
+ SlashInterceptor{}.InterceptMsg(cmd)
+
+ // If the message is a PM not for the narrator, we reject it
+ if cmd.toUser != nil && (cmd.toUser.ID != b.narratorID && cmd.authUser.ID != b.narratorID) {
+ cmd.err = errors.New("PM not allowed at this room")
+ return
+ }
+
+ // Spectator can chat all the time
+ if cmd.groupID != nil && *cmd.groupID == b.spectatorGroupID {
+ return
+ }
+
+ if cmd.authUser.IsModerator() && cmd.message == "/stop" {
+ b.Narrate(fmt.Sprintf("@%s used /stop", cmd.authUser.Username), nil, nil)
+ b.cancel()
+ cmd.err = ErrRedirect
+ return
+ } else if cmd.authUser.IsModerator() && cmd.message == "/ready" {
+ b.Narrate(fmt.Sprintf("@%s used /ready", cmd.authUser.Username), nil, nil)
+ b.readyCh <- true
+ cmd.err = ErrRedirect
+ return
+ } else if cmd.authUser.IsModerator() && cmd.message == "/tuto" {
+ b.Narrate(tuto, nil, nil)
+ cmd.err = ErrRedirect
+ return
+ } else if cmd.authUser.IsModerator() && cmd.message == "/clear" {
+ _ = cmd.db.DeleteChatRoomMessages(b.roomID)
+ b.Narrate(tuto, nil, nil)
+ cmd.err = ErrRedirect
+ return
+ }
+
+ // Anyone can talk during these states
+ if b.state == PreGameState || b.state == EndGameState {
+ if b.state == PreGameState {
+ b.InterceptPreGameMsg(cmd)
+ }
+ return
+ }
+
+ // Otherwise, non-playing people cannot talk in public chat
+ if !b.isAlivePlayer(cmd.authUser.ID) {
+ cmd.err = errors.New("public chat disabled")
+ return
+ }
+
+ switch b.state {
+ case DayState:
+ case VoteState:
+ b.InterceptVoteMsg(cmd)
+ case NightState:
+ b.InterceptNightMsg(cmd)
+ default:
+ cmd.err = errors.New("public chat disabled")
+ return
+ }
+}
+
+// Wait until we receive the votes from all the players
+func (b *Werewolf) waitVotes() (votes []string) {
+ for len(votes) < len(b.playersAlive) {
+ var vote string
+ select {
+ case vote = <-b.votesCh:
+ case <-time.After(15 * time.Second):
+ b.Narrate(fmt.Sprintf("Waiting votes %d/%d", len(votes), len(b.playersAlive)), nil, nil)
+ continue
+ case <-b.ctx.Done():
+ return
+ }
+ votes = append(votes, vote)
+ }
+ return
+}
+
+func (b *Werewolf) waitNameFromWerewolf() (name string) {
+ for {
+ select {
+ case name = <-b.werewolfCh:
+ case <-time.After(15 * time.Second):
+ b.Narrate("Waiting reply from werewolf", nil, nil)
+ continue
+ case <-b.ctx.Done():
+ return
+ }
+ if b.isValidPlayerName(name) {
+ break
+ }
+ b.Narrate(ErrInvalidPlayerName.Error(), nil, &b.werewolfGroupID)
+ }
+ return name
+}
+
+func (b *Werewolf) waitNameFromSeer() (name string) {
+ for {
+ select {
+ case name = <-b.seerCh:
+ case <-time.After(15 * time.Second):
+ b.Narrate("Waiting reply from seer", nil, nil)
+ continue
+ case <-b.ctx.Done():
+ return
+ }
+ if b.isValidPlayerName(name) {
+ break
+ }
+ b.Narrate(ErrInvalidPlayerName.Error(), b.seerID, nil)
+ }
+ return name
+}
+
+func (b *Werewolf) waitNameFromHealer() (name string) {
+ for {
+ select {
+ case name = <-b.healerCh:
+ case <-time.After(15 * time.Second):
+ b.Narrate("Waiting reply from healer", nil, nil)
+ continue
+ case <-b.ctx.Done():
+ return
+ }
+ if b.isValidPlayerName(name) {
+ break
+ }
+ b.Narrate(ErrInvalidPlayerName.Error(), b.healerID, nil)
+ }
+ return name
+}
+
+// Return either a name is a valid alive player name or not
+func (b *Werewolf) isValidPlayerName(name string) bool {
+ name = strings.TrimSpace(name)
+ for _, player := range b.playersAlive {
+ if string(player.Username) == name {
+ return true
+ }
+ }
+ return false
+}
+
+// Narrate register a chat message on behalf of the narrator user
+func (b *Werewolf) Narrate(msg string, toUserID *database.UserID, groupID *database.GroupID) {
+ html, _, _ := ProcessRawMessage(b.db, msg, "", b.narratorID, b.roomID, nil, true)
+ b.NarrateRaw(html, toUserID, groupID)
+}
+
+func (b *Werewolf) NarrateRaw(msg string, toUserID *database.UserID, groupID *database.GroupID) {
+ _, _ = b.db.CreateOrEditMessage(nil, msg, msg, "", b.roomID, b.narratorID, toUserID, nil, groupID, false, false, false)
+}
+
+// Display roles assigned at beginning of the Game
+func (b *Werewolf) displayRoles() {
+ msg := "Roles were:\n"
+ for _, player := range b.players {
+ msg += player.Username.AtStr() + " : " + player.Role + "\n"
+ }
+ b.Narrate(msg, nil, nil)
+}
+
+func (b *Werewolf) StartGame(db *database.DkfDB) {
+ defer func() {
+ b.displayRoles()
+ b.reset()
+ }()
+ b.ctx, b.cancel = context.WithCancel(context.Background())
+ // Assign roles
+ playersArr := make([]*Player, 0)
+ for _, player := range b.playersAlive {
+ playersArr = append(playersArr, player)
+ }
+ rand.Shuffle(len(playersArr), func(i, j int) { playersArr[i], playersArr[j] = playersArr[j], playersArr[i] })
+ for idx, player := range playersArr {
+ if idx == 0 {
+ b.werewolfSet.Insert(player.UserID)
+ _, _ = db.AddUserToRoomGroup(b.roomID, b.werewolfGroupID, player.UserID)
+ player.Role = WerewolfRole
+ werewolfMsg := "During the day you seem to be a regular Townsperson.\n" +
+ "However, you’ve been kissed by the Night and transform into a Werewolf when the sun sets.\n" +
+ "Your new nature compels you to kill and eat a Townsperson every night."
+ b.Narrate(werewolfMsg, &player.UserID, nil)
+ } else if idx == 1 {
+ b.townspersonSet.Insert(player.UserID)
+ b.healerID = &player.UserID
+ player.Role = HealerRole
+ healerMsg := "You’re a Townsperson with the unique ability to save lives.\n" +
+ "During the night, you’ll get a chance to protect another Townsperson from death if they are attacked by the Werewolves.\n" +
+ "You can choose to protect yourself."
+ b.Narrate(healerMsg, &player.UserID, nil)
+ } else if idx == 2 {
+ b.townspersonSet.Insert(player.UserID)
+ b.seerID = &player.UserID
+ player.Role = SeerRole
+ seerMsg := "You’re a Townsperson with the unique ability to peer into a person’s soul and see their true nature.\n" +
+ "During the night, you’ll get a chance to see if another Townsperson is a Werewolf.\n" +
+ "However, use this information wisely because it can lead to you being targeted by the Werewolves the next night if they deduce your identity."
+ b.Narrate(seerMsg, &player.UserID, nil)
+ } else {
+ b.townspersonSet.Insert(player.UserID)
+ player.Role = TownspeopleRole
+ townspersonMsg := "You’re a regular member of the town.\n" +
+ "Perhaps you’re a baker, merchant, or soldier.\n" +
+ "Your job is to save the town by eliminating the Werewolves that have infiltrated your town and started feeding on your neighbors.\n" +
+ "Also, try to avoid getting killed yourself."
+ b.Narrate(townspersonMsg, &player.UserID, nil)
+ }
+ }
+ b.state = DayState
+ b.Narrate("Players: "+b.alivePlayersStr(), nil, nil)
+ b.Narrate("Day 1: It is day time. Players can now introduce themselves. (5min)", nil, nil)
+
+ select {
+ case <-time.After(5 * time.Minute):
+ case <-b.readyCh:
+ case <-b.ctx.Done():
+ b.Narrate("STOP SIGNAL - Game is being stopped", nil, nil)
+ return
+ }
+
+ for {
+ b.state = NightState
+ b.Narrate("Townspeople, go to sleep", nil, nil)
+ playerNameToKill := b.processWerewolf()
+ b.processSeer()
+ playerNameToSave := b.processHealer()
+
+ b.state = DayState
+ b.Narrate("Townspeople, wake up", nil, nil)
+ if playerNameToKill == playerNameToSave {
+ b.Narrate("Someone was attacked last night, but they survived", nil, nil)
+ } else {
+ b.Narrate("Everyone wakes up to see a trail of blood leading to the forest.\n"+
+ "There you find @"+playerNameToKill+"’s mangled remains by the Great Oak.\n"+
+ "Curiously, there are deep claw marks in the bark of the surrounding trees.\n"+
+ "It looks like @"+playerNameToKill+" put up a fight.", nil, nil)
+ b.kill(db, database.Username(playerNameToKill))
+ }
+
+ b.Narrate("Players still alive: "+b.alivePlayersStr(), nil, nil)
+ if b.werewolfSet.Len() == 0 {
+ b.Narrate("Townspeople win", nil, nil)
+ break
+ } else if b.townspersonSet.Len() <= 1 {
+ b.Narrate("Werewolf win", nil, nil)
+ break
+ }
+
+ b.Narrate("Townspeople now have 5min to discuss the events", nil, nil)
+
+ select {
+ case <-time.After(5 * time.Minute):
+ case <-b.readyCh:
+ case <-b.ctx.Done():
+ b.Narrate("STOP SIGNAL - Game is being stopped", nil, nil)
+ return
+ }
+
+ b.state = VoteState
+ b.voted = hashset.New[database.UserID]()
+ b.Narrate("It's now time to vote for execution. PM me the name you vote to execute or \"none\"", nil, nil)
+ killName := b.killVote()
+ if killName == "" {
+ b.Narrate("Townspeople do not want to execute anyone", nil, nil)
+ } else {
+ b.Narrate("Townspeople execute @"+killName, nil, nil)
+ b.kill(db, database.Username(killName))
+ }
+
+ b.Narrate("Players still alive: "+b.alivePlayersStr(), nil, nil)
+
+ if b.werewolfSet.Len() == 0 {
+ b.Narrate("Townspeople win", nil, nil)
+ break
+ } else if b.townspersonSet.Len() == 1 {
+ b.Narrate("Werewolf win", nil, nil)
+ break
+ }
+ }
+ b.state = EndGameState
+ b.Narrate("Game ended", nil, nil)
+}
+
+// Return the names of alive players. ie: "user1, user2, user3"
+func (b *Werewolf) alivePlayersStr() (out string) {
+ arr := make([]string, 0)
+ for _, player := range b.playersAlive {
+ arr = append(arr, player.Username.AtStr())
+ }
+ sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })
+ return strings.Join(arr, ", ")
+}
+
+// Kill a player
+func (b *Werewolf) kill(db *database.DkfDB, playerName database.Username) {
+ player, found := b.playersAlive[playerName]
+ if !found {
+ return
+ }
+ delete(b.playersAlive, playerName)
+ switch player.Role {
+ case WerewolfRole:
+ b.werewolfSet.Remove(player.UserID)
+ _ = db.RmUserFromRoomGroup(b.roomID, b.werewolfGroupID, player.UserID)
+ case TownspeopleRole:
+ b.townspersonSet.Remove(player.UserID)
+ case HealerRole:
+ b.townspersonSet.Remove(player.UserID)
+ b.healerID = nil
+ case SeerRole:
+ b.townspersonSet.Remove(player.UserID)
+ b.seerID = nil
+ }
+ _, _ = db.AddUserToRoomGroup(b.roomID, b.deadGroupID, player.UserID)
+}
+
+// Return the name of the player name that receive the most vote
+func (b *Werewolf) killVote() string {
+
+ // Send a PM to all players saying they have to vote for a name
+ for _, player := range b.playersAlive {
+ msg := "Who do you vote to kill? (name | none)"
+ msg += b.createKillVoteForm()
+ b.NarrateRaw(msg, &player.UserID, nil)
+ }
+
+ votes := b.waitVotes()
+ // Get the max voted name
+ maxName := "none"
+ maxCount := 0
+ voteMap := make(map[string]int) // keep track of how many votes for each values
+ for _, vote := range votes {
+ tmp := voteMap[vote]
+ tmp++
+ voteMap[vote] = tmp
+ if tmp > maxCount {
+ maxCount = tmp
+ maxName = vote
+ }
+ }
+ if maxName == "none" {
+ return ""
+ }
+ return maxName
+}
+
+func (b *Werewolf) getAlivePlayersArr(includeWerewolves bool) []database.Username {
+ arr := make([]database.Username, 0)
+ for _, player := range b.playersAlive {
+ if !includeWerewolves && b.werewolfSet.Contains(player.UserID) {
+ continue
+ }
+ arr = append(arr, player.Username)
+ }
+ sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })
+ return arr
+}
+
+func (b *Werewolf) createPickUserForm() string {
+ arr := b.getAlivePlayersArr(true)
+
+ htmlTmpl := `
+<form method="post" action="/api/v1/werewolf">
+ {{ range $idx, $p := .Arr }}
+ <input type="radio" ID="player{{ $idx }}" name="message" value="/pm 0 {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
+ {{ end }}
+ <button type="submit" name="btn_submit">ok</button>
+</form>`
+ data := map[string]any{
+ "Arr": arr,
+ }
+ var buf bytes.Buffer
+ _ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
+ return buf.String()
+}
+
+func (b *Werewolf) createKillVoteForm() string {
+ arr := b.getAlivePlayersArr(true)
+
+ htmlTmpl := `
+<form method="post" action="/api/v1/werewolf">
+ {{ range $idx, $p := .Arr }}
+ <input type="radio" ID="player{{ $idx }}" name="message" value="/pm 0 {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
+ {{ end }}
+ <input type="radio" ID="none" name="message" value="/pm 0 none" /><label for="none">none</label><br />
+ <button type="submit" name="btn_submit">ok</button>
+</form>`
+ data := map[string]any{
+ "Arr": arr,
+ }
+ var buf bytes.Buffer
+ _ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
+ return buf.String()
+}
+
+func (b *Werewolf) createWerewolfPickUserForm() string {
+ arr := b.getAlivePlayersArr(false)
+
+ htmlTmpl := `
+<form method="post" action="/api/v1/werewolf">
+ {{ range $idx, $p := .Arr }}
+ <input type="radio" ID="player{{ $idx }}" name="message" value="/g werewolf {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
+ {{ end }}
+ <button type="submit" name="btn_submit">ok</button>
+</form>`
+ data := map[string]any{
+ "Arr": arr,
+ }
+ var buf bytes.Buffer
+ _ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
+ return buf.String()
+}
+
+func (b *Werewolf) processWerewolf() string {
+ b.UnlockGroup("werewolf")
+ msg := "Werewolf, who do you want to kill?"
+ msg += b.createWerewolfPickUserForm()
+ b.NarrateRaw(msg, nil, &b.werewolfGroupID)
+ name := b.waitNameFromWerewolf()
+ b.Narrate(name+" will be killed", nil, &b.werewolfGroupID)
+ b.LockGroup("werewolf")
+ return name
+}
+
+func (b *Werewolf) processSeer() {
+ if b.seerID == nil {
+ return
+ }
+ msg := "Seer, who do you want to identify?"
+ msg += b.createPickUserForm()
+ b.NarrateRaw(msg, b.seerID, nil)
+ name := b.waitNameFromSeer()
+ player := b.playersAlive[database.Username(name)]
+ b.Narrate(name+" is a "+player.Role, b.seerID, nil)
+}
+
+func (b *Werewolf) processHealer() string {
+ if b.healerID == nil {
+ return ""
+ }
+ msg := "Healer, who do you want to save?"
+ msg += b.createPickUserForm()
+ b.NarrateRaw(msg, b.healerID, nil)
+ name := b.waitNameFromHealer()
+ b.Narrate(name+" will survive the night", b.healerID, nil)
+ return name
+}
+
+func (b *Werewolf) LockGroups() {
+ b.LockGroup("werewolf")
+}
+
+func (b *Werewolf) LockGroup(groupName string) {
+ group, _ := b.db.GetRoomGroupByName(b.roomID, groupName)
+ group.Locked = true
+ group.DoSave(b.db)
+}
+
+func (b *Werewolf) UnlockGroup(groupName string) {
+ group, _ := b.db.GetRoomGroupByName(b.roomID, groupName)
+ group.Locked = false
+ group.DoSave(b.db)
+}
+
+type Player struct {
+ UserID database.UserID
+ Username database.Username
+ Role string
+}
+
+func (b *Werewolf) reset() {
+ b.ctx, b.cancel = context.WithCancel(context.Background())
+ b.state = PreGameState
+ b.players = make(map[database.Username]*Player)
+ b.playersAlive = make(map[database.Username]*Player)
+ b.werewolfSet = hashset.New[database.UserID]()
+ b.spectatorSet = hashset.New[database.UserID]()
+ b.townspersonSet = hashset.New[database.UserID]()
+ b.voted = hashset.New[database.UserID]()
+ b.werewolfCh = make(chan string)
+ b.seerCh = make(chan string)
+ b.healerCh = make(chan string)
+ b.votesCh = make(chan string)
+ b.readyCh = make(chan bool)
+ _ = b.db.ClearRoomGroup(b.roomID, b.werewolfGroupID)
+ _ = b.db.ClearRoomGroup(b.roomID, b.spectatorGroupID)
+ _ = b.db.ClearRoomGroup(b.roomID, b.deadGroupID)
+}
+
+func NewWerewolf(db *database.DkfDB) *Werewolf {
+ // Prepare room
+ room, err := db.GetChatRoomByName("werewolf")
+ if err != nil {
+ logrus.Error("#werewolf room not found")
+ return nil
+ }
+ zeroUser, _ := db.GetUserByUsername(config.NullUsername)
+ _ = db.DeleteChatRoomGroups(room.ID)
+ werewolfGroup, _ := db.CreateChatRoomGroup(room.ID, "werewolf", "#ffffff")
+ werewolfGroup.Locked = true
+ werewolfGroup.DoSave(db)
+ spectatorGroup, _ := db.CreateChatRoomGroup(room.ID, "spectator", "#ffffff")
+ deadGroup, _ := db.CreateChatRoomGroup(room.ID, "dead", "#ffffff")
+
+ b := new(Werewolf)
+ b.db = db
+ b.werewolfGroupID = werewolfGroup.ID
+ b.spectatorGroupID = spectatorGroup.ID
+ b.deadGroupID = deadGroup.ID
+ b.narratorID = zeroUser.ID
+ b.roomID = room.ID
+ b.reset()
+ return b
+}
diff --git a/pkg/web/handlers/api/v1/msgInterceptor.go b/pkg/web/handlers/api/v1/msgInterceptor.go
@@ -1,196 +0,0 @@
-package v1
-
-import (
- "dkforest/pkg/config"
- "dkforest/pkg/database"
- "dkforest/pkg/managers"
- "dkforest/pkg/utils"
- "errors"
- "fmt"
- "github.com/microcosm-cc/bluemonday"
- html2 "html"
- "strings"
-)
-
-type MsgInterceptor struct{}
-
-func (i MsgInterceptor) InterceptMsg(cmd *Command) {
- if cmd.room.ReadOnly {
- if cmd.room.OwnerUserID != nil && *cmd.room.OwnerUserID != cmd.authUser.ID {
- cmd.err = fmt.Errorf("room is read-only")
- return
- }
- }
-
- // Only check maximum length of message if we are uploading a file
- // Trim whitespaces and ensure minimum length
- minLen := utils.Ternary(cmd.upload != nil, 0, minMsgLen)
- if !utils.ValidateRuneLength(strings.TrimSpace(cmd.message), minLen, maxMsgLen) {
- cmd.dataMessage = cmd.origMessage
- cmd.err = fmt.Errorf("%d - %d characters", minLen, maxMsgLen)
- return
- }
-
- html, taggedUsersIDsMap, err := ProcessRawMessage(cmd.db, cmd.message, cmd.roomKey, cmd.authUser.ID, cmd.room.ID, cmd.upload, cmd.authUser.CanUseMultiline)
- if err != nil {
- cmd.dataMessage = cmd.origMessage
- cmd.err = err
- return
- }
-
- if len(strings.TrimSpace(html)) <= len("<p></p>") {
- cmd.dataMessage = cmd.origMessage
- cmd.err = errors.New("empty message")
- return
- }
-
- toUserID := database.UserPtrID(cmd.toUser)
-
- msgID, _ := cmd.db.CreateOrEditMessage(cmd.editMsg, html, cmd.origMessage, cmd.roomKey, cmd.room.ID, cmd.authUser.ID, toUserID, cmd.upload, cmd.groupID, cmd.hellbanMsg, cmd.modMsg, cmd.systemMsg)
-
- if !cmd.skipInboxes {
- sendInboxes(cmd.db, cmd.room, cmd.authUser, cmd.toUser, msgID, cmd.groupID, html, cmd.modMsg, taggedUsersIDsMap)
- }
-
- // Count public messages in #general room
- if cmd.room.ID == config.GeneralRoomID && cmd.toUser == nil {
- cmd.authUser.GeneralMessagesCount++
- generalRoomKarma(cmd.db, cmd.authUser)
- cmd.authUser.DoSave(cmd.db)
- }
-
- // Update chat read marker
- cmd.db.UpdateChatReadMarker(cmd.authUser.ID, cmd.room.ID)
-
- // Update user activity
- isPM := cmd.toUser != nil
- updateUserActivity(isPM, cmd.modMsg, cmd.room, cmd.authUser)
-}
-
-func generalRoomKarma(db *database.DkfDB, authUser *database.User) {
- // Hellban users ain't getting karma
- if authUser.IsHellbanned {
- return
- }
- messagesCount := authUser.GeneralMessagesCount
- if messagesCount%100 == 0 {
- description := fmt.Sprintf("sent %d messages", messagesCount)
- authUser.IncrKarma(db, 1, description)
- } else if messagesCount == 20 {
- authUser.IncrKarma(db, 1, "first 20 messages sent")
- }
-}
-
-var msgPolicy = bluemonday.NewPolicy().
- AllowElements("a", "p", "span", "strong", "del", "code", "pre", "em", "ul", "li", "br", "small", "i").
- AllowAttrs("href", "rel", "target").OnElements("a").
- AllowAttrs("tabindex", "style").OnElements("pre").
- AllowAttrs("style", "class", "title").OnElements("span").
- AllowAttrs("style").OnElements("small")
-
-// ProcessRawMessage return the new html, and a map of tagged users used for notifications
-// This function takes an "unsafe" user input "in", and return html which will be safe to render.
-func ProcessRawMessage(db *database.DkfDB, in, roomKey string, authUserID database.UserID, roomID database.RoomID,
- upload *database.Upload, canUseMultiline bool) (string, map[database.UserID]database.User, error) {
- html, quoted := convertQuote(db, in, roomKey, roomID) // Get raw quote text which is not safe to render
- html = convertNewLines(html, canUseMultiline)
- html = html2.EscapeString(html) // Makes user input safe to render
- // All html generated from this point on shall be safe to render.
- html = convertPGPClearsignToFile(db, html, authUserID)
- html = convertPGPMessageToFile(db, html, authUserID)
- html = convertPGPPublicKeyToFile(db, html, authUserID)
- html = convertAgeMessageToFile(db, html, authUserID)
- html = convertLinksWithoutScheme(html)
- html = convertMarkdown(html)
- html = convertBangShortcuts(html)
- html = convertArchiveLinks(db, html, roomID, authUserID)
- html = convertLinks(html, roomID, db.GetUserByUsername, db.GetLinkByShorthand, db.GetChatMessageByUUID)
- html = linkDefaultRooms(html)
- html, taggedUsersIDsMap := colorifyTaggedUsers(html, db.GetUsersByUsername)
- html = linkRoomTags(db, html)
- html = emojiReplacer.Replace(html)
- html = styleQuote(html, quoted)
- html = appendUploadLink(html, upload)
- if quoted != nil { // Add quoted message owner for inboxes
- taggedUsersIDsMap[quoted.UserID] = quoted.User
- }
- html = msgPolicy.Sanitize(html)
- return html, taggedUsersIDsMap, nil
-}
-
-func sendInboxes(db *database.DkfDB, room database.ChatRoom, authUser, toUser *database.User, msgID int64, groupID *database.GroupID, html string, modMsg bool,
- taggedUsersIDsMap map[database.UserID]database.User) {
- // Only have chat inbox for unencrypted messages
- if room.IsProtected() {
- return
- }
- // If user is hellbanned, do not send inboxes
- if authUser.IsHellbanned {
- return
- }
- // Early return if we don't need to send inboxes
- if toUser == nil && len(taggedUsersIDsMap) == 0 {
- return
- }
-
- blacklistedBy, _ := db.GetPmBlacklistedByUsers(authUser.ID)
- blacklistedByMap := make(map[database.UserID]struct{})
- for _, b := range blacklistedBy {
- blacklistedByMap[b.UserID] = struct{}{}
- }
-
- ignoredBy, _ := db.GetIgnoredByUsers(authUser.ID)
- ignoredByMap := make(map[database.UserID]struct{})
- for _, b := range ignoredBy {
- ignoredByMap[b.UserID] = struct{}{}
- }
-
- sendInbox := func(user database.User, isPM, modCh bool) {
- if !managers.ActiveUsers.IsUserActiveInRoom(user.ID, room) || user.AFK {
- // Do not send notification if receiver is blacklisting you
- if _, ok := blacklistedByMap[user.ID]; ok {
- return
- }
- // Do not send notification if receiver is ignoring you
- if _, ok := ignoredByMap[user.ID]; ok {
- return
- }
- db.CreateInboxMessage(html, room.ID, authUser.ID, user.ID, isPM, modCh, &msgID)
- }
- }
-
- // If the message is a PM, only notify the receiver, not the tagged people in it.
- if toUser != nil {
- sendInbox(*toUser, true, false)
- } else if room.Name == "moderators" { // Only tags other moderators on "moderators" room
- for _, user := range taggedUsersIDsMap {
- if user.IsModerator() {
- sendInbox(user, false, false)
- }
- }
- } else if modMsg { // Only tags other moderators on /m messages
- for _, user := range taggedUsersIDsMap {
- if user.IsModerator() {
- sendInbox(user, false, true)
- }
- }
- } else if groupID != nil { // Only tags other people in the group
- for _, user := range taggedUsersIDsMap {
- if db.IsUserInGroupByID(user.ID, *groupID) {
- sendInbox(user, false, false)
- }
- }
- } else { // Otherwise, notify tagged people
- for _, user := range taggedUsersIDsMap {
- sendInbox(user, false, false)
- }
- }
-}
-
-func updateUserActivity(isPM, modMsg bool, room database.ChatRoom, authUser *database.User) {
- // We do not update user presence when they send private messages or moderators group message
- if isPM || modMsg {
- return
- }
- managers.ActiveUsers.UpdateUserInRoom(room, managers.NewUserInfoUpdateActivity(authUser))
-}
diff --git a/pkg/web/handlers/api/v1/slashInterceptor.go b/pkg/web/handlers/api/v1/slashInterceptor.go
@@ -1,1805 +0,0 @@
-package v1
-
-import (
- "dkforest/pkg/clockwork"
- "dkforest/pkg/config"
- "dkforest/pkg/database"
- dutils "dkforest/pkg/database/utils"
- "dkforest/pkg/managers"
- "dkforest/pkg/utils"
- "errors"
- "fmt"
- "github.com/ProtonMail/go-crypto/openpgp/clearsign"
- "github.com/ProtonMail/go-crypto/openpgp/packet"
- "github.com/asaskevich/govalidator"
- "github.com/dustin/go-humanize"
- "github.com/sirupsen/logrus"
- "html"
- "os"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "time"
-)
-
-// SlashInterceptor handle all forward slash commands.
-//
-// If by the end of this function, the c.err is set, it will trigger
-// different behavior according to the type of error it holds.
-// if c.err is set to ErrRedirect, the chat-bar iframe will refresh completely.
-// if c.err is set to ErrStop, no further processing of the user input will be done,
-// and the chat iframe will be rendered instead of redirected.
-// This is useful to keep a prefix in the text box (eg: /pm user )
-// if c.err is set to an instance of ErrSuccess,
-// a green message will appear beside the text box.
-// otherwise if c.err is set to a different error,
-// text box is retested to original message,
-// and a red message will appear beside the text box.
-type SlashInterceptor struct{}
-
-func (i SlashInterceptor) InterceptMsg(c *Command) {
- if !strings.HasPrefix(c.message, "/") {
- return
- }
- handled := handleUserCmd(c) ||
- handlePrivateRoomCmd(c) ||
- handlePrivateRoomOwnerCmd(c) ||
- handleModeratorCmd(c) ||
- handleAdminCmd(c)
- if !handled {
- c.err = errors.New("invalid slash command")
- }
-}
-
-func handleUserCmd(c *Command) (handled bool) {
- return handleIgnoreCmd(c) ||
- handleUnIgnoreCmd(c) ||
- handleToggleAutocomplete(c) ||
- handleTutorialCmd(c) ||
- handleDeleteMsgCmd(c) ||
- handleHideMsgCmd(c) ||
- handleUnHideMsgCmd(c) ||
- handleListIgnoredCmd(c) ||
- handleListPmWhitelistCmd(c) ||
- handleSetPmModeWhitelistCmd(c) ||
- handleSetPmModeStandardCmd(c) ||
- handleTogglePmBlacklistedUser(c) ||
- handleTogglePmWhitelistedUser(c) ||
- handleGroupChatCmd(c) ||
- handleMeCmd(c) ||
- handleEditCmd(c) ||
- handlePMCmd(c) ||
- handleEditLastCmd(c) ||
- handleSubscribeCmd(c) ||
- handleUnsubscribeCmd(c) ||
- handleProfileCmd(c) ||
- handleInboxCmd(c) ||
- handleChessCmd(c) ||
- handleHbmCmd(c) ||
- handleHbmtCmd(c) ||
- handleTokenCmd(c) ||
- handleMd5Cmd(c) ||
- handleSha1Cmd(c) ||
- handleSha256Cmd(c) ||
- handleSha512Cmd(c) ||
- handleDiceCmd(c) ||
- handleRandCmd(c) ||
- handleChoiceCmd(c) ||
- handleListMemes(c) ||
- handleSuccessCmd(c) ||
- handleAfkCmd(c) ||
- handleDateCmd(c) ||
- handleUpdateReadMarkerCmd(c) ||
- handleCodeCmd(c) ||
- handleErrorCmd(c)
-}
-
-func handlePrivateRoomCmd(c *Command) (handled bool) {
- return handleGetModeCmd(c) ||
- handleWhitelistCmd(c)
-}
-
-func handlePrivateRoomOwnerCmd(c *Command) (handled bool) {
- if (c.room.OwnerUserID != nil && *c.room.OwnerUserID == c.authUser.ID) || c.authUser.IsAdmin {
- return handleAddGroupCmd(c) ||
- handleRmGroupCmd(c) ||
- handleLockGroupCmd(c) ||
- handleUnlockGroupCmd(c) ||
- handleGroupUsersCmd(c) ||
- handleListGroupsCmd(c) ||
- handleGroupAddUserCmd(c) ||
- handleGroupRmUserCmd(c) ||
- handleSetModeWhitelistCmd(c) ||
- handleSetModeStandardCmd(c) ||
- handleGetRoomWhitelistCmd(c) ||
- handleToggleReadOnlyCmd(c)
- }
- return false
-}
-
-func handleModeratorCmd(c *Command) (handled bool) {
- if c.authUser.IsModerator() {
- return handleModeratorGroupCmd(c) ||
- handleListModeratorsCmd(c) ||
- handleKickCmd(c) ||
- handleKickKeepCmd(c) ||
- handleKickSilentCmd(c) ||
- handleKickKeepSilentCmd(c) ||
- handleUnkickCmd(c) ||
- handleLogoutCmd(c) ||
- handleForceCaptchaCmd(c) ||
- handleResetTutorialCmd(c) ||
- handleHellbanCmd(c) ||
- handleUnhellbanCmd(c)
- }
- return false
-}
-
-func handleAdminCmd(c *Command) (handled bool) {
- if c.authUser.IsAdmin {
- return handleSystemCmd(c) ||
- handleSetChatRoomExternalLink(c) ||
- handlePurge(c) ||
- handleRename(c) ||
- handleNewMeme(c) ||
- handleRenameMeme(c) ||
- handleRemoveMeme(c) ||
- handleRefreshCmd(c)
- }
- return false
-}
-
-func handleModeratorGroupCmd(c *Command) (handled bool) {
- if strings.HasPrefix(c.message, "/m ") || strings.HasPrefix(c.message, "/n ") {
- if strings.HasPrefix(c.message, "/n ") {
- c.message = strings.Replace(c.message, "/n ", "/m ", 1)
- }
- c.message = strings.TrimPrefix(c.message, "/m ")
- c.redirectQP.Set(redirectModQP, "1")
- c.modMsg = true
- if handleMeCmd(c) {
- return true
- } else if handleCodeCmd(c) {
- return true
- }
- return true
- }
- return false
-}
-
-func handleListModeratorsCmd(c *Command) (handled bool) {
- if c.message == "/moderators" || c.message == "/mods" {
- mods, err := c.db.GetModeratorsUsers()
- if err != nil {
- c.err = err
- return true
- }
- msg := "Moderators:\n"
- if len(mods) > 0 {
- msg += "\n"
- for _, mod := range mods {
- msg += mod.Username.AtStr() + "\n"
- }
- } else {
- msg += "no moderators"
- }
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleKickCmd(c *Command) (handled bool) {
- if m := kickRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- if err := kickCmd(c, username, true, false); err != nil {
- c.err = err
- return true
- }
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-// Kick a user but keep the messages
-func handleKickKeepCmd(c *Command) (handled bool) {
- if m := kickKeepRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- if err := kickCmd(c, username, false, false); err != nil {
- c.err = err
- return true
- }
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-// Kick a user, no system message in chat
-func handleKickSilentCmd(c *Command) (handled bool) {
- if m := kickSilentRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- if err := kickCmd(c, username, true, true); err != nil {
- c.err = err
- return true
- }
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-// Kick a user, keep the messages, no system message in chat
-func handleKickKeepSilentCmd(c *Command) (handled bool) {
- if m := kickKeepSilentRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- if err := kickCmd(c, username, false, true); err != nil {
- c.err = err
- return true
- }
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func kickCmd(c *Command, username database.Username, purge, silent bool) error {
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- return ErrUsernameNotFound
- }
- return dutils.Kick(c.db, user, *c.authUser, purge, silent)
-}
-
-var ErrUsernameNotFound = errors.New("username not found")
-var ErrUnauthorized = errors.New("unauthorized")
-
-func handleUnkickCmd(c *Command) (handled bool) {
- if m := unkickRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrUsernameNotFound
- return true
- }
- if user.Verified {
- c.err = errors.New("user already not kicked")
- return true
- }
- c.db.NewAudit(*c.authUser, fmt.Sprintf("unkick %s #%d", user.Username, user.ID))
- user.Verified = true
- user.DoSave(c.db)
-
- // Display unkick message
- c.db.CreateUnkickMsg(user, *c.authUser)
-
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleForceCaptchaCmd(c *Command) (handled bool) {
- if m := forceCaptchaRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrUsernameNotFound
- return true
- }
- if c.authUser.IsAdmin || !user.IsModerator() || c.authUser.Username == username {
- c.db.NewAudit(*c.authUser, fmt.Sprintf("force captcha %s #%d", user.Username, user.ID))
- user.CaptchaRequired = true
- user.DoSave(c.db)
- }
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleLogoutCmd(c *Command) (handled bool) {
- if m := logoutRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrUsernameNotFound
- return true
- }
- if !c.authUser.IsAdmin && user.Vetted {
- c.err = ErrUnauthorized
- return true
- }
- if c.authUser.IsAdmin || !user.IsModerator() {
- c.db.NewAudit(*c.authUser, fmt.Sprintf("logout %s #%d", user.Username, user.ID))
-
- _ = c.db.DeleteUserSessions(user.ID)
-
- // Remove user from the user cache
- managers.ActiveUsers.RemoveUser(user.ID)
- }
-
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleResetTutorialCmd(c *Command) (handled bool) {
- if m := rtutoRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrUsernameNotFound
- return true
- }
- if !c.authUser.IsAdmin && user.Vetted {
- c.err = ErrUnauthorized
- return true
- }
- if c.authUser.IsAdmin || !user.IsModerator() {
- c.db.NewAudit(*c.authUser, fmt.Sprintf("rtuto %s #%d", user.Username, user.ID))
- user.ChatTutorial = 0
- user.DoSave(c.db)
- }
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleHellbanCmd(c *Command) (handled bool) {
- if m := hellbanRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrUsernameNotFound
- return true
- }
- if !c.authUser.IsAdmin && (user.Vetted || user.IsModerator()) {
- c.err = ErrUnauthorized
- return true
- }
- c.db.NewAudit(*c.authUser, fmt.Sprintf("hellban %s #%d", user.Username, user.ID))
- user.HellBan(c.db)
- managers.ActiveUsers.UpdateUserHBInRooms(managers.NewUserInfo(&user))
-
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleUnhellbanCmd(c *Command) (handled bool) {
- if m := unhellbanRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrUsernameNotFound
- return true
- }
- if !c.authUser.IsAdmin && (user.Vetted || user.IsModerator()) {
- c.err = ErrUnauthorized
- return true
- }
- c.db.NewAudit(*c.authUser, fmt.Sprintf("unhellban %s #%d", user.Username, user.ID))
- user.UnHellBan(c.db)
- managers.ActiveUsers.UpdateUserHBInRooms(managers.NewUserInfo(&user))
-
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleHbmCmd(c *Command) (handled bool) {
- if !c.authUser.CanSeeHB() {
- return
- }
- if strings.HasPrefix(c.message, "/hbm ") {
- c.message = strings.TrimPrefix(c.message, "/hbm ")
- c.hellbanMsg = true
- c.redirectQP.Set(redirectHbmQP, "1")
- return true
- }
- return
-}
-
-func handleHbmtCmd(c *Command) (handled bool) {
- if !c.authUser.CanSeeHB() {
- return
- }
- if m := hbmtRgx.FindStringSubmatch(c.message); len(m) == 2 {
- date := m[1]
- if dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock()); err == nil {
- if msg, err := c.db.GetRoomChatMessageByDate(c.room.ID, c.authUser.ID, dt.UTC()); err == nil {
- msg.IsHellbanned = !msg.IsHellbanned
- msg.DoSave(c.db)
- } else {
- c.err = errors.New("no message found at this timestamp")
- return true
- }
- }
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleDiceCmd(c *Command) (handled bool) {
- if strings.HasPrefix(c.message, "/dice") {
- dice := utils.RandInt(1, 6)
- raw := fmt.Sprintf(`rolling dice for @%s ... "%d"`, c.authUser.Username, dice)
- msg := fmt.Sprintf(`rolling dice for @%s ... "<span style="color: white;">%d</span>"`, c.authUser.Username, dice)
- msg, _ = colorifyTaggedUsers(msg, c.db.GetUsersByUsername)
- go func() {
- time.Sleep(time.Second)
- c.zeroPublicMsg(raw, msg)
- }()
- return true
- }
- return
-}
-
-func handleRandCmd(c *Command) (handled bool) {
- if strings.HasPrefix(c.message, "/rand") {
- min := 1
- max := 6
- var dice int
- if m := randRgx.FindStringSubmatch(c.message); len(m) == 3 {
- var err error
- min, err = strconv.Atoi(m[1])
- if err != nil {
- c.err = err
- return true
- }
- max, err = strconv.Atoi(m[2])
- if err != nil {
- c.err = err
- return true
- }
- if max <= min {
- c.err = errors.New("max must be greater than min")
- return true
- }
- } else if c.message != "/rand" {
- c.err = errors.New("invalid /rand command")
- return true
- }
- dice = utils.RandInt(min, max)
- raw := fmt.Sprintf(`rolling dice for @%s ... "%d"`, c.authUser.Username, dice)
- msg := fmt.Sprintf(`rolling dice for @%s ... "<span style="color: white;">%d</span>"`, c.authUser.Username, dice)
- msg, _ = colorifyTaggedUsers(msg, c.db.GetUsersByUsername)
- go func() {
- time.Sleep(time.Second)
- c.zeroPublicMsg(raw, msg)
- }()
- return true
- }
- return
-}
-
-func handleChoiceCmd(c *Command) (handled bool) {
- if strings.HasPrefix(c.message, "/choice ") {
- tmp := html.EscapeString(strings.TrimPrefix(c.message, "/choice "))
- words := strings.Fields(tmp)
- answer := utils.RandChoice(words)
- raw := fmt.Sprintf(`@%s choice %s ... "%s"`, c.authUser.Username, words, answer)
- msg := fmt.Sprintf(`@%s choice %s ... "<span style="color: white;">%s</span>"`, c.authUser.Username, words, answer)
- msg, _ = colorifyTaggedUsers(msg, c.db.GetUsersByUsername)
- go func() {
- time.Sleep(time.Second)
- c.zeroPublicMsg(raw, msg)
- }()
- c.skipInboxes = true
- return true
- }
- return
-}
-
-func handleTokenCmd(c *Command) (handled bool) {
- if c.message == "/token" {
- c.zeroMsg(utils.GenerateToken10())
- c.err = ErrRedirect
- return true
- } else if m := tokenRgx.FindStringSubmatch(c.message); len(m) == 2 {
- n, _ := strconv.Atoi(m[1])
- if n < 1 || n > 32 {
- c.err = errors.New("value must be [1;32]")
- return true
- }
- n = utils.Clamp(n, 1, 32)
- c.zeroMsg(utils.GenerateTokenN(n))
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleMd5Cmd(c *Command) (handled bool) {
- return handleHasherCmd(c, "/md5 ", utils.MD5)
-}
-
-func handleSha1Cmd(c *Command) (handled bool) {
- return handleHasherCmd(c, "/sha1 ", utils.Sha1)
-}
-
-func handleSha256Cmd(c *Command) (handled bool) {
- return handleHasherCmd(c, "/sha256 ", utils.Sha256)
-}
-
-func handleSha512Cmd(c *Command) (handled bool) {
- return handleHasherCmd(c, "/sha512 ", utils.Sha512)
-}
-
-func handleHasherCmd(c *Command, prefix string, fn func([]byte) string) (handled bool) {
- if strings.HasPrefix(c.message, prefix) {
- c.message = strings.TrimPrefix(c.message, prefix)
- c.dataMessage = prefix
- c.zeroMsg(fn([]byte(c.message)))
- c.err = ErrStop
- return true
- }
- return
-}
-
-func handleRmGroupCmd(c *Command) (handled bool) {
- if m := rmGroupRgx.FindStringSubmatch(c.message); len(m) == 2 {
- groupName := m[1]
- if err := c.db.DeleteChatRoomGroup(c.room.ID, groupName); err != nil {
- c.err = err
- return true
- }
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleLockGroupCmd(c *Command) (handled bool) {
- if m := lockGroupRgx.FindStringSubmatch(c.message); len(m) == 2 {
- groupName := m[1]
- group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
- if err != nil {
- c.err = err
- return true
- }
- group.Locked = true
- group.DoSave(c.db)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleUnlockGroupCmd(c *Command) (handled bool) {
- if m := unlockGroupRgx.FindStringSubmatch(c.message); len(m) == 2 {
- groupName := m[1]
- group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
- if err != nil {
- c.err = err
- return true
- }
- group.Locked = false
- group.DoSave(c.db)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleGroupUsersCmd(c *Command) (handled bool) {
- if m := groupUsersRgx.FindStringSubmatch(c.message); len(m) == 2 {
- groupName := m[1]
- group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
- if err != nil {
- c.err = err
- return true
- }
- users, err := c.db.GetRoomGroupUsers(c.room.ID, group.ID)
- sort.Slice(users, func(i, j int) bool {
- return users[i].User.Username < users[j].User.Username
- })
- msg := ""
- if len(users) > 0 {
- msg += "\n"
- for _, user := range users {
- msg += user.User.Username.AtStr() + "\n"
- }
- } else {
- msg += "no user in th group: " + groupName
- }
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleListGroupsCmd(c *Command) (handled bool) {
- if c.message == "/groups" {
- groups, err := c.db.GetRoomGroups(c.room.ID)
- if err != nil {
- c.err = err
- return true
- }
- msg := ""
- if len(groups) > 0 {
- msg += "\n"
- for _, group := range groups {
- msg += group.Name + " (" + group.Color + ")\n"
- }
- } else {
- msg += "no groups"
- }
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleGroupAddUserCmd(c *Command) (handled bool) {
- if m := groupAddUserRgx.FindStringSubmatch(c.message); len(m) == 3 {
- groupName := m[1]
- username := database.Username(m[2])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = err
- return true
- }
- group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
- if err != nil {
- c.err = err
- return true
- }
- _, err = c.db.AddUserToRoomGroup(c.room.ID, group.ID, user.ID)
- if err != nil {
- c.err = err
- return true
- }
- c.err = ErrRedirect
- return true
- } else if strings.HasPrefix(c.message, "/gadduser ") {
- c.err = errors.New("invalid /gadduser command")
- }
- return false
-}
-
-func handleGroupRmUserCmd(c *Command) (handled bool) {
- if m := groupRmUserRgx.FindStringSubmatch(c.message); len(m) == 3 {
- groupName := m[1]
- username := database.Username(m[2])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = err
- return true
- }
- group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
- if err != nil {
- c.err = err
- return true
- }
- err = c.db.RmUserFromRoomGroup(c.room.ID, group.ID, user.ID)
- if err != nil {
- c.err = err
- return true
- }
- c.err = ErrRedirect
- return true
- } else if strings.HasPrefix(c.message, "/grmuser ") {
- c.err = errors.New("invalid /grmuser command")
- }
- return false
-}
-
-func handleSetModeWhitelistCmd(c *Command) (handled bool) {
- if c.message == "/mode user-whitelist" {
- c.room.Mode = database.UserWhitelistRoomMode
- c.room.DoSave(c.db)
- msg := `room mode set to "user whitelist"`
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleSetModeStandardCmd(c *Command) (handled bool) {
- if c.message == "/mode standard" {
- c.room.Mode = database.NormalRoomMode
- c.room.DoSave(c.db)
- msg := `room mode set to "standard"`
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleGetRoomWhitelistCmd(c *Command) (handled bool) {
- if m := whitelistUserRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- var msg string
- if err != nil {
- msg = fmt.Sprintf(`username "%s" not found`, username)
- } else {
- if _, err := c.db.WhitelistUser(c.room.ID, user.ID); err != nil {
- if err := c.db.DeWhitelistUser(c.room.ID, user.ID); err != nil {
- msg = fmt.Sprintf("failed to toggle @%s in whitelist", user.Username)
- } else {
- msg = fmt.Sprintf("@%s removed from whitelist", user.Username)
- }
- } else {
- msg = fmt.Sprintf("@%s added to whitelist", user.Username)
- }
- }
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleToggleReadOnlyCmd(c *Command) (handled bool) {
- if c.message == "/ro" {
- c.room.ReadOnly = !c.room.ReadOnly
- c.room.DoSave(c.db)
- if c.room.ReadOnly {
- c.err = NewErrSuccess("room is now read-only")
- } else {
- c.err = NewErrSuccess("room is no longer read-only")
- }
- return true
- }
- return
-}
-
-func handleAddGroupCmd(c *Command) (handled bool) {
- if m := addGroupRgx.FindStringSubmatch(c.message); len(m) == 2 {
- name := m[1]
- _, err := c.db.CreateChatRoomGroup(c.room.ID, name, "#fff")
- if err != nil {
- c.err = err
- return true
- }
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleWhitelistCmd(c *Command) (handled bool) {
- if c.message == "/whitelist" || c.message == "/wl" {
- whitelistedUsers, _ := c.db.GetWhitelistedUsers(c.room.ID)
- var msg string
- if len(whitelistedUsers) > 0 {
- usernames := make([]string, 0)
- for _, whitelistedUser := range whitelistedUsers {
- usernames = append(usernames, whitelistedUser.User.Username.AtStr())
- }
- sort.Slice(usernames, func(i, j int) bool { return usernames[i] < usernames[j] })
- msg = "whitelisted users: " + strings.Join(usernames, ", ")
- } else {
- msg = "no whitelisted user"
- }
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleGetModeCmd(c *Command) (handled bool) {
- if c.message == "/mode" {
- var msg string
- if c.room.Mode == database.NormalRoomMode {
- msg = `room is in "standard" mode`
- } else if c.room.Mode == database.UserWhitelistRoomMode {
- msg = `room is in "user whitelist" mode`
- }
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleMeCmd(c *Command) (handled bool) {
- if c.message == "/me " {
- c.err = errors.New("invalid /me command")
- return true
- }
- if strings.HasPrefix(c.message, "/me ") {
- return true
- }
- return
-}
-
-func handleEditCmd(c *Command) (handled bool) {
- if m := editRgx.FindStringSubmatch(c.message); len(m) == 3 {
- date := m[1]
- newMsg := m[2]
- if dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock()); err == nil {
- if time.Since(dt) <= config.EditMessageTimeLimit {
- if msg, err := c.db.GetRoomChatMessageByDate(c.room.ID, c.authUser.ID, dt.UTC()); err == nil {
- c.editMsg = &msg
- c.origMessage = newMsg
- c.message = newMsg
-
- // If we're editing a message which contains a link to an uploaded file,
- // we need to re-add the link to the html.
- if msg.UploadID != nil {
- if newUpload, err := c.db.GetUploadByID(*msg.UploadID); err == nil {
- c.upload = &newUpload
- }
- }
-
- if pmRgx.MatchString(c.message) {
- handlePMCmd(c)
- } else if c.authUser.IsModerator() && strings.HasPrefix(c.message, "/m ") {
- handleModeratorGroupCmd(c)
- } else if strings.HasPrefix(c.message, "/hbm ") {
- handleHbmCmd(c)
- } else if strings.HasPrefix(c.message, "/g ") {
- handleGroupChatCmd(c)
- } else if strings.HasPrefix(c.message, "/system ") || strings.HasPrefix(c.message, "/sys ") {
- handleSystemCmd(c)
- }
- }
- }
- }
- return true
- }
- return
-}
-
-func handleEditLastCmd(c *Command) (handled bool) {
- if c.message == "/e" {
- msg, err := c.db.GetUserLastChatMessageInRoom(c.authUser.ID, c.room.ID)
- if err != nil {
- return true
- }
- c.redirectQP.Set(redirectEditQP, msg.CreatedAt.Format("15:04:05"))
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-var ErrPMDenied = errors.New("you cannot pm/inbox this user")
-var Err20Msgs = errors.New("you need 20 public messages to unlock PMs/Inbox; or be whitelisted")
-var ErrOther20Msgs = errors.New("dest user must be whitelisted or have 20 public messages")
-
-func canUserInboxOther(db *database.DkfDB, user, other database.User) error {
- doesNotMatter := false
- _, err := canUserPmOther(db, user, other, doesNotMatter)
- return err
-}
-
-func canUserPmOther(db *database.DkfDB, user, other database.User, roomIsPrivate bool) (skipInbox bool, err error) {
- errPMDenied := ErrPMDenied
-
- if user.ID == other.ID {
- return false, errors.New("cannot /pm yourself")
- }
-
- if db.IsUserPmWhitelisted(user.ID, other.ID) {
- return false, nil
- }
-
- switch other.PmMode {
- case database.PmModeWhitelist:
- // We are in whitelist mode, and user is not whitelisted
- return false, errPMDenied
-
- case database.PmModeStandard:
- if !user.CanSendPM() {
- // In private rooms, can send PM but inboxes will be skipped if not enough public messages
- if roomIsPrivate {
- return true, nil
- }
- // Need at least 20 public messages to send PM in a public room
- return false, Err20Msgs
- }
-
- // User on blacklist cannot PM/Inbox
- if db.IsUserPmBlacklisted(user.ID, other.ID) {
- return false, errPMDenied
- }
- // Other doesn't want PM from new users
- if !user.AccountOldEnough() && other.BlockNewUsersPm {
- return false, errPMDenied
- }
-
- if !other.CanSendPM() {
- if db.IsUserPmWhitelisted(other.ID, user.ID) {
- return true, nil
- }
- // In private rooms, can send PM but inboxes will be skipped if not enough public messages
- if roomIsPrivate {
- return true, nil
- }
- return false, ErrOther20Msgs
- }
-
- return false, nil
- }
-
- // Should never go here
- return false, nil
-}
-
-func handlePMCmd(c *Command) (handled bool) {
- if m := pmRgx.FindStringSubmatch(c.message); len(m) == 3 {
- username := database.Username(m[1])
- newMsg := m[2]
-
- // Chat helpers
- if username == config.NullUsername {
- return handlePm0(c, newMsg)
- }
-
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = errors.New("invalid username")
- return true
- }
-
- c.skipInboxes, c.err = canUserPmOther(c.db, *c.authUser, user, c.room.IsOwned())
- if c.err != nil {
- return true
- }
-
- c.toUser = &user
- c.message = newMsg
- c.redirectQP.Set(redirectPmQP, string(user.Username))
-
- if newMsg == "/d" || strings.HasPrefix(newMsg, "/d ") {
- handled = handleDeleteMsgCmd(c)
- if c.err != nil && c.err != ErrRedirect {
- return handled
- }
- c.err = ErrRedirect
- return handled
- }
-
- if handleCodeCmd(c) {
- return true
- }
-
- return true
- } else if strings.HasPrefix(c.message, "/pm ") {
- c.err = errors.New("invalid /pm command")
- return true
- }
- return false
-}
-
-// Handle PMs sent to user 0 (/pm 0 msg)
-func handlePm0(c *Command, msg string) (handled bool) {
- c.redirectQP.Set(redirectPmQP, "0")
- if msg == "ping" {
- c.zeroMsg("pong")
- c.err = ErrRedirect
- return true
-
- } else if msg == "talk" {
- c.zeroMsg("talking")
- c.err = ErrRedirect
- return true
-
- } else if msg == "pgp" || msg == "gpg" {
- pkey := c.authUser.GPGPublicKey
- if pkey == "" {
- c.message = "I could not find a public pgp key in your profile."
- } else {
- msg := "This is a sample text"
- if encrypted, err := utils.GeneratePgpEncryptedMessage(pkey, msg); err != nil {
- c.message = err.Error()
- } else {
- c.message = strings.Join(strings.Split(encrypted, "\n"), " ")
- }
- }
- c.zeroProcMsg(c.message)
- c.err = ErrRedirect
- return true
-
- } else if pgpMsg := extractPGPMessage(msg); pgpMsg != "" {
- decrypted, err := utils.PgpDecryptMessage(config.NullUserPrivateKey, pgpMsg)
- if err != nil {
- c.message = err.Error()
- } else {
- c.message = "Decrypted message: " + decrypted
- }
- c.zeroProcMsg(c.message)
- c.err = ErrRedirect
- return true
-
- } else if b, _ := clearsign.Decode([]byte(msg)); b != nil {
- if p, err := packet.Read(b.ArmoredSignature.Body); err == nil {
- if sig, ok := p.(*packet.Signature); ok {
- zero := c.getZeroUser()
- msg := fmt.Sprintf("<br />"+
- "<table %s>"+
- "<tr><td align=\"right\">Signature made: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
- "<tr><td align=\"right\">Fingerprint: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
- "<tr><td align=\"right\">Issuer: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
- "</table>",
- zero.GenerateChatStyle(),
- sig.CreationTime.Format(time.RFC1123),
- utils.FormatPgPFingerprint(sig.IssuerFingerprint),
- utils.Ternary(sig.SignerUserId != nil, *sig.SignerUserId, "n/a"))
- c.zeroMsg(msg)
- c.err = ErrRedirect
- return true
- }
- }
-
- } else if c.upload != nil {
-
- // If we sent a clearsign file to @0, the bot will reply with information about the signature
- if c.upload.FileSize < config.MaxFileSizeBeforeDownload {
- if file, err := c.db.GetUploadByFileName(c.upload.FileName); err == nil {
- if _, by, err := file.GetContent(); err == nil {
- if b, _ := clearsign.Decode(by); b != nil {
- if p, err := packet.Read(b.ArmoredSignature.Body); err == nil {
- if sig, ok := p.(*packet.Signature); ok {
- zero := c.getZeroUser()
- msg := fmt.Sprintf("<br />"+
- "<table %s>"+
- "<tr><td align=\"right\">File: </td><td><span style=\"color: #82e17f;\">%s</span> (<span style=\"color: #82e17f;\">%s</span>)</td></tr>"+
- "<tr><td align=\"right\">Signature made: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
- "<tr><td align=\"right\">Fingerprint: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
- "<tr><td align=\"right\">Issuer: </td><td><span style=\"color: #82e17f;\">%s</span></td></tr>"+
- "</table>",
- zero.GenerateChatStyle(),
- c.upload.OrigFileName,
- humanize.Bytes(uint64(c.upload.FileSize)),
- sig.CreationTime.Format(time.RFC1123),
- utils.FormatPgPFingerprint(sig.IssuerFingerprint),
- utils.Ternary(sig.SignerUserId != nil, *sig.SignerUserId, "n/a"))
- c.zeroMsg(msg)
- c.err = ErrRedirect
- return true
- }
- }
- }
- }
- }
- }
- }
-
- zeroUser := c.getZeroUser()
- c.toUser = &zeroUser
- c.message = msg
-
- return true
-}
-
-func handleSubscribeCmd(c *Command) (handled bool) {
- if c.message == "/subscribe" {
- _ = c.db.SubscribeToRoom(c.authUser.ID, c.room.ID)
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleUnsubscribeCmd(c *Command) (handled bool) {
- if m := unsubscribeRgx.FindStringSubmatch(c.message); len(m) == 2 {
- room, err := c.db.GetChatRoomByName(m[1])
- if err != nil {
- c.err = err
- return true
- }
- _ = c.db.UnsubscribeFromRoom(c.authUser.ID, room.ID)
- c.err = ErrRedirect
- return true
-
- } else if c.message == "/unsubscribe" {
- _ = c.db.UnsubscribeFromRoom(c.authUser.ID, c.room.ID)
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleGroupChatCmd(c *Command) (handled bool) {
- if m := groupRgx.FindStringSubmatch(c.message); len(m) == 3 {
- groupName := m[1]
- c.message = m[2]
- group, err := c.db.GetRoomGroupByName(c.room.ID, groupName)
- if err != nil {
- c.err = err
- return true
- }
- if group.Locked {
- c.err = errors.New("group is locked")
- return true
- }
- c.redirectQP.Set(redirectGroupQP, group.Name)
- c.groupID = &group.ID
- return true
- } else if strings.HasPrefix(c.message, "/g ") {
- c.err = errors.New("invalid /g command")
- return true
- }
- return false
-}
-
-func handleListIgnoredCmd(c *Command) (handled bool) {
- if c.message == "/i" || c.message == "/ignore" {
- ignoredUsers, _ := c.db.GetIgnoredUsers(c.authUser.ID)
- sort.Slice(ignoredUsers, func(i, j int) bool {
- return ignoredUsers[i].IgnoredUser.Username < ignoredUsers[j].IgnoredUser.Username
- })
- msg := ""
- if len(ignoredUsers) > 0 {
- msg += "\n"
- for _, ignoredUser := range ignoredUsers {
- msg += ignoredUser.IgnoredUser.Username.AtStr() + "\n"
- }
- } else {
- msg += "no ignored users"
- }
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleListPmWhitelistCmd(c *Command) (handled bool) {
- if c.message == "/pmwhitelist" {
- pmWhitelistUsers, _ := c.db.GetPmWhitelistedUsers(c.authUser.ID)
- sort.Slice(pmWhitelistUsers, func(i, j int) bool {
- return pmWhitelistUsers[i].WhitelistedUser.Username < pmWhitelistUsers[j].WhitelistedUser.Username
- })
- msg := ""
- if len(pmWhitelistUsers) > 0 {
- msg += "\n"
- for _, ignoredUser := range pmWhitelistUsers {
- msg += ignoredUser.WhitelistedUser.Username.AtStr() + "\n"
- }
- } else {
- msg += "no PM whitelisted users"
- }
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleSetPmModeWhitelistCmd(c *Command) (handled bool) {
- if c.message == "/setpmmode whitelist" {
- c.authUser.PmMode = database.PmModeWhitelist
- c.authUser.DoSave(c.db)
- msg := `pm mode set to "whitelist"`
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleSetPmModeStandardCmd(c *Command) (handled bool) {
- if c.message == "/setpmmode standard" {
- c.authUser.PmMode = database.PmModeStandard
- c.authUser.DoSave(c.db)
- msg := `pm mode set to "standard"`
- c.zeroProcMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return false
-}
-
-func handleTogglePmBlacklistedUser(c *Command) (handled bool) {
- if m := pmToggleBlacklistUserRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrRedirect
- return true
- }
- if c.db.ToggleBlacklistedUser(c.authUser.ID, user.ID) {
- c.err = NewErrSuccess("added to blacklist")
- } else {
- c.err = NewErrSuccess("removed from blacklist")
- }
- return true
- }
- return false
-}
-
-func handleTogglePmWhitelistedUser(c *Command) (handled bool) {
- if m := pmToggleWhitelistUserRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrRedirect
- return true
- }
- if c.db.ToggleWhitelistedUser(c.authUser.ID, user.ID) {
- c.err = NewErrSuccess("added to whitelist")
- } else {
- c.err = NewErrSuccess("removed from whitelist")
- }
- return true
- }
- return false
-}
-
-func handleChessCmd(c *Command) (handled bool) {
- if m := chessRgx.FindStringSubmatch(c.message); len(m) == 3 {
- username := database.Username(m[1])
- color := m[2]
- player1 := *c.authUser
- player2, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = errors.New("invalid username")
- return true
- }
- if color == "r" {
- color = utils.RandChoice([]string{"w", "b"})
- }
- if color == "b" {
- player1, player2 = player2, player1
- }
- if _, err := ChessInstance.NewGame1(c.roomKey, c.room.ID, player1, player2); err != nil {
- c.err = err
- return true
- }
- c.err = NewErrSuccess("chess game created")
- return true
- }
- return
-}
-
-func handleInboxCmd(c *Command) (handled bool) {
- if m := inboxRgx.FindStringSubmatch(c.message); len(m) == 4 {
- username := database.Username(m[1])
- encryptRaw := m[2]
- message := m[3]
- tryEncrypt := false
- if encryptRaw == " -e" {
- tryEncrypt = true
- }
- toUser, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = errors.New("invalid username")
- return true
- }
-
- if err := canUserInboxOther(c.db, *c.authUser, toUser); err != nil {
- c.err = err
- return true
- }
-
- html := message
- if tryEncrypt {
- if toUser.GPGPublicKey == "" {
- c.err = errors.New("user has no pgp public key")
- return true
- }
- html, err = utils.GeneratePgpEncryptedMessage(toUser.GPGPublicKey, message)
- if err != nil {
- c.err = errors.New("failed to encrypt")
- return true
- }
- html = strings.Join(strings.Split(html, "\n"), " ")
- }
-
- html, _, _ = ProcessRawMessage(c.db, html, c.roomKey, c.authUser.ID, c.room.ID, nil, c.authUser.CanUseMultiline)
- c.db.CreateInboxMessage(html, c.room.ID, c.authUser.ID, toUser.ID, true, false, nil)
-
- c.dataMessage = "/inbox " + string(username) + " "
- c.err = NewErrSuccess("inbox sent")
- return true
-
- } else if strings.HasPrefix(c.message, "/inbox ") {
- c.err = errors.New("invalid /inbox command")
- return true
- }
- return
-}
-
-func handleProfileCmd(c *Command) (handled bool) {
- if m := profileRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrUsernameNotFound
- return true
- }
- profile := `/u/` + user.Username
- c.zeroMsg(fmt.Sprintf(`[<a href="%s" rel="noopener noreferrer" target="_blank">profile of %s</a>]`, profile, user.Username))
- c.err = ErrRedirect
- return true
- } else if strings.HasPrefix(c.message, "/p ") {
- c.err = errors.New("invalid profile command")
- return true
- }
- return
-}
-
-type tutorialSteps struct {
- SendMessage bool
- EditMessage bool
- SendPM bool
- EditPM bool
- SendQuote bool
- TagSomeone bool
- VisitProfile bool
-}
-
-func handleTutorialCmd(c *Command) (handled bool) {
- if c.message == "/tuto" && false {
- name := "tuto_" + utils.GenerateToken10()
- room, _ := c.db.CreateRoom(name, "", c.authUser.ID, false)
- c.err = ErrRedirect
- c.zeroProcMsg("Tutorial here -> #" + room.Name)
- c.zeroPublicProcMsgRoom("Welcome to the tutorial", "", room.ID)
- return true
- }
- return
-}
-
-func handleDeleteMsgCmd(c *Command) (handled bool) {
- delMsgFn := func(msg database.ChatMessage) {
- if msg.RoomID == config.GeneralRoomID && msg.ToUserID == nil {
- msg.User.GeneralMessagesCount--
- msg.User.DoSave(c.db)
- }
- _ = msg.Delete(c.db)
- }
- if c.message == "/d" {
- if msg, err := c.db.GetUserLastChatMessageInRoom(c.authUser.ID, c.room.ID); err != nil {
- c.err = errors.New("unable to find last message")
- return true
- } else if msg.TooOldToDelete() {
- c.err = errors.New("message is to old to be deleted")
- return true
- } else {
- delMsgFn(msg)
- }
- c.err = ErrRedirect
- return true
-
- } else if m := deleteMsgRgx.FindStringSubmatch(c.message); len(m) >= 3 {
- if len(m) == 3 {
- date := m[1]
- matchUsername := m[2]
- dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
- if err != nil {
- logrus.Error(err)
- c.err = err
- return true
- }
- msgs, err := c.db.GetRoomChatMessagesByDate(c.room.ID, dt.UTC())
- if err != nil {
- c.err = err
- return true
- }
- if len(msgs) == 0 {
- c.err = errors.New("failed to find msg")
- return true
-
- } else if len(msgs) == 1 {
- msg := msgs[0]
- if !c.authUser.IsModerator() {
- if msg.User.Username != c.authUser.Username {
- c.err = errors.New("failed to find msg")
- return true
- }
- if msg.TooOldToDelete() {
- c.err = errors.New("message is to old to be deleted")
- return true
- }
- delMsgFn(msg)
- c.err = ErrRedirect
- return true
- }
- // Moderator
- _ = msg.Delete(c.db)
- c.err = ErrRedirect
- return true
-
- } else if len(msgs) > 1 {
- if !c.authUser.IsModerator() {
- var msg database.ChatMessage
- for _, msgTmp := range msgs {
- if msgTmp.User.Username == c.authUser.Username {
- msg = msgTmp
- break
- }
- }
- if msg.UUID == "" {
- c.err = errors.New("failed to find msg")
- return true
- }
- if msg.TooOldToDelete() {
- c.err = errors.New("message is to old to be deleted")
- return true
- }
- delMsgFn(msg)
- c.err = ErrRedirect
- return true
-
- }
-
- // Moderator
- if matchUsername == "" {
- c.err = errors.New("more the 1 msg with this timestamp")
- return true
- }
- var msg database.ChatMessage
- for _, msgTmp := range msgs {
- if string(msgTmp.User.Username) == matchUsername {
- msg = msgTmp
- break
- }
- }
- if msg.UUID == "" {
- c.err = errors.New("failed to find msg")
- return true
- }
- _ = msg.Delete(c.db)
- c.err = ErrRedirect
- return true
- }
- }
- return true
-
- } else if strings.HasPrefix(c.message, "/d ") {
- c.err = errors.New("invalid /d command")
- return true
- }
- return
-}
-
-func handleHideMsgCmd(c *Command) (handled bool) {
- if m := hideRgx.FindStringSubmatch(c.message); len(m) == 2 {
- date := m[1]
- dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
- if err != nil {
- logrus.Error(err)
- c.err = err
- return true
- }
- msgs, err := c.db.GetRoomChatMessagesByDate(c.room.ID, dt.UTC())
- if err != nil {
- c.err = err
- return true
- }
- if len(msgs) == 1 {
- c.db.IgnoreMessage(c.authUser.ID, msgs[0].ID)
- c.err = ErrRedirect
- } else {
- c.err = errors.New("more than 1 message")
- }
- return true
- }
- return
-}
-
-func handleUnHideMsgCmd(c *Command) (handled bool) {
- if m := unhideRgx.FindStringSubmatch(c.message); len(m) == 2 {
- date := m[1]
- dt, err := utils.ParsePrevDatetimeAt(date, clockwork.NewRealClock())
- if err != nil {
- logrus.Error(err)
- c.err = err
- return true
- }
- msgs, err := c.db.GetRoomChatMessagesByDate(c.room.ID, dt.UTC())
- if err != nil {
- c.err = err
- return true
- }
- if len(msgs) == 1 {
- c.db.UnIgnoreMessage(c.authUser.ID, msgs[0].ID)
- c.err = ErrRedirect
- } else {
- c.err = errors.New("more than 1 message")
- }
- return true
- }
- return
-}
-
-func handleIgnoreCmd(c *Command) (handled bool) {
- if m := ignoreRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrRedirect
- return true
- }
- c.db.IgnoreUser(c.authUser.ID, user.ID)
- database.MsgPubSub.Pub("refresh_"+string(c.authUser.Username), database.ChatMessageType{Typ: database.ForceRefresh})
- c.err = ErrRedirect
- return true
- } else if strings.HasPrefix(c.message, "/ignore ") || strings.HasPrefix(c.message, "/i ") {
- c.err = errors.New("invalid ignore command")
- return true
- }
- return
-}
-
-func handleUnIgnoreCmd(c *Command) (handled bool) {
- if m := unIgnoreRgx.FindStringSubmatch(c.message); len(m) == 2 {
- username := database.Username(m[1])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = ErrRedirect
- return true
- }
- c.db.UnIgnoreUser(c.authUser.ID, user.ID)
- database.MsgPubSub.Pub("refresh_"+string(c.authUser.Username), database.ChatMessageType{Typ: database.ForceRefresh})
- c.err = ErrRedirect
- return true
- } else if strings.HasPrefix(c.message, "/unignore ") || strings.HasPrefix(c.message, "/ui ") {
- c.err = errors.New("invalid unignore command")
- return true
- }
- return
-}
-
-func handleToggleAutocomplete(c *Command) (handled bool) {
- if c.message == "/toggle-autocomplete" {
- c.authUser.AutocompleteCommandsEnabled = !c.authUser.AutocompleteCommandsEnabled
- c.authUser.DoSave(c.db)
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleAfkCmd(c *Command) (handled bool) {
- if c.message == "/afk" {
- c.authUser.AFK = !c.authUser.AFK
- c.authUser.DoSave(c.db)
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleDateCmd(c *Command) (handled bool) {
- if c.message == "/date" {
- c.zeroMsg(time.Now().Format(time.RFC1123))
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleSuccessCmd(c *Command) (handled bool) {
- if c.message == "/success" {
- c.err = NewErrSuccess("success message")
- return true
- }
- return
-}
-
-func handleErrorCmd(c *Command) (handled bool) {
- if c.message == "/error" {
- c.err = errors.New("error message")
- return true
- }
- return
-}
-
-func handleSystemCmd(c *Command) (handled bool) {
- if strings.HasPrefix(c.message, "/sys ") {
- c.message = strings.Replace(c.message, "/sys ", "/system ", 1)
- }
- if strings.HasPrefix(c.message, "/system ") {
- c.message = strings.TrimPrefix(c.message, "/system ")
- c.systemMsg = true
- return true
- }
- return false
-}
-
-func handleSetChatRoomExternalLink(c *Command) (handled bool) {
- if m := setUrlRgx.FindStringSubmatch(c.message); len(m) == 2 {
- externalURL := m[1]
- if !govalidator.IsURL(externalURL) {
- externalURL = ""
- }
- room, err := c.db.GetChatRoomByID(c.room.ID)
- if err != nil {
- c.err = err
- return true
- }
- room.ExternalLink = externalURL
- room.DoSave(c.db)
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handlePurge(c *Command) (handled bool) {
- if m := purgeRgx.FindStringSubmatch(c.message); len(m) == 3 {
- isHB := m[1] == " -hb"
- username := database.Username(m[2])
- user, err := c.db.GetUserByUsername(username)
- if err != nil {
- c.err = err
- return true
- }
- c.db.NewAudit(*c.authUser, fmt.Sprintf("purge %s #%d", user.Username, user.ID))
- if isHB {
- _ = c.db.DeleteUserHbChatMessages(user.ID)
- } else {
- _ = c.db.DeleteUserChatMessages(user.ID)
- }
- database.MsgPubSub.Pub(database.RefreshTopic, database.ChatMessageType{Typ: database.ForceRefresh})
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleRename(c *Command) (handled bool) {
- if m := renameRgx.FindStringSubmatch(c.message); len(m) == 3 {
- oldUsername := database.Username(m[1])
- newUsername := database.Username(m[2])
- user, err := c.db.GetUserByUsername(oldUsername)
- if err != nil {
- c.err = err
- return true
- }
- c.db.NewAudit(*c.authUser, fmt.Sprintf("rename %s -> %s #%d", user.Username, newUsername, user.ID))
-
- if err := c.db.CanRenameTo(oldUsername, newUsername); err != nil {
- c.err = err
- return true
- }
-
- managers.ActiveUsers.RemoveUser(user.ID)
- user.Username = newUsername
- user.DoSave(c.db)
-
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleNewMeme(c *Command) (handled bool) {
- if m := memeRgx.FindStringSubmatch(c.message); len(m) == 2 {
- if c.upload == nil {
- c.err = errors.New("no file uploaded")
- return true
- }
- slug := m[1]
- oldPath := filepath.Join(config.Global.ProjectUploadsPath(), c.upload.FileName)
- newPath := filepath.Join(config.Global.ProjectMemesPath(), c.upload.FileName)
- _ = os.Rename(oldPath, newPath)
-
- if err := c.db.DB().Delete(&c.upload).Error; err != nil {
- logrus.Error(err)
- }
-
- meme := database.Meme{
- Slug: slug,
- FileName: c.upload.FileName,
- OrigFileName: c.upload.OrigFileName,
- FileSize: c.upload.FileSize,
- }
- if err := c.db.DB().Create(&meme).Error; err != nil {
- _ = os.Remove(newPath)
- logrus.Error(err)
- }
-
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleRemoveMeme(c *Command) (handled bool) {
- if m := memeRemoveRgx.FindStringSubmatch(c.message); len(m) == 2 {
- slug := m[1]
- meme, err := c.db.GetMemeBySlug(slug)
- if err != nil {
- c.err = errors.New("meme not found")
- return true
- }
- if err := meme.Delete(c.db); err != nil {
- c.err = err
- return true
- }
- c.err = NewErrSuccess("meme removed")
- return true
- }
- return
-}
-
-func handleRenameMeme(c *Command) (handled bool) {
- if m := memeRenameRgx.FindStringSubmatch(c.message); len(m) == 3 {
- slug := m[1]
- newSlug := m[2]
- meme, err := c.db.GetMemeBySlug(slug)
- if err != nil {
- c.err = errors.New("meme not found")
- return true
- }
- meme.Slug = newSlug
- meme.DoSave(c.db)
- c.err = NewErrSuccess("meme renamed")
- return true
- }
- return
-}
-
-func handleListMemes(c *Command) (handled bool) {
- if m := memesRgx.FindStringSubmatch(c.message); len(m) == 1 {
- memes, _ := c.db.GetMemes()
- msg := ""
- for _, m := range memes {
- msg += fmt.Sprintf(`<a href="/memes/%s" rel="noopener noreferrer" target="_blank">%s</a>`, m.Slug, m.Slug)
- if c.authUser.IsAdmin {
- msg += fmt.Sprintf(` (%s)`, humanize.Bytes(uint64(m.FileSize)))
- }
- msg += "<br />"
- }
- c.zeroMsg(msg)
- c.err = ErrRedirect
- return true
- }
- return
-}
-
-func handleRefreshCmd(c *Command) (handled bool) {
- if c.message == "/refresh" {
- c.err = ErrRedirect
- database.MsgPubSub.Pub(database.RefreshTopic, database.ChatMessageType{Typ: database.ForceRefresh})
- return true
- }
- return
-}
-
-func handleCodeCmd(c *Command) (handled bool) {
- if c.message == "/code" {
- c.err = ErrRedirect
- if !c.authUser.CanUseMultiline {
- c.err = errors.New("multiline is disabled for your account")
- return true
- } else if !c.authUser.UseStream {
- c.err = errors.New("only work on stream version of this chat")
- return true
- }
- payload := database.ChatMessageType{}
- if c.modMsg {
- payload.IsMod = true
- }
- if c.toUser != nil {
- toUserUsername := c.toUser.Username
- payload.ToUserUsername = &toUserUsername
- }
- // ModalManager.Pub("code", payload)
- database.MsgPubSub.Pub("modal_code_show_"+c.authUser.ID.String()+"_"+c.room.ID.String(), payload)
- return true
- }
- return
-}
-
-func handleUpdateReadMarkerCmd(c *Command) (handled bool) {
- if c.message == "/r" {
- c.db.UpdateChatReadMarker(c.authUser.ID, c.room.ID)
- c.err = ErrRedirect
- return true
- }
- return
-}
diff --git a/pkg/web/handlers/api/v1/snippetInterceptor.go b/pkg/web/handlers/api/v1/snippetInterceptor.go
@@ -1,57 +0,0 @@
-package v1
-
-import (
- "dkforest/pkg/database"
- "dkforest/pkg/managers"
- "strings"
-)
-
-type SnippetInterceptor struct{}
-
-func (i SnippetInterceptor) InterceptMsg(cmd *Command) {
- // Snippets actually mutate the original message,
- // to simulate that the user actually typed the text
- cmd.origMessage = snippets(cmd.db, cmd.authUser.ID, cmd.origMessage)
-
- cmd.origMessage = autocompleteTags(cmd.origMessage)
-
- cmd.message = cmd.origMessage
-}
-
-func snippets(db *database.DkfDB, authUserID database.UserID, html string) string {
- if snippetRgx.MatchString(html) {
- userSnippets, _ := db.GetUserSnippets(authUserID)
- if len(userSnippets) > 0 {
- // Build hashmap for fast lookup
- m := make(map[string]string)
- for _, snippet := range userSnippets {
- m["!"+snippet.Name] = snippet.Text
- }
- html = snippetRgx.ReplaceAllStringFunc(html, func(s string) string {
- // If snippet name exists, use the mapped value
- if v, ok := m[s]; ok {
- return v
- }
- return s
- })
- }
- }
- return html
-}
-
-func autocompleteTags(html string) string {
- activeUsers := managers.ActiveUsers.GetActiveUsers()
- html = autoTagRgx.ReplaceAllStringFunc(html, func(s string) string {
- s1 := strings.TrimPrefix(s, "@")
- s1 = strings.TrimSuffix(s1, "*")
- s1 = strings.ToLower(s1)
- for _, au := range activeUsers {
- l := strings.ToLower(string(au.Username))
- if strings.HasPrefix(l, s1) {
- return au.Username.AtStr()
- }
- }
- return s
- })
- return html
-}
diff --git a/pkg/web/handlers/api/v1/spamInterceptor.go b/pkg/web/handlers/api/v1/spamInterceptor.go
@@ -1,265 +0,0 @@
-package v1
-
-import (
- "dkforest/pkg/config"
- "dkforest/pkg/database"
- dutils "dkforest/pkg/database/utils"
- "dkforest/pkg/utils"
- "errors"
- "github.com/sirupsen/logrus"
- "regexp"
- "strings"
- "sync"
- "time"
-)
-
-type SpamInterceptor struct{}
-
-type Filter struct {
- IsRegex bool
- Term string
- Rgx *regexp.Regexp
- Kick bool
- Hb bool
-}
-
-var filters []Filter
-var filtersMtx sync.RWMutex
-
-func LoadFilters(db *database.DkfDB) {
- filtersMtx.Lock()
- defer filtersMtx.Unlock()
- filters = make([]Filter, 0)
- dbFilters, _ := db.GetSpamFilters()
- for _, dbFilter := range dbFilters {
- f := Filter{IsRegex: dbFilter.IsRegex}
- if dbFilter.Action == 1 {
- f.Kick = true
- } else if dbFilter.Action == 2 {
- f.Hb = true
- }
- if dbFilter.IsRegex {
- f.Rgx = regexp.MustCompile(dbFilter.Filter)
- } else {
- f.Term = dbFilter.Filter
- }
- filters = append(filters, f)
- }
-}
-
-// Check the filters that we have in the database.
-func checkDynamicFilters(c *Command, lowerCaseMessage string, silentSelfKick bool) error {
- filtersMtx.RLock()
- defer filtersMtx.RUnlock()
- for _, f := range filters {
- isMatch := (f.IsRegex && f.Rgx.MatchString(c.message)) ||
- (!f.IsRegex && strings.Contains(lowerCaseMessage, f.Term))
- if isMatch {
- if f.Hb {
- dutils.SelfHellBan(c.db, c.authUser)
- return ErrSilent
- }
- if f.Kick {
- _ = dutils.SelfKick(c.db, *c.authUser, silentSelfKick)
- }
- return ErrSpamFilterTriggered
- }
- }
- return nil
-}
-
-func (i SpamInterceptor) InterceptMsg(c *Command) {
- lowerCaseMessage := strings.ToLower(c.message)
- silentSelfKick := config.SilentSelfKick.Load()
-
- if err := checkDynamicFilters(c, lowerCaseMessage, silentSelfKick); err != nil {
- if !errors.Is(err, ErrSilent) {
- c.err = err
- }
- return
- }
-
- if c.room.IsOfficialRoom() {
- if err := checkSpam(c.db, c.origMessage, lowerCaseMessage, c.authUser); err != nil {
- c.err = err
- return
- }
- }
-
- // Check CP links
- if checkCPLinks(c.db, c.message) {
- c.err = errors.New("forbidden url")
- return
- }
-
- if !c.authUser.CanUseUppercase {
- c.message = strings.ToLower(c.message)
- }
-}
-
-var ErrSilent = errors.New("")
-var ErrSpamFilterTriggered = errors.New("spam filter triggered")
-
-func checkSpam(db *database.DkfDB, origMessage, lowerCaseMessage string, authUser *database.User) error {
- silentSelfKick := config.SilentSelfKick.Load()
-
- // Kick retard new users
- if time.Since(authUser.CreatedAt) < 5*time.Hour {
- if strings.Contains(lowerCaseMessage, "fucked up links") ||
- strings.Contains(lowerCaseMessage, "i wanna see gore") ||
- strings.Contains(lowerCaseMessage, "how can i make money") ||
- strings.Contains(lowerCaseMessage, "any links for scary stuff") {
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
- }
- if authUser.GeneralMessagesCount < 20 || time.Since(authUser.CreatedAt) < 5*time.Hour {
- if strings.Contains(lowerCaseMessage, "cp link") {
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
- }
-
- if strings.Contains(lowerCaseMessage, "#dorkforest") {
- if authUser.IsModerator() {
- return ErrSpamFilterTriggered
- }
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
-
- // Auto kick upper case typing retards
- if authUser.GeneralMessagesCount <= 5 {
- count, total := utils.CountUppercase(origMessage)
- pct := float64(count) / float64(total)
- if total > 5 && pct > 0.8 {
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
- }
-
- // Auto HB "new here"/"legit market" retards
- if autoHellbanCheck(authUser, lowerCaseMessage) {
- dutils.SelfHellBan(db, authUser)
- return nil
- }
-
- if autoKickSpammers(authUser, lowerCaseMessage) {
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
-
- tot, wordsMap := utils.WordCount(lowerCaseMessage)
- if tot >= 5 {
- totalUniqueWords := len(wordsMap)
- uniqueRatio := float64(totalUniqueWords) / float64(tot)
- repeatedWordsCount := 0
- for word, count := range wordsMap {
- if len(word) >= 5 && count > 10 {
- repeatedWordsCount++
- }
- }
- retardRatio := float64(repeatedWordsCount) / float64(totalUniqueWords)
- //fmt.Println(tot, totalUniqueWords, uniqueRatio, repeatedWordsCount, retardRatio, wordsMap)
- if uniqueRatio < 0.2 {
- logrus.Error("failed unique ratio: " + origMessage)
- return errors.New("failed unique ratio")
- }
- if retardRatio > 0.1 {
- logrus.Error("failed retard ratio: " + origMessage)
- return errors.New("failed retard ratio")
- }
- }
-
- if authUser.GeneralMessagesCount < 10 {
- if autoKickProfanity(tot, wordsMap) {
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
- }
-
- if authUser.GeneralMessagesCount < 4 {
- if (wordsMap["need"] > 0 && wordsMap["help"] > 0) ||
- (wordsMap["help"] > 0 && wordsMap["me"] > 0) ||
- (wordsMap["make"] > 0 && wordsMap["money"] > 0) ||
- wordsMap["porn"] > 0 ||
- wordsMap["murder"] > 0 {
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
- }
-
- if authUser.GeneralMessagesCount < 10 {
- if ((wordsMap["learn"] > 0 || wordsMap["teach"] > 0) && (wordsMap["hacking"] > 0 || wordsMap["hack"] > 0)) ||
- (wordsMap["cook"] > 0 && wordsMap["meth"] > 0) ||
- (wordsMap["creepy"] > 0 && (wordsMap["site"] > 0 || wordsMap["sites"] > 0)) ||
- (wordsMap["porn"] > 0 && (wordsMap["link"] > 0 || wordsMap["links"] > 0)) ||
- (wordsMap["topic"] > 0 && wordsMap["link"] > 0) {
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
- }
-
- if authUser.GeneralMessagesCount < 20 || time.Since(authUser.CreatedAt) < 5*time.Hour {
- if wordsMap["cp"] > 0 && (wordsMap["link"] > 0 || wordsMap["links"] > 0) {
- _ = dutils.SelfKick(db, *authUser, silentSelfKick)
- return ErrSpamFilterTriggered
- }
- }
-
- return nil
-}
-
-func autoKickProfanityTmp(orig string) bool {
- tot, m := utils.WordCount(strings.ToLower(orig))
- return autoKickProfanity(tot, m)
-}
-
-func autoKickProfanity(tot int, wordsMap map[string]int) bool {
- if tot > 4 && countProfanity(wordsMap) >= 4 {
- return true
- }
- return false
-}
-
-func countProfanity(wordsMap map[string]int) int {
- profanityWords := []string{"anus", "asshole", "cock", "dick", "nigger", "niggers", "nigga", "niggas", "sex", "rape", "porn",
- "cunt", "murder", "fuck", "blood", "corpse", "hole", "slut", "bitch", "shit", "poop", "butt", "faggot",
- "submissive", "slurping", "suck", "nuts", "gore", "stupid", "dumb", "jerking", "rotten", "rotted", "stinky"}
- profanity := 0
- for _, w := range profanityWords {
- if n, ok := wordsMap[w]; ok {
- profanity += n
- }
- }
- return profanity
-}
-
-var spamCharsRgx = regexp.MustCompile("[^a-z0-9]+")
-
-func autoKickSpammers(authUser *database.User, lowerCaseMessage string) bool {
- if authUser.GeneralMessagesCount <= 10 {
- processedString := spamCharsRgx.ReplaceAllString(lowerCaseMessage, "")
- return strings.Contains(processedString, "lemybeauty") ||
- strings.Contains(processedString, "blacktorcc") ||
- strings.Contains(processedString, "profjerry") ||
- strings.Contains(processedString, "shopdarkse")
- }
- return false
-}
-
-func autoHellbanCheck(authUser *database.User, lowerCaseMessage string) bool {
- checks := []string{
- "new here",
- "legit market",
- "help me",
- }
- if authUser.GeneralMessagesCount <= 5 {
- for _, check := range checks {
- if strings.Contains(lowerCaseMessage, check) {
- return true
- }
- }
- }
- return false
-}
diff --git a/pkg/web/handlers/api/v1/spamInterceptor_test.go b/pkg/web/handlers/api/v1/spamInterceptor_test.go
@@ -1,69 +0,0 @@
-package v1
-
-import (
- "dkforest/pkg/database"
- "github.com/stretchr/testify/assert"
- "testing"
-)
-
-func Test_autoHellbanCheck(t *testing.T) {
- type args struct {
- authUser *database.User
- lowerCaseMessage string
- }
- tests := []struct {
- name string
- args args
- want bool
- }{
- {name: "", args: args{authUser: &database.User{GeneralMessagesCount: 2}, lowerCaseMessage: "hi new here"}, want: true},
- {name: "", args: args{authUser: &database.User{GeneralMessagesCount: 2}, lowerCaseMessage: "hello anybody know of any legit market places ? its getting tough on here to find any that actually do what they supposed to "}, want: true},
- {name: "", args: args{authUser: &database.User{GeneralMessagesCount: 2}, lowerCaseMessage: "Hello Guys and Ladys someone can help me? I Have a Little problem.."}, want: true},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- assert.Equalf(t, tt.want, autoHellbanCheck(tt.args.authUser, tt.args.lowerCaseMessage), "autoHellbanCheck(%v, %v)", tt.args.authUser, tt.args.lowerCaseMessage)
- })
- }
-}
-
-func Test_autoKickSpammers(t *testing.T) {
- type args struct {
- authUser *database.User
- lowerCaseMessage string
- }
- tests := []struct {
- name string
- args args
- want bool
- }{
- {name: "", args: args{authUser: &database.User{GeneralMessagesCount: 2}, lowerCaseMessage: "blablabla l e m y _ b e a u t y on "}, want: true},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- assert.Equalf(t, tt.want, autoKickSpammers(tt.args.authUser, tt.args.lowerCaseMessage), "autoKickSpammers(%v, %v)", tt.args.authUser, tt.args.lowerCaseMessage)
- })
- }
-}
-
-func Test_autoKickProfanityTmp(t *testing.T) {
- type args struct {
- orig string
- }
- tests := []struct {
- name string
- args args
- want bool
- }{
- {"", args{orig: "biden is dumb fuck can suck my dick stupid nigger"}, true},
- {"", args{orig: "u can suck his nuts like the submissive faggot u are. slurping eye contact for deep man love with dirty butthole sniffing."}, true},
- {"", args{orig: "how to tear a human slut bitch from the cunt to the part in her hairline then shit into the chest cavity for happy dumpling poop soup"}, true},
- {"", args{orig: "lets murder a nun and fuck the blood scabs into her corpse pussy hole"}, true},
- {"", args{orig: "quick question, whats the best method to plant a grenaed in old ladys stinky rotted cunt hole to blast her bloods on a hotel walls"}, true},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- assert.Equalf(t, tt.want, autoKickProfanityTmp(tt.args.orig), "autoKickProfanityTmp(%v)", tt.args.orig)
- })
- }
-}
diff --git a/pkg/web/handlers/api/v1/topBarHandler.go b/pkg/web/handlers/api/v1/topBarHandler.go
@@ -1,117 +1,31 @@
package v1
import (
- bf "dkforest/pkg/blackfriday/v2"
"dkforest/pkg/clockwork"
"dkforest/pkg/config"
"dkforest/pkg/database"
dutils "dkforest/pkg/database/utils"
- "dkforest/pkg/hashset"
- "dkforest/pkg/levenshtein"
- "dkforest/pkg/managers"
"dkforest/pkg/utils"
- "errors"
+ "dkforest/pkg/web/handlers/api/v1/interceptors"
"fmt"
- "github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/dustin/go-humanize"
"github.com/labstack/echo"
- html2 "html"
- "math"
"net/http"
"net/url"
- "regexp"
"strings"
"time"
)
-const (
- agePrefix = "-----BEGIN AGE ENCRYPTED FILE-----"
- ageSuffix = "-----END AGE ENCRYPTED FILE-----"
- pgpPrefix = "-----BEGIN PGP MESSAGE-----"
- pgpSuffix = "-----END PGP MESSAGE-----"
- pgpPKeyPrefix = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
- pgpPKeySuffix = "-----END PGP PUBLIC KEY BLOCK-----"
- pgpSignedPrefix = "-----BEGIN PGP SIGNED MESSAGE-----"
- pgpSignedSuffix = "-----END PGP SIGNATURE-----"
-)
-
-var emojiReplacer = strings.NewReplacer(
- ":):", `<span class="emoji" title=":):">☺</span>`,
- ":smile:", `<span class="emoji" title=":smile:">☺</span>`,
- ":happy:", `<span class="emoji" title=":happy:">😃</span>`,
- ":see-no-evil:", `<span class="emoji" title=":see-no-evil:">🙈</span>`,
- ":hear-no-evil:", `<span class="emoji" title=":hear-no-evil:">🙉</span>`,
- ":speak-no-evil:", `<span class="emoji" title=":speak-no-evil:">🙊</span>`,
- ":poop:", `<span class="emoji" title=":poop:">💩</span>`,
- ":+1:", `<span class="emoji" title=":+1:">👍</span>`,
- ":evil:", `<span class="emoji" title=":evil:">😈</span>`,
- ":cat-happy:", `<span class="emoji" title=":cat-happy:">😸</span>`,
- ":eyes:", `<span class="emoji" title=":eyes:">👀</span>`,
- ":wave:", `<span class="emoji" title=":wave:">👋</span>`,
- ":clap:", `<span class="emoji" title=":clap:">👏</span>`,
- ":fire:", `<span class="emoji" title=":fire:">🔥</span>`,
- ":sparkles:", `<span class="emoji" title=":sparkles:">✨</span>`,
- ":sweat:", `<span class="emoji" title=":sweat:">💦</span>`,
- ":heart:", `<span class="emoji" title=":heart:">❤</span>`,
- ":broken-heart:", `<span class="emoji" title=":broken-heart:">💔</span>`,
- ":zzz:", `<span class="emoji" title=":zzz:">💤</span>`,
- ":praise:", `<span class="emoji" title=":praise:">🙌</span>`,
- ":joy:", `<span class="emoji" title=":joy:">😂</span>`,
- ":sob:", `<span class="emoji" title=":sob:">😭</span>`,
- ":scream:", `<span class="emoji" title=":scream:">😱</span>`,
- ":heart-eyes:", `<span class="emoji" title=":heart-eyes:">😍</span>`,
- ":blush:", `<span class="emoji" title=":blush:">☺</span>`,
- ":crazy:", `<span class="emoji" title=":crazy:">😜</span>`,
- ":angry:", `<span class="emoji" title=":angry:">😡</span>`,
- ":triumph:", `<span class="emoji" title=":triumph:">😤</span>`,
- ":vomit:", `<span class="emoji" title=":vomit:">🤮</span>`,
- ":skull:", `<span class="emoji" title=":skull:">💀</span>`,
- ":alien:", `<span class="emoji" title=":alien:">👽</span>`,
- ":sleeping:", `<span class="emoji" title=":sleeping:">😴</span>`,
- ":tongue:", `<span class="emoji" title=":tongue:">😛</span>`,
- ":cool:", `<span class="emoji" title=":cool:">😎</span>`,
- ":wink:", `<span class="emoji" title=":wink:">😉</span>`,
- ":thinking:", `<span class="emoji" title=":thinking:">🤔</span>`,
- ":happy-sweat:", `<span class="emoji" title=":happy-sweat:">😅</span>`,
- ":nerd:", `<span class="emoji" title=":nerd:">🤓</span>`,
- ":fox:", `<span class="emoji" title=":fox:">🦊</span>`,
- ":popcorn:", `<span class="emoji" title=":popcorn:">🍿</span>`,
- ":shrug:", `¯\_(ツ)_/¯`,
- ":flip:", `(╯°□°)╯︵ ┻━┻`,
- ":flip-all:", `┻━┻︵ \(°□°)/ ︵ ┻━┻`,
- ":fix-table:", `(ヘ・_・)ヘ┳━┳`,
- ":disap:", `ಠ_ಠ`,
-)
-
-var ErrRedirect = errors.New("redirect")
-var ErrStop = errors.New("stop")
-
-const minMsgLen = 1
-const maxMsgLen = 10000
-
-const (
- redirectPmQP = "pm"
- redirectEditQP = "e"
- redirectGroupQP = "g"
- redirectModQP = "m"
- redirectHbmQP = "hbm"
- redirectTagQP = "tag"
- redirectHTagQP = "htag"
- redirectMTagQP = "mtag"
- redirectQuoteQP = "quote"
- redirectMultilineQP = "ml"
-)
-
func getDataMessagePrefix(db *database.DkfDB, c echo.Context, roomKey string, room database.ChatRoom, authUser *database.User) (out string, err error) {
- pm := c.QueryParam(redirectPmQP)
- edit := c.QueryParam(redirectEditQP)
- group := c.QueryParam(redirectGroupQP)
- mod := c.QueryParam(redirectModQP)
- hbm := c.QueryParam(redirectHbmQP)
- tag := c.QueryParam(redirectTagQP)
- htag := c.QueryParam(redirectHTagQP)
- mtag := c.QueryParam(redirectMTagQP)
- quote := c.QueryParam(redirectQuoteQP)
+ pm := c.QueryParam(interceptors.RedirectPmQP)
+ edit := c.QueryParam(interceptors.RedirectEditQP)
+ group := c.QueryParam(interceptors.RedirectGroupQP)
+ mod := c.QueryParam(interceptors.RedirectModQP)
+ hbm := c.QueryParam(interceptors.RedirectHbmQP)
+ tag := c.QueryParam(interceptors.RedirectTagQP)
+ htag := c.QueryParam(interceptors.RedirectHTagQP)
+ mtag := c.QueryParam(interceptors.RedirectMTagQP)
+ quote := c.QueryParam(interceptors.RedirectQuoteQP)
if pm != "" {
out = "/pm " + pm + " "
@@ -218,18 +132,18 @@ func ChatTopBarHandler(c echo.Context) error {
data.RoomName = c.Param("roomName")
queryParams := c.QueryParams()
- origMl := utils.DoParseBool(c.QueryParam(redirectMultilineQP))
+ origMl := utils.DoParseBool(c.QueryParam(interceptors.RedirectMultilineQP))
data.QueryParams = queryParams.Encode()
- queryParams.Set(redirectMultilineQP, "1")
+ queryParams.Set(interceptors.RedirectMultilineQP, "1")
data.QueryParamsMl = queryParams.Encode()
- queryParams.Del(redirectMultilineQP)
+ queryParams.Del(interceptors.RedirectMultilineQP)
data.QueryParamsNml = queryParams.Encode()
redirectQP := url.Values{}
if authUser.CanUseMultiline {
data.Multiline = origMl
if data.Multiline {
- redirectQP.Set(redirectMultilineQP, "1")
+ redirectQP.Set(interceptors.RedirectMultilineQP, "1")
}
}
@@ -269,42 +183,38 @@ func ChatTopBarHandler(c echo.Context) error {
origMessage := strings.TrimSpace(c.Request().PostFormValue("message"))
- cmd := NewCommand(c, origMessage, room, roomKey)
- cmd.redirectQP = redirectQP
-
- type Interceptor interface {
- InterceptMsg(*Command)
- }
-
- interceptors := []Interceptor{
- SnippetInterceptor{},
- SpamInterceptor{},
- ChessInstance,
- BattleshipInstance,
- WWInstance,
- BangInterceptor{},
- UploadInterceptor{},
- SlashInterceptor{},
- CodeModalInterceptor{},
- MsgInterceptor{},
+ cmd := interceptors.NewCommand(c, origMessage, room, roomKey)
+ cmd.SetRedirectQP(redirectQP)
+
+ interceptors := []interceptors.Interceptor{
+ interceptors.SnippetInterceptor{},
+ interceptors.SpamInterceptor{},
+ interceptors.ChessInstance,
+ interceptors.BattleshipInstance,
+ interceptors.WWInstance,
+ interceptors.BangInterceptor{},
+ interceptors.UploadInterceptor{},
+ interceptors.SlashInterceptor{},
+ interceptors.CodeModalInterceptor{},
+ interceptors.MsgInterceptor{},
}
for _, interceptor := range interceptors {
interceptor.InterceptMsg(cmd)
- data.Message = cmd.dataMessage
- if cmd.err != nil {
- return handleCmdError(cmd.err, c, data, cmd.redirectURL(), cmd.origMessage)
+ data.Message = cmd.DataMessage()
+ if cmd.Err() != nil {
+ return handleCmdError(cmd.Err(), c, data, cmd.RedirectURL(), cmd.OrigMessage())
}
}
- return c.Redirect(http.StatusFound, cmd.redirectURL())
+ return c.Redirect(http.StatusFound, cmd.RedirectURL())
}
func handleCmdError(err error, ctx echo.Context, data chatTopBarData, redirectURL, origMessage string) error {
- if err == ErrRedirect {
+ if err == interceptors.ErrRedirect {
return ctx.Redirect(http.StatusFound, redirectURL)
- } else if err == ErrStop {
+ } else if err == interceptors.ErrStop {
return ctx.Render(http.StatusOK, "chat-top-bar", data)
- } else if serr, ok := err.(*ErrSuccess); ok {
+ } else if serr, ok := err.(*interceptors.ErrSuccess); ok {
data.Success = serr.Error()
return ctx.Render(http.StatusOK, "chat-top-bar", data)
}
@@ -313,29 +223,6 @@ func handleCmdError(err error, ctx echo.Context, data chatTopBarData, redirectUR
return ctx.Render(http.StatusOK, "chat-top-bar", data)
}
-func convertMarkdown(in string) string {
- out := strings.Replace(in, "\r", "", -1)
- resBytes := bf.Run([]byte(out), bf.WithRenderer(utils.MyRenderer(false, false)), bf.WithExtensions(
- bf.NoIntraEmphasis|bf.Tables|bf.FencedCode|
- bf.Strikethrough|bf.SpaceHeadings|
- bf.DefinitionLists|bf.HardLineBreak|bf.NoLink))
- out = string(resBytes)
- return out
-}
-
-func replTextPrefixSuffix(msg, prefix, suffix, repl string) (out string) {
- out = msg
- pgpPIdx := strings.Index(msg, prefix)
- pgpSIdx := strings.Index(msg, suffix)
- if pgpPIdx != -1 && pgpSIdx != -1 {
- newMsg := msg[:pgpPIdx]
- newMsg += repl
- newMsg += msg[pgpSIdx+len(suffix):]
- out = newMsg
- }
- return
-}
-
func handleGetQuote(db *database.DkfDB, msgUUID, roomKey string, room database.ChatRoom, authUser *database.User) (dataMessage string, err error) {
quoted, err := db.GetRoomChatMessageByUUID(room.ID, msgUUID)
if err != nil {
@@ -359,7 +246,7 @@ func handleGetQuote(db *database.DkfDB, msgUUID, roomKey string, room database.C
}
// Append the actual quoted text
- dataMessage = prefix + getQuoteTxt(db, roomKey, quoted) + " "
+ dataMessage = prefix + interceptors.GetQuoteTxt(db, roomKey, quoted) + " "
return
}
@@ -377,877 +264,3 @@ func handleGetEdit(db *database.DkfDB, hourMinSec, roomKey string, room database
}
return dataMessage, nil
}
-
-type Command struct {
- err error
-
- // Data that can be mutated
- redirectQP url.Values // RedirectURL Query Parameters
- origMessage string // This is the original text that the user input (can be changed by /e)
- dataMessage string // This is what the user will have in his input box
- message string // Un-sanitized message received from the user
- room database.ChatRoom // Room the user is in
- roomKey string // Room password (if any)
- authUser *database.User // Authenticated user (sender of the message)
- db *database.DkfDB // Database instance
- toUser *database.User // If not nil, will be a PM
- upload *database.Upload // If the message contains an uploaded file
- editMsg *database.ChatMessage // If we're editing a message
- groupID *database.GroupID // If the message is for a subgroup
- hellbanMsg bool // Is the message will be marked HB
- systemMsg bool // Is the message system
- modMsg bool // Is the message part of the "moderators" group
- c echo.Context
- zeroUser *database.User // Cache the zero (@0) user
- skipInboxes bool
-}
-
-func NewCommand(c echo.Context, origMessage string, room database.ChatRoom, roomKey string) *Command {
- authUser := c.Get("authUser").(*database.User)
- db := c.Get("database").(*database.DkfDB)
- return &Command{
- c: c,
- authUser: authUser,
- db: db,
- hellbanMsg: authUser.IsHellbanned,
- redirectQP: url.Values{},
- origMessage: origMessage,
- message: origMessage,
- room: room,
- roomKey: roomKey,
- }
-}
-
-func (c *Command) redirectURL() string {
- return fmt.Sprintf("/api/v1/chat/top-bar/%s?%s", c.room.Name, c.redirectQP.Encode())
-}
-
-// Lazy loading and cache of the zero user
-func (c *Command) getZeroUser() database.User {
- if c.zeroUser == nil {
- zeroUser := dutils.GetZeroUser(c.db)
- c.zeroUser = &zeroUser
- }
- return *c.zeroUser
-}
-
-// Have the "zero user" send a processed message to the authUser
-func (c *Command) zeroProcMsg(rawMsg string) {
- c.zeroProcMsgRoom(rawMsg, c.roomKey, c.room.ID)
-}
-
-// Have the "zero user" send a processed message in the specified room
-func (c *Command) zeroPublicProcMsgRoom(rawMsg, roomKey string, roomID database.RoomID) {
- c.zeroProcMsgRoomToUser(rawMsg, roomKey, roomID, nil)
-}
-
-// Have the "zero user" send a processed message to the authUser in the specified room
-func (c *Command) zeroProcMsgRoom(rawMsg, roomKey string, roomID database.RoomID) {
- c.zeroProcMsgRoomToUser(rawMsg, roomKey, roomID, c.authUser)
-}
-
-// Have the "zero user" send a "processed message" PM to a user in a specific room.
-func (c *Command) zeroProcMsgRoomToUser(rawMsg, roomKey string, roomID database.RoomID, toUser *database.User) {
- procMsg, _, _ := ProcessRawMessage(c.db, rawMsg, roomKey, c.authUser.ID, roomID, nil, true)
- c.zeroRawMsg(toUser, rawMsg, procMsg)
-}
-
-// Have the "zero usser" send an unprocessed private message to the authUser
-func (c *Command) zeroMsg(msg string) {
- c.zeroRawMsg(c.authUser, msg, msg)
-}
-
-// Have the "zero usser" send an unprocessed message in the current room
-func (c *Command) zeroPublicMsg(raw, msg string) {
- c.zeroRawMsg(nil, raw, msg)
-}
-
-func (c *Command) zeroRawMsg(user2 *database.User, raw, msg string) {
- zeroUser := c.getZeroUser()
- c.rawMsg(zeroUser, user2, raw, msg)
-}
-
-func (c *Command) rawMsg(user1 database.User, user2 *database.User, raw, msg string) {
- if c.room.ReadOnly {
- return
- }
- rawMsgRoom(c.db, user1, user2, raw, msg, c.roomKey, c.room.ID)
-}
-
-func rawMsgRoom(db *database.DkfDB, user1 database.User, user2 *database.User, raw, msg, roomKey string, roomID database.RoomID) {
- var toUserID *database.UserID
- if user2 != nil {
- toUserID = &user2.ID
- }
- _, _ = db.CreateMsg(raw, msg, roomKey, roomID, user1.ID, toUserID)
-}
-
-type ErrSuccess struct {
- msg string
-}
-
-func NewErrSuccess(msg string) *ErrSuccess {
- return &ErrSuccess{msg: msg}
-}
-
-func (e ErrSuccess) Error() string {
- return e.msg
-}
-
-func appendUploadLink(html string, upload *database.Upload) string {
- if upload != nil {
- if html != "" {
- html += " "
- }
- html += `[` + upload.GetHTMLLink() + `]`
- }
- return html
-}
-
-func checkCPLinks(db *database.DkfDB, html string) bool {
- m1 := onionV3Rgx.FindAllStringSubmatch(html, -1)
- m2 := onionV2Rgx.FindAllStringSubmatch(html, -1)
- for _, m := range append(m1, m2...) {
- hash := utils.MD5([]byte(m[0]))
- if _, err := db.GetOnionBlacklist(hash); err == nil {
- return true
- }
- }
- return false
-}
-
-func linkDefaultRooms(html string) string {
- r := strings.NewReplacer(
- "#general", `<a href="/chat/general" target="_top">#general</a>`,
- "#programming", `<a href="/chat/programming" target="_top">#programming</a>`,
- "#hacking", `<a href="/chat/hacking" target="_top">#hacking</a>`,
- "#suggestions", `<a href="/chat/suggestions" target="_top">#suggestions</a>`,
- "#announcements", `<a href="/chat/announcements" target="_top">#announcements</a>`,
- )
- return r.Replace(html)
-}
-
-// Convert timestamps such as 01:23:45 to an archive link if a message with that timestamp exists.
-// eg: "Some text 14:31:46 some more text"
-func convertArchiveLinks(db *database.DkfDB, html string, roomID database.RoomID, authUserID database.UserID) string {
- start, rest := "", html
-
- // Do not replace timestamps that are inside a quote text
- const quoteSuffix = `”`
- endOfQuoteIdx := strings.LastIndex(html, quoteSuffix)
- if endOfQuoteIdx != -1 {
- start, rest = html[:endOfQuoteIdx], html[endOfQuoteIdx:]
- }
-
- archiveRgx := regexp.MustCompile(`(\d{2}-\d{2} )?\d{2}:\d{2}:\d{2}`)
- if archiveRgx.MatchString(rest) {
- rest = archiveRgx.ReplaceAllStringFunc(rest, func(s string) string {
- var dt time.Time
- var err error
- if len(s) == 8 { // HH:MM:SS
- dt, err = utils.ParsePrevDatetimeAt(s, clockwork.NewRealClock())
- } else if len(s) == 14 { // mm-dd HH:MM:SS
- dt, err = utils.ParsePrevDatetimeAt2(s, clockwork.NewRealClock())
- }
- if err != nil {
- return s
- }
- if msgs, err := db.GetRoomChatMessagesByDate(roomID, dt.UTC()); err == nil && len(msgs) > 0 {
- msg := msgs[0]
- if len(msgs) > 1 {
- for _, msgTmp := range msgs {
- if msgTmp.User.ID == authUserID || (msgTmp.ToUserID != nil && *msgTmp.ToUserID == authUserID) {
- msg = msgTmp
- break
- }
- }
- }
- return fmt.Sprintf(`<a href="/chat/%s/archive#%s" target="_blank" rel="noopener noreferrer">%s</a>`, msg.Room.Name, msg.UUID, s)
- }
- return s
- })
- }
- return start + rest
-}
-
-func convertBangShortcuts(html string) string {
- r := strings.NewReplacer(
- "!bhc", config.BhcOnion,
- "!cryptbb", config.CryptbbOnion,
- "!dread", config.DreadOnion,
- "!dkf", config.DkfOnion,
- "!rroom", config.DkfOnion+`/red-room`,
- "!dnmx", config.DnmxOnion,
- "!whonix", config.WhonixOnion,
- "!age", config.AgeUrl,
- "!chattor", config.ChattorOnion,
- "!lulbins", config.LulbinsOnion,
- )
- return r.Replace(html)
-}
-
-type getUsersByUsernameFn func(usernames []string) ([]database.User, error)
-
-// Update the given html to add user style for tags.
-// Return the new html, and a map[userID]User of tagged users.
-func colorifyTaggedUsers(html string, getUsersByUsername getUsersByUsernameFn) (string, map[database.UserID]database.User) {
- usernameMatches := tagRgx.FindAllStringSubmatch(html, -1)
- usernames := hashset.New[string]()
- for _, usernameMatch := range usernameMatches {
- usernames.Insert(usernameMatch[1])
- }
- taggedUsers, _ := getUsersByUsername(usernames.ToArray())
-
- taggedUsersMap := make(map[string]database.User)
- taggedUsersIDsMap := make(map[database.UserID]database.User)
- for _, taggedUser := range taggedUsers {
- taggedUsersMap[strings.ToLower(taggedUser.Username.AtStr())] = taggedUser
- if taggedUser.Username != config.NullUsername {
- taggedUsersIDsMap[taggedUser.ID] = taggedUser
- }
- }
-
- if len(usernameMatches) > 0 {
- html = tagRgx.ReplaceAllStringFunc(html, func(s string) string {
- lowerS := strings.ToLower(s)
- if user, ok := taggedUsersMap[lowerS]; ok {
- return fmt.Sprintf("<span %s>@%s</span>", user.GenerateChatStyle1(), user.Username)
- }
-
- // Not found, try to fix typos using levenshtein
- activeUsers := managers.ActiveUsers.GetActiveUsers()
- if len(activeUsers) > 0 {
- minDist := math.MaxInt
- minAu := activeUsers[0]
- for _, au := range activeUsers {
- lowerAu := strings.ToLower(string(au.Username))
- d := levenshtein.ComputeDistance(lowerS, lowerAu)
- if d < minDist {
- minDist = d
- minAu = au
- }
- }
- if minDist <= 3 {
- if users, _ := getUsersByUsername([]string{minAu.Username.String()}); len(users) > 0 {
- user := users[0]
- return fmt.Sprintf("<span %s>@%s</span>", user.GenerateChatStyle1(), user.Username)
- }
- }
- }
-
- return s
- })
- }
- return html, taggedUsersIDsMap
-}
-
-func linkRoomTags(db *database.DkfDB, html string) string {
- if roomTagRgx.MatchString(html) {
- html = roomTagRgx.ReplaceAllStringFunc(html, func(s string) string {
- if room, err := db.GetChatRoomByName(strings.TrimPrefix(s, "#")); err == nil {
- return `<a href="/chat/` + room.Name + `" target="_top">` + s + `</a>`
- }
- return s
- })
- }
- return html
-}
-
-// Given a roomID and hourMinSec (01:23:45) and a username, retrieve the message from database that fits the predicates.
-func getQuotedChatMessage(db *database.DkfDB, hourMinSec string, username database.Username, roomID database.RoomID) (quoted *database.ChatMessage) {
- if dt, err := utils.ParsePrevDatetimeAt(hourMinSec, clockwork.NewRealClock()); err == nil {
- if msgs, err := db.GetRoomChatMessagesByDate(roomID, dt.UTC()); err == nil && len(msgs) > 0 {
- msg := msgs[0]
- if len(msgs) > 1 {
- for _, msgTmp := range msgs {
- if msgTmp.User.Username == username {
- msg = msgTmp
- break
- }
- }
- }
- quoted = &msg
- }
- }
- return
-}
-
-// Given a chat message, return the text to be used as a quote.
-func getQuoteTxt(db *database.DkfDB, roomKey string, quoted database.ChatMessage) (out string) {
- var err error
- decrypted, err := quoted.GetRawMessage(roomKey)
- if err != nil {
- return
- }
- if quoted.ToUserID != nil {
- if m := pmRgx.FindStringSubmatch(decrypted); len(m) == 3 {
- decrypted = m[2]
- }
- } else if quoted.Moderators {
- decrypted = strings.TrimPrefix(decrypted, "/m ")
- } else if quoted.IsHellbanned {
- decrypted = strings.TrimPrefix(decrypted, "/hbm ")
- }
- isMe := false
- if strings.HasPrefix(decrypted, "/me") {
- isMe = true
- decrypted = strings.TrimPrefix(decrypted, "/me ")
- }
-
- startIdx := 0
- if strings.HasPrefix(decrypted, `“[`) {
- startIdx = strings.Index(decrypted, `” `)
- if startIdx == -1 {
- startIdx = 0
- } else {
- startIdx += len(`” `)
- }
- }
-
- decrypted = replTextPrefixSuffix(decrypted, agePrefix, ageSuffix, "[age.txt]")
- decrypted = replTextPrefixSuffix(decrypted, pgpPrefix, pgpSuffix, "[pgp.txt]")
- decrypted = replTextPrefixSuffix(decrypted, pgpPKeyPrefix, pgpPKeySuffix, "[pgp_pkey.txt]")
-
- remaining := " "
- if !quoted.System {
- remaining += fmt.Sprintf(`%s `, quoted.User.Username)
- }
- if quoted.UploadID != nil {
- if upload, err := db.GetUploadByID(*quoted.UploadID); err == nil {
- if decrypted != "" {
- decrypted += " "
- }
- decrypted += `[` + upload.OrigFileName + `]`
- }
- }
- if !isMe {
- remaining += "- "
- }
-
- toBeQuoted := decrypted[startIdx:]
- toBeQuoted = strings.ReplaceAll(toBeQuoted, "\n", ` `)
- toBeQuoted = strings.ReplaceAll(toBeQuoted, `“`, `"`)
- toBeQuoted = strings.ReplaceAll(toBeQuoted, `”`, `"`)
-
- remaining += utils.TruncStr2(toBeQuoted, 70, "…")
- return `“[` + quoted.CreatedAt.Format("15:04:05") + "]" + remaining + `”`
-}
-
-// This function will get the raw user input message which is not safe to directly render.
-//
-// To prevent people from altering the text of the quote,
-// we retrieve the original quoted message using the timestamp and username,
-// and we use the original message text.
-//
-// eg: we received altered quote, and return original quote ->
-// “[01:23:45] username - Some maliciously altered quote” Some text
-// “[01:23:45] username - The original text” Some text
-func convertQuote(db *database.DkfDB, origHtml string, roomKey string, roomID database.RoomID) (html string, quoted *database.ChatMessage) {
- const quotePrefix = `“[`
- const quoteSuffix = `”`
- html = origHtml
- idx := strings.Index(origHtml, quoteSuffix)
- if strings.HasPrefix(origHtml, quotePrefix) && idx > -1 {
- prefixLen := len(quotePrefix)
- suffixLen := len(quoteSuffix)
- if len(origHtml) > prefixLen+9 {
- hourMinSec := origHtml[prefixLen : prefixLen+8]
- username := database.Username(origHtml[prefixLen+10 : strings.Index(origHtml[prefixLen+10:], " ")+prefixLen+10])
- if quoted = getQuotedChatMessage(db, hourMinSec, username, roomID); quoted != nil {
- html = getQuoteTxt(db, roomKey, *quoted)
- html += origHtml[idx+suffixLen:]
- }
- }
- }
- return html, quoted
-}
-
-func styleQuote(origHtml string, quoted *database.ChatMessage) (html string) {
- const quoteSuffix = `”`
- html = origHtml
- if quoted != nil {
- idx := strings.Index(origHtml, quoteSuffix)
- prefixLen := len(`<p>“[`)
- suffixLen := len(quoteSuffix)
- dateLen := 8 // 01:23:45 --> 8
-
- // <p>“[01:23:45] username - quoted text” user text</p>
- date := origHtml[prefixLen : prefixLen+dateLen] // `01:23:45`
- quoteTxt := origHtml[prefixLen+dateLen+1 : idx] // ` username - quoted text`
- userTxt := origHtml[idx+suffixLen:] // ` user text</p>`
-
- sb := strings.Builder{}
- sb.WriteString(`<p>“<small style="opacity: 0.8;"><i>[`)
-
- // Date link
- sb.WriteString(`<a href="/chat/`)
- sb.WriteString(quoted.Room.Name)
- sb.WriteString(`/archive#`)
- sb.WriteString(quoted.UUID)
- sb.WriteString(`" target="_blank" rel="noopener noreferrer">`)
- sb.WriteString(date)
- sb.WriteString(`</a>`)
-
- sb.WriteString(`]<span `)
- sb.WriteString(quoted.User.GenerateChatStyle1())
- sb.WriteString(`>`)
- sb.WriteString(quoteTxt)
- sb.WriteString(`</span></i></small>”`)
- sb.WriteString(userTxt)
- html = sb.String()
- }
- return html
-}
-
-var noSchemeOnionLinkRgx = regexp.MustCompile(`\s[a-z2-7]{56}\.onion`)
-
-// Fix up onion links that are missing the http scheme. This often happen when copy/pasting a link.
-func convertLinksWithoutScheme(in string) string {
- html := noSchemeOnionLinkRgx.ReplaceAllStringFunc(in, func(s string) string {
- return " http://" + strings.TrimSpace(s)
- })
- return html
-}
-
-var linkRgxStr = `(http|ftp|https):\/\/([\w\-_]+(?:(?:\.[\w\-_]+)+))([\w\-\.,@?^=%&:/~\+#\(\)]*[\w\-\@?^=%&/~\+#\(\)])?`
-var profileRgxStr = `/u/\w{3,20}`
-var linkShorthandRgxStr = `/l/\w{3,20}`
-var dkfArchiveRgx = regexp.MustCompile(`/chat/([\w_]{3,50})/archive\?uuid=([\w-]{36})#[\w-]{36}`)
-var linkOrProfileRgx = regexp.MustCompile(`(` + linkRgxStr + `|` + profileRgxStr + `|` + linkShorthandRgxStr + `)`)
-var userProfileLinkRgx = regexp.MustCompile(`^` + profileRgxStr + `$`)
-var linkShorthandPageLinkRgx = regexp.MustCompile(`^` + linkShorthandRgxStr + `$`)
-var youtubeComIDRgx = regexp.MustCompile(`watch\?v=([\w-]+)`)
-var youtubeComShortsIDRgx = regexp.MustCompile(`/shorts/([\w-]+)`)
-var youtuBeIDRgx = regexp.MustCompile(`https://youtu\.be/([\w-]+)`)
-var yewtubeBeIDRgx = youtubeComIDRgx
-var invidiousIDRgx = youtubeComIDRgx
-
-func makeHtmlLink(label, link string) string {
- // We replace @ to prevent colorifyTaggedUsers from trying to generate html inside the links.
- r := strings.NewReplacer("@", "@", "#", "#")
- label = r.Replace(label)
- link = r.Replace(link)
- return fmt.Sprintf(`<a href="%s" rel="noopener noreferrer" target="_blank">%s</a>`, link, label)
-}
-
-func splitQuote(in string) (string, string) {
- const quotePrefix = `<p>“[`
- const quoteSuffix = `”`
- idx := strings.Index(in, quoteSuffix)
- if idx == -1 || !strings.HasPrefix(in, quotePrefix) {
- return "", in
- }
- return in[:idx], in[idx:]
-}
-
-func convertLinks(in string,
- roomID database.RoomID,
- getUserByUsername func(database.Username) (database.User, error),
- getLinkByShorthand func(string) (database.Link, error),
- getChatMessageByUUID func(string) (database.ChatMessage, error)) string {
- quote, rest := splitQuote(in)
-
- libredditURLs := []string{
- "http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion",
- "http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion",
- "http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion",
- "http://inytumdgnri7xsqtvpntjevaelxtgbjqkuqhtf6txxhwbll2fwqtakqd.onion",
- "http://liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion",
- "http://kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion",
- "http://ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion",
- "http://ledditqo2mxfvlgobxnlhrkq4dh34jss6evfkdkb2thlvy6dn4f4gpyd.onion",
- "http://ol5begilptoou34emq2sshf3may3hlblvipdjtybbovpb7c7zodxmtqd.onion",
- "http://lbrdtjaj7567ptdd4rv74lv27qhxfkraabnyphgcvptl64ijx2tijwid.onion",
- }
-
- invidiousURLs := []string{
- "http://c7hqkpkpemu6e7emz5b4vyz7idjgdvgaaa3dyimmeojqbgpea3xqjoid.onion",
- "http://kbjggqkzv65ivcqj6bumvp337z6264huv5kpkwuv6gu5yjiskvan7fad.onion",
- "http://grwp24hodrefzvjjuccrkw3mjq4tzhaaq32amf33dzpmuxe7ilepcmad.onion"}
-
- wikilessURLs := []string{
- "http://c2pesewpalbi6lbfc5hf53q4g3ovnxe4s7tfa6k2aqkf7jd7a7dlz5ad.onion",
- "http://dj2tbh2nqfxyfmvq33cjmhuw7nb6am7thzd3zsjvizeqf374fixbrxyd.onion"}
-
- nitterURLs := []string{
- "http://nitraeju2mipeziu2wtcrqsxg7h62v5y4eqgwi75uprynkj74gevvuqd.onion"}
-
- rimgoURLs := []string{
- "http://be7udfhmnzqyt7cxysg6c4pbawarvaofjjywp35nhd5qamewdfxl6sid.onion"}
-
- knownOnions := [][]string{
- {"http://dkfgit.onion", config.DkfGitOnion},
- {"http://dread.onion", config.DreadOnion},
- {"http://cryptbb.onion", config.CryptbbOnion},
- {"http://blkhat.onion", config.BhcOnion},
- {"http://dnmx.onion", config.DnmxOnion},
- {"http://whonix.onion", config.WhonixOnion},
- {"http://chattor.onion", config.ChattorOnion},
- {"http://lulbins.onion", config.LulbinsOnion},
- }
-
- newRest := linkOrProfileRgx.ReplaceAllStringFunc(rest, func(link string) string {
- // Convert all occurrences of "/u/username" to a link to user profile page if the user exists
- if userProfileLinkRgx.MatchString(link) {
- user, err := getUserByUsername(database.Username(strings.TrimPrefix(link, "/u/")))
- if err != nil {
- return link
- }
- href := "/u/" + string(user.Username)
- return makeHtmlLink(href, href)
- }
-
- // Convert all occurrences of "/l/shorthand" to a link to link page if the shorthand exists
- if linkShorthandPageLinkRgx.MatchString(link) {
- l, err := getLinkByShorthand(strings.TrimPrefix(link, "/l/"))
- if err != nil {
- return link
- }
- href := "/l/" + *l.Shorthand
- return makeHtmlLink(href, href)
- }
-
- // Handle reddit links
- if strings.HasPrefix(link, "https://www.reddit.com/") {
- old := strings.Replace(link, "https://www.reddit.com/", "https://old.reddit.com/", 1)
- libredditLink := utils.RandChoice(libredditURLs)
- libredditLink = strings.Replace(link, "https://www.reddit.com", libredditLink, 1)
- oldHtmlLink := makeHtmlLink("old", old)
- libredditHtmlLink := makeHtmlLink("libredditLink", libredditLink)
- htmlLink := makeHtmlLink(link, link)
- return htmlLink + ` (` + oldHtmlLink + ` | ` + libredditHtmlLink + `)`
- } else if strings.HasPrefix(link, "https://old.reddit.com/") {
- libredditLink := utils.RandChoice(libredditURLs)
- libredditLink = strings.Replace(link, "https://old.reddit.com", libredditLink, 1)
- libredditHtmlLink := makeHtmlLink("libredditLink", libredditLink)
- htmlLink := makeHtmlLink(link, link)
- return htmlLink + ` (` + libredditHtmlLink + `)`
- }
- for _, libredditURL := range libredditURLs {
- if strings.HasPrefix(link, libredditURL) {
- newPrefix := strings.Replace(link, libredditURL, "http://reddit.onion", 1)
- old := strings.Replace(link, libredditURL, "https://old.reddit.com", 1)
- oldHtmlLink := makeHtmlLink("old", old)
- htmlLink := makeHtmlLink(newPrefix, link)
- return htmlLink + ` (` + oldHtmlLink + `)`
- }
- }
-
- // Append YouTube link to invidious link
- for _, invidiousURL := range invidiousURLs {
- if strings.HasPrefix(link, invidiousURL) {
- if strings.Contains(link, ".onion/watch?v=") {
- newPrefix := strings.Replace(link, invidiousURL, "http://invidious.onion", 1)
- m := invidiousIDRgx.FindStringSubmatch(link)
- if len(m) == 2 {
- videoID := m[1]
- youtubeLink := "https://www.youtube.com/watch?v=" + videoID
- youtubeHtmlLink := makeHtmlLink("Youtube", youtubeLink)
- htmlLink := makeHtmlLink(newPrefix, link)
- return htmlLink + ` (` + youtubeHtmlLink + `)`
- }
- }
- }
- }
- // Unknown invidious links
- if strings.Contains(link, ".onion/watch?v=") {
- m := invidiousIDRgx.FindStringSubmatch(link)
- if len(m) == 2 {
- videoID := m[1]
- youtubeLink := "https://www.youtube.com/watch?v=" + videoID
- youtubeHtmlLink := makeHtmlLink("Youtube", youtubeLink)
- htmlLink := makeHtmlLink(link, link)
- return htmlLink + ` (` + youtubeHtmlLink + `)`
- }
- }
-
- // Append wikiless link to wikipedia link
- if strings.HasPrefix(link, "https://en.wikipedia.org/") {
- wikilessLink := utils.RandChoice(wikilessURLs)
- wikilessLink = strings.Replace(link, "https://en.wikipedia.org", wikilessLink, 1)
- wikilessHtmlLink := makeHtmlLink("Wikiless", wikilessLink)
- htmlLink := makeHtmlLink(link, link)
- return htmlLink + ` (` + wikilessHtmlLink + `)`
- }
- for _, wikilessURL := range wikilessURLs {
- if strings.HasPrefix(link, wikilessURL) {
- newPrefix := strings.Replace(link, wikilessURL, "http://wikiless.onion", 1)
- wikipediaPrefix := strings.Replace(link, wikilessURL, "https://en.wikipedia.org", 1)
- wikipediaHtmlLink := makeHtmlLink("Wikipedia", wikipediaPrefix)
- htmlLink := makeHtmlLink(newPrefix, link)
- return htmlLink + ` (` + wikipediaHtmlLink + `)`
- }
- }
-
- // Append nitter link to twitter link
- if strings.HasPrefix(link, "https://twitter.com/") {
- nitterLink := utils.RandChoice(nitterURLs)
- nitterLink = strings.Replace(link, "https://twitter.com", nitterLink, 1)
- nitterHtmlLink := makeHtmlLink("Nitter", nitterLink)
- htmlLink := makeHtmlLink(link, link)
- return htmlLink + ` (` + nitterHtmlLink + `)`
- }
- for _, nitterURL := range nitterURLs {
- if strings.HasPrefix(link, nitterURL) {
- newPrefix := strings.Replace(link, nitterURL, "http://nitter.onion", 1)
- twitterPrefix := strings.Replace(link, nitterURL, "https://twitter.com", 1)
- twitterHtmlLink := makeHtmlLink("Twitter", twitterPrefix)
- htmlLink := makeHtmlLink(newPrefix, link)
- return htmlLink + ` (` + twitterHtmlLink + `)`
- }
- }
-
- // Append rimgo link to imgur link
- if strings.HasPrefix(link, "https://imgur.com/") {
- rimgoLink := utils.RandChoice(rimgoURLs)
- rimgoLink = strings.Replace(link, "https://imgur.com", rimgoLink, 1)
- rimgoHtmlLink := makeHtmlLink("Rimgo", rimgoLink)
- htmlLink := makeHtmlLink(link, link)
- return htmlLink + ` (` + rimgoHtmlLink + `)`
- }
- for _, rimgoURL := range rimgoURLs {
- if strings.HasPrefix(link, rimgoURL) {
- newPrefix := strings.Replace(link, rimgoURL, "http://rimgo.onion", 1)
- imgurPrefix := strings.Replace(link, rimgoURL, "https://imgur.com", 1)
- imgurHtmlLink := makeHtmlLink("Imgur", imgurPrefix)
- htmlLink := makeHtmlLink(newPrefix, link)
- return htmlLink + ` (` + imgurHtmlLink + `)`
- }
- }
-
- // Append invidious link to YouTube/yewtube link
- var videoID string
- var m []string
- var isShortUrl, isYewtube bool
- if strings.HasPrefix(link, "https://youtu.be/") {
- m = youtuBeIDRgx.FindStringSubmatch(link)
- } else if strings.HasPrefix(link, "https://www.youtube.com/watch?v=") {
- m = youtubeComIDRgx.FindStringSubmatch(link)
- } else if strings.HasPrefix(link, "https://yewtu.be/") || strings.HasPrefix(link, "https://www.yewtu.be/") {
- m = yewtubeBeIDRgx.FindStringSubmatch(link)
- isYewtube = true
- } else if strings.HasPrefix(link, "https://www.youtube.com/shorts/") {
- m = youtubeComShortsIDRgx.FindStringSubmatch(link)
- isShortUrl = true
- }
- if len(m) == 2 {
- videoID = m[1]
- }
- if videoID != "" {
- invidiousLink := utils.RandChoice(invidiousURLs) + "/watch?v=" + videoID + "&local=true"
- invidiousHtmlLink := makeHtmlLink("Invidious", invidiousLink)
- htmlLink := makeHtmlLink(link, link)
- youtubeLink := "https://www.youtube.com/watch?v=" + videoID
- youtubeHtmlLink := makeHtmlLink("YT", youtubeLink)
- out := htmlLink + ` (` + invidiousHtmlLink + `)`
- if isShortUrl || isYewtube {
- out = htmlLink + ` (` + youtubeHtmlLink + ` | ` + invidiousHtmlLink + `)`
- }
- return out
- }
-
- // Special case for dkf links.
- {
- dkfLocalPrefix := "http://127.0.0.1:8080"
- dkfShortPrefix := "http://dkf.onion"
- dkfLongPrefix := config.DkfOnion
- hasLocalPrefix := strings.HasPrefix(link, dkfLocalPrefix)
- hasDkfShortPrefix := strings.HasPrefix(link, dkfShortPrefix)
- hasDkfLongPrefix := strings.HasPrefix(link, dkfLongPrefix)
- if hasLocalPrefix || hasDkfLongPrefix || hasDkfShortPrefix {
- var trimmed string
- if hasLocalPrefix {
- trimmed = strings.TrimPrefix(link, dkfLocalPrefix)
- } else if hasDkfLongPrefix {
- trimmed = strings.TrimPrefix(link, dkfLongPrefix)
- } else if hasDkfShortPrefix {
- trimmed = strings.TrimPrefix(link, dkfShortPrefix)
- }
- label := dkfShortPrefix + trimmed
- href := trimmed
- // Shorten archive links
- if m := dkfArchiveRgx.FindStringSubmatch(label); len(m) == 3 {
- if msg, err := getChatMessageByUUID(m[2]); err == nil {
- if roomID == msg.RoomID {
- label = msg.CreatedAt.Format("[Jan 02 03:04:05]")
- } else {
- label = msg.CreatedAt.Format("[#" + m[1] + " Jan 02 03:04:05]")
- }
- }
- }
- // Allows to have messages such as: "my profile is /u/username :)"
- if userProfileLinkRgx.MatchString(trimmed) {
- if user, err := getUserByUsername(database.Username(strings.TrimPrefix(trimmed, "/u/"))); err == nil {
- label = "/u/" + string(user.Username)
- href = "/u/" + string(user.Username)
- }
- } else if linkShorthandPageLinkRgx.MatchString(trimmed) {
- // Convert all occurrences of "/l/shorthand" to a link to link page if the shorthand exists
- if l, err := getLinkByShorthand(strings.TrimPrefix(trimmed, "/l/")); err == nil {
- label = "/l/" + *l.Shorthand
- href = "/l/" + *l.Shorthand
- }
- }
- return makeHtmlLink(label, href)
- }
- }
-
- for _, el := range knownOnions {
- shortPrefix := el[0]
- longPrefix := el[1]
- if strings.HasPrefix(link, longPrefix) {
- return makeHtmlLink(shortPrefix+strings.TrimPrefix(link, longPrefix), link)
- } else if strings.HasPrefix(link, shortPrefix) {
- return makeHtmlLink(link, longPrefix+strings.TrimPrefix(link, shortPrefix))
- }
- }
- return makeHtmlLink(link, link)
- })
-
- return quote + newRest
-}
-
-func convertNewLines(html string, canUseMultiline bool) string {
- if !canUseMultiline {
- html = strings.ReplaceAll(html, "\n", "")
- }
- return html
-}
-
-func extractPGPMessage(html string) (out string) {
- pgpPrefixL := pgpPrefix
- pgpSuffixL := pgpSuffix
- startIdx := strings.Index(html, pgpPrefixL)
- endIdx := strings.Index(html, pgpSuffixL)
- if startIdx != -1 && endIdx != -1 {
- out = html[startIdx : endIdx+len(pgpSuffixL)]
- out = strings.TrimSpace(out)
- out = strings.TrimPrefix(out, pgpPrefixL)
- out = strings.TrimSuffix(out, pgpSuffixL)
- out = strings.Join(strings.Split(out, " "), "\n")
- out = pgpPrefixL + out
- out += pgpSuffixL
- }
- return out
-}
-
-// Auto convert pasted pgp public key into uploaded file
-func convertPGPPublicKeyToFile(db *database.DkfDB, html string, authUserID database.UserID) string {
- pgpPKeyPrefixL := pgpPKeyPrefix
- pgpPKeySuffixL := pgpPKeySuffix
- startIdx := strings.Index(html, pgpPKeyPrefixL)
- endIdx := strings.Index(html, pgpPKeySuffixL)
- if startIdx != -1 && endIdx != -1 {
- pkeySubSlice := html[startIdx : endIdx+len(pgpPKeySuffixL)]
- unescapedPkey := html2.UnescapeString(pkeySubSlice)
- tmp := convertInlinePGPPublicKey(unescapedPkey)
- upload, _ := db.CreateUpload("pgp_pkey.txt", []byte(tmp), authUserID)
- msgBefore := html[0:startIdx]
- msgAfter := html[endIdx+len(pgpPKeySuffixL):]
- html = msgBefore + ` [` + upload.GetHTMLLink() + `] ` + msgAfter
- html = strings.TrimSpace(html)
- }
- return html
-}
-
-func convertPGPClearsignToFile(db *database.DkfDB, html string, authUserID database.UserID) string {
- if b, _ := clearsign.Decode([]byte(html)); b != nil {
- pgpSignedPrefixL := pgpSignedPrefix
- pgpSignedSuffixL := pgpSignedSuffix
- startIdx := strings.Index(html, pgpSignedPrefixL)
- endIdx := strings.Index(html, pgpSignedSuffixL)
- tmp := html[startIdx : endIdx+len(pgpSignedSuffixL)]
- upload, _ := db.CreateUpload("pgp_clearsign.txt", []byte(tmp), authUserID)
- msgBefore := html[0:startIdx]
- msgAfter := html[endIdx+len(pgpSignedSuffixL):]
- html = msgBefore + ` [` + upload.GetHTMLLink() + `] ` + msgAfter
- html = strings.TrimSpace(html)
- }
- return html
-}
-
-// Auto convert pasted pgp message into uploaded file
-func convertPGPMessageToFile(db *database.DkfDB, html string, authUserID database.UserID) string {
- pgpPrefixL := pgpPrefix
- pgpSuffixL := pgpSuffix
- startIdx := strings.Index(html, pgpPrefixL)
- endIdx := strings.Index(html, pgpSuffixL)
- if startIdx != -1 && endIdx != -1 {
- tmp := html[startIdx : endIdx+len(pgpSuffixL)]
- tmp = strings.TrimSpace(tmp)
- tmp = strings.TrimPrefix(tmp, pgpPrefixL)
- tmp = strings.TrimSuffix(tmp, pgpSuffixL)
- tmp = strings.Join(strings.Split(tmp, " "), "\n")
- tmp = pgpPrefixL + tmp
- tmp += pgpSuffixL
- upload, _ := db.CreateUpload("pgp.txt", []byte(tmp), authUserID)
- msgBefore := html[0:startIdx]
- msgAfter := html[endIdx+len(pgpSuffixL):]
- html = msgBefore + ` [` + upload.GetHTMLLink() + `] ` + msgAfter
- html = strings.TrimSpace(html)
- }
- return html
-}
-
-// Auto convert pasted age message into uploaded file
-func convertAgeMessageToFile(db *database.DkfDB, html string, authUserID database.UserID) string {
- agePrefixL := agePrefix
- ageSuffixL := ageSuffix
- startIdx := strings.Index(html, agePrefixL)
- endIdx := strings.Index(html, ageSuffixL)
- if startIdx != -1 && endIdx != -1 {
- tmp := html[startIdx : endIdx+len(ageSuffixL)]
- tmp = strings.TrimSpace(tmp)
- tmp = strings.TrimPrefix(tmp, agePrefixL)
- tmp = strings.TrimSuffix(tmp, ageSuffixL)
- tmp = strings.Join(strings.Split(tmp, " "), "\n")
- tmp = agePrefixL + tmp
- tmp += ageSuffixL
- upload, _ := db.CreateUpload("age.txt", []byte(tmp), authUserID)
- msgBefore := html[0:startIdx]
- msgAfter := html[endIdx+len(ageSuffixL):]
- html = msgBefore + ` [` + upload.GetHTMLLink() + `] ` + msgAfter
- html = strings.TrimSpace(html)
- }
- return html
-}
-
-func convertInlinePGPPublicKey(inlinePKey string) string {
- pgpPKeyPrefixL := pgpPKeyPrefix
- pgpPKeySuffixL := pgpPKeySuffix
- // If it contains new lines, it was probably pasted using multi-line text box
- if strings.Contains(inlinePKey, "\n") {
- return inlinePKey
- }
- inlinePKey = strings.TrimSpace(inlinePKey)
- inlinePKey = strings.TrimPrefix(inlinePKey, pgpPKeyPrefixL)
- inlinePKey = strings.TrimSuffix(inlinePKey, pgpPKeySuffixL)
- inlinePKey = strings.TrimSpace(inlinePKey)
- commentsParts := strings.Split(inlinePKey, "Comment: ")
- commentsParts, lastCommentPart := commentsParts[:len(commentsParts)-1], commentsParts[len(commentsParts)-1]
- newCommentsParts := make([]string, 0)
- for idx := range commentsParts {
- if commentsParts[idx] != "" {
- commentsParts[idx] = "Comment: " + commentsParts[idx]
- commentsParts[idx] = strings.TrimSpace(commentsParts[idx])
- newCommentsParts = append(newCommentsParts, commentsParts[idx])
- }
- }
-
- rgx := regexp.MustCompile(`\s\s(\w|\+|/){64}`)
- m := rgx.FindStringIndex(lastCommentPart)
- commentsStr := ""
- key := ""
- if len(m) == 2 {
- idx := m[0]
- lastCommentP1 := lastCommentPart[:idx]
- lastCommentP2 := lastCommentPart[idx+2:]
- key = strings.Join(strings.Split(lastCommentP2, " "), "\n")
- commentsStr = strings.Join(newCommentsParts, "\n")
- commentsStr += "\nComment: " + lastCommentP1 + "\n\n"
- } else {
- key = "\n" + strings.Join(strings.Split(lastCommentPart, " "), "\n")
- }
- inlinePKey = pgpPKeyPrefixL + "\n" + commentsStr + key + "\n" + pgpPKeySuffixL
- return inlinePKey
-}
diff --git a/pkg/web/handlers/api/v1/uploadInterceptor.go b/pkg/web/handlers/api/v1/uploadInterceptor.go
@@ -1,77 +0,0 @@
-package v1
-
-import (
- "dkforest/pkg/config"
- "dkforest/pkg/database"
- "dkforest/pkg/utils"
- hutils "dkforest/pkg/web/handlers/utils"
- "errors"
- "fmt"
- "github.com/asaskevich/govalidator"
- "github.com/dustin/go-humanize"
- "github.com/sirupsen/logrus"
- "io/ioutil"
- "mime/multipart"
-)
-
-type UploadInterceptor struct{}
-
-func (i UploadInterceptor) InterceptMsg(cmd *Command) {
- if file, handler, uploadErr := cmd.c.Request().FormFile("file"); uploadErr == nil {
- // Save file on disk & database & append file link to html
- var err error
- cmd.upload, err = handleUploadedFile(cmd.db, file, handler, cmd.authUser)
- if err != nil {
- cmd.err = err
- return
- }
- }
-}
-
-func handleUploadedFile(db *database.DkfDB, file multipart.File, handler *multipart.FileHeader, authUser *database.User) (*database.Upload, error) {
- defer file.Close()
- if !authUser.CanUpload() {
- return nil, hutils.AccountTooYoungErr
- }
- userSizeUploaded := db.GetUserTotalUploadSize(authUser.ID)
- if handler.Size+userSizeUploaded > config.MaxUserTotalUploadSize {
- return nil, fmt.Errorf("user upload limit reached (%s)", humanize.Bytes(config.MaxUserTotalUploadSize))
- }
- origFileName := handler.Filename
- if handler.Size > config.MaxUserFileUploadSize {
- return nil, fmt.Errorf("the maximum file size is %s", humanize.Bytes(config.MaxUserFileUploadSize))
- }
- if !govalidator.StringLength(origFileName, "3", "50") {
- return nil, errors.New("invalid file name, 3-50 characters")
- }
- if !govalidator.IsPrintableASCII(origFileName) {
- return nil, errors.New("file name must be ascii printable only")
- }
- origFileName = tzRgx.ReplaceAllString(origFileName, "xxxx-xx-xx at xx.xx.xx XX")
- origFileName = tz1Rgx.ReplaceAllString(origFileName, "xxxx-xx-xx xx-xx-xx")
- origFileName = tz3Rgx.ReplaceAllString(origFileName, "xxxx-xx-xx xxxxxx")
- origFileName = tz4Rgx.ReplaceAllString(origFileName, "xxxx-xx-xx_xx_xx_xx")
- fileBytes, err := ioutil.ReadAll(file)
- if err != nil {
- return nil, err
- }
-
- // Validate image type and determine extension
- mimeType := handler.Header.Get("Content-Type")
- if mimeType == "image/jpeg" {
- fileBytes, err = utils.ReencodeJpg(fileBytes)
- } else if mimeType == "image/png" {
- fileBytes, err = utils.ReencodePng(fileBytes)
- }
- if err != nil {
- return nil, err
- }
-
- // Uploaded files are encrypted on disk
- upload, err := db.CreateEncryptedUploadWithSize(origFileName, fileBytes, authUser.ID, handler.Size)
- if err != nil {
- logrus.Error(err)
- return nil, err
- }
- return upload, nil
-}
diff --git a/pkg/web/handlers/api/v1/werewolf.go b/pkg/web/handlers/api/v1/werewolf.go
@@ -1,701 +0,0 @@
-package v1
-
-import (
- "bytes"
- "context"
- "dkforest/pkg/config"
- "dkforest/pkg/database"
- "dkforest/pkg/hashset"
- "dkforest/pkg/utils"
- "errors"
- "fmt"
- "github.com/sirupsen/logrus"
- "html/template"
- "math/rand"
- "sort"
- "strings"
- "time"
-)
-
-var WWInstance *Werewolf
-
-const (
- PreGameState = iota + 1
- DayState
- NightState
- VoteState
- EndGameState
-)
-
-const (
- TownspeopleRole = "townspeople"
- WerewolfRole = "werewolf"
- SeerRole = "seer"
- HealerRole = "healer"
-)
-
-var ErrInvalidPlayerName = errors.New("unknown player name, please send a valid name")
-
-type Werewolf struct {
- db *database.DkfDB
- ctx context.Context
- cancel context.CancelFunc
- readyCh chan bool
- narratorID database.UserID
- roomID database.RoomID
- werewolfGroupID database.GroupID
- spectatorGroupID database.GroupID
- deadGroupID database.GroupID
- players map[database.Username]*Player
- playersAlive map[database.Username]*Player
- state int64
- werewolfSet *hashset.HashSet[database.UserID]
- spectatorSet *hashset.HashSet[database.UserID]
- townspersonSet *hashset.HashSet[database.UserID]
- healerID *database.UserID
- seerID *database.UserID
- werewolfCh chan string
- seerCh chan string
- healerCh chan string
- votesCh chan string
- voted *hashset.HashSet[database.UserID] // Keep track of which user voted already
-}
-
-// Return either or not the userID is an active player (alive)
-func (b *Werewolf) isAlivePlayer(userID database.UserID) bool {
- for _, player := range b.playersAlive {
- if player.UserID == userID {
- return true
- }
- }
- return false
-}
-
-func (b *Werewolf) InterceptPreGameMsg(cmd *Command) {
- if cmd.message == "/players" {
- b.Narrate("Registered players: "+b.alivePlayersStr(), nil, nil)
- cmd.err = ErrRedirect
- return
-
- } else if cmd.message == "/join" {
- if cmd.authUser.IsHellbanned {
- cmd.err = ErrRedirect
- return
- }
- if _, found := b.players[cmd.authUser.Username]; found {
- cmd.err = ErrRedirect
- return
- }
- player := &Player{
- UserID: cmd.authUser.ID,
- Username: cmd.authUser.Username,
- }
- b.players[cmd.authUser.Username] = player
- b.playersAlive[cmd.authUser.Username] = player
- b.Narrate(cmd.authUser.Username.AtStr()+" joined the Game", nil, nil)
- cmd.err = ErrRedirect
- return
-
- } else if cmd.message == "/spectate" {
- b.spectatorSet.Insert(cmd.authUser.ID)
- b.Narrate(cmd.authUser.Username.AtStr()+" spectate the Game", nil, nil)
- cmd.err = ErrRedirect
- return
-
- } else if cmd.message == "/start" {
- b.cancel()
- time.Sleep(time.Second)
- utils.SGo(func() {
- b.StartGame(cmd.db)
- })
- cmd.err = ErrRedirect
- return
- }
-}
-
-func (b *Werewolf) InterceptNightMsg(cmd *Command) {
- if cmd.groupID != nil && *cmd.groupID == b.werewolfGroupID {
- select {
- case b.werewolfCh <- cmd.message:
- cmd.err = ErrRedirect
- default:
- cmd.err = errors.New("narrator doesn't need your input")
- }
- return
- } else if b.isForNarrator(cmd) && b.seerID != nil && cmd.authUser.ID == *b.seerID {
- select {
- case b.seerCh <- cmd.message:
- cmd.err = ErrRedirect
- default:
- cmd.err = errors.New("narrator doesn't need your input")
- }
- return
- } else if b.isForNarrator(cmd) && b.healerID != nil && cmd.authUser.ID == *b.healerID {
- select {
- case b.healerCh <- cmd.message:
- cmd.err = ErrRedirect
- default:
- cmd.err = errors.New("narrator doesn't need your input")
- }
- return
- }
- cmd.err = errors.New("chat disabled")
- return
-}
-
-// Return either or not the message is a PM for the narrator
-func (b *Werewolf) isForNarrator(cmd *Command) bool {
- return cmd.toUser != nil && cmd.toUser.ID == b.narratorID
-}
-
-func (b *Werewolf) InterceptVoteMsg(cmd *Command) {
- if !b.isAlivePlayer(cmd.authUser.ID) || !b.isForNarrator(cmd) {
- cmd.err = errors.New("chat disabled")
- return
- }
- if b.isForNarrator(cmd) {
- if !b.voted.Contains(cmd.authUser.ID) {
- name := cmd.message
- if b.isValidPlayerName(name) {
- b.votesCh <- name
- } else {
- b.Narrate(ErrInvalidPlayerName.Error(), &cmd.authUser.ID, nil)
- }
- } else {
- b.Narrate("You have already voted", &cmd.authUser.ID, nil)
- }
- }
-}
-
-var tuto = `Tutorial:
-"/join" to join the Game
-"/players" list the players that have joined the Game
-"/start" to start the Game
-"/stop" to stop the Game
-"/ready" will skip the 5min conversation
-"/tuto" will display this tutorial
-"/clear" will reset the room and display this tutorial
-
-Werewolf: To kill someone during the night, you have to reply in the "werewolf" group with the name of the person to kill (no @)
-Seer/Healer: You have reply to the narrator with the name (eg: "/pm 0 n0tr1v")
-Townspeople: To vote, you have to pm the narrator with a name (eg: "/pm 0 n0tr1v")`
-
-func (b *Werewolf) InterceptMsg(cmd *Command) {
- if cmd.room.ID != b.roomID {
- return
- }
-
- SlashInterceptor{}.InterceptMsg(cmd)
-
- // If the message is a PM not for the narrator, we reject it
- if cmd.toUser != nil && (cmd.toUser.ID != b.narratorID && cmd.authUser.ID != b.narratorID) {
- cmd.err = errors.New("PM not allowed at this room")
- return
- }
-
- // Spectator can chat all the time
- if cmd.groupID != nil && *cmd.groupID == b.spectatorGroupID {
- return
- }
-
- if cmd.authUser.IsModerator() && cmd.message == "/stop" {
- b.Narrate(fmt.Sprintf("@%s used /stop", cmd.authUser.Username), nil, nil)
- b.cancel()
- cmd.err = ErrRedirect
- return
- } else if cmd.authUser.IsModerator() && cmd.message == "/ready" {
- b.Narrate(fmt.Sprintf("@%s used /ready", cmd.authUser.Username), nil, nil)
- b.readyCh <- true
- cmd.err = ErrRedirect
- return
- } else if cmd.authUser.IsModerator() && cmd.message == "/tuto" {
- b.Narrate(tuto, nil, nil)
- cmd.err = ErrRedirect
- return
- } else if cmd.authUser.IsModerator() && cmd.message == "/clear" {
- _ = cmd.db.DeleteChatRoomMessages(b.roomID)
- b.Narrate(tuto, nil, nil)
- cmd.err = ErrRedirect
- return
- }
-
- // Anyone can talk during these states
- if b.state == PreGameState || b.state == EndGameState {
- if b.state == PreGameState {
- b.InterceptPreGameMsg(cmd)
- }
- return
- }
-
- // Otherwise, non-playing people cannot talk in public chat
- if !b.isAlivePlayer(cmd.authUser.ID) {
- cmd.err = errors.New("public chat disabled")
- return
- }
-
- switch b.state {
- case DayState:
- case VoteState:
- b.InterceptVoteMsg(cmd)
- case NightState:
- b.InterceptNightMsg(cmd)
- default:
- cmd.err = errors.New("public chat disabled")
- return
- }
-}
-
-// Wait until we receive the votes from all the players
-func (b *Werewolf) waitVotes() (votes []string) {
- for len(votes) < len(b.playersAlive) {
- var vote string
- select {
- case vote = <-b.votesCh:
- case <-time.After(15 * time.Second):
- b.Narrate(fmt.Sprintf("Waiting votes %d/%d", len(votes), len(b.playersAlive)), nil, nil)
- continue
- case <-b.ctx.Done():
- return
- }
- votes = append(votes, vote)
- }
- return
-}
-
-func (b *Werewolf) waitNameFromWerewolf() (name string) {
- for {
- select {
- case name = <-b.werewolfCh:
- case <-time.After(15 * time.Second):
- b.Narrate("Waiting reply from werewolf", nil, nil)
- continue
- case <-b.ctx.Done():
- return
- }
- if b.isValidPlayerName(name) {
- break
- }
- b.Narrate(ErrInvalidPlayerName.Error(), nil, &b.werewolfGroupID)
- }
- return name
-}
-
-func (b *Werewolf) waitNameFromSeer() (name string) {
- for {
- select {
- case name = <-b.seerCh:
- case <-time.After(15 * time.Second):
- b.Narrate("Waiting reply from seer", nil, nil)
- continue
- case <-b.ctx.Done():
- return
- }
- if b.isValidPlayerName(name) {
- break
- }
- b.Narrate(ErrInvalidPlayerName.Error(), b.seerID, nil)
- }
- return name
-}
-
-func (b *Werewolf) waitNameFromHealer() (name string) {
- for {
- select {
- case name = <-b.healerCh:
- case <-time.After(15 * time.Second):
- b.Narrate("Waiting reply from healer", nil, nil)
- continue
- case <-b.ctx.Done():
- return
- }
- if b.isValidPlayerName(name) {
- break
- }
- b.Narrate(ErrInvalidPlayerName.Error(), b.healerID, nil)
- }
- return name
-}
-
-// Return either a name is a valid alive player name or not
-func (b *Werewolf) isValidPlayerName(name string) bool {
- name = strings.TrimSpace(name)
- for _, player := range b.playersAlive {
- if string(player.Username) == name {
- return true
- }
- }
- return false
-}
-
-// Narrate register a chat message on behalf of the narrator user
-func (b *Werewolf) Narrate(msg string, toUserID *database.UserID, groupID *database.GroupID) {
- html, _, _ := ProcessRawMessage(b.db, msg, "", b.narratorID, b.roomID, nil, true)
- b.NarrateRaw(html, toUserID, groupID)
-}
-
-func (b *Werewolf) NarrateRaw(msg string, toUserID *database.UserID, groupID *database.GroupID) {
- _, _ = b.db.CreateOrEditMessage(nil, msg, msg, "", b.roomID, b.narratorID, toUserID, nil, groupID, false, false, false)
-}
-
-// Display roles assigned at beginning of the Game
-func (b *Werewolf) displayRoles() {
- msg := "Roles were:\n"
- for _, player := range b.players {
- msg += player.Username.AtStr() + " : " + player.Role + "\n"
- }
- b.Narrate(msg, nil, nil)
-}
-
-func (b *Werewolf) StartGame(db *database.DkfDB) {
- defer func() {
- b.displayRoles()
- b.reset()
- }()
- b.ctx, b.cancel = context.WithCancel(context.Background())
- // Assign roles
- playersArr := make([]*Player, 0)
- for _, player := range b.playersAlive {
- playersArr = append(playersArr, player)
- }
- rand.Shuffle(len(playersArr), func(i, j int) { playersArr[i], playersArr[j] = playersArr[j], playersArr[i] })
- for idx, player := range playersArr {
- if idx == 0 {
- b.werewolfSet.Insert(player.UserID)
- _, _ = db.AddUserToRoomGroup(b.roomID, b.werewolfGroupID, player.UserID)
- player.Role = WerewolfRole
- werewolfMsg := "During the day you seem to be a regular Townsperson.\n" +
- "However, you’ve been kissed by the Night and transform into a Werewolf when the sun sets.\n" +
- "Your new nature compels you to kill and eat a Townsperson every night."
- b.Narrate(werewolfMsg, &player.UserID, nil)
- } else if idx == 1 {
- b.townspersonSet.Insert(player.UserID)
- b.healerID = &player.UserID
- player.Role = HealerRole
- healerMsg := "You’re a Townsperson with the unique ability to save lives.\n" +
- "During the night, you’ll get a chance to protect another Townsperson from death if they are attacked by the Werewolves.\n" +
- "You can choose to protect yourself."
- b.Narrate(healerMsg, &player.UserID, nil)
- } else if idx == 2 {
- b.townspersonSet.Insert(player.UserID)
- b.seerID = &player.UserID
- player.Role = SeerRole
- seerMsg := "You’re a Townsperson with the unique ability to peer into a person’s soul and see their true nature.\n" +
- "During the night, you’ll get a chance to see if another Townsperson is a Werewolf.\n" +
- "However, use this information wisely because it can lead to you being targeted by the Werewolves the next night if they deduce your identity."
- b.Narrate(seerMsg, &player.UserID, nil)
- } else {
- b.townspersonSet.Insert(player.UserID)
- player.Role = TownspeopleRole
- townspersonMsg := "You’re a regular member of the town.\n" +
- "Perhaps you’re a baker, merchant, or soldier.\n" +
- "Your job is to save the town by eliminating the Werewolves that have infiltrated your town and started feeding on your neighbors.\n" +
- "Also, try to avoid getting killed yourself."
- b.Narrate(townspersonMsg, &player.UserID, nil)
- }
- }
- b.state = DayState
- b.Narrate("Players: "+b.alivePlayersStr(), nil, nil)
- b.Narrate("Day 1: It is day time. Players can now introduce themselves. (5min)", nil, nil)
-
- select {
- case <-time.After(5 * time.Minute):
- case <-b.readyCh:
- case <-b.ctx.Done():
- b.Narrate("STOP SIGNAL - Game is being stopped", nil, nil)
- return
- }
-
- for {
- b.state = NightState
- b.Narrate("Townspeople, go to sleep", nil, nil)
- playerNameToKill := b.processWerewolf()
- b.processSeer()
- playerNameToSave := b.processHealer()
-
- b.state = DayState
- b.Narrate("Townspeople, wake up", nil, nil)
- if playerNameToKill == playerNameToSave {
- b.Narrate("Someone was attacked last night, but they survived", nil, nil)
- } else {
- b.Narrate("Everyone wakes up to see a trail of blood leading to the forest.\n"+
- "There you find @"+playerNameToKill+"’s mangled remains by the Great Oak.\n"+
- "Curiously, there are deep claw marks in the bark of the surrounding trees.\n"+
- "It looks like @"+playerNameToKill+" put up a fight.", nil, nil)
- b.kill(db, database.Username(playerNameToKill))
- }
-
- b.Narrate("Players still alive: "+b.alivePlayersStr(), nil, nil)
- if b.werewolfSet.Len() == 0 {
- b.Narrate("Townspeople win", nil, nil)
- break
- } else if b.townspersonSet.Len() <= 1 {
- b.Narrate("Werewolf win", nil, nil)
- break
- }
-
- b.Narrate("Townspeople now have 5min to discuss the events", nil, nil)
-
- select {
- case <-time.After(5 * time.Minute):
- case <-b.readyCh:
- case <-b.ctx.Done():
- b.Narrate("STOP SIGNAL - Game is being stopped", nil, nil)
- return
- }
-
- b.state = VoteState
- b.voted = hashset.New[database.UserID]()
- b.Narrate("It's now time to vote for execution. PM me the name you vote to execute or \"none\"", nil, nil)
- killName := b.killVote()
- if killName == "" {
- b.Narrate("Townspeople do not want to execute anyone", nil, nil)
- } else {
- b.Narrate("Townspeople execute @"+killName, nil, nil)
- b.kill(db, database.Username(killName))
- }
-
- b.Narrate("Players still alive: "+b.alivePlayersStr(), nil, nil)
-
- if b.werewolfSet.Len() == 0 {
- b.Narrate("Townspeople win", nil, nil)
- break
- } else if b.townspersonSet.Len() == 1 {
- b.Narrate("Werewolf win", nil, nil)
- break
- }
- }
- b.state = EndGameState
- b.Narrate("Game ended", nil, nil)
-}
-
-// Return the names of alive players. ie: "user1, user2, user3"
-func (b *Werewolf) alivePlayersStr() (out string) {
- arr := make([]string, 0)
- for _, player := range b.playersAlive {
- arr = append(arr, player.Username.AtStr())
- }
- sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })
- return strings.Join(arr, ", ")
-}
-
-// Kill a player
-func (b *Werewolf) kill(db *database.DkfDB, playerName database.Username) {
- player, found := b.playersAlive[playerName]
- if !found {
- return
- }
- delete(b.playersAlive, playerName)
- switch player.Role {
- case WerewolfRole:
- b.werewolfSet.Remove(player.UserID)
- _ = db.RmUserFromRoomGroup(b.roomID, b.werewolfGroupID, player.UserID)
- case TownspeopleRole:
- b.townspersonSet.Remove(player.UserID)
- case HealerRole:
- b.townspersonSet.Remove(player.UserID)
- b.healerID = nil
- case SeerRole:
- b.townspersonSet.Remove(player.UserID)
- b.seerID = nil
- }
- _, _ = db.AddUserToRoomGroup(b.roomID, b.deadGroupID, player.UserID)
-}
-
-// Return the name of the player name that receive the most vote
-func (b *Werewolf) killVote() string {
-
- // Send a PM to all players saying they have to vote for a name
- for _, player := range b.playersAlive {
- msg := "Who do you vote to kill? (name | none)"
- msg += b.createKillVoteForm()
- b.NarrateRaw(msg, &player.UserID, nil)
- }
-
- votes := b.waitVotes()
- // Get the max voted name
- maxName := "none"
- maxCount := 0
- voteMap := make(map[string]int) // keep track of how many votes for each values
- for _, vote := range votes {
- tmp := voteMap[vote]
- tmp++
- voteMap[vote] = tmp
- if tmp > maxCount {
- maxCount = tmp
- maxName = vote
- }
- }
- if maxName == "none" {
- return ""
- }
- return maxName
-}
-
-func (b *Werewolf) getAlivePlayersArr(includeWerewolves bool) []database.Username {
- arr := make([]database.Username, 0)
- for _, player := range b.playersAlive {
- if !includeWerewolves && b.werewolfSet.Contains(player.UserID) {
- continue
- }
- arr = append(arr, player.Username)
- }
- sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] })
- return arr
-}
-
-func (b *Werewolf) createPickUserForm() string {
- arr := b.getAlivePlayersArr(true)
-
- htmlTmpl := `
-<form method="post" action="/api/v1/werewolf">
- {{ range $idx, $p := .Arr }}
- <input type="radio" ID="player{{ $idx }}" name="message" value="/pm 0 {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
- {{ end }}
- <button type="submit" name="btn_submit">ok</button>
-</form>`
- data := map[string]any{
- "Arr": arr,
- }
- var buf bytes.Buffer
- _ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
- return buf.String()
-}
-
-func (b *Werewolf) createKillVoteForm() string {
- arr := b.getAlivePlayersArr(true)
-
- htmlTmpl := `
-<form method="post" action="/api/v1/werewolf">
- {{ range $idx, $p := .Arr }}
- <input type="radio" ID="player{{ $idx }}" name="message" value="/pm 0 {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
- {{ end }}
- <input type="radio" ID="none" name="message" value="/pm 0 none" /><label for="none">none</label><br />
- <button type="submit" name="btn_submit">ok</button>
-</form>`
- data := map[string]any{
- "Arr": arr,
- }
- var buf bytes.Buffer
- _ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
- return buf.String()
-}
-
-func (b *Werewolf) createWerewolfPickUserForm() string {
- arr := b.getAlivePlayersArr(false)
-
- htmlTmpl := `
-<form method="post" action="/api/v1/werewolf">
- {{ range $idx, $p := .Arr }}
- <input type="radio" ID="player{{ $idx }}" name="message" value="/g werewolf {{ $p }}" /><label for="player{{ $idx }}">{{ $p }}</label><br />
- {{ end }}
- <button type="submit" name="btn_submit">ok</button>
-</form>`
- data := map[string]any{
- "Arr": arr,
- }
- var buf bytes.Buffer
- _ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
- return buf.String()
-}
-
-func (b *Werewolf) processWerewolf() string {
- b.UnlockGroup("werewolf")
- msg := "Werewolf, who do you want to kill?"
- msg += b.createWerewolfPickUserForm()
- b.NarrateRaw(msg, nil, &b.werewolfGroupID)
- name := b.waitNameFromWerewolf()
- b.Narrate(name+" will be killed", nil, &b.werewolfGroupID)
- b.LockGroup("werewolf")
- return name
-}
-
-func (b *Werewolf) processSeer() {
- if b.seerID == nil {
- return
- }
- msg := "Seer, who do you want to identify?"
- msg += b.createPickUserForm()
- b.NarrateRaw(msg, b.seerID, nil)
- name := b.waitNameFromSeer()
- player := b.playersAlive[database.Username(name)]
- b.Narrate(name+" is a "+player.Role, b.seerID, nil)
-}
-
-func (b *Werewolf) processHealer() string {
- if b.healerID == nil {
- return ""
- }
- msg := "Healer, who do you want to save?"
- msg += b.createPickUserForm()
- b.NarrateRaw(msg, b.healerID, nil)
- name := b.waitNameFromHealer()
- b.Narrate(name+" will survive the night", b.healerID, nil)
- return name
-}
-
-func (b *Werewolf) LockGroups() {
- b.LockGroup("werewolf")
-}
-
-func (b *Werewolf) LockGroup(groupName string) {
- group, _ := b.db.GetRoomGroupByName(b.roomID, groupName)
- group.Locked = true
- group.DoSave(b.db)
-}
-
-func (b *Werewolf) UnlockGroup(groupName string) {
- group, _ := b.db.GetRoomGroupByName(b.roomID, groupName)
- group.Locked = false
- group.DoSave(b.db)
-}
-
-type Player struct {
- UserID database.UserID
- Username database.Username
- Role string
-}
-
-func (b *Werewolf) reset() {
- b.ctx, b.cancel = context.WithCancel(context.Background())
- b.state = PreGameState
- b.players = make(map[database.Username]*Player)
- b.playersAlive = make(map[database.Username]*Player)
- b.werewolfSet = hashset.New[database.UserID]()
- b.spectatorSet = hashset.New[database.UserID]()
- b.townspersonSet = hashset.New[database.UserID]()
- b.voted = hashset.New[database.UserID]()
- b.werewolfCh = make(chan string)
- b.seerCh = make(chan string)
- b.healerCh = make(chan string)
- b.votesCh = make(chan string)
- b.readyCh = make(chan bool)
- _ = b.db.ClearRoomGroup(b.roomID, b.werewolfGroupID)
- _ = b.db.ClearRoomGroup(b.roomID, b.spectatorGroupID)
- _ = b.db.ClearRoomGroup(b.roomID, b.deadGroupID)
-}
-
-func NewWerewolf(db *database.DkfDB) *Werewolf {
- // Prepare room
- room, err := db.GetChatRoomByName("werewolf")
- if err != nil {
- logrus.Error("#werewolf room not found")
- return nil
- }
- zeroUser, _ := db.GetUserByUsername(config.NullUsername)
- _ = db.DeleteChatRoomGroups(room.ID)
- werewolfGroup, _ := db.CreateChatRoomGroup(room.ID, "werewolf", "#ffffff")
- werewolfGroup.Locked = true
- werewolfGroup.DoSave(db)
- spectatorGroup, _ := db.CreateChatRoomGroup(room.ID, "spectator", "#ffffff")
- deadGroup, _ := db.CreateChatRoomGroup(room.ID, "dead", "#ffffff")
-
- b := new(Werewolf)
- b.db = db
- b.werewolfGroupID = werewolfGroup.ID
- b.spectatorGroupID = spectatorGroup.ID
- b.deadGroupID = deadGroup.ID
- b.narratorID = zeroUser.ID
- b.roomID = room.ID
- b.reset()
- return b
-}
diff --git a/pkg/web/handlers/data.go b/pkg/web/handlers/data.go
@@ -3,7 +3,7 @@ package handlers
import (
"dkforest/pkg/managers"
"dkforest/pkg/odometer"
- v1 "dkforest/pkg/web/handlers/api/v1"
+ "dkforest/pkg/web/handlers/api/v1/interceptors"
"time"
"dkforest/pkg/database"
@@ -912,7 +912,7 @@ type stego1RoadChallengeData struct {
}
type chessData struct {
- Games []v1.ChessGame
+ Games []interceptors.ChessGame
Error string
Username database.Username
}
diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go
@@ -11,6 +11,7 @@ import (
"dkforest/pkg/pubsub"
"dkforest/pkg/utils/crypto"
v1 "dkforest/pkg/web/handlers/api/v1"
+ "dkforest/pkg/web/handlers/api/v1/interceptors"
"dkforest/pkg/web/handlers/streamModals"
"encoding/base64"
"encoding/csv"
@@ -4743,7 +4744,7 @@ func ChessHandler(c echo.Context) error {
authUser := c.Get("authUser").(*database.User)
db := c.Get("database").(*database.DkfDB)
var data chessData
- data.Games = v1.ChessInstance.GetGames()
+ data.Games = interceptors.ChessInstance.GetGames()
if c.Request().Method == http.MethodPost {
data.Username = database.Username(c.Request().PostFormValue("username"))
@@ -4752,7 +4753,7 @@ func ChessHandler(c echo.Context) error {
data.Error = "invalid username"
return c.Render(http.StatusOK, "chess", data)
}
- if _, err := v1.ChessInstance.NewGame1("", config.GeneralRoomID, *authUser, player2); err != nil {
+ if _, err := interceptors.ChessInstance.NewGame1("", config.GeneralRoomID, *authUser, player2); err != nil {
data.Error = err.Error()
return c.Render(http.StatusOK, "chess", data)
}
@@ -4817,7 +4818,7 @@ func ChessGameHandler(c echo.Context) error {
//db := c.Get("database").(*database.DkfDB)
key := c.Param("key")
- g := v1.ChessInstance.GetGame(key)
+ g := interceptors.ChessInstance.GetGame(key)
if g == nil {
// Chess debug
//user1, _ := db.GetUserByID(1)
@@ -4834,9 +4835,9 @@ func ChessGameHandler(c echo.Context) error {
if msg == "resign" {
resignColor := utils.Ternary(isFlipped, chess.Black, chess.White)
g.Game.Resign(resignColor)
- v1.ChessPubSub.Pub(key, true)
+ interceptors.ChessPubSub.Pub(key, true)
} else {
- if err := v1.ChessInstance.SendMove(key, authUser.ID, g, c); err != nil {
+ if err := interceptors.ChessInstance.SendMove(key, authUser.ID, g, c); err != nil {
logrus.Error(err)
}
}
@@ -4881,7 +4882,7 @@ func ChessGameHandler(c echo.Context) error {
authorizedChannels := make([]string, 0)
authorizedChannels = append(authorizedChannels, key)
- sub := v1.ChessPubSub.Subscribe(authorizedChannels)
+ sub := interceptors.ChessPubSub.Subscribe(authorizedChannels)
defer sub.Close()
var card1 string