chessbot/main.go

350 lines
8.2 KiB
Go
Raw Normal View History

2024-09-18 01:15:24 +00:00
package main
import (
2024-09-25 23:00:41 +00:00
"errors"
2024-09-23 04:07:34 +00:00
"fmt"
2024-09-23 00:55:55 +00:00
"log"
2024-09-25 23:00:41 +00:00
"math/rand"
2024-09-23 00:55:55 +00:00
"strings"
"time"
2024-09-18 13:52:40 +00:00
"github.com/ekzyis/chessbot/chess"
2024-09-23 00:55:55 +00:00
"github.com/ekzyis/chessbot/db"
"github.com/ekzyis/chessbot/sn"
)
var (
c = sn.GetClient()
me *sn.User
2024-09-18 01:15:24 +00:00
)
func main() {
2024-09-23 00:55:55 +00:00
for {
updateMe()
2024-09-23 00:55:55 +00:00
tickGameStart(c)
2024-09-23 03:35:43 +00:00
tickGameProgress(c)
2024-09-23 04:35:55 +00:00
time.Sleep(15 * time.Second)
2024-09-23 00:55:55 +00:00
}
}
func updateMe() {
var (
oldMe *sn.User
err error
warnThreshold = 100
)
maybeWarn := func() {
if me.Privates.Sats < warnThreshold {
log.Printf("~~~ warning: low balance ~~~\n")
}
}
if me == nil {
// make sure first update is successful
if me, err = c.Me(); err != nil {
log.Fatalf("failed to fetch me: %v\n", err)
}
log.Printf("fetched me: id=%d name=%s balance=%d\n", me.Id, me.Name, me.Privates.Sats)
maybeWarn()
return
}
oldMe = me
if me, err = c.Me(); err != nil {
log.Printf("failed to update me: %v\n", err)
me = oldMe
} else {
log.Printf("updated me. balance: %d\n", me.Privates.Sats)
}
maybeWarn()
}
2024-09-23 00:55:55 +00:00
func tickGameStart(c *sn.Client) {
2024-09-18 01:15:24 +00:00
var (
2024-09-23 00:55:55 +00:00
mentions []sn.Notification
err error
2024-09-18 01:15:24 +00:00
)
2024-09-23 00:55:55 +00:00
if mentions, err = c.Mentions(); err != nil {
2024-09-25 21:22:42 +00:00
log.Printf("failed to fetch mentions: %v\n", err)
2024-09-23 00:55:55 +00:00
return
}
2024-10-16 01:30:18 +00:00
log.Printf("fetched %d mentions\n", len(mentions))
2024-09-23 00:55:55 +00:00
for _, n := range mentions {
2024-10-16 17:11:59 +00:00
if !isRecent(n.Item.CreatedAt) {
2024-10-16 01:25:21 +00:00
log.Printf("ignoring old mention %d\n", n.Item.Id)
continue
}
if handled, err := alreadyHandled(n.Item.Id); err != nil {
2024-09-25 21:22:42 +00:00
log.Printf("failed to check for existing reply to game start in item %d: %v\n", n.Item.Id, err)
2024-09-23 00:55:55 +00:00
continue
} else if handled {
2024-09-23 00:55:55 +00:00
// TODO: check if move changed
2024-09-25 21:22:42 +00:00
log.Printf("reply to game start in item %d already exists\n", n.Item.Id)
2024-09-23 00:55:55 +00:00
continue
}
2024-09-25 21:22:42 +00:00
if err = handleGameStart(&n.Item); err != nil {
2024-10-16 17:55:26 +00:00
handleError(&n.Item, err)
2024-09-25 21:22:42 +00:00
} else {
log.Printf("started new game via item %d\n", n.Item.Id)
2024-09-23 03:35:43 +00:00
}
}
}
func tickGameProgress(c *sn.Client) {
var (
replies []sn.Notification
err error
)
if replies, err = c.Replies(); err != nil {
2024-09-25 21:22:42 +00:00
log.Printf("failed to fetch replies: %v\n", err)
2024-09-23 03:35:43 +00:00
return
}
log.Printf("fetched %d replies\n", len(replies))
for _, n := range replies {
2024-10-16 17:11:59 +00:00
if !isRecent(n.Item.CreatedAt) {
2024-10-16 01:25:21 +00:00
log.Printf("ignoring old reply %d\n", n.Item.Id)
continue
}
if handled, err := alreadyHandled(n.Item.Id); err != nil {
2024-09-25 21:22:42 +00:00
log.Printf("failed to check for existing reply to game update in item %d: %v\n", n.Item.Id, err)
2024-09-23 03:35:43 +00:00
continue
} else if handled {
2024-09-23 03:35:43 +00:00
// TODO: check if move changed
2024-09-25 21:22:42 +00:00
log.Printf("reply to game update in item %d already exists\n", n.Item.Id)
2024-09-23 03:35:43 +00:00
continue
}
2024-10-16 17:51:55 +00:00
if parent, err := c.Item(n.Item.ParentId); err != nil {
log.Printf("failed to fetch parent %d of %d\n", n.Item.ParentId, n.Item.Id)
continue
} else if parent.User.Id != me.Id {
2024-10-16 17:51:55 +00:00
log.Printf("ignoring nested reply %d\n", n.Item.Id)
continue
}
2024-09-25 21:22:42 +00:00
if err = handleGameProgress(&n.Item); err != nil {
2024-10-16 17:55:26 +00:00
handleError(&n.Item, err)
2024-09-25 21:22:42 +00:00
} else {
log.Printf("updated game via item %d\n", n.Item.Id)
}
}
}
func handleGameStart(req *sn.Item) error {
var (
move string
2024-09-25 21:22:42 +00:00
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)
}
2024-09-26 17:25:33 +00:00
if move, err = parseGameStart(req.Text); err != nil {
return err
}
2024-09-25 21:22:42 +00:00
// create board with initial move(s)
if b, err = chess.NewGame(move); err != nil {
2024-09-25 23:00:41 +00:00
if rand.Float32() > 0.99 {
// easter egg error message
return errors.New("Nice try, fed.")
}
2024-09-25 21:22:42 +00:00
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)
}
2024-10-18 19:36:43 +00:00
// reply with algebraic notation, image and info
infoMove := "e4"
if len(b.Moves) > 0 {
infoMove = "e5"
}
info := fmt.Sprintf("_A new chess game has been started!_\n\n"+
"_Reply with a move like `%s` to continue the game. "+
"See [here](https://stacker.news/chess#how-to-continue) for details._", infoMove)
2024-10-18 19:36:43 +00:00
res = strings.Trim(fmt.Sprintf("%s\n\n%s\n\n%s", b.AlgebraicNotation(), imgUrl, info), " ")
2024-09-25 21:22:42 +00:00
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 == me.Id {
2024-09-23 03:35:43 +00:00
continue
}
2024-09-26 17:25:33 +00:00
var moves string
if moves, err = parseGameProgress(item.Text); err != nil {
return err
}
2024-09-23 04:52:58 +00:00
2024-09-25 21:22:42 +00:00
// parse and execute existing moves
if err = b.Parse(moves); err != nil {
2024-09-26 09:20:52 +00:00
return err
2024-09-23 03:35:43 +00:00
}
2024-09-25 21:22:42 +00:00
}
2024-09-23 03:35:43 +00:00
2024-09-25 21:22:42 +00:00
// parse and execute new move
2024-09-26 17:25:33 +00:00
if move, err = parseGameProgress(move); err != nil {
return err
}
2024-09-25 21:22:42 +00:00
if err = b.Parse(move); err != nil {
2024-09-25 23:00:41 +00:00
if rand.Float32() > 0.99 {
// easter egg error message
return errors.New("Nice try, fed.")
}
2024-09-26 09:20:52 +00:00
return err
2024-09-25 21:22:42 +00:00
}
2024-09-23 03:35:43 +00:00
2024-09-25 21:22:42 +00:00
// 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)
}
2024-09-23 03:35:43 +00:00
2024-09-25 21:22:42 +00:00
// 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)
}
2024-09-23 03:35:43 +00:00
2024-09-25 21:22:42 +00:00
return nil
}
2024-09-23 00:55:55 +00:00
2024-10-16 17:55:26 +00:00
func handleError(req *sn.Item, err error) {
// don't reply to mentions that we failed to parse as a game start
// to support unrelated mentions
if err.Error() == "failed to parse game start" {
log.Printf("ignoring error for item %d: %v\n", req.Id, err)
return
}
if err.Error() == "failed to parse game update" {
log.Printf("ignoring error for item %d: %v\n", req.Id, err)
return
}
if _, err2 := createComment(req.Id, fmt.Sprintf("`%v`", err)); err2 != nil {
log.Printf("failed to reply with error to item %d: %v\n", req.Id, err2)
} else {
log.Printf("replied to game start in item %d with error: %v\n", req.Id, err)
}
}
2024-09-25 21:22:42 +00:00
func createComment(parentId int, text string) (*sn.Item, error) {
var (
commentId int
err error
)
2024-09-23 03:35:43 +00:00
2024-09-25 21:22:42 +00:00
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)
}
2024-09-23 00:55:55 +00:00
2024-09-25 21:22:42 +00:00
if err = db.InsertItem(comment); err != nil {
2024-10-16 01:13:55 +00:00
return nil, fmt.Errorf("failed to insert item %d into db: %v\n", comment.Id, err)
2024-09-23 00:55:55 +00:00
}
2024-09-25 21:22:42 +00:00
return comment, nil
2024-09-18 01:15:24 +00:00
}
func parseGameStart(input string) (string, error) {
2024-09-26 17:25:33 +00:00
for _, line := range strings.Split(input, "\n") {
line = strings.Trim(line, " ")
2024-10-16 03:15:38 +00:00
var found bool
if line, found = strings.CutPrefix(line, "@chess"); !found {
2024-09-26 17:25:33 +00:00
continue
}
2024-10-16 03:15:38 +00:00
return strings.Trim(line, " "), nil
2024-09-26 17:25:33 +00:00
}
return "", errors.New("failed to parse game start")
}
func parseGameProgress(input string) (string, error) {
lines := strings.Split(input, "\n")
words := strings.Split(input, " ")
if len(lines) == 1 && len(words) == 1 {
2024-10-16 03:15:38 +00:00
return strings.Trim(strings.ReplaceAll(input, "@chess", ""), " "), nil
2024-09-26 17:25:33 +00:00
}
for _, line := range strings.Split(input, "\n") {
2024-10-16 03:15:38 +00:00
line = strings.Trim(line, " ")
var found bool
if line, found = strings.CutPrefix(line, "@chess"); !found {
continue
}
2024-10-16 03:15:38 +00:00
return strings.Trim(line, " "), nil
}
2024-09-26 17:25:33 +00:00
return "", errors.New("failed to parse game update")
}
2024-10-16 17:11:59 +00:00
func isRecent(t time.Time) bool {
x := time.Now().Add(-30 * time.Second)
return t.After(x)
}
func alreadyHandled(id int) (bool, error) {
return db.ItemHasReply(id, me.Id)
}