From 516478d258aea7ff6d01633a74f110b78024dafb Mon Sep 17 00:00:00 2001 From: ekzyis Date: Wed, 25 Sep 2024 23:22:42 +0200 Subject: [PATCH] Reply with errors --- chess/board.go | 8 +- db/db.go | 5 +- main.go | 237 +++++++++++++++++++++++++++---------------------- 3 files changed, 140 insertions(+), 110 deletions(-) diff --git a/chess/board.go b/chess/board.go index 26539c0..372f82f 100644 --- a/chess/board.go +++ b/chess/board.go @@ -730,12 +730,12 @@ func getXY(position string) (int, int, error) { posX = runes[0] posY = runes[1] - if posX < 'a' && posX > 'h' { - return -1, -1, fmt.Errorf("invalid posX: %s", position) + if posX < 'a' || posX > 'h' { + return -1, -1, fmt.Errorf("invalid move: %s", position) } - if posY < '1' && posY > '8' { - return -1, -1, fmt.Errorf("invalid posY: %s", position) + if posY < '1' || posY > '8' { + return -1, -1, fmt.Errorf("invalid move: %s", position) } // image origin (0,0) is at top-left corner (a8) diff --git a/db/db.go b/db/db.go index 1be585b..0225dfb 100644 --- a/db/db.go +++ b/db/db.go @@ -57,8 +57,9 @@ func ItemHasReply(parentId int, userId int) (bool, error) { } func InsertItem(item *sn.Item) error { - if _, err := db.Exec( - `INSERT INTO items(id, user_id, text, parent_id) VALUES (?, ?, ?, NULLIF(?, 0))`, + if _, err := db.Exec(``+ + `INSERT INTO items(id, user_id, text, parent_id) VALUES (?, ?, ?, NULLIF(?, 0)) `+ + `ON CONFLICT DO UPDATE SET text = EXCLUDED.text, updated_at = CURRENT_TIMESTAMP`, item.Id, item.User.Id, item.Text, item.ParentId); err != nil { return err } diff --git a/main.go b/main.go index 7458ada..9876121 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func tickGameStart(c *sn.Client) { ) if mentions, err = c.Mentions(); err != nil { - log.Printf("mentions error: %v\n", err) + log.Printf("failed to fetch mentions: %v\n", err) return } @@ -42,53 +42,23 @@ func tickGameStart(c *sn.Client) { for _, n := range mentions { if exists, err := db.ItemHasReply(n.Item.Id, meId); err != nil { - log.Printf("error during game start check: %v\n", err) + log.Printf("failed to check for existing reply to game start in item %d: %v\n", n.Item.Id, err) continue } else if exists { // TODO: check if move changed - log.Printf("game start already exists: id=%d\n", n.Item.Id) + log.Printf("reply to game start in item %d already exists\n", n.Item.Id) continue } - move := strings.Trim(strings.ReplaceAll(n.Item.Text, "@chess", ""), " ") - - var b *chess.Board - if b, err = chess.NewGame(move); err != nil { - log.Printf("error creating new game: %v: id=%d\n", err, n.Item.Id) - continue + if err = handleGameStart(&n.Item); err != nil { + if _, err2 := createComment(n.Item.Id, fmt.Sprintf("`%v`", err)); err2 != nil { + log.Printf("failed to reply with error to item %d: %v\n", n.Item.Id, err2) + } else { + log.Printf("replied to game start in item %d with error: %v\n", n.Item.Id, err) + } + } else { + log.Printf("started new game via item %d\n", n.Item.Id) } - - img := b.Image() - var imgUrl string - if imgUrl, err = c.UploadImage(img); err != nil { - log.Printf("error uploading image: %v\n", err) - continue - } - - n.Item.ParentId = 0 - if err = db.InsertItem(&n.Item); err != nil { - log.Printf("error inserting item into db: %v: id=%d\n", err, n.Item.Id) - continue - } - - text := strings.Trim(fmt.Sprintf("%s\n\n%s", b.AlgebraicNotation(), imgUrl), " ") - var cId int - if cId, err = c.CreateComment(n.Item.Id, text); err != nil { - log.Printf("error creating reply: %v\n", err) - continue - } - - var item *sn.Item - if item, err = c.Item(cId); err != nil { - log.Printf("error fetching item: %v: id=%d\n", cId) - continue - } - if err = db.InsertItem(item); err != nil { - log.Printf("error inserting item into db: %v: id=%d\n", err, item.Id) - continue - } - - log.Printf("started new game: id=%d\n", n.Item.Id) } } @@ -99,7 +69,7 @@ func tickGameProgress(c *sn.Client) { ) if replies, err = c.Replies(); err != nil { - log.Printf("replies error: %v\n", err) + log.Printf("failed to fetch replies: %v\n", err) return } @@ -108,75 +78,134 @@ func tickGameProgress(c *sn.Client) { for _, n := range replies { if exists, err := db.ItemHasReply(n.Item.Id, meId); err != nil { - log.Printf("error during game update check: %v\n", err) + log.Printf("failed to check for existing reply to game update in item %d: %v\n", n.Item.Id, err) continue } else if exists { // TODO: check if move changed - log.Printf("game update already exists: id=%d\n", n.Item.Id) + log.Printf("reply to game update in item %d already exists\n", n.Item.Id) continue } - var thread []sn.Item - if thread, err = db.GetThread(n.Item.ParentId); err != nil { - log.Printf("error fetching thread: %v: id=%d\n", err, n.Item.ParentId) - continue - } - - b := chess.NewBoard() - // TODO: better parsing of moves in replies using regexp for example or enforce a specific format - // since players might include more than just the move in their replies - move := strings.Trim(n.Item.Text, " ") - for _, item := range thread { - if item.User.Id == meId { - continue - } - - moves := strings.Trim(strings.ReplaceAll(item.Text, "@chess", ""), " ") - if err = b.Parse(moves); err != nil { - log.Printf("error moving piece: %v: id=%d\n", err, item.Id) - break + if err = handleGameProgress(&n.Item); err != nil { + if _, err2 := createComment(n.Item.Id, fmt.Sprintf("`%v`", err)); err2 != nil { + log.Printf("failed to reply with error to item %d: %v\n", n.Item.Id, err2) + } else { + log.Printf("replied to game start in item %d with error: %v\n", n.Item.Id, err) } + } else { + log.Printf("updated game via item %d\n", n.Item.Id) } - - // continue with next reply if the thread loop failed - if err != nil { - continue - } - - if err = b.Parse(move); err != nil { - log.Printf("error moving piece: %v: id=%d\n", err, n.Item.ParentId) - continue - } - - img := b.Image() - var imgUrl string - if imgUrl, err = c.UploadImage(img); err != nil { - log.Printf("error uploading image: %v\n", err) - continue - } - - if err = db.InsertItem(&n.Item); err != nil { - log.Printf("error inserting item into db: %v: id=%d\n", err, n.Item.Id) - continue - } - - text := fmt.Sprintf("`%s`\n\n%s", b.AlgebraicNotation(), imgUrl) - var cId int - if cId, err = c.CreateComment(n.Item.Id, text); err != nil { - log.Printf("error creating reply: %v\n", err) - continue - } - - var item *sn.Item - if item, err = c.Item(cId); err != nil { - log.Printf("error fetching item: %v: id=%d\n", cId) - continue - } - if err = db.InsertItem(item); err != nil { - log.Printf("error inserting item into db: %v: id=%d\n", err, item.Id) - continue - } - - log.Printf("continued game: move=%s id=%d\n", move, cId) } } + +func handleGameStart(req *sn.Item) error { + var ( + move = strings.Trim(strings.ReplaceAll(req.Text, "@chess", ""), " ") + b *chess.Board + imgUrl string + res string + err error + ) + + // Immediately save game start request to db so we can store our reply to it in case of error. + // We set parentId to 0 such that parent_id will be NULL in the db and not hit foreign key constraints. + req.ParentId = 0 + + if err = db.InsertItem(req); err != nil { + return fmt.Errorf("failed to insert item %d into db: %v\n", req.Id, err) + } + + // create board with initial move(s) + if b, err = chess.NewGame(move); err != nil { + return fmt.Errorf("failed to create new game from item %d: %v\n", req.Id, err) + } + + // upload image of board + if imgUrl, err = c.UploadImage(b.Image()); err != nil { + return fmt.Errorf("failed to upload image for item %d: %v\n", req.Id, err) + } + + // reply with algebraic notation and image + res = strings.Trim(fmt.Sprintf("%s\n\n%s", b.AlgebraicNotation(), imgUrl), " ") + if _, err = createComment(req.Id, res); err != nil { + return fmt.Errorf("failed to reply to item %d: %v\n", req.Id, err) + } + + return nil +} + +func handleGameProgress(req *sn.Item) error { + var ( + thread []sn.Item + b = chess.NewBoard() + move string = strings.Trim(req.Text, " ") + imgUrl string + res string + err error + ) + + // immediately save game update request to db so we can store our reply to it in case of error + if err = db.InsertItem(req); err != nil { + return fmt.Errorf("failed to insert item %d into db: %v\n", req.Id, err) + } + + // fetch thread to reconstruct all moves so far + if thread, err = db.GetThread(req.ParentId); err != nil { + return fmt.Errorf("failed to fetch thread for item %d: %v\n", req.ParentId, err) + } + + for _, item := range thread { + if item.User.Id == meId { + continue + } + + // TODO: better parsing of moves in replies using regexp for example or enforce a specific format + // since players might include more than just the move in their replies + moves := strings.Trim(strings.ReplaceAll(item.Text, "@chess", ""), " ") + + // parse and execute existing moves + if err = b.Parse(moves); err != nil { + return fmt.Errorf("failed to parse move %s: %v\n", moves, err) + } + } + + // parse and execute new move + if err = b.Parse(move); err != nil { + return fmt.Errorf("failed to parse move %s: %v\n", move, err) + } + + // upload image of updated board + if imgUrl, err = c.UploadImage(b.Image()); err != nil { + return fmt.Errorf("failed to upload image for item %d: %v\n", req.Id, err) + } + + // reply with algebraic notation and image + res = strings.Trim(fmt.Sprintf("%s\n\n%s", b.AlgebraicNotation(), imgUrl), " ") + if _, err = createComment(req.Id, res); err != nil { + return fmt.Errorf("failed to reply to item %d: %v\n", req.Id, err) + } + + return nil +} + +func createComment(parentId int, text string) (*sn.Item, error) { + var ( + commentId int + err error + ) + + if commentId, err = c.CreateComment(parentId, text); err != nil { + return nil, fmt.Errorf("failed to reply to item %d: %v\n", parentId, err) + } + + var comment *sn.Item + if comment, err = c.Item(commentId); err != nil { + return nil, fmt.Errorf("failed to fetch item %d: %v\n", commentId, err) + } + + if err = db.InsertItem(comment); err != nil { + return nil, fmt.Errorf("failed to insert item %d into db: %v\n", err, comment.Id) + } + + return comment, nil +}