Files
tiennm99 f60b661444 feat(noitu): send the separate noitubot game from /noitubot
/noitubot now sends its own BotFather game, noitubot, so players can tell a
solo game from a /noitu room card. Both games share the Play handler and page.
A noitubot Play press skips the card lookup and always plays solo.
2026-10-09 16:42:31 +07:00

81 lines
2.9 KiB
Go

package noitu
import (
"context"
"net/url"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/tiennm99/tiennm99bot/internal/log"
)
const (
msgDisabled = "Trò chơi nối từ chưa được cấu hình trên máy chủ này."
msgNoGameTarget = "Không xác định được tin nhắn trò chơi. Hãy gửi /noitubot để chơi với bot, hoặc /noitu trong nhóm để chơi cùng nhau."
)
// handlePlay answers a Play press with the game URL carrying a signed token,
// or with an alert when the game cannot open. A press on a /noitu card
// marks the token PvP, so the page joins the card's room. Every path answers
// the query, so the client never keeps spinning.
func (s *service) handlePlay(ctx context.Context, b *bot.Bot, update *models.Update) error {
q := update.CallbackQuery
if !s.enabled() {
return answerAlert(ctx, b, q.ID, msgDisabled)
}
c, form := playClaims(q)
if form == "" {
return answerAlert(ctx, b, q.ID, msgNoGameTarget)
}
// Cards are noitu games and live only in groups, whose chat IDs are
// negative; a noitubot game or a private chat never needs the lookup, so a
// storage fault cannot block a solo game.
if q.GameShortName == ShortName && c.InlineID == "" && c.ChatID < 0 {
card, ok, err := s.lookupCard(ctx, c.ChatID, c.MessageID)
if err != nil {
_ = answerAlert(ctx, b, q.ID, msgCardLookup)
return err
}
c.PvP, c.ThreadID = ok, card.ThreadID
}
c.Expiry = s.cfg.now().Add(tokenTTL).Unix()
token, err := signToken(s.cfg.key, c)
if err != nil {
_ = answerAlert(ctx, b, q.ID, msgDisabled)
return err
}
// The form tells whether ?game= shares arrive as inline messages; the IDs
// themselves are never logged.
log.Debug("noitu play", "address", form, "pvp", c.PvP)
_, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{
CallbackQueryID: q.ID,
URL: s.cfg.baseURL + routePrefix + "?t=" + url.QueryEscape(token),
})
return err
}
// playClaims addresses the game message: the message itself when the bot can
// see it, the inaccessible message's chat and id otherwise, and the inline
// message id for a game sent via the bot. form is "" when none is present.
func playClaims(q *models.CallbackQuery) (claims, string) {
c := claims{UserID: q.From.ID, Name: truncateName(q.From.FirstName)}
switch {
case q.Message.Message != nil:
c.ChatID, c.MessageID = q.Message.Message.Chat.ID, q.Message.Message.ID
return c, "message"
case q.Message.InaccessibleMessage != nil:
c.ChatID, c.MessageID = q.Message.InaccessibleMessage.Chat.ID, q.Message.InaccessibleMessage.MessageID
return c, "inaccessible_message"
case q.InlineMessageID != "":
c.InlineID = q.InlineMessageID
return c, "inline_message"
}
return c, ""
}
func answerAlert(ctx context.Context, b *bot.Bot, id, text string) error {
_, err := b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: id, Text: text, ShowAlert: true})
return err
}