poker implementation
This commit is contained in:
parent
3a8a7c2a5c
commit
e0ba5347a1
@ -1,3 +1,5 @@
|
||||
POKER_BOT_POSTGRES_DSN=postgres://app:app@db:5432/app?sslmode=disable
|
||||
POKER_BOT_TELEGRAM_TOKEN=token
|
||||
POKER_BOT_WEBHOOK_URL=https://vegapokerbot.proxy.neur0tx.site/webhook
|
||||
POKER_BOT_LISTEN=:3333
|
||||
POKER_BOT_DEBUG=false
|
||||
|
@ -23,7 +23,7 @@ services:
|
||||
environment:
|
||||
GOCACHE: /go
|
||||
ports:
|
||||
- ${MG_TELEGRAM_ADDRESS:-8090}:8090
|
||||
- 3333:3333
|
||||
command: make build run
|
||||
|
||||
networks:
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger"
|
||||
"github.com/mymmrac/telego"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@ -18,6 +20,7 @@ type App struct {
|
||||
Config *config.Config
|
||||
Repositories *db.Repositories
|
||||
Handlers map[handler.Type]handler.Handler
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (a *App) Start() error {
|
||||
@ -41,7 +44,10 @@ func (a *App) Start() error {
|
||||
}
|
||||
a.initHandlers()
|
||||
a.Logger.Info("Vega Poker Bot is running")
|
||||
return a.longPoll()
|
||||
if a.Config.Listen == "" || !a.isValidURL(a.Config.WebhookURL) {
|
||||
return a.longPoll()
|
||||
}
|
||||
return a.listenWebhook()
|
||||
}
|
||||
|
||||
func (a *App) Log() *zap.SugaredLogger {
|
||||
@ -128,23 +134,60 @@ func (a *App) handler(update telego.Update) handler.Handler {
|
||||
return a.Handlers[handler.Noop]
|
||||
}
|
||||
|
||||
func (a *App) processUpdate(update telego.Update) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
a.Logger.Errorf("recovered from panic inside the handler: %v", r)
|
||||
}
|
||||
}()
|
||||
if err := a.handler(update).Handle(update); err != nil {
|
||||
a.Logger.Errorf("error while handling the update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) longPoll() error {
|
||||
_ = a.Telegram.DeleteWebhook(&telego.DeleteWebhookParams{})
|
||||
updates, err := a.Telegram.UpdatesViaLongPolling(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.Telegram.StopLongPolling()
|
||||
for update := range updates {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
a.Logger.Errorf("recovered from panic inside the handler: %v", r)
|
||||
}
|
||||
}()
|
||||
if err := a.handler(update).Handle(update); err != nil {
|
||||
a.Logger.Errorf("error while handling the update: %s", err)
|
||||
}
|
||||
}()
|
||||
go a.processUpdate(update)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) listenWebhook() error {
|
||||
uri, _ := url.Parse(a.Config.WebhookURL)
|
||||
err := a.Telegram.SetWebhook(&telego.SetWebhookParams{
|
||||
URL: a.Config.WebhookURL,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.Logger.Debugf("fetching updates from webhook: %s", uri.Path)
|
||||
updates, err := a.Telegram.UpdatesViaWebhook(uri.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
_ = a.Telegram.StartWebhook(a.Config.Listen)
|
||||
}()
|
||||
defer func() {
|
||||
_ = a.Telegram.StopWebhook()
|
||||
}()
|
||||
for update := range updates {
|
||||
go a.processUpdate(update)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) isValidURL(uri string) bool {
|
||||
if uri == "" {
|
||||
return false
|
||||
}
|
||||
_, err := url.Parse(uri)
|
||||
return err == nil
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
type Config struct {
|
||||
PostgresDSN string `default:"" env:"POSTGRES_DSN" yaml:"postgres_dsn" toml:"postgres_dsn"`
|
||||
TelegramToken string `default:"" env:"TELEGRAM_TOKEN" yaml:"telegram_token" toml:"telegram_token"`
|
||||
WebhookURL string `default:"" env:"WEBHOOK_URL" yaml:"webhook_url" toml:"webhook_url"`
|
||||
Listen string `default:"" env:"LISTEN" yaml:"listen" toml:"listen"`
|
||||
Debug bool `default:"false" env:"DEBUG" yaml:"debug" toml:"debug"`
|
||||
}
|
||||
|
||||
|
@ -2,35 +2,289 @@ package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"github.com/mymmrac/telego"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CallbackQueryHandler struct {
|
||||
app iface.App
|
||||
iface.Base
|
||||
}
|
||||
|
||||
func NewCallbackQueryHandler(app iface.App) Handler {
|
||||
return &CallbackQueryHandler{app: app}
|
||||
return &CallbackQueryHandler{iface.NewBase(app, 0, 0)}
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) Handle(wh telego.Update) error {
|
||||
cq := wh.CallbackQuery
|
||||
user, err := h.app.DB().ForUser().ByTelegramID(cq.From.ID)
|
||||
if cq.Message.GetChat().Type == telego.ChatTypePrivate {
|
||||
user, err := h.App.DB().ForUser().ByTelegramID(cq.From.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user != nil && user.ID > 0 {
|
||||
return h.handleSetupKey(cq, user)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var pl util.Payload
|
||||
if err := json.Unmarshal([]byte(cq.Data), &pl); err != nil {
|
||||
return err
|
||||
}
|
||||
chat, err := h.App.DB().ForChat().ByTelegramID(pl.Chat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user != nil && user.ID > 0 {
|
||||
return h.handleSetupKey(cq, user)
|
||||
user, err := h.App.DB().ForUser().ByID(chat.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// @todo implement poker polls handling.
|
||||
return errors.New("not implemented yet")
|
||||
loc := h.Localizer(user.Language)
|
||||
switch pl.Action {
|
||||
case util.PayloadActionVote:
|
||||
poll, found, err := h.findPollByMessage(cq.Message, loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return h.handleVote(cq.From, chat, cq.Message, poll, pl, loc)
|
||||
case util.PayloadActionVoteFinish:
|
||||
poll, found, err := h.findPollByMessage(cq.Message, loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return h.handleVoteFinish(cq.From, chat, cq.Message, poll, pl, loc)
|
||||
case util.PayloadActionVoteSendRedmine:
|
||||
poll, found, err := h.findPollByMessage(cq.Message, loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return h.handleVoteSendRedmine(cq.From, chat, cq.Message, poll, pl, loc)
|
||||
default:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleVoteFinish(from telego.User, chat *model.Chat,
|
||||
msg telego.MaybeInaccessibleMessage, poll store.PollState, pl util.Payload, loc locale.Localizer) error {
|
||||
dotOrColon := "."
|
||||
if poll.Subject != "" {
|
||||
dotOrColon = ": "
|
||||
}
|
||||
if len(poll.Votes) == 0 {
|
||||
store.Polls.Delete(msg.GetMessageID())
|
||||
_, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(pl.Chat),
|
||||
MessageID: msg.GetMessageID(),
|
||||
Text: fmt.Sprintf("%s\n%s\n%s",
|
||||
loc.Template("poker_start", map[string]interface{}{
|
||||
"Name": poll.Initiator,
|
||||
"DotOrColon": dotOrColon,
|
||||
"Subject": poll.Subject,
|
||||
}),
|
||||
loc.Template("poker_ended", map[string]interface{}{"Name": from.Username}),
|
||||
loc.Message("poker_ended_no_results"),
|
||||
),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
h.App.Log().Debugf("finished poll: %#v", poll)
|
||||
return err
|
||||
}
|
||||
|
||||
poll.Calculate()
|
||||
store.Polls.Set(msg.GetMessageID(), poll)
|
||||
_, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(pl.Chat),
|
||||
MessageID: msg.GetMessageID(),
|
||||
Text: fmt.Sprintf("%s\n%s\n\n%s\n%s\n\n%s",
|
||||
loc.Template("poker_start", map[string]interface{}{
|
||||
"Name": poll.Initiator,
|
||||
"DotOrColon": dotOrColon,
|
||||
"Subject": poll.Subject,
|
||||
}),
|
||||
loc.Template("poker_ended", map[string]interface{}{"Name": from.Username}),
|
||||
loc.Message("voted"),
|
||||
h.voters(poll.Votes, loc),
|
||||
loc.Template("poker_results", poll.Result),
|
||||
),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
ReplyMarkup: h.getPollAfterEndKeyboard(chat, poll, loc),
|
||||
})
|
||||
h.App.Log().Debugf("finished poll: %#v", poll)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleVoteSendRedmine(from telego.User, chat *model.Chat,
|
||||
msg telego.MaybeInaccessibleMessage, poll store.PollState, pl util.Payload, loc locale.Localizer) error {
|
||||
dotOrColon := "."
|
||||
if poll.Subject != "" {
|
||||
dotOrColon = ": "
|
||||
}
|
||||
updateMessage := func(tag string) error {
|
||||
_, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(pl.Chat),
|
||||
MessageID: msg.GetMessageID(),
|
||||
Text: fmt.Sprintf("%s\n%s\n\n%s\n%s\n\n%s\n\n%s",
|
||||
loc.Template("poker_start", map[string]interface{}{
|
||||
"Name": poll.Initiator,
|
||||
"DotOrColon": dotOrColon,
|
||||
"Subject": poll.Subject,
|
||||
}),
|
||||
loc.Template("poker_ended", map[string]interface{}{"Name": from.Username}),
|
||||
loc.Message("voted"),
|
||||
h.voters(poll.Votes, loc),
|
||||
loc.Template("poker_results", poll.Result),
|
||||
loc.Message(tag),
|
||||
),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
h.App.Log().Debugf("received vote send for poll: %#v", poll)
|
||||
if !poll.CanRedmine || poll.TaskID == 0 {
|
||||
store.Polls.Delete(msg.GetMessageID())
|
||||
h.App.Log().Errorf("cannot send info to Redmine for %d: CanRedmine: %t", poll.TaskID, poll.CanRedmine)
|
||||
return updateMessage("poker_redmine_cannot_send")
|
||||
}
|
||||
integrationData, err := h.App.DB().ForIntegration().LoadForChatAndType(chat.ID, model.RedmineIntegration)
|
||||
if err != nil {
|
||||
store.Polls.Delete(msg.GetMessageID())
|
||||
h.App.Log().Errorf("cannot send info to Redmine for %d: %s", poll.TaskID, err)
|
||||
return updateMessage("poker_redmine_cannot_send")
|
||||
}
|
||||
|
||||
err = integration.New(*integrationData, h.App.Log()).PushVoteResult(poll.TaskID, poll.Result.RoundHalf)
|
||||
if err != nil {
|
||||
store.Polls.Delete(msg.GetMessageID())
|
||||
h.App.Log().Errorf("cannot send info to Redmine for %d: %s", poll.TaskID, err)
|
||||
return updateMessage("poker_redmine_cannot_send")
|
||||
}
|
||||
|
||||
return updateMessage("poker_redmine_sent")
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleVote(from telego.User, chat *model.Chat,
|
||||
msg telego.MaybeInaccessibleMessage, poll store.PollState, pl util.Payload, loc locale.Localizer) error {
|
||||
var found bool
|
||||
for i, storedVote := range poll.Votes {
|
||||
if storedVote.ID == from.ID {
|
||||
storedVote.Name = h.pollMemberName(&from)
|
||||
storedVote.Vote = pl.Vote().Vote
|
||||
poll.Votes[i] = storedVote
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
poll.Votes = append(poll.Votes, store.MemberVote{
|
||||
Member: util.Member{
|
||||
ID: from.ID,
|
||||
Name: h.pollMemberName(&from),
|
||||
},
|
||||
Vote: pl.Vote().Vote,
|
||||
})
|
||||
}
|
||||
store.Polls.Set(msg.GetMessageID(), poll)
|
||||
|
||||
h.App.Log().Debugf("processed vote %#v for poll: %#v", pl.Vote(), poll)
|
||||
dotOrColon := "."
|
||||
if poll.Subject != "" {
|
||||
dotOrColon = ": "
|
||||
}
|
||||
_, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(pl.Chat),
|
||||
MessageID: msg.GetMessageID(),
|
||||
Text: fmt.Sprintf("%s\n%s\n%s",
|
||||
loc.Template("poker_start", map[string]interface{}{
|
||||
"Name": poll.Initiator,
|
||||
"DotOrColon": dotOrColon,
|
||||
"Subject": poll.Subject,
|
||||
}),
|
||||
loc.Message("voted"),
|
||||
h.votersWithoutVotes(poll.Votes, loc),
|
||||
),
|
||||
ReplyMarkup: h.getPollKeyboard(chat, loc),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) pollMemberName(user *telego.User) string {
|
||||
return fmt.Sprintf("%s (@%s)", strings.TrimRight(user.FirstName+" "+user.LastName, " "), user.Username)
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) voters(votes []store.MemberVote, loc locale.Localizer) string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(20 * len(votes))
|
||||
for _, vote := range votes {
|
||||
sb.WriteString(loc.Template("vote_result", map[string]interface{}{
|
||||
"Name": vote.Name,
|
||||
"Vote": vote.Vote,
|
||||
}) + "\n")
|
||||
}
|
||||
return strings.TrimRight(sb.String(), "\n")
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) votersWithoutVotes(votes []store.MemberVote, loc locale.Localizer) string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(20 * len(votes))
|
||||
for _, vote := range votes {
|
||||
sb.WriteString(loc.Template("voter", map[string]interface{}{"Name": vote.Name}) + "\n")
|
||||
}
|
||||
return strings.TrimRight(sb.String(), "\n")
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) getPollKeyboard(chat *model.Chat, loc locale.Localizer) *telego.InlineKeyboardMarkup {
|
||||
switch chat.KeyboardType {
|
||||
case model.StandardKeyboard:
|
||||
return util.StandardVoteKeyboard(chat.TelegramID, loc)
|
||||
case model.StoryPointsKeyboard:
|
||||
return util.StoryPointsVoteKeyboard(chat.TelegramID, loc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) getPollAfterEndKeyboard(chat *model.Chat, poll store.PollState, loc locale.Localizer) *telego.InlineKeyboardMarkup {
|
||||
rm, err := h.App.DB().ForIntegration().LoadForChatAndType(chat.ID, model.RedmineIntegration)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if rm != nil && rm.ID > 0 && poll.CanRedmine && poll.TaskID > 0 {
|
||||
return util.SendRedmineKeyboard(chat.TelegramID, poll.Result.RoundHalf, loc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) findPollByMessage(msg telego.MaybeInaccessibleMessage, loc locale.Localizer) (store.PollState, bool, error) {
|
||||
poll, found := store.Polls.Get(msg.GetMessageID())
|
||||
if !found {
|
||||
_, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(msg.GetChat().ID),
|
||||
MessageID: msg.GetMessageID(),
|
||||
Text: loc.Message("cannot_find_poll"),
|
||||
})
|
||||
return store.PollState{}, false, err
|
||||
}
|
||||
return poll, true, nil
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleSetupKey(cq *telego.CallbackQuery, user *model.User) error {
|
||||
@ -58,7 +312,7 @@ func (h *CallbackQueryHandler) handleSetupKey(cq *telego.CallbackQuery, user *mo
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleChooseKeyboard(pl util.Payload, msgID int, user *model.User) error {
|
||||
cr := h.app.DB().ForChat()
|
||||
cr := h.App.DB().ForChat()
|
||||
result := pl.KeyboardChoice()
|
||||
if pl.User != user.TelegramID {
|
||||
return nil
|
||||
@ -79,13 +333,13 @@ func (h *CallbackQueryHandler) handleChooseKeyboard(pl util.Payload, msgID int,
|
||||
if model.KeyboardType(result.Type) == model.StoryPointsKeyboard {
|
||||
kbTypeName = "sp_vote_keyboard"
|
||||
}
|
||||
loc := h.app.Localizer(user.Language)
|
||||
_, _ = h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
loc := h.Localizer(user.Language)
|
||||
_, _ = h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Template("chosen_keyboard", map[string]interface{}{"Name": loc.Message(kbTypeName)}),
|
||||
})
|
||||
_, err = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
_, err = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
Text: loc.Message("ask_for_redmine"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
@ -106,16 +360,16 @@ func (h *CallbackQueryHandler) handleChooseKeyboard(pl util.Payload, msgID int,
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleAnswerRedmine(answer util.Answer, chatID int64, msgID int, user *model.User) error {
|
||||
loc := h.app.Localizer(user.Language)
|
||||
loc := h.Localizer(user.Language)
|
||||
if !answer.Result {
|
||||
_, _ = h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
_, _ = h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message("redmine_will_not_be_configured"),
|
||||
})
|
||||
return util.SendSetupDone(h.app.TG(), user.ChatID, loc)
|
||||
return util.SendSetupDone(h.App.TG(), user.ChatID, loc)
|
||||
}
|
||||
_, err := h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
_, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message("please_send_redmine_url"),
|
||||
@ -125,13 +379,13 @@ func (h *CallbackQueryHandler) handleAnswerRedmine(answer util.Answer, chatID in
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleAnswerRedmineHours(answer util.Answer, chatID int64, msgID int, user *model.User) error {
|
||||
loc := h.app.Localizer(user.Language)
|
||||
loc := h.Localizer(user.Language)
|
||||
message := "redmine_hours_will_not_be_configured"
|
||||
if answer.Result {
|
||||
message = "redmine_hours_will_be_configured"
|
||||
}
|
||||
|
||||
_, _ = h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
_, _ = h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message(message),
|
||||
@ -145,7 +399,7 @@ func (h *CallbackQueryHandler) handleAnswerRedmineHours(answer util.Answer, chat
|
||||
}
|
||||
}
|
||||
|
||||
_, err := h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
Text: loc.Message("should_send_poker_to_redmine"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
@ -167,16 +421,16 @@ func (h *CallbackQueryHandler) handleAnswerRedmineHours(answer util.Answer, chat
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleAnswerRedmineSendResults(answer util.Answer, msgID int, user *model.User) error {
|
||||
loc := h.app.Localizer(user.Language)
|
||||
loc := h.Localizer(user.Language)
|
||||
if !answer.Result {
|
||||
_, _ = h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
_, _ = h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message("redmine_poker_will_not_be_configured"),
|
||||
})
|
||||
return util.SendSetupDone(h.app.TG(), user.ChatID, loc)
|
||||
return util.SendSetupDone(h.App.TG(), user.ChatID, loc)
|
||||
}
|
||||
_, err := h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
_, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message("specify_result_field"),
|
||||
@ -187,14 +441,14 @@ func (h *CallbackQueryHandler) handleAnswerRedmineSendResults(answer util.Answer
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) updateRedmineIntegration(rs *store.RedmineSetup, loc locale.Localizer, user *model.User) error {
|
||||
dbChat, err := h.app.DB().ForChat().ByTelegramID(rs.Chat)
|
||||
dbChat, err := h.App.DB().ForChat().ByTelegramID(rs.Chat)
|
||||
if dbChat == nil || dbChat.ID == 0 {
|
||||
_ = util.SendInternalError(h.app.TG(), user.ChatID, loc)
|
||||
_ = util.SendInternalError(h.App.TG(), user.ChatID, loc)
|
||||
return err
|
||||
}
|
||||
savedRedmine, err := h.app.DB().ForIntegration().LoadForChatAndType(dbChat.ID, model.RedmineIntegration)
|
||||
savedRedmine, err := h.App.DB().ForIntegration().LoadForChatAndType(dbChat.ID, model.RedmineIntegration)
|
||||
if savedRedmine == nil || savedRedmine.ID == 0 {
|
||||
_ = util.SendInternalError(h.app.TG(), user.ChatID, loc)
|
||||
_ = util.SendInternalError(h.App.TG(), user.ChatID, loc)
|
||||
return err
|
||||
}
|
||||
var shouldUpdate bool
|
||||
@ -209,7 +463,7 @@ func (h *CallbackQueryHandler) updateRedmineIntegration(rs *store.RedmineSetup,
|
||||
}
|
||||
if shouldUpdate {
|
||||
savedRedmine.StoreRedmine(savedRS)
|
||||
return h.app.DB().ForIntegration().Save(savedRedmine)
|
||||
return h.App.DB().ForIntegration().Save(savedRedmine)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"github.com/mymmrac/telego"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
type Poll struct {
|
||||
iface.Base
|
||||
}
|
||||
|
||||
func NewPoll(app iface.App, userID, chatID int64) *Poll {
|
||||
return &Poll{iface.NewBase(app, userID, chatID)}
|
||||
}
|
||||
|
||||
func (h *Poll) Handle(wh telego.Update) error {
|
||||
if wh.Message.Chat.Type == telego.ChatTypePrivate {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(wh.Message.Chat.ID),
|
||||
Text: h.Localizer(wh.Message.From.LanguageCode).Message("use_this_command_in_group"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
// @todo implement poker polls.
|
||||
return nil
|
||||
}
|
@ -13,21 +13,21 @@ import (
|
||||
const MaxChatMembers = 32
|
||||
|
||||
type ChatMemberUpdatedHandler struct {
|
||||
app iface.App
|
||||
iface.Base
|
||||
}
|
||||
|
||||
func NewChatMemberUpdatedHandler(app iface.App) *ChatMemberUpdatedHandler {
|
||||
return &ChatMemberUpdatedHandler{app: app}
|
||||
return &ChatMemberUpdatedHandler{iface.NewBase(app, 0, 0)}
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) Handle(wh telego.Update) error {
|
||||
cm := wh.MyChatMember
|
||||
if !cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.app.TGProfile().ID &&
|
||||
cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.app.TGProfile().ID {
|
||||
if !cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.App.TGProfile().ID &&
|
||||
cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.App.TGProfile().ID {
|
||||
return h.handleAddToChat(wh.MyChatMember.Chat)
|
||||
}
|
||||
if cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.app.TGProfile().ID &&
|
||||
!cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.app.TGProfile().ID {
|
||||
if cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.App.TGProfile().ID &&
|
||||
!cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.App.TGProfile().ID {
|
||||
return h.handleRemoveFromChat(wh.MyChatMember.Chat)
|
||||
}
|
||||
|
||||
@ -36,42 +36,42 @@ func (h *ChatMemberUpdatedHandler) Handle(wh telego.Update) error {
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
|
||||
cr := h.app.DB().ForChat()
|
||||
cr := h.App.DB().ForChat()
|
||||
chat, err := cr.ByTelegramID(tgChat.ID)
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, nil)
|
||||
_ = util.SendInternalError(h.App.TG(), tgChat.ID, nil)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := h.getRegisteredAdmin(tgChat.ID)
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, nil)
|
||||
_ = util.SendInternalError(h.App.TG(), tgChat.ID, nil)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
if user == nil || user.ID == 0 {
|
||||
_, err = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
_, err = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(tgChat.ID),
|
||||
Text: h.app.Localizer(language.English.String()).Message("you_should_register_first"),
|
||||
Text: h.Localizer(language.English.String()).Message("you_should_register_first"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
loc := h.app.Localizer(user.Language)
|
||||
loc := h.Localizer(user.Language)
|
||||
|
||||
totalMembers, err := h.app.TG().GetChatMemberCount(&telego.GetChatMemberCountParams{
|
||||
totalMembers, err := h.App.TG().GetChatMemberCount(&telego.GetChatMemberCountParams{
|
||||
ChatID: tu.ID(tgChat.ID),
|
||||
})
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, nil)
|
||||
_ = util.SendInternalError(h.App.TG(), tgChat.ID, nil)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
if *totalMembers > MaxChatMembers {
|
||||
_, err = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
_, err = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(tgChat.ID),
|
||||
Text: loc.Template("too_many_members_in_the_group", map[string]interface{}{"Limit": MaxChatMembers}),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
@ -87,27 +87,27 @@ func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
|
||||
}
|
||||
err := cr.Save(chat)
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, loc)
|
||||
_ = util.SendInternalError(h.App.TG(), tgChat.ID, loc)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
chat.UserID = user.ID
|
||||
err := h.app.DB().ForIntegration().DeleteForChat(chat.ID)
|
||||
err := h.App.DB().ForIntegration().DeleteForChat(chat.ID)
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, loc)
|
||||
_ = util.SendInternalError(h.App.TG(), tgChat.ID, loc)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
err = cr.Save(chat)
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, loc)
|
||||
_ = util.SendInternalError(h.App.TG(), tgChat.ID, loc)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
_, _ = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
Text: loc.Template("bot_was_added", map[string]interface{}{"Name": tgChat.Title}),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
@ -117,17 +117,17 @@ func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) handleRemoveFromChat(tgChat telego.Chat) error {
|
||||
cr := h.app.DB().ForChat()
|
||||
cr := h.App.DB().ForChat()
|
||||
chat, err := cr.ByTelegramID(tgChat.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chat != nil && chat.ID > 0 {
|
||||
user, _ := h.app.DB().ForUser().ByID(chat.UserID)
|
||||
user, _ := h.App.DB().ForUser().ByID(chat.UserID)
|
||||
if user != nil && user.ID > 0 && user.ChatID > 0 {
|
||||
_, _ = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
_, _ = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
Text: h.app.Localizer(user.Language).Template(
|
||||
Text: h.Localizer(user.Language).Template(
|
||||
"bot_was_removed_from_group", map[string]interface{}{"Name": tgChat.Title}),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
@ -138,7 +138,7 @@ func (h *ChatMemberUpdatedHandler) handleRemoveFromChat(tgChat telego.Chat) erro
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) sendKeyboardChooser(chat telego.Chat, user *model.User, loc locale.Localizer) error {
|
||||
_, err := h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
Text: loc.Template("choose_keyboard", map[string]interface{}{"Name": chat.Title}),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
@ -165,7 +165,7 @@ func (h *ChatMemberUpdatedHandler) sendKeyboardChooser(chat telego.Chat, user *m
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) getRegisteredAdmin(chatID int64) (*model.User, error) {
|
||||
admins, err := h.app.TG().GetChatAdministrators(&telego.GetChatAdministratorsParams{
|
||||
admins, err := h.App.TG().GetChatAdministrators(&telego.GetChatAdministratorsParams{
|
||||
ChatID: tu.ID(chatID),
|
||||
})
|
||||
if err != nil {
|
||||
@ -175,7 +175,7 @@ func (h *ChatMemberUpdatedHandler) getRegisteredAdmin(chatID int64) (*model.User
|
||||
for i, admin := range admins {
|
||||
adminIDs[i] = admin.MemberUser().ID
|
||||
}
|
||||
dbAdmins, err := h.app.DB().ForUser().ByTelegramIDs(adminIDs)
|
||||
dbAdmins, err := h.App.DB().ForUser().ByTelegramIDs(adminIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -186,7 +186,7 @@ func (h *ChatMemberUpdatedHandler) getRegisteredAdmin(chatID int64) (*model.User
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) leaveChat(chatID int64) {
|
||||
_ = h.app.TG().LeaveChat(&telego.LeaveChatParams{
|
||||
_ = h.App.TG().LeaveChat(&telego.LeaveChatParams{
|
||||
ChatID: tu.ID(chatID),
|
||||
})
|
||||
}
|
||||
|
109
internal/handler/group/poll.go
Normal file
109
internal/handler/group/poll.go
Normal file
@ -0,0 +1,109 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration"
|
||||
"github.com/mymmrac/telego"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
"golang.org/x/text/language"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Poll struct {
|
||||
iface.Base
|
||||
}
|
||||
|
||||
func NewPoll(app iface.App, userID, chatID int64) *Poll {
|
||||
return &Poll{iface.NewBase(app, userID, chatID)}
|
||||
}
|
||||
|
||||
func (h *Poll) Handle(wh telego.Update) error {
|
||||
if wh.Message.Chat.Type == telego.ChatTypePrivate {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(wh.Message.Chat.ID),
|
||||
Text: h.Localizer(wh.Message.From.LanguageCode).Message("use_this_command_in_group"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
chat, err := h.App.DB().ForChat().ByTelegramIDWithIntegrations(wh.Message.Chat.ID)
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.App.TG(), wh.Message.Chat.ID, h.Localizer(language.English.String()))
|
||||
return err
|
||||
}
|
||||
user, err := h.App.DB().ForUser().ByID(chat.UserID)
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.App.TG(), wh.Message.Chat.ID, h.Localizer(language.English.String()))
|
||||
return err
|
||||
}
|
||||
loc := h.Localizer(user.Language)
|
||||
_ = loc
|
||||
if len(wh.Message.Entities) != 1 ||
|
||||
(len(wh.Message.Entities) == 1 && wh.Message.Entities[0].Type != telego.EntityTypeBotCommand) ||
|
||||
(len(wh.Message.Entities) == 1 && wh.Message.Entities[0].Offset != 0) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
taskID int
|
||||
canRedmine bool
|
||||
)
|
||||
taskInfo := strings.TrimSpace(wh.Message.Text[wh.Message.Entities[0].Length:])
|
||||
if taskInfo != "" {
|
||||
for _, integrationData := range chat.Integrations {
|
||||
id, info := integration.New(integrationData, h.App.Log()).GetTaskInfo(taskInfo)
|
||||
if id > 0 {
|
||||
taskID = id
|
||||
taskInfo = info
|
||||
canRedmine = integrationData.Type == model.RedmineIntegration
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dotOrColon := "."
|
||||
if taskInfo != "" {
|
||||
dotOrColon = ": "
|
||||
}
|
||||
|
||||
var kb telego.ReplyMarkup
|
||||
switch chat.KeyboardType {
|
||||
case model.StandardKeyboard:
|
||||
kb = util.StandardVoteKeyboard(wh.Message.Chat.ID, loc)
|
||||
case model.StoryPointsKeyboard:
|
||||
kb = util.StoryPointsVoteKeyboard(wh.Message.Chat.ID, loc)
|
||||
}
|
||||
|
||||
msg, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(wh.Message.Chat.ID),
|
||||
Text: fmt.Sprintf("%s\n%s",
|
||||
loc.Template("poker_start", map[string]interface{}{
|
||||
"Name": wh.Message.From.Username,
|
||||
"DotOrColon": dotOrColon,
|
||||
"Subject": taskInfo,
|
||||
}),
|
||||
loc.Message("no_votes_yet"),
|
||||
),
|
||||
ReplyMarkup: kb,
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store.Polls.Set(msg.MessageID, store.PollState{
|
||||
Initiator: wh.Message.From.Username,
|
||||
Subject: taskInfo,
|
||||
TaskID: taskID,
|
||||
Votes: []store.MemberVote{},
|
||||
Result: store.PollResult{},
|
||||
CanRedmine: canRedmine,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
package handler
|
||||
|
||||
import "github.com/mymmrac/telego"
|
||||
|
||||
func StandardVoteKeyboard() *telego.InlineKeyboardMarkup {
|
||||
return &telego.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "0",
|
||||
},
|
||||
{
|
||||
Text: "0.5",
|
||||
},
|
||||
{
|
||||
Text: "1",
|
||||
},
|
||||
{
|
||||
Text: "2",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
{
|
||||
Text: "3",
|
||||
},
|
||||
{
|
||||
Text: "5",
|
||||
},
|
||||
{
|
||||
Text: "8",
|
||||
},
|
||||
{
|
||||
Text: "13",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
{
|
||||
Text: "20",
|
||||
},
|
||||
{
|
||||
Text: "40",
|
||||
},
|
||||
{
|
||||
Text: "100",
|
||||
},
|
||||
{
|
||||
Text: "?",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func StoryPointsVoteKeyboard() *telego.InlineKeyboardMarkup {
|
||||
return &telego.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "0.5",
|
||||
},
|
||||
{
|
||||
Text: "1",
|
||||
},
|
||||
{
|
||||
Text: "1.5",
|
||||
},
|
||||
{
|
||||
Text: "2",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "2.5",
|
||||
},
|
||||
{
|
||||
Text: "3",
|
||||
},
|
||||
{
|
||||
Text: "3.5",
|
||||
},
|
||||
{
|
||||
Text: "4",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "4.5",
|
||||
},
|
||||
{
|
||||
Text: "5",
|
||||
},
|
||||
{
|
||||
Text: "5.5",
|
||||
},
|
||||
{
|
||||
Text: "6",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "6.5",
|
||||
},
|
||||
{
|
||||
Text: "7",
|
||||
},
|
||||
{
|
||||
Text: "7.5",
|
||||
},
|
||||
{
|
||||
Text: "8",
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "8.5",
|
||||
},
|
||||
{
|
||||
Text: "9",
|
||||
},
|
||||
{
|
||||
Text: "9.5",
|
||||
},
|
||||
{
|
||||
Text: "10",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/chat"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/group"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
@ -11,39 +10,34 @@ import (
|
||||
)
|
||||
|
||||
type MessageHandler struct {
|
||||
app iface.App
|
||||
iface.Base
|
||||
}
|
||||
|
||||
func NewMessageHandler(app iface.App) Handler {
|
||||
return &MessageHandler{app: app}
|
||||
return &MessageHandler{iface.NewBase(app, 0, 0)}
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Handle(wh telego.Update) error {
|
||||
if wh.Message.From != nil &&
|
||||
wh.Message.Chat.Type == telego.ChatTypePrivate {
|
||||
if util.MatchCommand("poll", wh.Message) {
|
||||
return chat.NewPoll(h.app, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
if wh.Message.From == nil {
|
||||
return nil
|
||||
}
|
||||
if wh.Message.Chat.Type == telego.ChatTypePrivate {
|
||||
if util.MatchCommand("start", wh.Message) {
|
||||
return wizard.NewRegister(h.app, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
return wizard.NewRegister(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
if util.MatchCommand("help", wh.Message) {
|
||||
return wizard.NewHelpCommand(h.app, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
return wizard.NewHelpCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
||||
setup, found := store.RedmineSetups.Get(wh.Message.Chat.ID)
|
||||
if found {
|
||||
return wizard.NewRedmineSetup(h.app, wh.Message.From.ID, wh.Message.Chat.ID, setup).Handle(wh)
|
||||
return wizard.NewRedmineSetup(h.App, wh.Message.From.ID, wh.Message.Chat.ID, setup).Handle(wh)
|
||||
}
|
||||
|
||||
return wizard.NewUnknownCommand(h.app, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
||||
// TODO: Remove debug statement below.
|
||||
h.app.Log().Debugf("New Message: %s", func(msg *telego.Message) string {
|
||||
data, _ := json.Marshal(msg)
|
||||
return string(data)
|
||||
}(wh.Message))
|
||||
if util.MatchCommand("poll", wh.Message) {
|
||||
return group.NewPoll(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
||||
return nil
|
||||
return wizard.NewUnknownCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
cache, err := otter.MustBuilder[int64, PollState](10_000).
|
||||
Cost(func(key int64, value PollState) uint32 {
|
||||
cache, err := otter.MustBuilder[int, PollState](10_000).
|
||||
Cost(func(key int, value PollState) uint32 {
|
||||
return 1
|
||||
}).
|
||||
WithTTL(time.Hour * 24 * 7).
|
||||
|
@ -3,24 +3,43 @@ package store
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"github.com/maypok86/otter"
|
||||
"math"
|
||||
)
|
||||
|
||||
type MemberVote struct {
|
||||
util.Member
|
||||
Vote float32
|
||||
Vote float64
|
||||
}
|
||||
|
||||
type PollResult struct {
|
||||
Max float32
|
||||
Min float32
|
||||
Avg float32
|
||||
Halved float32
|
||||
Max float64
|
||||
Min float64
|
||||
Avg float64
|
||||
RoundHalf float64
|
||||
}
|
||||
|
||||
type PollState struct {
|
||||
Members []util.Member
|
||||
Votes []MemberVote
|
||||
Result PollResult
|
||||
Initiator string
|
||||
Subject string
|
||||
TaskID int
|
||||
Votes []MemberVote
|
||||
Result PollResult
|
||||
CanRedmine bool
|
||||
}
|
||||
|
||||
var Polls otter.Cache[int64, PollState]
|
||||
func (p *PollState) Calculate() {
|
||||
var sum float64
|
||||
for _, vote := range p.Votes {
|
||||
sum += vote.Vote
|
||||
if vote.Vote > p.Result.Max {
|
||||
p.Result.Max = vote.Vote
|
||||
}
|
||||
if vote.Vote < p.Result.Min {
|
||||
p.Result.Min = vote.Vote
|
||||
}
|
||||
}
|
||||
p.Result.Avg = sum / float64(len(p.Votes))
|
||||
p.Result.RoundHalf = math.Round(p.Result.Avg/0.5) * 0.5
|
||||
}
|
||||
|
||||
var Polls otter.Cache[int, PollState]
|
||||
|
191
internal/handler/util/keyboard.go
Normal file
191
internal/handler/util/keyboard.go
Normal file
@ -0,0 +1,191 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"github.com/mymmrac/telego"
|
||||
)
|
||||
|
||||
func StandardVoteKeyboard(chatID int64, loc locale.Localizer) *telego.InlineKeyboardMarkup {
|
||||
return &telego.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "0",
|
||||
CallbackData: NewVotePayload(chatID, 0).String(),
|
||||
},
|
||||
{
|
||||
Text: "0.5",
|
||||
CallbackData: NewVotePayload(chatID, 0.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "1",
|
||||
CallbackData: NewVotePayload(chatID, 1).String(),
|
||||
},
|
||||
{
|
||||
Text: "2",
|
||||
CallbackData: NewVotePayload(chatID, 2).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
{
|
||||
Text: "3",
|
||||
CallbackData: NewVotePayload(chatID, 3).String(),
|
||||
},
|
||||
{
|
||||
Text: "5",
|
||||
CallbackData: NewVotePayload(chatID, 5).String(),
|
||||
},
|
||||
{
|
||||
Text: "8",
|
||||
CallbackData: NewVotePayload(chatID, 8).String(),
|
||||
},
|
||||
{
|
||||
Text: "13",
|
||||
CallbackData: NewVotePayload(chatID, 13).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
{
|
||||
Text: "20",
|
||||
CallbackData: NewVotePayload(chatID, 20).String(),
|
||||
},
|
||||
{
|
||||
Text: "40",
|
||||
CallbackData: NewVotePayload(chatID, 40).String(),
|
||||
},
|
||||
{
|
||||
Text: "100",
|
||||
CallbackData: NewVotePayload(chatID, 100).String(),
|
||||
},
|
||||
{
|
||||
Text: "?",
|
||||
CallbackData: NewVotePayload(chatID, 0).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: loc.Message("finish_vote"),
|
||||
CallbackData: NewVoteFinishPayload(chatID).String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func StoryPointsVoteKeyboard(chatID int64, loc locale.Localizer) *telego.InlineKeyboardMarkup {
|
||||
return &telego.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: "0.5",
|
||||
CallbackData: NewVotePayload(chatID, 0.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "1",
|
||||
CallbackData: NewVotePayload(chatID, 1).String(),
|
||||
},
|
||||
{
|
||||
Text: "1.5",
|
||||
CallbackData: NewVotePayload(chatID, 1.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "2",
|
||||
CallbackData: NewVotePayload(chatID, 2).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "2.5",
|
||||
CallbackData: NewVotePayload(chatID, 2.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "3",
|
||||
CallbackData: NewVotePayload(chatID, 3).String(),
|
||||
},
|
||||
{
|
||||
Text: "3.5",
|
||||
CallbackData: NewVotePayload(chatID, 3.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "4",
|
||||
CallbackData: NewVotePayload(chatID, 4).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "4.5",
|
||||
CallbackData: NewVotePayload(chatID, 4.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "5",
|
||||
CallbackData: NewVotePayload(chatID, 5).String(),
|
||||
},
|
||||
{
|
||||
Text: "5.5",
|
||||
CallbackData: NewVotePayload(chatID, 5.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "6",
|
||||
CallbackData: NewVotePayload(chatID, 6).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "6.5",
|
||||
CallbackData: NewVotePayload(chatID, 6.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "7",
|
||||
CallbackData: NewVotePayload(chatID, 7).String(),
|
||||
},
|
||||
{
|
||||
Text: "7.5",
|
||||
CallbackData: NewVotePayload(chatID, 7.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "8",
|
||||
CallbackData: NewVotePayload(chatID, 8).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: "8.5",
|
||||
CallbackData: NewVotePayload(chatID, 8.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "9",
|
||||
CallbackData: NewVotePayload(chatID, 9).String(),
|
||||
},
|
||||
{
|
||||
Text: "9.5",
|
||||
CallbackData: NewVotePayload(chatID, 9.5).String(),
|
||||
},
|
||||
{
|
||||
Text: "10",
|
||||
CallbackData: NewVotePayload(chatID, 10).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: loc.Message("finish_vote"),
|
||||
CallbackData: NewVoteFinishPayload(chatID).String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func SendRedmineKeyboard(chatID int64, result float64, loc locale.Localizer) *telego.InlineKeyboardMarkup {
|
||||
return &telego.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: loc.Message("send_result_redmine"),
|
||||
CallbackData: NewRedmineSendResultPayload(chatID, result).String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -10,6 +10,8 @@ const (
|
||||
PayloadActionYesNo = iota
|
||||
PayloadActionVote
|
||||
PayloadActionChooseKeyboard
|
||||
PayloadActionVoteFinish
|
||||
PayloadActionVoteSendRedmine
|
||||
)
|
||||
|
||||
type QuestionType uint8
|
||||
@ -21,10 +23,11 @@ const (
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
User int64 `json:"u,omitempty"`
|
||||
Chat int64 `json:"c,omitempty"`
|
||||
Action PayloadAction `json:"a"`
|
||||
Data json.RawMessage `json:"d,omitempty"`
|
||||
User int64 `json:"u,omitempty"`
|
||||
Chat int64 `json:"c,omitempty"`
|
||||
Message int `json:"m,omitempty"`
|
||||
Action PayloadAction `json:"a"`
|
||||
Data json.RawMessage `json:"d,omitempty"`
|
||||
}
|
||||
|
||||
func (p Payload) String() string {
|
||||
@ -66,7 +69,7 @@ type KBChooserData struct {
|
||||
}
|
||||
|
||||
type Vote struct {
|
||||
Vote float32 `json:"v"`
|
||||
Vote float64 `json:"v"`
|
||||
}
|
||||
|
||||
type Answer struct {
|
||||
@ -85,7 +88,7 @@ func NewKeyboardChooserPayload(userID, chatID int64, kbType uint8) *Payload {
|
||||
}
|
||||
}
|
||||
|
||||
func NewVotePayload(chatID int64, vote float32) *Payload {
|
||||
func NewVotePayload(chatID int64, vote float64) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionVote,
|
||||
Chat: chatID,
|
||||
@ -93,6 +96,13 @@ func NewVotePayload(chatID int64, vote float32) *Payload {
|
||||
}
|
||||
}
|
||||
|
||||
func NewVoteFinishPayload(chatID int64) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionVoteFinish,
|
||||
Chat: chatID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRedmineQuestionPayload(userID int64, chatID int64, isYes bool) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionYesNo,
|
||||
@ -120,6 +130,14 @@ func NewRedmineSendResultQuestionPayload(userID int64, chatID int64, isYes bool)
|
||||
}
|
||||
}
|
||||
|
||||
func NewRedmineSendResultPayload(chatID int64, resultToSend float64) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionVoteSendRedmine,
|
||||
Chat: chatID,
|
||||
Data: marshal(Vote{Vote: resultToSend}),
|
||||
}
|
||||
}
|
||||
|
||||
func marshal(val interface{}) json.RawMessage {
|
||||
data, _ := json.Marshal(val)
|
||||
return data
|
||||
|
@ -5,12 +5,13 @@ import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration/null"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration/redmine"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func New(dbModel model.Integration) iface.Integration {
|
||||
func New(dbModel model.Integration, log *zap.SugaredLogger) iface.Integration {
|
||||
switch dbModel.Type {
|
||||
case model.RedmineIntegration:
|
||||
return redmine.New(dbModel.Params)
|
||||
return redmine.New(dbModel.Params, log)
|
||||
default:
|
||||
return null.New()
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package iface
|
||||
|
||||
type Integration interface {
|
||||
GetTaskInfoText(input string) string
|
||||
GetTaskInfo(input string) (int, string)
|
||||
PushVoteResult(id int, result float64) error
|
||||
}
|
||||
|
@ -8,6 +8,10 @@ func New() iface.Integration {
|
||||
return &Null{}
|
||||
}
|
||||
|
||||
func (n *Null) GetTaskInfoText() string {
|
||||
return ""
|
||||
func (n *Null) GetTaskInfo(string) (int, string) {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
func (n *Null) PushVoteResult(id int, result float64) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -14,19 +16,21 @@ type Client struct {
|
||||
URL string
|
||||
Key string
|
||||
client *http.Client
|
||||
log *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func New(url, key string) *Client {
|
||||
func New(url, key string, log *zap.SugaredLogger) *Client {
|
||||
return &Client{
|
||||
URL: strings.TrimSuffix(url, "/"),
|
||||
Key: key,
|
||||
client: &http.Client{Timeout: time.Second * 2},
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Issue(id uint64) (*Issue, error) {
|
||||
func (c *Client) Issue(id int) (*Issue, error) {
|
||||
var resp IssueResponse
|
||||
err := c.sendJSONRequest(http.MethodGet, "/issues/"+strconv.FormatUint(id, 10), nil, &resp)
|
||||
_, err := c.sendJSONRequest(http.MethodGet, "/issues/"+strconv.Itoa(id), nil, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -36,31 +40,59 @@ func (c *Client) Issue(id uint64) (*Issue, error) {
|
||||
return resp.Issue, nil
|
||||
}
|
||||
|
||||
func (c *Client) sendRequest(method, path string, body io.Reader) (*http.Response, error) {
|
||||
func (c *Client) UpdateIssue(id int, issue *Issue) error {
|
||||
st, err := c.sendJSONRequest(http.MethodPut, "/issues/"+strconv.Itoa(id), IssueResponse{Issue: issue}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if st != http.StatusOK {
|
||||
return errors.New("unexpected status code: " + strconv.Itoa(st))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) sendRequest(method, path string, headers map[string]string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, c.URL+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("X-Redmine-API-Key", c.Key)
|
||||
for name, value := range headers {
|
||||
req.Header.Set(name, value)
|
||||
}
|
||||
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) sendJSONRequest(method, path string, body, out interface{}) error {
|
||||
func (c *Client) sendJSONRequest(method, path string, body, out interface{}) (int, error) {
|
||||
var buf io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
buf = bytes.NewBuffer(data)
|
||||
}
|
||||
|
||||
resp, err := c.sendRequest(method, path+".json", buf)
|
||||
path = path + ".json"
|
||||
c.log.Debugf("[Redmine Request] %s %s; body: %s", method, path, func() string {
|
||||
if buf != nil {
|
||||
return buf.(*bytes.Buffer).String()
|
||||
}
|
||||
return "<nil>"
|
||||
}())
|
||||
|
||||
resp, err := c.sendRequest(method, path, map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
}, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
if out != nil {
|
||||
return resp.StatusCode, json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
c.log.Debugf("[Redmine Response] code: %d; body: %#v", resp.StatusCode, out)
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ErrorResponse struct {
|
||||
Errors []string `json:"errors"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func (e ErrorResponse) Error() string {
|
||||
@ -34,26 +36,49 @@ type CustomField struct {
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
ID int `json:"id"`
|
||||
Project NameWithID `json:"project"`
|
||||
Tracker NameWithID `json:"tracker"`
|
||||
Status NameWithID `json:"status"`
|
||||
Priority NameWithID `json:"priority"`
|
||||
Author NameWithID `json:"author"`
|
||||
AssignedTo NameWithID `json:"assigned_to"`
|
||||
Parent NameWithID `json:"parent"`
|
||||
Subject string `json:"subject"`
|
||||
Description string `json:"description"`
|
||||
StartDate interface{} `json:"start_date"`
|
||||
DueDate interface{} `json:"due_date"`
|
||||
DoneRatio int `json:"done_ratio"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
EstimatedHours interface{} `json:"estimated_hours"`
|
||||
TotalEstimatedHours interface{} `json:"total_estimated_hours"`
|
||||
SpentHours float64 `json:"spent_hours"`
|
||||
TotalSpentHours float64 `json:"total_spent_hours"`
|
||||
CustomFields []CustomField `json:"custom_fields"`
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
UpdatedOn time.Time `json:"updated_on"`
|
||||
ClosedOn interface{} `json:"closed_on"`
|
||||
ID int `json:"id,omitempty"`
|
||||
Project *NameWithID `json:"project,omitempty"`
|
||||
Tracker *NameWithID `json:"tracker,omitempty"`
|
||||
Status *NameWithID `json:"status,omitempty"`
|
||||
Priority *NameWithID `json:"priority,omitempty"`
|
||||
Author *NameWithID `json:"author,omitempty"`
|
||||
AssignedTo *NameWithID `json:"assigned_to,omitempty"`
|
||||
Parent *NameWithID `json:"parent,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
StartDate interface{} `json:"start_date,omitempty"`
|
||||
DueDate interface{} `json:"due_date,omitempty"`
|
||||
DoneRatio int `json:"done_ratio,omitempty"`
|
||||
IsPrivate bool `json:"is_private,omitempty"`
|
||||
EstimatedHours Hours `json:"estimated_hours,omitempty"`
|
||||
TotalEstimatedHours Hours `json:"total_estimated_hours,omitempty"`
|
||||
SpentHours Hours `json:"spent_hours,omitempty"`
|
||||
TotalSpentHours Hours `json:"total_spent_hours,omitempty"`
|
||||
CustomFields []CustomField `json:"custom_fields,omitempty"`
|
||||
CreatedOn *time.Time `json:"created_on,omitempty"`
|
||||
UpdatedOn *time.Time `json:"updated_on,omitempty"`
|
||||
ClosedOn interface{} `json:"closed_on,omitempty"`
|
||||
}
|
||||
|
||||
type Hours string
|
||||
|
||||
func (h Hours) MarshalJSON() ([]byte, error) {
|
||||
f, err := strconv.ParseFloat(string(h), 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(strconv.FormatFloat(f, 'f', 2, 64)), nil
|
||||
}
|
||||
|
||||
func (h *Hours) UnmarshalJSON(b []byte) error {
|
||||
f, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
var output string
|
||||
defer func() {
|
||||
*h = Hours(output)
|
||||
}()
|
||||
return json.Unmarshal(b, &output)
|
||||
}
|
||||
*h = Hours(strconv.FormatFloat(f, 'f', 2, 64))
|
||||
return nil
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration/null"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/integration/redmine/api"
|
||||
"go.uber.org/zap"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -15,10 +16,12 @@ var issueNumberMatcher = regexp.MustCompile(`(?m)\/issues\/(\d+)`)
|
||||
var sprintNames = []string{"sprint", "спринт"}
|
||||
|
||||
type Redmine struct {
|
||||
api *api.Client
|
||||
api *api.Client
|
||||
log *zap.SugaredLogger
|
||||
spFieldName string
|
||||
}
|
||||
|
||||
func New(params map[string]interface{}) iface.Integration {
|
||||
func New(params map[string]interface{}, log *zap.SugaredLogger) iface.Integration {
|
||||
uri, ok := params["url"]
|
||||
if !ok {
|
||||
return null.New()
|
||||
@ -35,28 +38,57 @@ func New(params map[string]interface{}) iface.Integration {
|
||||
if !ok {
|
||||
return null.New()
|
||||
}
|
||||
return &Redmine{api: api.New(uriString, keyString)}
|
||||
var spField string
|
||||
if spFieldName, ok := params["sp_field"]; ok {
|
||||
if strField, ok := spFieldName.(string); ok {
|
||||
spField = strField
|
||||
}
|
||||
}
|
||||
return &Redmine{api: api.New(uriString, keyString, log), log: log, spFieldName: spField}
|
||||
}
|
||||
|
||||
func (r *Redmine) GetTaskInfoText(input string) string {
|
||||
func (r *Redmine) GetTaskInfo(input string) (int, string) {
|
||||
taskNumber := r.getTaskNumber(input)
|
||||
if taskNumber == 0 {
|
||||
return ""
|
||||
r.log.Debugf("[Redmine] Cannot extract task number from %s", input)
|
||||
return 0, ""
|
||||
}
|
||||
task, err := r.api.Issue(taskNumber)
|
||||
if err != nil {
|
||||
return ""
|
||||
r.log.Errorf("[Redmine] Cannot get Redmine issue %d: %s", taskNumber, err)
|
||||
return 0, ""
|
||||
}
|
||||
sprint := r.getSprint(task.CustomFields)
|
||||
if sprint != "" {
|
||||
sprint = fmt.Sprintf(" - **%s**", sprint)
|
||||
sprint = fmt.Sprintf(" - _%s_", sprint)
|
||||
}
|
||||
return fmt.Sprintf("[(%s) %s: %s](%s/issues/%d)%s",
|
||||
task.Project.Name, task.Tracker.Name, task.Subject, r.api.URL, taskNumber, sprint)
|
||||
taskInfo := strings.ReplaceAll(fmt.Sprintf("_(%s) %s_: [%s](%s/issues/%d)%s",
|
||||
task.Project.Name, task.Tracker.Name, task.Subject, r.api.URL, taskNumber, sprint), "*", "")
|
||||
return task.ID, "\n" + taskInfo + ""
|
||||
}
|
||||
|
||||
func (r *Redmine) getTaskNumber(input string) uint64 {
|
||||
num, err := strconv.ParseUint(input, 10, 64)
|
||||
func (r *Redmine) PushVoteResult(id int, result float64) error {
|
||||
task, err := r.api.Issue(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issue := &api.Issue{
|
||||
EstimatedHours: api.Hours(strconv.FormatFloat(result*8, 'f', 0, 64)),
|
||||
}
|
||||
if r.spFieldName != "" {
|
||||
for _, field := range task.CustomFields {
|
||||
field := field
|
||||
if field.Name == r.spFieldName {
|
||||
field.Value = strconv.FormatFloat(result, 'f', 1, 64)
|
||||
issue.CustomFields = []api.CustomField{field}
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.api.UpdateIssue(id, issue)
|
||||
}
|
||||
|
||||
func (r *Redmine) getTaskNumber(input string) int {
|
||||
num, err := strconv.Atoi(input)
|
||||
if err == nil && num > 0 {
|
||||
return num
|
||||
}
|
||||
@ -64,7 +96,7 @@ func (r *Redmine) getTaskNumber(input string) uint64 {
|
||||
if len(matches) < 2 {
|
||||
return 0
|
||||
}
|
||||
number, err := strconv.ParseUint(matches[1], 10, 64)
|
||||
number, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
@ -28,3 +28,15 @@ should_send_estimated_hours_redmine: "🕑 Do you want the poker result to be co
|
||||
setup_done: "✔️ Done, now your chat is connected to the bot!\nUse the command /poll@vegapokerbot in the connected chat to start a vote."
|
||||
yes: "✔️ Yes"
|
||||
no: "✖️ No"
|
||||
poker_start: "@{{.Name}} started poker{{.DotOrColon}}{{.Subject}}"
|
||||
poker_ended: "Voting was finished by @{{.Name}}"
|
||||
poker_ended_no_results: "_There were no votes._"
|
||||
no_votes_yet: "_There are no votes yet..._"
|
||||
voted: "Voted:"
|
||||
voter: "• 👨💻 {{.Name}}"
|
||||
vote_result: "• 👨💻 {{.Name}}: {{.Vote}}"
|
||||
poker_results: "Result:\n• Max: {{.Max}}\n• Min: {{.Min}}\n• Average: {{.Avg}}\n• Rounded to half: {{.RoundHalf}}"
|
||||
poker_redmine_cannot_send: "😢 _Cannot send results to Redmine._"
|
||||
poker_redmine_sent: "📕 _Results were sent to Redmine._"
|
||||
finish_vote: "🏁 Finish vote"
|
||||
cannot_find_poll: "Cannot find this poll. Please start it again if you need it."
|
||||
|
@ -11,6 +11,7 @@ unknown_command: "❔ Неизвестная команда. Используй
|
||||
help_output: "📄 Доступные команды:\n\n• /start - регистрация в боте;\n• /poll@vegapokerbot <task> - запускает poker в группе, работает только в подключенных группах;\n• /help - выводит эту справку."
|
||||
choose_at_least_one: "❌ Вы должны выбрать хотя бы одного участника."
|
||||
ask_for_redmine: "📕 Настроить интеграцию с Redmine?"
|
||||
send_result_redmine: "📕 Отправить результат в Redmine"
|
||||
redmine_will_not_be_configured: "👌 Интеграция с Redmine не будет настроена. Вы не сможете отправлять результаты poker в Redmine или получать информацию о задачах."
|
||||
redmine_hours_will_not_be_configured: "👌 Оценка временных затрат не будет передаваться в Redmine."
|
||||
redmine_hours_will_be_configured: "👌 Оценка временных затрат будет передаваться в Redmine."
|
||||
@ -28,3 +29,15 @@ should_send_estimated_hours_redmine: "🕑 Передавать в поле *О
|
||||
setup_done: "✔️ Готово, чат подключен к боту!\nИспользуйте команду /poll@vegapokerbot в подключенном чате для запуска голосования."
|
||||
yes: "✔️ Да"
|
||||
no: "✖️ Нет"
|
||||
poker_start: "@{{.Name}} запустил покер{{.DotOrColon}}{{.Subject}}"
|
||||
no_votes_yet: "_Пока еще никто не проголосовал..._"
|
||||
poker_ended: "Голосование было завершено @{{.Name}}"
|
||||
poker_ended_no_results: "_Никто не проголосовал._"
|
||||
voted: "Проголосовали:"
|
||||
voter: "• 👨💻 {{.Name}}"
|
||||
vote_result: "• 👨💻 {{.Name}}: {{.Vote}}"
|
||||
poker_results: "Результат:\n• Максимум: {{.Max}}\n• Минимум: {{.Min}}\n• Среднее: {{.Avg}}\n• Округление до половины: {{.RoundHalf}}"
|
||||
poker_redmine_cannot_send: "😢 _Не удалось отправить результаты в Redmine._"
|
||||
poker_redmine_sent: "📕 _Результаты были отправлены в Redmine._"
|
||||
finish_vote: "🏁 Завершить голосование"
|
||||
cannot_find_poll: "Не удалось найти это голосование. Запустите его заново если оно вам нужно."
|
||||
|
@ -4,7 +4,7 @@ import "github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
type Localizer interface {
|
||||
Message(string) string
|
||||
Template(string, map[string]interface{}) string
|
||||
Template(string, interface{}) string
|
||||
}
|
||||
|
||||
type localizer struct {
|
||||
@ -15,7 +15,7 @@ func (l *localizer) Message(str string) string {
|
||||
return l.Template(str, nil)
|
||||
}
|
||||
|
||||
func (l *localizer) Template(str string, tpl map[string]interface{}) string {
|
||||
func (l *localizer) Template(str string, tpl interface{}) string {
|
||||
return l.loc.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: str,
|
||||
TemplateData: tpl,
|
||||
|
Loading…
Reference in New Issue
Block a user