first commit
This commit is contained in:
commit
7fdb46d208
37
config.go
Normal file
37
config.go
Normal 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
120
context.go
Normal 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
223
dataBase.go
Normal 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
12
go.mod
Normal 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
21
go.sum
Normal 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
218
handlers.go
Normal 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
63
httpServer.go
Normal 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
174
main.go
Normal 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
39
messageCleaner.go
Normal 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
169
playersBoard.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user