commit 7fdb46d208e8c7ab370a82c7b14d4b9c726f82f3 Author: Timofey.Kovalev Date: Thu Jun 10 21:12:47 2021 +0300 first commit diff --git a/config.go b/config.go new file mode 100644 index 0000000..c052fc5 --- /dev/null +++ b/config.go @@ -0,0 +1,37 @@ +package main + +import ( + "encoding/json" + "os" + + "github.com/pkg/errors" +) + +type Config struct { + BotToken string `json:"bot-token"` + ChatID int64 `json:"chat-id"` + + Postgres struct { + Host string `json:"host"` + Port uint32 `json:"port"` + User string `json:"user"` + Password string `json:"password"` + DBName string `json:"database"` + } `json:"postgres"` + + Http string `json:"http"` +} + +func readConfig(fileName string, c *Config) error { + file, err := os.Open(fileName) + if err != nil { + return errors.Wrapf(err, "Failed to open file [%s]", fileName) + } + + err = json.NewDecoder(file).Decode(c) + if err != nil { + return errors.Wrap(err, "Failed to parse json config") + } + + return nil +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..eb4fd73 --- /dev/null +++ b/context.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "github.com/sirupsen/logrus" + "os" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" +) + +type contextKey uint8 + +const ( + _ contextKey = iota + ctxKeyLogger + ctxKeyBotApi + ctxKeyConfig + ctxKeyCommandHandler + ctxKeyMessageCleaner + ctxKeyDataBase + ctxKeyPlayersBoard +) + +func createLogger(debug bool) *logrus.Logger { + logLevel := logrus.InfoLevel + if debug { + logLevel = logrus.DebugLevel + } + + return &logrus.Logger{ + Out: os.Stderr, + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "02 Jan 06 15:04", + //PadLevelText: true, + QuoteEmptyFields: true, + }, + Hooks: make(logrus.LevelHooks), + Level: logLevel, + ExitFunc: os.Exit, + ReportCaller: false, + } +} + +func createContext(config *Config, bot *tgbotapi.BotAPI, mc *messageCleaner, ch *commandHandler, db *dbLayer, pb *playersBoard) (context.Context, func()) { + ctx, cancelFunc := context.WithCancel(context.Background()) + + ctx = context.WithValue(ctx, ctxKeyLogger, createLogger(true)) + ctx = context.WithValue(ctx, ctxKeyBotApi, bot) + ctx = context.WithValue(ctx, ctxKeyConfig, config) + ctx = context.WithValue(ctx, ctxKeyMessageCleaner, mc) + ctx = context.WithValue(ctx, ctxKeyCommandHandler, ch) + ctx = context.WithValue(ctx, ctxKeyDataBase, db) + ctx = context.WithValue(ctx, ctxKeyPlayersBoard, pb) + + return ctx, cancelFunc +} + +func getLogger(ctx context.Context) *logrus.Logger { + v := ctx.Value(ctxKeyLogger) + if logger, ok := v.(*logrus.Logger); ok { + return logger + } + + panic("Failed to get logger from ctx") +} + +func getBotApi(ctx context.Context) *tgbotapi.BotAPI { + v := ctx.Value(ctxKeyBotApi) + if botApi, ok := v.(*tgbotapi.BotAPI); ok { + return botApi + } + + panic("Failed to get bot api from ctx") +} + +func getConfig(ctx context.Context) *Config { + v := ctx.Value(ctxKeyConfig) + if config, ok := v.(*Config); ok { + return config + } + + panic("Failed to get config from ctx") +} + +func getMessageCleaner(ctx context.Context) *messageCleaner { + v := ctx.Value(ctxKeyMessageCleaner) + if mc, ok := v.(*messageCleaner); ok { + return mc + } + + panic("Failed to get message cleaner from ctx") +} + +func getCommandHandler(ctx context.Context) *commandHandler { + v := ctx.Value(ctxKeyCommandHandler) + if ch, ok := v.(*commandHandler); ok { + return ch + } + + panic("Failed to get command handler from ctx") +} + +func getDB(ctx context.Context) *dbLayer { + v := ctx.Value(ctxKeyDataBase) + if db, ok := v.(*dbLayer); ok { + return db + } + + panic("Failed to get db from ctx") +} + +func getDPlayersBoard(ctx context.Context) *playersBoard { + v := ctx.Value(ctxKeyPlayersBoard) + if pb, ok := v.(*playersBoard); ok { + return pb + } + + panic("Failed to get players board from ctx") +} diff --git a/dataBase.go b/dataBase.go new file mode 100644 index 0000000..0fbd9be --- /dev/null +++ b/dataBase.go @@ -0,0 +1,223 @@ +package main + +import ( + "context" + "database/sql" + "encoding/hex" + "fmt" + "github.com/pkg/errors" + "math/rand" + "time" + + _ "github.com/lib/pq" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +type dbLayer struct { + db *sql.DB +} + +func connectToDataBase(host string, port uint32, user string, password string, dbName string) (*dbLayer, error) { + db, err := sql.Open("postgres", + fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbName, + ), + ) + + if err != nil { + return nil, errors.Wrap(err, "Failed to connect to database") + } + + return &dbLayer{ + db: db, + }, nil +} + +func (db *dbLayer) execInTransaction(ctx context.Context, query string, args ...interface{}) error { + tx, err := db.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return errors.Wrap(err, "Failed to begin transaction") + } + + getLogger(ctx).Debugf("[Run sql query]: %s, [%v]", query, args) + + _, err = tx.ExecContext(ctx, query, args...) + if err != nil { + _ = tx.Rollback() + + return errors.Wrapf(err, "Failed to run db query [%s] [%v]", query, args) + } + + err = tx.Commit() + if err != nil { + return errors.Wrap(err, "Failed to commit transaction") + } + + return nil +} + +type Player struct { + id string + name string + lastOnline time.Time + onlineDuration time.Duration + deaths int + level int +} + +func fetchPlayer(r *sql.Rows, p *Player) error { + var ( + err error + lastOnline string + onlineDuration int + ) + + err = r.Scan(&p.id, &p.name, &lastOnline, &onlineDuration, &p.deaths, &p.level) + if err != nil { + return errors.Wrap(err, "Failed to fetch data base") + } + + p.lastOnline, err = time.Parse(time.RFC3339, lastOnline) + if err != nil { + return errors.Wrapf(err, "Failed to parse date [%s]", lastOnline) + } + + p.onlineDuration = time.Second * time.Duration(onlineDuration) + + return nil +} + +func (db *dbLayer) getPlayers(ctx context.Context) ([]Player, error) { + var players []Player + + query := "SELECT id, name, last_login, online_duration, deaths, level FROM players" + + rows, err := db.db.QueryContext(ctx, query) + if err != nil { + return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) + } + + defer rows.Close() + + for rows.Next() { + p := Player{} + + err = fetchPlayer(rows, &p) + if err != nil { + return nil, err + } + + players = append(players, p) + } + + return players, nil +} + +func (db *dbLayer) getPlayerByID(ctx context.Context, id string) (*Player, error) { + query := "SELECT id, name, last_login, online_duration, deaths, level FROM players WHERE id = $1" + + rows, err := db.db.QueryContext(ctx, query, id) + if err != nil { + return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) + } + + defer rows.Close() + + if rows.Next() { + var p Player + + err = fetchPlayer(rows, &p) + if err != nil { + return nil, err + } + + return &p, nil + } + + return nil, nil +} + +func (db *dbLayer) getPlayerByName(ctx context.Context, name string) (*Player, error) { + query := "SELECT id, name, last_login, online_duration, deaths, level FROM players WHERE name = $1 LIMIT 1" + + rows, err := db.db.QueryContext(ctx, query, name) + if err != nil { + return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) + } + + defer rows.Close() + + if rows.Next() { + var p Player + + err = fetchPlayer(rows, &p) + if err != nil { + return nil, err + } + + return &p, nil + } + + return nil, nil +} + +func (db *dbLayer) createPlayer(ctx context.Context, name string, lastOnline time.Time) (*Player, error) { + id := uuid() + + err := db.execInTransaction(ctx, "INSERT INTO players (id, name, last_login) VALUES ($1, $2, $3)", id, name, lastOnline.Format(time.RFC3339)) + + if err != nil { + return nil, err + } + + return &Player{ + id: id, + name: name, + lastOnline: lastOnline, + }, nil +} + +func (db *dbLayer) increasePlayerOnlineDuration(ctx context.Context, playerID string, d time.Duration) error { + onlineDurationSecond := int(d / time.Second) + if onlineDurationSecond == 0 { + return nil + } + + return db.execInTransaction(ctx, "UPDATE players SET online_duration = online_duration + $2 WHERE id = $1", playerID, onlineDurationSecond) +} + +func (db *dbLayer) updatePlayerLastOnline(ctx context.Context, playerID string, lastOnline time.Time) error { + return db.execInTransaction(ctx, "UPDATE players SET last_login = $2 WHERE id = $1", playerID, lastOnline.Format(time.RFC3339)) +} + +func (db *dbLayer) increasePlayerDeath(ctx context.Context, playerID string, deaths int) error { + return db.execInTransaction(ctx, "UPDATE players SET deaths = deaths + $2 WHERE id = $1", playerID, deaths) +} + +func (db *dbLayer) increasePlayerEntryKills(ctx context.Context, playerID string, entity string, count int) error { + return db.execInTransaction(ctx, + "INSERT INTO killings (player_id, entity, count) VALUES ($1, $2, $3) ON CONFLICT (player_id, entity) DO UPDATE SET count = killings.count + excluded.count", + playerID, entity, count, + ) +} + +func (db *dbLayer) updatePlayerLevel(ctx context.Context, playerID string, level int) error { + return db.execInTransaction(ctx, "UPDATE players SET level = $2 WHERE id = $1", playerID, level) +} + +func uuid() string { + token := make([]byte, 18) + rand.Read(token) + result := make([]byte, 36) + hex.Encode(result, token) + + result[8] = 45 + result[13] = 45 + result[18] = 45 + result[23] = 45 + + return string(result) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e4a3405 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module mk-statistics + +go 1.16 + +require ( + github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible + github.com/jessevdk/go-flags v1.5.0 + github.com/lib/pq v1.10.2 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.8.1 + github.com/technoweenie/multipartstreamer v1.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fff4f84 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +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/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..78a1469 --- /dev/null +++ b/handlers.go @@ -0,0 +1,218 @@ +package main + +import ( + "context" + "fmt" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" +) + +type command interface { + run(context.Context) +} + +type commandHandler struct { + commandChan chan command +} + +func newCommandHandler() *commandHandler { + return &commandHandler{ + commandChan: make(chan command, 50), + } +} + +func (h *commandHandler) handle(c command) { + h.commandChan <- c +} + +func (h *commandHandler) run(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case c := <-h.commandChan: // TODO if close ?? + c.run(ctx) + } + } +} + +type joinCommand struct { + name string +} + +func (c *joinCommand) run(ctx context.Context) { + botApi := getBotApi(ctx) + cfg := getConfig(ctx) + mc := getMessageCleaner(ctx) + db := getDB(ctx) + log := getLogger(ctx) + pb := getDPlayersBoard(ctx) + + mess := tgbotapi.NewMessage(cfg.ChatID, fmt.Sprintf("%s вошел в матрицу", c.name)) + m, err := botApi.Send(mess) + if err != nil { + log.Error(err) + } else { + mc.add(ctx, m.MessageID, cfg.ChatID, time.Second*30) + } + + p, err := db.getPlayerByName(ctx, c.name) + if err != nil { + log.Error(err) + return + } + + if p == nil { + p, err = db.createPlayer(ctx, c.name, time.Now()) + if err != nil { + log.Error(err) + return + } + } else { + err = db.updatePlayerLastOnline(ctx, p.id, time.Now()) + if err != nil { + log.Error(err) + return + } + } + + err = pb.getPlayerBoard(p.id).setOnline(true).updatePlayerInfo(ctx) + if err != nil { + log.Error(err) + } +} + +type quitCommand struct { + name string +} + +func (c *quitCommand) run(ctx context.Context) { + botApi := getBotApi(ctx) + cfg := getConfig(ctx) + mc := getMessageCleaner(ctx) + db := getDB(ctx) + log := getLogger(ctx) + pb := getDPlayersBoard(ctx) + + mess := tgbotapi.NewMessage(cfg.ChatID, fmt.Sprintf("%s покинул матрицу", c.name)) + m, err := botApi.Send(mess) + if err != nil { + log.Error(err) + } else { + mc.add(ctx, m.MessageID, cfg.ChatID, time.Second*30) + } + + p, err := db.getPlayerByName(ctx, c.name) + if err != nil { + log.Error(err) + return + } + + err = db.increasePlayerOnlineDuration(ctx, p.id, time.Now().Sub(p.lastOnline)) + if err != nil { + log.Error(err) + return + } + + err = pb.getPlayerBoard(p.id).setOnline(false).updatePlayerInfo(ctx) + if err != nil { + log.Error(err) + } +} + +type deathCommand struct { + name string + deathType string +} + +func (c *deathCommand) run(ctx context.Context) { + db := getDB(ctx) + log := getLogger(ctx) + pb := getDPlayersBoard(ctx) + + p, err := db.getPlayerByName(ctx, c.name) + if err != nil { + log.Error(err) + return + } + + err = db.increasePlayerDeath(ctx, p.id, 1) + if err != nil { + log.Error(err) + return + } + + err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) + if err != nil { + log.Error(err) + } +} + +type killCommand struct { + name string + entity string +} + +func (c *killCommand) run(ctx context.Context) { + botApi := getBotApi(ctx) + cfg := getConfig(ctx) + mc := getMessageCleaner(ctx) + db := getDB(ctx) + log := getLogger(ctx) + pb := getDPlayersBoard(ctx) + + mess := tgbotapi.NewMessage(cfg.ChatID, fmt.Sprintf("%s убил %s", c.name, c.entity)) + m, err := botApi.Send(mess) + if err != nil { + log.Error(err) + } else { + mc.add(ctx, m.MessageID, cfg.ChatID, time.Second*30) + } + + p, err := db.getPlayerByName(ctx, c.name) + if err != nil { + log.Error(err) + return + } + + err = db.increasePlayerEntryKills(ctx, p.id, c.entity, 1) + if err != nil { + log.Error(err) + return + } + + err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) + if err != nil { + log.Error(err) + } +} + +type changeLevelCommand struct { + name string + newLevel int +} + +func (c *changeLevelCommand) run(ctx context.Context) { + db := getDB(ctx) + log := getLogger(ctx) + pb := getDPlayersBoard(ctx) + + p, err := db.getPlayerByName(ctx, c.name) + if err != nil { + log.Error(err) + return + } + + err = db.updatePlayerLevel(ctx, p.id, c.newLevel) + if err != nil { + log.Error(err) + return + } + + err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) + if err != nil { + log.Error(err) + } + +} diff --git a/httpServer.go b/httpServer.go new file mode 100644 index 0000000..8b6d474 --- /dev/null +++ b/httpServer.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "encoding/json" + "net/http" +) + +type Data struct { + Type string `json:"type"` + DisplayName string `json:"displayName"` + DeathType string `json:"deathType"` + KillerName string `json:"killerName"` + EntityName string `json:"entityName"` + NewLevel int `json:"newLevel"` +} + +type httpHandler struct { + ctx context.Context +} + +func newHttpHandler(ctx context.Context) *httpHandler { + return &httpHandler{ + ctx: ctx, + } +} + +func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + data := Data{} + + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + panic(err) + } + + getLogger(h.ctx).Debugf("[http server] Data: %v", data) + + switch data.Type { + case "join": + getCommandHandler(h.ctx).handle(&joinCommand{ + name: data.DisplayName, + }) + case "quit": + getCommandHandler(h.ctx).handle(&quitCommand{ + name: data.DisplayName, + }) + case "death": + getCommandHandler(h.ctx).handle(&deathCommand{ + name: data.DisplayName, + deathType: data.DeathType, + }) + case "playerKilledEntity": + getCommandHandler(h.ctx).handle(&killCommand{ + name: data.DisplayName, + entity: data.EntityName, + }) + case "playerLevelChange": + getCommandHandler(h.ctx).handle(&changeLevelCommand{ + name: data.DisplayName, + newLevel: data.NewLevel, + }) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d01b95b --- /dev/null +++ b/main.go @@ -0,0 +1,174 @@ +package main + +import ( + "context" + "fmt" + "log" + "net" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" + "github.com/jessevdk/go-flags" +) + +type Options struct { + ConfigFile string `short:"c" long:"config" description:"Config file name" required:"true"` +} + +func main() { + var ( + opt Options + cfg Config + wg sync.WaitGroup + ) + + _, err := flags.ParseArgs(&opt, os.Args) + if err != nil { + os.Exit(1) + } + + err = readConfig(opt.ConfigFile, &cfg) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to run: [%s]\n", err.Error()) + os.Exit(2) + } + + bot, err := tgbotapi.NewBotAPI(cfg.BotToken) + if err != nil { + log.Panic(err) + } + + db, err := connectToDataBase( + cfg.Postgres.Host, + cfg.Postgres.Port, + cfg.Postgres.User, + cfg.Postgres.Password, + cfg.Postgres.DBName, + ) + + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to connect db: [%s]\n", err.Error()) + os.Exit(5) + } + + mc := newMessageCleaner(bot, &wg) + + ch := newCommandHandler() + + pb := newPlayersBoard() + + ctx, cancelFunc := createContext(&cfg, bot, mc, ch, db, pb) + + getLogger(ctx).Infof("Run...") + + err = pb.load(ctx) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed to show players board: [%s]\n", err.Error()) + os.Exit(5) + } + + runPlayerBoard(ctx, pb, &wg) + runCommandHandler(ctx, ch, &wg) + runHttpServer(ctx, cfg.Http, &wg) + runSignalHandler(ctx, cancelFunc) + + wg.Wait() +} + +func runPlayerBoard(ctx context.Context, pb *playersBoard, wg *sync.WaitGroup) { + wg.Add(1) + + go func() { + defer wg.Done() + + pb.run(ctx) + }() +} + +func runCommandHandler(ctx context.Context, ch *commandHandler, wg *sync.WaitGroup) { + wg.Add(1) + + go func() { + defer wg.Done() + + ch.run(ctx) + }() +} + +func runHttpServer(ctx context.Context, addr string, wg *sync.WaitGroup) { + server := http.Server{ + Addr: addr, + Handler: newHttpHandler(ctx), + } + + getLogger(ctx).Infof("Run http server on: [%s]", addr) + + ls, err := (&net.ListenConfig{}).Listen(ctx, "tcp", addr) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Failed open port: [%s]\n", err.Error()) + os.Exit(3) + } + + go func() { + <-ctx.Done() + _ = server.Close() + }() + + wg.Add(1) + + go func() { + defer wg.Done() + + err = server.Serve(ls) + if err != nil && err != http.ErrServerClosed { + _, _ = fmt.Fprintf(os.Stderr, "Failed to run http server: [%s]\n", err.Error()) + os.Exit(4) + } + }() +} + +func runSignalHandler(ctx context.Context, cancelFunc func()) { + sigs := make(chan os.Signal, 1) + + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + go func() { + for { + switch <-sigs { + case syscall.SIGINT, syscall.SIGTERM: + getLogger(ctx).Infof("Interrapt, exit.") + cancelFunc() + } + } + }() +} + +// ===== +//mess := tgbotapi.NewMessage(-1001155171053, "") +//m, err := bot.Send(mess) + +//var updateID int +// +//for { +// updates, _ := bot.GetUpdates(tgbotapi.UpdateConfig{ +// Offset: updateID + 1, +// Timeout: 30, +// }) +// +// for _, u := range updates { +// updateID = u.UpdateID +// +// fmt.Println(u.Message.Chat.ID, u.Message.Text) +// +// mess := tgbotapi.NewMessage(51716267, u.Message.Text) +// bot.Send(mess) +// } +// +// fmt.Println("qq") +//} + +// ===== diff --git a/messageCleaner.go b/messageCleaner.go new file mode 100644 index 0000000..ecfcef4 --- /dev/null +++ b/messageCleaner.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + "sync" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" +) + +type messageCleaner struct { + botApi *tgbotapi.BotAPI + wg *sync.WaitGroup +} + +func newMessageCleaner(bot *tgbotapi.BotAPI, wg *sync.WaitGroup) *messageCleaner { + return &messageCleaner{ + botApi: bot, + wg: wg, + } +} + +func (m *messageCleaner) add(ctx context.Context, messID int, chatID int64, d time.Duration) { + m.wg.Add(1) + + go func() { + defer m.wg.Done() + + select { + case <-time.NewTimer(d).C: + case <-ctx.Done(): + } + + _, _ = m.botApi.DeleteMessage(tgbotapi.DeleteMessageConfig{ + MessageID: messID, + ChatID: chatID, + }) + }() +} diff --git a/playersBoard.go b/playersBoard.go new file mode 100644 index 0000000..fee521a --- /dev/null +++ b/playersBoard.go @@ -0,0 +1,169 @@ +package main + +import ( + "context" + "fmt" + "sync" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" + "github.com/pkg/errors" +) + +type playerInfo struct { + playerID string + playerMessageID int + isOnline bool +} + +func (p *playerInfo) setOnline(v bool) *playerInfo { + p.isOnline = v + return p +} + +const ( + emojiUp = "\xE2\xAC\x86" + emojiDeaths = "\xF0\x9F\x92\x80" + emojiTime = "\xF0\x9F\x95\x90" +) + +func (p *playerInfo) updatePlayerInfo(ctx context.Context) error { + botApi := getBotApi(ctx) + conf := getConfig(ctx) + db := getDB(ctx) + + player, err := db.getPlayerByID(ctx, p.playerID) + if err != nil { + return err + } + + test := `*%s* | %s +%s Уровень: *%s* +%s Смертей: *%d* +%s Время в игре: %s +` + + access := "\xE2\x9D\x8C offLine" + if p.isOnline { + access = "\xE2\x9C\x85 onLine" + } + + level := "-" + if player.level >= 0 { + level = fmt.Sprintf("%d", player.level) + } + + test = fmt.Sprintf(test, player.name, access, + emojiUp, level, + emojiDeaths, player.deaths, + emojiTime, player.onlineDuration.String()) + + if p.playerMessageID != 0 { + mess := tgbotapi.NewEditMessageText(conf.ChatID, p.playerMessageID, test) + mess.ParseMode = tgbotapi.ModeMarkdown + + _, err := botApi.Send(mess) + if err != nil { + return errors.Wrap(err, "Failed to update message") + } + + return nil + } + + mess := tgbotapi.NewMessage(conf.ChatID, test) + mess.ParseMode = tgbotapi.ModeMarkdown + + m, err := botApi.Send(mess) + if err != nil { + return errors.Wrap(err, "Failed to send message") + } + + p.playerMessageID = m.MessageID + + return nil +} + +func (p *playerInfo) deletePlayerInfo(ctx context.Context) error { + botApi := getBotApi(ctx) + conf := getConfig(ctx) + + mess := tgbotapi.NewDeleteMessage(conf.ChatID, p.playerMessageID) + _, err := botApi.Send(mess) + if err != nil { + return errors.Wrap(err, "Failed to send message") + } + + return nil +} + +type playersBoard struct { + mx sync.Mutex + players []playerInfo +} + +func newPlayersBoard() *playersBoard { + return &playersBoard{ + players: make([]playerInfo, 0), + } +} + +func (pb *playersBoard) load(ctx context.Context) error { + pb.mx.Lock() + defer pb.mx.Unlock() + + db := getDB(ctx) + + players, err := db.getPlayers(ctx) + if err != nil { + return err + } + + pb.players = make([]playerInfo, 0, len(players)) + + for _, p := range players { + pi := playerInfo{ + playerID: p.id, + } + + err := pi.updatePlayerInfo(ctx) + if err != nil { + return err + } + + pb.players = append(pb.players, pi) + } + + return nil +} + +func (pb *playersBoard) getPlayerBoard(playerID string) *playerInfo { + for i := range pb.players { + if pb.players[i].playerID == playerID { + return &pb.players[i] + } + } + + pb.mx.Lock() + defer pb.mx.Unlock() + + pb.players = append(pb.players, playerInfo{ + playerID: playerID, + }) + + return &pb.players[len(pb.players)-1] +} + +func (pb *playersBoard) run(ctx context.Context) { + log := getLogger(ctx) + + <-ctx.Done() + + pb.mx.Lock() + defer pb.mx.Unlock() + + for _, p := range pb.players { + err := p.deletePlayerInfo(ctx) + if err != nil { + log.Error(err) + } + } +}