1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-09-03 09:26:23 -07:00

Implement dice rolling

This commit is contained in:
Eggbertx 2025-02-09 17:38:15 -08:00
parent d5ac9bff11
commit e65d363a84
7 changed files with 174 additions and 30 deletions

View file

@ -6,6 +6,7 @@ import (
"os"
"path"
"strconv"
"strings"
"github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/config"
@ -29,16 +30,16 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
if editBtn == "Edit post" {
var err error
if len(checkedPosts) == 0 {
server.ServeErrorPage(writer, "You need to select one post to edit.")
server.ServeError(writer, server.NewServerError("You need to select one post to edit", http.StatusBadRequest), wantsJSON, nil)
return
} else if len(checkedPosts) > 1 {
server.ServeErrorPage(writer, "You can only edit one post at a time.")
server.ServeError(writer, server.NewServerError("You can only edit one post at a time", http.StatusBadRequest), wantsJSON, nil)
return
}
rank := manage.GetStaffRank(request)
if password == "" && rank == 0 {
server.ServeErrorPage(writer, "Password required for post editing")
server.ServeError(writer, server.NewServerError("Password required for post editing", http.StatusUnauthorized), wantsJSON, nil)
return
}
passwordMD5 := gcutil.Md5Sum(password)
@ -47,26 +48,32 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
if err != nil {
errEv.Err(err).Caller().
Msg("Error getting post information")
server.ServeError(writer, server.NewServerError("Error getting post information", http.StatusInternalServerError), wantsJSON, nil)
return
}
errEv.Int("postID", post.ID)
if post.Password != passwordMD5 && rank == 0 {
server.ServeErrorPage(writer, "Wrong password")
server.ServeError(writer, server.NewServerError("Wrong password", http.StatusUnauthorized), wantsJSON, nil)
return
}
board, err := post.GetBoard()
if err != nil {
errEv.Err(err).Caller().Msg("Unable to get board ID from post")
server.ServeErrorPage(writer, "Unable to get board ID from post: "+err.Error())
server.ServeError(writer, server.NewServerError("Unable to get board ID from post", http.StatusInternalServerError), wantsJSON, nil)
return
}
if strings.Contains(string(post.Message), `<span class="dice-roll">`) && !config.GetBoardConfig(board.Dir).AllowDiceRerolls {
server.ServeError(writer, server.NewServerError("Dice rerolls are not allowed on this board", http.StatusBadRequest), wantsJSON, nil)
return
}
errEv.Str("board", board.Dir)
upload, err := post.GetUpload()
if err != nil {
errEv.Err(err).Caller().Send()
server.ServeErrorPage(writer, "Error getting post upload info: "+err.Error())
server.ServeError(writer, server.NewServerError("Error getting post upload info: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
@ -87,7 +94,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
if err = serverutil.MinifyTemplate(gctemplates.PostEdit, data, &buf, "text/html"); err != nil {
errEv.Err(err).Caller().
Msg("Error executing edit post template")
server.ServeError(writer, "Error executing edit post template: "+err.Error(), wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Error executing edit post template: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
writer.Write(buf.Bytes())
@ -101,7 +108,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
errEv.Err(err).Caller().
Str("postid", postIDstr).
Msg("Invalid form data")
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]any{
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, map[string]any{
"postid": postid,
})
return
@ -110,7 +117,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
post, err := gcsql.GetPostFromID(postid, true)
if err != nil {
errEv.Err(err).Caller().Msg("Unable to find post")
server.ServeError(writer, "Unable to find post", wantsJSON, map[string]any{
server.ServeError(writer, server.NewServerError("Unable to find post", http.StatusBadRequest), wantsJSON, map[string]any{
"postid": postid,
})
return
@ -121,7 +128,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
errEv.Err(err).Caller().
Str("boardID", boardIDstr).
Msg("Invalid form data")
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, nil)
return
}
gcutil.LogInt("boardID", boardid, infoEv, errEv)
@ -129,13 +136,13 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
rank := manage.GetStaffRank(request)
passwordMD5 := gcutil.Md5Sum(password)
if post.Password != passwordMD5 && rank == 0 {
server.ServeError(writer, "Wrong password", wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Wrong password", http.StatusUnauthorized), wantsJSON, nil)
return
}
board, err := gcsql.GetBoardFromID(boardid)
if err != nil {
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]any{
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, map[string]any{
"boardid": boardid,
})
errEv.Err(err).Caller().Msg("Invalid form data")
@ -145,18 +152,18 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
if doEdit == "upload" {
oldUpload, err := post.GetUpload()
if err != nil {
errEv.Err(err).Caller().Send()
server.ServeError(writer, err.Error(), wantsJSON, nil)
errEv.Err(err).Caller().Msg("Unable to get post upload")
server.ServeError(writer, server.NewServerError("Error getting post upload info: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
upload, err := uploads.AttachUploadFromRequest(request, writer, post, board, gcutil.LogInfo(), errEv)
if err != nil {
server.ServeError(writer, err.Error(), wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Unable to attach upload:"+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
if upload == nil {
server.ServeError(writer, "Missing upload replacement", wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Missing upload replacement", http.StatusBadRequest), wantsJSON, nil)
return
}
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
@ -167,7 +174,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
path.Join(documentRoot, board.Dir, "thumb", oldUpload.Filename))
if err = post.UnlinkUploads(false); err != nil {
errEv.Err(err).Caller().Send()
server.ServeError(writer, "Error unlinking old upload from post: "+err.Error(), wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Error unlinking old upload from post: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
return
}
if oldUpload.Filename != "deleted" {
@ -184,7 +191,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
Str("newFilename", upload.Filename).
Str("newOriginalFilename", upload.OriginalFilename).
Send()
server.ServeError(writer, "Error attaching new upload: "+err.Error(), wantsJSON, map[string]any{
server.ServeError(writer, server.NewServerError("Error attaching new upload: "+err.Error(), http.StatusInternalServerError), wantsJSON, map[string]any{
"filename": upload.OriginalFilename,
})
filePath = path.Join(documentRoot, board.Dir, "src", upload.Filename)
@ -200,17 +207,15 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
var recovered bool
_, err, recovered = events.TriggerEvent("message-pre-format", post, request)
if recovered {
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Recovered from a panic in an event handler (message-pre-format)", wantsJSON, map[string]any{
"postid": post.ID,
})
server.ServeError(writer, server.NewServerError("Recovered from a panic in an event handler (message-pre-format)",
http.StatusInternalServerError), wantsJSON, map[string]any{"postid": post.ID})
return
}
if err != nil {
errEv.Err(err).Caller().
Str("triggeredEvent", "message-pre-format").
Send()
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
server.ServeError(writer, server.NewServerError(err.Error(), http.StatusInternalServerError), wantsJSON, map[string]any{
"postid": post.ID,
})
return
@ -219,8 +224,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
// trigger the pre-format event
_, err, recovered := events.TriggerEvent("message-pre-format", post, request)
if recovered {
writer.WriteHeader(http.StatusInternalServerError)
server.ServeError(writer, "Recovered from a panic in an event handler (message-pre-format)", wantsJSON, nil)
server.ServeError(writer, server.NewServerError("Recovered from a panic in an event handler (message-pre-format)", http.StatusInternalServerError), wantsJSON, nil)
return
}
if err != nil {
@ -285,9 +289,9 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
}
if err = building.BuildBoards(false, boardid); err != nil {
server.ServeErrorPage(writer, "Error rebuilding boards: "+err.Error())
server.ServeError(writer, "Error rebuilding boards: "+err.Error(), wantsJSON, nil)
} else if err = building.BuildFrontPage(); err != nil {
server.ServeErrorPage(writer, "Error rebuilding front page: "+err.Error())
server.ServeError(writer, "Error rebuilding front page: "+err.Error(), wantsJSON, nil)
} else {
http.Redirect(writer, request, post.WebPath(), http.StatusFound)
infoEv.Msg("Post edited")

View file

@ -100,6 +100,8 @@
"EmbedHeight": 164,
"ImagesOpenNewTab": true,
"NewTabOnOutlinks": true,
"DisableBBcode": false,
"AllowDiceRerolls": false,
"MinifyHTML": true,
"MinifyJS": true,
@ -111,8 +113,8 @@
"SiteKey": "your site key goes here (if you want a captcha, make sure to replace '_Captcha' with 'Captcha'",
"AccountSecret": "your account secret key goes here"
},
"GeoIPType": "mmdb",
"GeoIPOptions": {
"_GeoIPType": "mmdb",
"_GeoIPOptions": {
"dbLocation": "/usr/share/geoip/GeoIP2.mmdb",
"isoCode": "en"
},

View file

@ -332,6 +332,7 @@ type PostConfig struct {
ImagesOpenNewTab bool
NewTabOnOutlinks bool
DisableBBcode bool
AllowDiceRerolls bool
}
func WriteConfig() error {

View file

@ -3,6 +3,7 @@ package posting
import (
"fmt"
"html/template"
"math/rand"
"regexp"
"strconv"
"strings"
@ -17,6 +18,7 @@ var (
msgfmtr MessageFormatter
urlRE = regexp.MustCompile(`https?://(\S+)`)
unsetBBcodeTags = []string{"center", "color", "img", "quote", "size"}
diceRoller = regexp.MustCompile(`(?i)\[(\d*)d(\d+)(?:([+-])(\d+))?\]`)
)
// InitPosting prepares the formatter and the temp post pruner
@ -131,3 +133,51 @@ func FormatMessage(message string, boardDir string) (template.HTML, error) {
}
return template.HTML(strings.Join(postLines, "<br />")), nil // skipcq: GSC-G203
}
func ApplyDiceRoll(p *gcsql.Post) (rollSum int, err error) {
words := strings.Split(string(p.Message), " ")
for w, word := range words {
roll := diceRoller.FindStringSubmatch(word)
if len(roll) == 0 {
continue
}
numDice := 1
if roll[1] != "" {
numDice, err = strconv.Atoi(roll[1])
if err != nil {
return 0, err
}
}
dieSize, err := strconv.Atoi(roll[2])
if err != nil {
return 0, err
}
if numDice < 1 || dieSize < 1 {
return 0, fmt.Errorf("dice roll too small")
}
for i := 0; i < numDice; i++ {
rollSum += rand.Intn(dieSize) + 1
switch roll[3] {
case "+":
mod, err := strconv.Atoi(roll[4])
if err != nil {
return 0, err
}
rollSum += mod
case "-":
mod, err := strconv.Atoi(roll[4])
if err != nil {
return 0, err
}
rollSum -= mod
}
}
words[w] = fmt.Sprintf(`<span class="dice-roll">%dd%d`, numDice, dieSize)
if roll[3] != "" {
words[w] += roll[3] + roll[4]
}
words[w] += fmt.Sprintf(" = %d</span>", rollSum)
}
p.Message = template.HTML(strings.Join(words, " "))
return
}

View file

@ -1,9 +1,11 @@
package posting
import (
"regexp"
"testing"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/stretchr/testify/assert"
lua "github.com/yuin/gopher-lua"
)
@ -34,6 +36,56 @@ end)`
luaBBCodeTestExpected = `<span class="lua">Lua test</span>`
)
var (
diceTestCases = []diceRollerTestCase{
{
desc: "[1d6]",
post: gcsql.Post{
MessageRaw: "before [1d6] after",
},
matcher: regexp.MustCompile(`before <span class="dice-roll">1d6 = \d</span> after`),
expectMin: 1,
expectMax: 6,
},
{
desc: "[1d6+1]",
post: gcsql.Post{
MessageRaw: "before [1d6+1] after",
},
matcher: regexp.MustCompile(`before <span class="dice-roll">1d6\+1 = \d</span> after`),
expectMin: 2,
expectMax: 7,
},
{
desc: "[1d6-1]",
post: gcsql.Post{
MessageRaw: "before [1d6-1] after",
},
matcher: regexp.MustCompile(`before <span class="dice-roll">1d6-1 = \d</span> after`),
expectMin: 0,
expectMax: 5,
},
{
desc: "[d8]",
post: gcsql.Post{
MessageRaw: "[d8]",
},
matcher: regexp.MustCompile(`<span class="dice-roll">1d8 = \d</span>`),
expectMin: 1,
expectMax: 8,
},
}
)
type diceRollerTestCase struct {
desc string
post gcsql.Post
expectError bool
matcher *regexp.Regexp
expectMin int
expectMax int
}
func TestBBCode(t *testing.T) {
config.SetVersion(versionStr)
var testFmtr MessageFormatter
@ -72,3 +124,35 @@ func TestLuaBBCode(t *testing.T) {
assert.Equal(t, "[b]Lua test[/b]", msgfmtr.bbCompiler.Compile("[b]Lua test[/b]"))
assert.Error(t, l.DoString(`bbcode.set_tag("lua", 1)`))
}
func diceRollRunner(t *testing.T, tC *diceRollerTestCase) {
var err error
tC.post.Message, err = FormatMessage(tC.post.MessageRaw, "")
assert.NoError(t, err)
result, err := ApplyDiceRoll(&tC.post)
if tC.expectError {
assert.Error(t, err)
assert.Equal(t, 0, result)
} else {
assert.NoError(t, err)
assert.Regexp(t, tC.matcher, tC.post.Message)
assert.GreaterOrEqual(t, result, tC.expectMin)
assert.LessOrEqual(t, result, tC.expectMax)
}
if t.Failed() {
t.FailNow()
}
}
func TestDiceRoll(t *testing.T) {
config.SetVersion(versionStr)
msgfmtr.Init()
for _, tC := range diceTestCases {
t.Run(tC.desc, func(t *testing.T) {
for i := 0; i < 100; i++ {
// Run the test case multiple times to account for randomness
diceRollRunner(t, &tC)
}
})
}
}

View file

@ -21,7 +21,6 @@ func luaTableToHTMLTag(l *lua.LState, table *lua.LTable) (*bbcode.HTMLTag, error
switch attrsLV.Type() {
case lua.LTTable:
attrsLT := attrsLV.(*lua.LTable)
fmt.Println("attrs size:", attrsLT.Len())
attrsLT.ForEach(func(key, val lua.LValue) {
if tag.Attrs == nil {
tag.Attrs = make(map[string]string)

View file

@ -226,6 +226,10 @@ func doFormatting(post *gcsql.Post, board *gcsql.Board, request *http.Request, e
errEv.Err(err).Caller().Msg("Unable to format message")
return errors.New("unable to format message")
}
if _, err = ApplyDiceRoll(post); err != nil {
errEv.Err(err).Caller().Msg("Error applying dice roll")
return err
}
return nil
}