diff --git a/cmd/anon3anon/config.go b/cmd/anon3anon/config.go new file mode 100644 index 0000000..f205b98 --- /dev/null +++ b/cmd/anon3anon/config.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/kelseyhightower/envconfig" + "github.com/pkg/errors" +) + +func parseEnv() (*config, error) { + c := new(config) + if err := envconfig.Process("", c); err != nil { + return nil, errors.Wrap(err, "failed to parse env") + } + return c, nil +} + +type config struct { + TelegramBotToken string `envconfig:"telegram_bot_token"` + OwnerChatID int64 `envconfig:"owner_chat_id"` +} diff --git a/cmd/anon3anon/main.go b/cmd/anon3anon/main.go index 447fbff..e8f1268 100644 --- a/cmd/anon3anon/main.go +++ b/cmd/anon3anon/main.go @@ -2,54 +2,35 @@ package main import ( "log" - "os" - "strconv" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/nightnoryu/anon3anon/pkg/app" + "github.com/nightnoryu/anon3anon/pkg/infrastructure" ) -const ( - telegramBotTokenEnvKey = "TELEGRAM_BOT_TOKEN" - ownerChatIDEnvKey = "OWNER_CHAT_ID" -) - -type config struct { - Token string - OwnerChatID int64 -} - -func getConfig() (config, error) { - token := os.Getenv(telegramBotTokenEnvKey) - ownerChatID, err := strconv.ParseInt(os.Getenv(ownerChatIDEnvKey), 10, 64) - if err != nil { - return config{}, err - } - - return config{ - Token: token, - OwnerChatID: ownerChatID, - }, nil -} - func main() { - config, err := getConfig() + conf, err := parseEnv() + if err != nil { + log.Fatal(err) + } + + log.Println(conf) + bot, err := tgbotapi.NewBotAPI(conf.TelegramBotToken) if err != nil { log.Panic(err) } - - bot, err := tgbotapi.NewBotAPI(config.Token) - if err != nil { - log.Panic(err) - } - - //bot.Debug = true - log.Printf("Authorized on account %s", bot.Self.UserName) - service := app.NewAnonymousQuestionsService(bot, config.OwnerChatID) + botAPI := infrastructure.NewBotAPI(bot, conf.OwnerChatID) + errorsChan := make(chan error) + service := app.NewAnonymousQuestionsService(botAPI, errorsChan) + go func() { + for err := range errorsChan { + log.Println(err) + } + }() - if err := service.ListenForMessages(); err != nil { - log.Panic(err) + if err = service.ServeMessages(); err != nil { + log.Fatal(err) } } diff --git a/go.mod b/go.mod index 5f7403d..30b6274 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/nightnoryu/anon3anon go 1.22.5 require ( - github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/pkg/errors v0.9.1 ) diff --git a/go.sum b/go.sum index ae23520..60abbdf 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/app/anonymousmessagesservice.go b/pkg/app/anonymousmessagesservice.go index bb69d8a..26400eb 100644 --- a/pkg/app/anonymousmessagesservice.go +++ b/pkg/app/anonymousmessagesservice.go @@ -1,66 +1,72 @@ package app -import ( - "log" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" -) - -func NewAnonymousQuestionsService(bot *tgbotapi.BotAPI, ownerChatID int64) AnonymousMessagesService { +func NewAnonymousQuestionsService(api BotAPI, errorsChan chan error) AnonymousMessagesService { return &anonymousMessagesService{ - bot: bot, - ownerChatID: ownerChatID, + api: api, + errorsChan: errorsChan, } } +const ( + messageSentReply = "*Сообщение отправлено!*" + newMessageNotification = "*Новое анонимное сообщение!*" +) + type AnonymousMessagesService interface { - ListenForMessages() error + ServeMessages() error } type anonymousMessagesService struct { - bot *tgbotapi.BotAPI - ownerChatID int64 + api BotAPI + errorsChan chan error } -func (s *anonymousMessagesService) ListenForMessages() error { - u := tgbotapi.NewUpdate(0) - u.Timeout = 60 - - updates := s.bot.GetUpdatesChan(u) - for update := range updates { - if update.Message == nil { - continue +func (s *anonymousMessagesService) ServeMessages() error { + return s.api.HandleUpdates(func(update MessageUpdate) { + if update.Command != nil { + err := s.handleCommand(update) + if err != nil { + s.errorsChan <- err + } + return } - log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) - - if update.Message.Text == "/start" { - reply := tgbotapi.NewMessage(update.Message.Chat.ID, "Жду твоих вопросов!") - s.bot.Send(reply) - continue + err := s.handleMessage(update.Message) + if err != nil { + s.errorsChan <- err } - if len(update.Message.Photo) > 0 { - var photos []interface{} - photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FileID(update.Message.Photo[0].FileID)) - photo.Caption = "*Новое анонимное сообщение!*" - photo.ParseMode = tgbotapi.ModeMarkdown - photos = append(photos, photo) - - mediaMsg := tgbotapi.NewMediaGroup(s.ownerChatID, photos) - - s.bot.Send(mediaMsg) - } else { - msg := tgbotapi.NewMessage(s.ownerChatID, "*Новое анонимное сообщение!*\n\n"+update.Message.Text) - msg.ParseMode = tgbotapi.ModeMarkdown - - s.bot.Send(msg) + err = s.pingClient(update.FromChatID) + if err != nil { + s.errorsChan <- err } + }) +} - reply := tgbotapi.NewMessage(update.Message.Chat.ID, "Сообщение отправлено!") - - s.bot.Send(reply) +func (s *anonymousMessagesService) handleCommand(update MessageUpdate) error { + if update.Command == nil { + return nil } - return nil + var msgText string + switch *update.Command { + case StartCommand: + msgText = "Жду твоих вопросов!" + case UnknownCommand: + msgText = "Неизвестная команда!" + } + + return s.api.SendMessage(update.FromChatID, Message{Text: msgText}) +} + +func (s *anonymousMessagesService) pingClient(chatID int64) error { + return s.api.SendMessage(chatID, Message{Text: messageSentReply}) +} + +func (s *anonymousMessagesService) handleMessage(message Message) error { + msgText := newMessageNotification + "\n\n" + message.Text + return s.api.SendMessageToOwner(Message{ + Text: msgText, + Image: message.Image, + }) } diff --git a/pkg/app/botapi.go b/pkg/app/botapi.go new file mode 100644 index 0000000..b2b4526 --- /dev/null +++ b/pkg/app/botapi.go @@ -0,0 +1,32 @@ +package app + +type BotAPI interface { + HandleUpdates(handler MessageUpdateHandler) error + SendMessage(chatID int64, message Message) error + SendMessageToOwner(message Message) error +} + +type MessageUpdateHandler func(MessageUpdate) + +type MessageUpdate struct { + Message + UpdateID int + FromChatID int64 + Command *Command +} + +type Message struct { + Text string + Image *Image +} + +type Command int + +const ( + UnknownCommand Command = iota + StartCommand +) + +type Image struct { + FileID string +} diff --git a/pkg/infrastructure/botapi.go b/pkg/infrastructure/botapi.go new file mode 100644 index 0000000..e3cd35f --- /dev/null +++ b/pkg/infrastructure/botapi.go @@ -0,0 +1,147 @@ +package infrastructure + +import ( + "log" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/pkg/errors" + + "github.com/nightnoryu/anon3anon/pkg/app" +) + +const ( + updateTimeoutInSeconds = 60 + startCommand = "/start" + messageParseMode = tgbotapi.ModeMarkdown +) + +type fileInfo struct { + FileID string + Size int +} + +func NewBotAPI(bot *tgbotapi.BotAPI, ownerChatID int64) app.BotAPI { + return &botAPI{ + bot: bot, + ownerChatID: ownerChatID, + } +} + +type botAPI struct { + bot *tgbotapi.BotAPI + ownerChatID int64 +} + +func (api *botAPI) HandleUpdates(handler app.MessageUpdateHandler) error { + u := tgbotapi.NewUpdate(0) + u.Timeout = updateTimeoutInSeconds + + updates := api.bot.GetUpdatesChan(u) + for update := range updates { + if update.Message == nil { + continue + } + + log.Printf("%+v\n", update.Message) + + messageUpdate := app.MessageUpdate{ + Message: api.hydrateMessage(update.Message), + UpdateID: update.UpdateID, + FromChatID: update.FromChat().ID, + Command: api.hydrateCommand(update.Message), + } + + handler(messageUpdate) + } + + return nil +} + +func (api *botAPI) SendMessage(chatID int64, message app.Message) error { + if message.Image != nil { + return api.sendPhotoMessage(chatID, message) + } + + return api.sendTextMessage(chatID, message) +} + +func (api *botAPI) SendMessageToOwner(message app.Message) error { + return api.SendMessage(api.ownerChatID, message) +} + +func (api *botAPI) hydrateMessage(msg *tgbotapi.Message) app.Message { + text := msg.Text + if len(text) == 0 { + text = msg.Caption + } + + return app.Message{ + Text: text, + Image: api.hydrateImage(msg.Photo), + } +} + +func (api *botAPI) sendTextMessage(chatID int64, message app.Message) error { + msg := tgbotapi.NewMessage( + chatID, + message.Text, + ) + msg.ParseMode = messageParseMode + + _, err := api.bot.Send(msg) + return errors.WithStack(err) +} + +func (api *botAPI) sendPhotoMessage(chatID int64, message app.Message) error { + photos := api.preparePhotos(message) + mediaMsg := tgbotapi.NewMediaGroup(chatID, photos) + + _, err := api.bot.Send(mediaMsg) + return errors.WithStack(err) +} + +func (api *botAPI) hydrateCommand(msg *tgbotapi.Message) *app.Command { + if !msg.IsCommand() { + return nil + } + + var cmd app.Command + switch msg.Command() { + case startCommand: + cmd = app.StartCommand + default: + cmd = app.UnknownCommand + } + + return &cmd +} + +func (api *botAPI) hydrateImage(photos []tgbotapi.PhotoSize) *app.Image { + if len(photos) == 0 { + return nil + } + + var originalFileID string + var originalFileSize int + for _, photo := range photos { + if photo.FileSize > originalFileSize { + originalFileID = photo.FileID + originalFileSize = photo.FileSize + } + } + + return &app.Image{ + FileID: originalFileID, + } +} + +func (api *botAPI) preparePhotos(message app.Message) []interface{} { + photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FileID(message.Image.FileID)) + photo.Caption = message.Text + photo.ParseMode = messageParseMode + + var photos []interface{} + photos = append(photos, photo) + + return photos +}