From 7eea9d932f2f8bb044d17116162b97e787b03ea8 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Mon, 23 Sep 2024 02:55:55 +0200 Subject: [PATCH] Upload image of board on mention --- .env.sample | 3 ++ .gitignore | 2 ++ chess/board.go | 35 ++++++++++++++++----- db/db.go | 66 +++++++++++++++++++++++++++++++++++++++ go.mod | 3 ++ go.sum | 6 ++++ main.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++++-- sn/sn.go | 64 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 .env.sample create mode 100644 db/db.go create mode 100644 sn/sn.go diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..c1d639d --- /dev/null +++ b/.env.sample @@ -0,0 +1,3 @@ +SN_BASE_URL= +SN_API_TOKEN= +SN_MEDIA_URL= diff --git a/.gitignore b/.gitignore index 02e139e..729536c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.png !assets/*.png +.env +*.sqlite3 diff --git a/chess/board.go b/chess/board.go index 0314f05..844363e 100644 --- a/chess/board.go +++ b/chess/board.go @@ -55,15 +55,20 @@ func NewBoard() *Board { return board } +func NewGame(move string) (*Board, error) { + board := NewBoard() + + if err := board.Move(move); err != nil { + return nil, err + } + + return board, nil +} + func (b *Board) Save(filename string) error { var ( - file *os.File - img *image.RGBA - piece *Piece - bg *image.Uniform - rect image.Rectangle - p = image.Point{0, 0} - err error + file *os.File + err error ) if file, err = os.Create(filename); err != nil { @@ -71,6 +76,18 @@ func (b *Board) Save(filename string) error { } defer file.Close() + return png.Encode(file, b.Image()) +} + +func (b *Board) Image() *image.RGBA { + var ( + img *image.RGBA + piece *Piece + bg *image.Uniform + rect image.Rectangle + p = image.Point{0, 0} + ) + img = image.NewRGBA(image.Rect(0, 0, 1024, 1024)) for yi := 0; yi < 8; yi++ { @@ -86,7 +103,7 @@ func (b *Board) Save(filename string) error { } } - return png.Encode(file, img) + return img } func (b *Board) SetPiece(name PieceName, color Color, position string) error { @@ -187,6 +204,8 @@ func (b *Board) Move(position string) error { default: err = fmt.Errorf("invalid move: %s", position) } + } else { + err = fmt.Errorf("invalid move: %s", position) } if err != nil { diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..c4a2962 --- /dev/null +++ b/db/db.go @@ -0,0 +1,66 @@ +package db + +import ( + "database/sql" + "log" + + sn "github.com/ekzyis/snappy" + _ "github.com/mattn/go-sqlite3" +) + +var ( + db = getDb() +) + +func getDb() *sql.DB { + var ( + db *sql.DB + err error + ) + if db, err = sql.Open("sqlite3", "chessbot.sqlite3?_foreign_keys=on"); err != nil { + log.Fatal(err) + } else { + if err = migrate(db); err != nil { + log.Fatal(err) + } + } + return db +} + +func migrate(db *sql.DB) error { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + text TEXT NOT NULL, + parent_id INTEGER REFERENCES items(id), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + `) + + return err +} + +func ItemHasReply(parentId int, userId int) (bool, error) { + var ( + count int + err error + ) + + if err = db.QueryRow(`SELECT COUNT(1) FROM items WHERE parent_id = ? AND user_id = ?`, parentId, userId).Scan(&count); err != nil { + return true, err + } + + return count > 0, nil +} + +func InsertItem(item *sn.Item) error { + if _, err := db.Exec( + `INSERT INTO items(id, user_id, text, parent_id) VALUES (?, ?, ?, NULLIF(?, 0))`, + item.Id, item.User.Id, item.Text, item.ParentId); err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod index 638b7c1..101b1c7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/ekzyis/chessbot go 1.23.0 require ( + github.com/ekzyis/snappy v0.5.5-0.20240923014110-b880c1256e13 + github.com/mattn/go-sqlite3 v1.14.23 github.com/stretchr/testify v1.9.0 golang.org/x/image v0.20.0 ) @@ -10,5 +12,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/guregu/null.v4 v4.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 91a2d70..228c7ff 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ekzyis/snappy v0.5.5-0.20240923014110-b880c1256e13 h1:yxDqMo4HzCVzhxPNTBpST0KVVHVMwGouzwfXsb4WYw4= +github.com/ekzyis/snappy v0.5.5-0.20240923014110-b880c1256e13/go.mod h1:UksYI0dU0+cnzz0LQjWB1P0QQP/ghx47e4atP99a5Lk= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -8,5 +12,7 @@ golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= +gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 52186ea..35f743f 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,93 @@ package main import ( + "log" + "strings" + "time" + "github.com/ekzyis/chessbot/chess" + "github.com/ekzyis/chessbot/db" + "github.com/ekzyis/chessbot/sn" +) + +var ( + c = sn.GetClient() + // TODO: fetch our id from SN API + meId = 21858 ) func main() { + + for { + tickGameStart(c) + // TODO: implement game progress + // tickGameProgress(c) + time.Sleep(1 * time.Minute) + } +} + +func tickGameStart(c *sn.Client) { var ( - b = chess.NewBoard() + mentions []sn.Notification + err error ) - b.Save("board.png") + if mentions, err = c.Mentions(); err != nil { + log.Printf("mentions error: %v\n", err) + return + } + + log.Printf("fetched %d mention(s)\n", len(mentions)) + + for _, n := range mentions { + + if exists, err := db.ItemHasReply(n.Item.Id, meId); err != nil { + log.Printf("error during reply check: %v\n", err) + continue + } else if exists { + // TODO: check if move changed + log.Printf("reply already exists: id=%d\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 + } + + img := b.Image() + var imgUrl string + if imgUrl, err = c.UploadImage(img); err != nil { + log.Printf("error uploading image: %v\n", err) + continue + } + + var cId int + parentId := n.Item.Id + if cId, err = c.CreateComment(parentId, imgUrl); err != nil { + log.Printf("error creating reply: %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 + } + + 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) + } } diff --git a/sn/sn.go b/sn/sn.go new file mode 100644 index 0000000..216b626 --- /dev/null +++ b/sn/sn.go @@ -0,0 +1,64 @@ +package sn + +import ( + "bufio" + "fmt" + "log" + "os" + "strings" + + snappy "github.com/ekzyis/snappy" +) + +type Client = snappy.Client +type Notification = snappy.Notification +type Item = snappy.Item + +var ( + c *Client +) + +func GetClient() *Client { + loadEnv() + + if c == nil { + c = snappy.NewClient( + snappy.WithBaseUrl(os.Getenv("SN_BASE_URL")), + snappy.WithApiKey(os.Getenv("SN_API_KEY")), + snappy.WithMediaUrl(os.Getenv("SN_MEDIA_URL")), + ) + } + return c +} + +func loadEnv() { + var ( + f *os.File + s *bufio.Scanner + err error + ) + + if f, err = os.Open(".env"); err != nil { + log.Fatalf("error opening .env: %v", err) + } + defer f.Close() + + s = bufio.NewScanner(f) + s.Split(bufio.ScanLines) + for s.Scan() { + line := s.Text() + parts := strings.SplitN(line, "=", 2) + + // Check if we have exactly 2 parts (key and value) + if len(parts) == 2 { + os.Setenv(parts[0], parts[1]) + } else { + log.Fatalf(".env: invalid line: %s\n", line) + } + } + + // Check for errors during scanning + if err = s.Err(); err != nil { + fmt.Println("error scanning .env:", err) + } +}