first commit

This commit is contained in:
Timofey.Kovalev 2021-06-10 21:12:47 +03:00
commit 7fdb46d208
10 changed files with 1076 additions and 0 deletions

37
config.go Normal file
View File

@ -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
}

120
context.go Normal file
View File

@ -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")
}

223
dataBase.go Normal file
View File

@ -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)
}

12
go.mod Normal file
View File

@ -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
)

21
go.sum Normal file
View File

@ -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=

218
handlers.go Normal file
View File

@ -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)
}
}

63
httpServer.go Normal file
View File

@ -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,
})
}
}

174
main.go Normal file
View File

@ -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")
//}
// =====

39
messageCleaner.go Normal file
View File

@ -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,
})
}()
}

169
playersBoard.go Normal file
View File

@ -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)
}
}
}