diff --git a/context.go b/context.go index ddf6c68..8d41e0b 100644 --- a/context.go +++ b/context.go @@ -20,7 +20,7 @@ const ( ctxKeyMessageCleaner ctxKeyDataBase ctxKeyPlayersBoard - ctxKeyFragMessages + ctxKeyMessageStorage ) func createLogger(debug bool) *logrus.Logger { @@ -44,17 +44,16 @@ func createLogger(debug bool) *logrus.Logger { } } -func createContext(config *Config, bot *tgbotapi.BotAPI, mc *messageCleaner, ch *commandHandler, db *dbLayer, pb *playersBoard, fms *fragMessages) (context.Context, func()) { +func createContext(config *Config, bot *tgbotapi.BotAPI, ch *commandHandler, db *dbLayer, pb *playersBoard, ms *messageStorage) (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) - ctx = context.WithValue(ctx, ctxKeyFragMessages, fms) + ctx = context.WithValue(ctx, ctxKeyMessageStorage, ms) return ctx, cancelFunc } @@ -86,15 +85,6 @@ func getConfig(ctx context.Context) *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 { @@ -122,11 +112,11 @@ func getDPlayersBoard(ctx context.Context) *playersBoard { panic("Failed to get players board from ctx") } -func getFragMessages(ctx context.Context) *fragMessages { - v := ctx.Value(ctxKeyFragMessages) - if fms, ok := v.(*fragMessages); ok { - return fms +func getMessageStorage(ctx context.Context) *messageStorage { + v := ctx.Value(ctxKeyMessageStorage) + if ms, ok := v.(*messageStorage); ok { + return ms } - panic("Failed to get frag messages from ctx") + panic("Failed to get message storage from ctx") } diff --git a/fragMessages.go b/fragMessages.go deleted file mode 100644 index 60247c0..0000000 --- a/fragMessages.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "context" - "fmt" - "sync" - "time" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" -) - -type fragMessage struct { - playerName string - entityName string - chatID int64 - messageID int - fragCount int - latestFragTime time.Time -} - -type fragMessages struct { - mx sync.Mutex - messages []*fragMessage -} - -func (f *fragMessage) increaseFrag() *fragMessage { - f.fragCount += 1 - f.latestFragTime = time.Now() - return f -} - -func newFragMessages() *fragMessages { - return &fragMessages{ - messages: make([]*fragMessage, 0), - } -} - -func (fms *fragMessages) addFrag(ctx context.Context, playerName string, entityName string) error { - botApi := getBotApi(ctx) - cfg := getConfig(ctx) - mc := getMessageCleaner(ctx) - - fms.mx.Lock() - defer fms.mx.Unlock() - - for _, fm := range fms.messages { - if fm.playerName == playerName && fm.entityName == entityName { - mc.resetTimer(fm.messageID, fm.chatID, time.Second*30) - fm.increaseFrag() - mess := tgbotapi.NewEditMessageText( - fm.chatID, - fm.messageID, - fmt.Sprintf("%s убил %s ✗ *%d*", fm.playerName, fm.entityName, fm.fragCount), - ) - mess.ParseMode = tgbotapi.ModeMarkdown - m, err := botApi.Send(mess) - if err != nil { - return err - } - - if m.MessageID != fm.messageID { - m.MessageID = fm.messageID - getLogger(ctx).Info("message id was changed") - } - - return nil - } - } - - mess := tgbotapi.NewMessage(cfg.ChatID, fmt.Sprintf("%s убил %s", playerName, entityName)) - m, err := botApi.Send(mess) - if err != nil { - return err - } - - mc.add(ctx, m.MessageID, cfg.ChatID, time.Second*30) - - fm := &fragMessage{ - playerName: playerName, - entityName: entityName, - chatID: cfg.ChatID, - messageID: m.MessageID, - fragCount: 1, - latestFragTime: time.Now(), - } - - fms.messages = append(fms.messages, fm) - - return nil -} diff --git a/handlers.go b/handlers.go index f67acdc..581196b 100644 --- a/handlers.go +++ b/handlers.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "time" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" ) type command interface { @@ -42,19 +40,21 @@ type joinCommand struct { } 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) + ms := getMessageStorage(ctx) + + err := ms.apply(ctx, fmt.Sprintf("join:%s", c.name), time.Minute/2, func(d interface{}) (string, interface{}) { + if v, _ := d.(bool); v { + return fmt.Sprintf("%s подключился снова", c.name), true + } + + return fmt.Sprintf("%s подключился", c.name), true + }) - 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) @@ -90,19 +90,21 @@ type quitCommand struct { } 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) + ms := getMessageStorage(ctx) + + err := ms.apply(ctx, fmt.Sprintf("quit:%s", c.name), time.Minute/2, func(d interface{}) (string, interface{}) { + if v, _ := d.(bool); v { + return fmt.Sprintf("%s отключился снова", c.name), true + } + + return fmt.Sprintf("%s отключился", c.name), true + }) - 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) @@ -138,6 +140,19 @@ func (c *deathCommand) run(ctx context.Context) { db := getDB(ctx) log := getLogger(ctx) pb := getDPlayersBoard(ctx) + ms := getMessageStorage(ctx) + + err := ms.apply(ctx, fmt.Sprintf("death:%s", c.name), time.Minute/2, func(d interface{}) (string, interface{}) { + if v, _ := d.(bool); v { + return fmt.Sprintf("%s умудрился опять помереть от %s", c.name, c.deathType), true + } + + return fmt.Sprintf("%s пал смертью храбрых от %s", c.name, c.deathType), true + }) + + if err != nil { + log.Error(err) + } p, err := db.getPlayerByName(ctx, c.name) if err != nil { @@ -165,12 +180,7 @@ func (c *killCommand) run(ctx context.Context) { db := getDB(ctx) log := getLogger(ctx) pb := getDPlayersBoard(ctx) - fms := getFragMessages(ctx) - - err := fms.addFrag(ctx, c.name, c.entity) - if err != nil { - log.Error(err) - } + ms := getMessageStorage(ctx) p, err := db.getPlayerByName(ctx, c.name) if err != nil { @@ -183,6 +193,21 @@ func (c *killCommand) run(ctx context.Context) { log.Error(err) } + f := func(d interface{}) (string, interface{}) { + if fragsNum, ok := d.(int); ok { + fragsNum++ + + return fmt.Sprintf("%s убил %s ✗ *%d*", p.name, c.entity, fragsNum), fragsNum + } + + return fmt.Sprintf("%s убил %s", p.name, c.entity), 1 + } + + err = ms.apply(ctx, fmt.Sprintf("kill:%s:%s", p.id, c.entity), time.Minute/2, f) + if err != nil { + log.Error(err) + } + err = pb.update(ctx, achievementMaxFrags, achievementPeaceable) if err != nil { log.Error(err) diff --git a/main.go b/main.go index 7fd9952..2d53081 100644 --- a/main.go +++ b/main.go @@ -55,15 +55,11 @@ func main() { os.Exit(5) } - mc := newMessageCleaner(bot, &wg) - ch := newCommandHandler() - pb := newPlayersBoard() + ms := NewMessageStorage() - fms := newFragMessages() - - ctx, cancelFunc := createContext(&cfg, bot, mc, ch, db, pb, fms) + ctx, cancelFunc := createContext(&cfg, bot, ch, db, pb, ms) getLogger(ctx).Infof("Run...") @@ -73,8 +69,8 @@ func main() { os.Exit(5) } - runPlayerBoard(ctx, pb, &wg) runCommandHandler(ctx, ch, &wg) + runMessageStorage(ctx, ms, &wg) runHttpServer(ctx, cfg.Http, &wg) runSignalHandler(ctx, cancelFunc) @@ -83,16 +79,6 @@ func main() { 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) @@ -103,6 +89,16 @@ func runCommandHandler(ctx context.Context, ch *commandHandler, wg *sync.WaitGro }() } +func runMessageStorage(ctx context.Context, ms *messageStorage, wg *sync.WaitGroup) { + wg.Add(1) + + go func() { + defer wg.Done() + + ms.run(ctx) + }() +} + func runHttpServer(ctx context.Context, addr string, wg *sync.WaitGroup) { server := http.Server{ Addr: addr, diff --git a/messageCleaner.go b/messageCleaner.go deleted file mode 100644 index 9b0f882..0000000 --- a/messageCleaner.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "context" - "sync" - "time" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" -) - -type messageToClean struct { - messID int - chatID int64 - timer *time.Timer -} - -type messageCleaner struct { - botApi *tgbotapi.BotAPI - wg *sync.WaitGroup - messages []*messageToClean - mx sync.Mutex -} - -func newMessageCleaner(bot *tgbotapi.BotAPI, wg *sync.WaitGroup) *messageCleaner { - return &messageCleaner{ - botApi: bot, - wg: wg, - messages: make([]*messageToClean, 0), - } -} - -func (m *messageCleaner) add(ctx context.Context, messID int, chatID int64, d time.Duration) { - m.mx.Lock() - defer m.mx.Unlock() - - for _, ms := range m.messages { - if ms.chatID == chatID && ms.messID == messID { - return - } - } - - m.wg.Add(1) - - go func() { - defer m.wg.Done() - - ms := &messageToClean{ - messID: messID, - chatID: chatID, - timer: time.NewTimer(d), - } - - m.messages = append(m.messages, ms) - - select { - case <-ms.timer.C: - case <-ctx.Done(): - } - - m.remove(messID, chatID) - - _, _ = m.botApi.DeleteMessage(tgbotapi.DeleteMessageConfig{ - MessageID: messID, - ChatID: chatID, - }) - }() -} - -func (m *messageCleaner) remove(messID int, chatID int64) { - m.mx.Lock() - defer m.mx.Unlock() - - for i, ms := range m.messages { - if ms.chatID == chatID && ms.messID == messID { - m.messages = append(m.messages[:i], m.messages[i+1:]...) - return - } - } -} - -func (m *messageCleaner) resetTimer(messID int, chatID int64, d time.Duration) { - m.mx.Lock() - defer m.mx.Unlock() - - for _, ms := range m.messages { - if ms.chatID == chatID && ms.messID == messID { - ms.timer.Reset(d) - return - } - } -} diff --git a/messageStorage.go b/messageStorage.go new file mode 100644 index 0000000..d5692af --- /dev/null +++ b/messageStorage.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "sync" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" +) + +type messageData struct { + key string + messID int + chatID int64 + data interface{} + timer *time.Timer +} + +type messageStorage struct { + m sync.Mutex + messages []*messageData +} + +func NewMessageStorage() *messageStorage { + return &messageStorage{ + messages: make([]*messageData, 0, 20), + } +} + +func deleteMessageFromTelegram(ctx context.Context, messID int, chatID int64) { + botApi := getBotApi(ctx) + + _, err := botApi.DeleteMessage(tgbotapi.DeleteMessageConfig{ + MessageID: messID, + ChatID: chatID, + }) + + if err != nil { + getLogger(ctx).Error(err) + } +} + +func (ms *messageStorage) run(ctx context.Context) { + <-ctx.Done() + + ms.m.Lock() + defer ms.m.Unlock() + + for _, m := range ms.messages { + deleteMessageFromTelegram(ctx, m.messID, m.chatID) + } + + ms.messages = ms.messages[:] +} + +func (ms *messageStorage) runTimeCleaner(ctx context.Context, md *messageData, d time.Duration) { + if d <= 0 { + md.timer = nil + return + } + + t := time.NewTimer(d) + md.timer = t + + go func() { + <-t.C + + ms.m.Lock() + defer ms.m.Unlock() + + if md.timer == t { + for i, m := range ms.messages { + if m == md { + deleteMessageFromTelegram(ctx, m.messID, m.chatID) + ms.messages = append(ms.messages[:i], ms.messages[i+1:]...) + + return + } + } + } + }() +} + +type messageChanger func(interface{}) (string, interface{}) + +func (ms *messageStorage) apply(ctx context.Context, key string, d time.Duration, mc messageChanger) error { + ms.m.Lock() + defer ms.m.Unlock() + + botApi := getBotApi(ctx) + cfg := getConfig(ctx) + + for _, md := range ms.messages { + if md.key == key { + text, data := mc(md.data) + + mess := tgbotapi.NewEditMessageText(md.chatID, md.messID, text) + mess.ParseMode = tgbotapi.ModeMarkdown + _, err := botApi.Send(mess) + if err != nil { + return err + } + + md.data = data + ms.runTimeCleaner(ctx, md, d) + + return nil + } + } + + text, data := mc(nil) + + mess := tgbotapi.NewMessage(cfg.ChatID, text) + mess.ParseMode = tgbotapi.ModeMarkdown + m, err := botApi.Send(mess) + if err != nil { + return err + } + + md := &messageData{ + key: key, + messID: m.MessageID, + chatID: cfg.ChatID, + data: data, + } + + ms.messages = append(ms.messages, md) + ms.runTimeCleaner(ctx, md, d) + + return nil +} diff --git a/playersBoard.go b/playersBoard.go index 2a37863..9a28847 100644 --- a/playersBoard.go +++ b/playersBoard.go @@ -7,9 +7,7 @@ import ( "sync" "time" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "github.com/hako/durafmt" - "github.com/pkg/errors" ) type achievement uint8 @@ -53,11 +51,10 @@ var allAchievement = []achievement{ } type playerInfo struct { - playerID string - playerMessageID int - isOnline bool - achievements []achievement - messageText string + playerID string + isOnline bool + achievements []achievement + messageText string } func (p *playerInfo) setOnline(v bool) *playerInfo { @@ -74,8 +71,6 @@ const ( ) func (p *playerInfo) updatePlayerInfo(ctx context.Context) error { - botApi := getBotApi(ctx) - conf := getConfig(ctx) db := getDB(ctx) lines := make([]string, 0, 10) @@ -132,46 +127,21 @@ func (p *playerInfo) updatePlayerInfo(ctx context.Context) error { text := strings.Join(lines, "\n") if text != p.messageText { + messStorage := getMessageStorage(ctx) + err := messStorage.apply(ctx, fmt.Sprintf("playerBoard:%s", p.playerID), 0, func(interface{}) (string, interface{}) { + return text, nil + }) - var c tgbotapi.Chattable - - if p.playerMessageID == 0 { - mess := tgbotapi.NewMessage(conf.ChatID, text) - mess.ParseMode = tgbotapi.ModeMarkdown - - c = mess - } else { - mess := tgbotapi.NewEditMessageText(conf.ChatID, p.playerMessageID, text) - mess.ParseMode = tgbotapi.ModeMarkdown - - c = mess - } - - m, err := botApi.Send(c) if err != nil { - return errors.Wrap(err, "Failed to send message") + return err } - p.playerMessageID = m.MessageID p.messageText = text } 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 -} - func (p *playerInfo) setAchievement(ach achievement) { for _, a := range p.achievements { if a == ach { @@ -249,11 +219,13 @@ func (pb *playersBoard) getPlayerBoard(playerID string) *playerInfo { pb.mx.Lock() defer pb.mx.Unlock() - pb.players = append(pb.players, &playerInfo{ + pi := &playerInfo{ playerID: playerID, - }) + } - return pb.players[len(pb.players)-1] + pb.players = append(pb.players, pi) + + return pi } func (pb *playersBoard) updateAchievements(ctx context.Context, ach achievement) error { @@ -347,19 +319,3 @@ func (pb *playersBoard) update(ctx context.Context, as ...achievement) error { return nil } - -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) - } - } -}