diff --git a/README.md b/README.md index d52cfc2..71f291a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The last example is the removal of the third participant ## Run manually - sudo -u tbot env $(sudo cat /var/lib/tbot/environment | xargs) tbot server + sudo -u tbot env $(sudo cat /var/lib/tbot/environment | xargs) tbot run ## Run as service Create config file: @@ -51,7 +51,7 @@ tbotd.service file: User = tbot Group = tbot EnvironmentFile = -/var/lib/tbot/environment - ExecStart = /usr/bin/tbot server + ExecStart = /usr/bin/tbot run PIDFile = /var/run/tbotd.pid Restart = always RestartSec = 60 diff --git a/app.go b/app.go index b7caafe..8ee7bdc 100644 --- a/app.go +++ b/app.go @@ -3,8 +3,8 @@ package main import ( "fmt" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" - srv "github.com/taras-by/tbot/server" "github.com/taras-by/tbot/store" + tlg "github.com/taras-by/tbot/telegram" "log" "runtime" ) @@ -19,7 +19,6 @@ type app struct { commit string date string version string - bot *tgbotapi.BotAPI storage *store.Storage } @@ -31,15 +30,10 @@ func newApp() (a *app) { date: Date, version: Version, } + a.printVersion() var err error - a.bot, err = tgbotapi.NewBotAPI(a.options.TelegramToken) - if err != nil { - log.Printf("Telegram connection Error. Token: %s", a.options.TelegramToken) - log.Panic(err.Error()) - } - a.storage, err = store.NewStorage(a.options.StorePath) if err != nil { log.Printf("storage creating error. Path: %s", a.options.StorePath) @@ -49,12 +43,26 @@ func newApp() (a *app) { return a } -func (a *app) newServer() (s *srv.Server) { - return &srv.Server{ - Bot: a.bot, +func (a *app) makeBotService() (s *tlg.BotService) { + + bot, err := tgbotapi.NewBotAPI(a.options.TelegramToken) + if err != nil { + log.Printf("Telegram connection Error. Token: %s", a.options.TelegramToken) + log.Panic(err.Error()) + } + log.Printf("Authorized on account %s", bot.Self.UserName) + + handler := &tlg.MessageHandler{ + Bot: bot, Storage: a.storage, Version: a.version, } + handler.Init() + + return &tlg.BotService{ + Bot: bot, + Handler: handler, + } } func (a app) printVersion() { diff --git a/server/server.go b/server/server.go deleted file mode 100644 index ca840b4..0000000 --- a/server/server.go +++ /dev/null @@ -1,360 +0,0 @@ -package server - -import ( - "fmt" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" - "github.com/taras-by/tbot/store" - "log" - "regexp" - "strconv" - "strings" - "time" -) - -const ( - maxLengthStringArgument = 50 -) - -type route struct { - botCommand string - argExpression string - command func(c conversation) -} - -type Server struct { - Bot *tgbotapi.BotAPI - Storage *store.Storage - Version string -} - -type conversation struct { - chatId int64 - args string - checker *regexp.Regexp - message *tgbotapi.Message -} - -func (s *Server) routes() []route { - return []route{ - {`add`, ``, s.addMe}, - {`add`, `^@(\S+)$`, s.addByLink}, - {`add`, `^\d+$`, s.addByNumber}, - {`add`, `^.+$`, s.addByName}, - {`rm`, ``, s.removeMe}, - {`rm`, `^@(\S+)$`, s.removeByLink}, - {`rm`, `^\d+$`, s.removeByNumber}, - {`rm`, `^.+$`, s.removeByName}, - {`list`, ``, s.list}, - {`ping`, ``, s.ping}, - {`reset`, ``, s.reset}, - {`start`, ``, s.help}, - {`help`, ``, s.help}, - } -} - -func (s *Server) Run() error { - - defer s.Storage.Close() - - log.Printf("Authorized on account %s", s.Bot.Self.UserName) - routes := s.routes() - updates, err := s.chatUpdates() - if err != nil { - return err - } - - for update := range updates { - if update.Message == nil { // ignore any non-Message Updates - continue - } - - log.Printf("Message: [%s] %s", update.Message.From.UserName, update.Message.Text) - - if update.Message.IsCommand() == false { // ignore any non-command Updates - continue - } - - args := strings.TrimSpace(update.Message.CommandArguments()) - cmd := update.Message.Command() - chatId := update.Message.Chat.ID - - if len([]rune(args)) > maxLengthStringArgument { - s.sendMessageToChat(chatId, store.Escape("Parameter too long")) - continue - } - - commandIsOk := false - for _, route := range routes { - - checker := regexp.MustCompile(route.argExpression) - argsIsMatched := false - - if route.argExpression != "" && checker.MatchString(args) { - argsIsMatched = true - } - - if route.argExpression == "" && args == "" { - argsIsMatched = true - } - - if route.botCommand == cmd && argsIsMatched { - commandIsOk = true - log.Printf("command: \"%v\", arguments: \"%v\"", cmd, args) - - c := conversation{ - chatId: chatId, - args: args, - checker: checker, - message: update.Message, - } - - route.command(c) - break - } - } - if commandIsOk == false { - s.sendMessageToChat(chatId, store.Escape("Wrong command")) - } - } - return nil -} - -func (s *Server) chatUpdates() (tgbotapi.UpdatesChannel, error) { - u := tgbotapi.NewUpdate(0) - u.Timeout = 60 - - updates, err := s.Bot.GetUpdatesChan(u) - return updates, err -} - -func (s *Server) list(c conversation) { - text := s.participantsText(c.chatId) - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) addMe(c conversation) { - - creationTime := time.Now() - existingParticipant, err := s.Storage.FindByLink("@"+c.message.From.UserName, c.chatId) - if err == nil { - if existingParticipant.IsUnresolved() == true { - creationTime = existingParticipant.Time - s.Storage.Delete(existingParticipant) - //sendMessageToChat(chatId, "Update unresolved") - } else { - s.sendMessageToChat(c.chatId, "You are already a participant") - return - } - } - - participant := s.Storage.Create( - store.Participant{ - User: store.User{ - Id: strconv.Itoa(c.message.From.ID), - UserName: c.message.From.UserName, - FirstName: c.message.From.FirstName, - LastName: c.message.From.LastName, - Type: store.UserTelegram, - }, - Time: creationTime, - ChatId: c.chatId, - }, - ) - - s.sendMessageToChat(c.chatId, fmt.Sprintf("Added %s", store.Escape(participant.Link()))) - text := s.participantsText(c.chatId) - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) addByLink(c conversation) { - - match := c.checker.FindStringSubmatch(c.args) - if len(match) != 2 { - s.sendMessageToChat(c.chatId, "Error") - return - } - - userName := match[1] - existingParticipant, err := s.Storage.FindByLink("@"+userName, c.chatId) - if err == nil && existingParticipant.Id() != "" { - s.sendMessageToChat(c.chatId, "User is already in the list of participants") - return - } - - participant := s.Storage.Create( - store.Participant{ - User: store.User{ - UserName: userName, - Type: store.UserUnresolved, - }, - Time: time.Now(), - ChatId: c.chatId, - }, - ) - - s.sendMessageToChat(c.chatId, fmt.Sprintf("Added %s", store.Escape(participant.Link()))) - text := s.participantsText(c.chatId) - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) addByName(c conversation) { - - participant := s.Storage.Create( - store.Participant{ - User: store.User{ - UserName: c.args, - Type: store.UserGuest, - }, - Time: time.Now(), - ChatId: c.chatId, - }, - ) - - s.sendMessageToChat(c.chatId, fmt.Sprintf("Added %s", store.Escape(participant.Link()))) - text := s.participantsText(c.chatId) - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) addByNumber(c conversation) { - s.sendMessageToChat(c.chatId, "Fail. UserName as an number") -} - -func (s *Server) removeMe(c conversation) { - - var participant store.Participant - var err error - - participant, err = s.Storage.Find(strconv.Itoa(c.message.From.ID), c.chatId) - if err != nil { - participant, err = s.Storage.FindByLink("@"+c.message.From.UserName, c.chatId) - if err != nil { - s.sendMessageToChat(c.chatId, "You are not a participant yet") - return - } - } - - s.Storage.Delete(participant) - s.sendMessageToChat(c.chatId, fmt.Sprintf("Removed %s", store.Escape(participant.Link()))) - - text := s.participantsText(c.chatId) - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) removeByNumber(c conversation) { - - var participant store.Participant - var err error - - numberString := string(c.checker.Find([]byte(c.args))) - number, err := strconv.Atoi(numberString) - if err != nil { - s.sendMessageToChat(c.chatId, "Wrong parameter") - return - } - - participant, err = s.Storage.FindByNumber(number, c.chatId) - if err != nil { - s.sendMessageToChat(c.chatId, store.Escape(err.Error())) - return - } - - s.Storage.Delete(participant) - s.sendMessageToChat(c.chatId, fmt.Sprintf("Removed %s", store.Escape(participant.Link()))) - - text := s.participantsText(c.chatId) - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) removeByLink(c conversation) { - - var participant store.Participant - var err error - - linkString := string(c.checker.Find([]byte(c.args))) - participant, err = s.Storage.FindByLink(linkString, c.chatId) - if err != nil { - s.sendMessageToChat(c.chatId, store.Escape(err.Error())) - return - } - - s.Storage.Delete(participant) - s.sendMessageToChat(c.chatId, fmt.Sprintf("Removed %s", store.Escape(participant.Link()))) - - text := s.participantsText(c.chatId) - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) removeByName(c conversation) { - - var participant store.Participant - var err error - - participant, err = s.Storage.FindByName(c.args, c.chatId) - if err != nil { - s.sendMessageToChat(c.chatId, store.Escape(err.Error())) - return - } - - s.Storage.Delete(participant) - s.sendMessageToChat(c.chatId, fmt.Sprintf("Removed %s", store.Escape(participant.Link()))) - - text := s.participantsText(c.chatId) - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) reset(c conversation) { - err := s.Storage.DeleteAll(c.chatId) - if err != nil { - s.sendMessageToChat(c.chatId, store.Escape(err.Error())) - return - } - - s.sendMessageToChat(c.chatId, "All participants was deleted") -} - -func (s *Server) help(c conversation) { - text := "*Help:*\n" + - "/list - participants list\n" + - "/add - add yourself or someone\n" + - "/rm - remove yourself or someone\n" + - "/reset - remove all\n" + - //"/ping - turn to non-participants\n" + - "/help - help\n" + - "\n" + - "*Examples:*\n" + - "``` /add @smith\n" + - " /add My brother John\n" + - " /rm @smith\n" + - " /rm My brother John\n" + - " /rm 3\n" + - "```\n" + - "The last example is the removal of the third participant\n\n" + - "_Version: " + s.Version + "_" - s.sendMessageToChat(c.chatId, text) -} - -func (s *Server) ping(c conversation) { - s.sendMessageToChat(c.chatId, "Turn to non-participants... *Not implemented*.\nWelcome to https://github.com/taras-by/tbot") -} - -func (s *Server) participantsText(chatId int64) (text string) { - participants := s.Storage.FindByChatId(chatId) - if len(participants) == 0 { - return "No participants" - } - text = "List of participants:\n" - for i, p := range participants { - text = text + fmt.Sprintf(" *%v)* %v\n", i+1, store.Escape(p.Name())) - } - return text -} - -func (s *Server) sendMessageToChat(chatId int64, text string) { - msg := tgbotapi.NewMessage(chatId, text) - msg.ParseMode = "markdown" - _, err := s.Bot.Send(msg) - if err != nil { - log.Print(err) - log.Print(text) - } -} diff --git a/tbot.go b/tbot.go index 65ef72e..26bf0be 100644 --- a/tbot.go +++ b/tbot.go @@ -25,8 +25,8 @@ const ( func main() { commands := map[string]command{ - "server": serverCmd(), - "show": showCmd(), + "run": runCmd(), + "show": showCmd(), } fs := flag.NewFlagSet("tbot", flag.ExitOnError) @@ -63,11 +63,11 @@ func showCmd() command { }} } -func serverCmd() command { +func runCmd() command { return command{fn: func([]string) error { a := newApp() - a.printVersion() - s := a.newServer() + defer a.storage.Close() + s := a.makeBotService() return s.Run() }} } diff --git a/telegram/handler.go b/telegram/handler.go new file mode 100644 index 0000000..025b656 --- /dev/null +++ b/telegram/handler.go @@ -0,0 +1,341 @@ +package telegram + +import ( + "fmt" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" + "github.com/taras-by/tbot/store" + "log" + "regexp" + "strconv" + "strings" + "time" +) + +const ( + maxLengthStringArgument = 50 +) + +type MessageHandler struct { + Bot *tgbotapi.BotAPI + Storage *store.Storage + routes []route + Version string +} + +type route struct { + botCommand string + argExpression string + command func(c conversation) +} + +type conversation struct { + chatId int64 + args string + checker *regexp.Regexp + message *tgbotapi.Message +} + +func (h *MessageHandler) Init() { + h.routes = []route{ + {`add`, ``, h.addMe}, + {`add`, `^@(\S+)$`, h.addByLink}, + {`add`, `^\d+$`, h.addByNumber}, + {`add`, `^.+$`, h.addByName}, + {`rm`, ``, h.removeMe}, + {`rm`, `^@(\S+)$`, h.removeByLink}, + {`rm`, `^\d+$`, h.removeByNumber}, + {`rm`, `^.+$`, h.removeByName}, + {`list`, ``, h.list}, + {`ping`, ``, h.ping}, + {`reset`, ``, h.reset}, + {`start`, ``, h.help}, + {`help`, ``, h.help}, + } +} + +func (h *MessageHandler) handle(message *tgbotapi.Message) { + if message == nil { // ignore any non-Message Updates + return + } + + log.Printf("Message: [%s] %s", message.From.UserName, message.Text) + + if message.IsCommand() == false { // ignore any non-command Updates + return + } + + args := strings.TrimSpace(message.CommandArguments()) + cmd := message.Command() + chatId := message.Chat.ID + + if len([]rune(args)) > maxLengthStringArgument { + h.sendMessageToChat(chatId, store.Escape("Parameter too long")) + return + } + + commandIsOk := false + for _, route := range h.routes { + + checker := regexp.MustCompile(route.argExpression) + argsIsMatched := false + + if route.argExpression != "" && checker.MatchString(args) { + argsIsMatched = true + } + + if route.argExpression == "" && args == "" { + argsIsMatched = true + } + + if route.botCommand == cmd && argsIsMatched { + commandIsOk = true + log.Printf("command: \"%v\", arguments: \"%v\"", cmd, args) + + c := conversation{ + chatId: chatId, + args: args, + checker: checker, + message: message, + } + + route.command(c) + break + } + } + if commandIsOk == false { + h.sendMessageToChat(chatId, store.Escape("Wrong command")) + return + } +} + +func (h *MessageHandler) list(c conversation) { + text := h.participantsText(c.chatId) + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) addMe(c conversation) { + + creationTime := time.Now() + existingParticipant, err := h.Storage.FindByLink("@"+c.message.From.UserName, c.chatId) + if err == nil { + if existingParticipant.IsUnresolved() == true { + creationTime = existingParticipant.Time + h.Storage.Delete(existingParticipant) + //sendMessageToChat(chatId, "Update unresolved") + } else { + h.sendMessageToChat(c.chatId, "You are already a participant") + return + } + } + + participant := h.Storage.Create( + store.Participant{ + User: store.User{ + Id: strconv.Itoa(c.message.From.ID), + UserName: c.message.From.UserName, + FirstName: c.message.From.FirstName, + LastName: c.message.From.LastName, + Type: store.UserTelegram, + }, + Time: creationTime, + ChatId: c.chatId, + }, + ) + + h.sendMessageToChat(c.chatId, fmt.Sprintf("Added %s", store.Escape(participant.Link()))) + text := h.participantsText(c.chatId) + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) addByLink(c conversation) { + + match := c.checker.FindStringSubmatch(c.args) + if len(match) != 2 { + h.sendMessageToChat(c.chatId, "Error") + return + } + + userName := match[1] + existingParticipant, err := h.Storage.FindByLink("@"+userName, c.chatId) + if err == nil && existingParticipant.Id() != "" { + h.sendMessageToChat(c.chatId, "User is already in the list of participants") + return + } + + participant := h.Storage.Create( + store.Participant{ + User: store.User{ + UserName: userName, + Type: store.UserUnresolved, + }, + Time: time.Now(), + ChatId: c.chatId, + }, + ) + + h.sendMessageToChat(c.chatId, fmt.Sprintf("Added %s", store.Escape(participant.Link()))) + text := h.participantsText(c.chatId) + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) addByName(c conversation) { + + participant := h.Storage.Create( + store.Participant{ + User: store.User{ + UserName: c.args, + Type: store.UserGuest, + }, + Time: time.Now(), + ChatId: c.chatId, + }, + ) + + h.sendMessageToChat(c.chatId, fmt.Sprintf("Added %s", store.Escape(participant.Link()))) + text := h.participantsText(c.chatId) + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) addByNumber(c conversation) { + h.sendMessageToChat(c.chatId, "Fail. UserName as an number") +} + +func (h *MessageHandler) removeMe(c conversation) { + + var participant store.Participant + var err error + + participant, err = h.Storage.Find(strconv.Itoa(c.message.From.ID), c.chatId) + if err != nil { + participant, err = h.Storage.FindByLink("@"+c.message.From.UserName, c.chatId) + if err != nil { + h.sendMessageToChat(c.chatId, "You are not a participant yet") + return + } + } + + h.Storage.Delete(participant) + h.sendMessageToChat(c.chatId, fmt.Sprintf("Removed %s", store.Escape(participant.Link()))) + + text := h.participantsText(c.chatId) + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) removeByNumber(c conversation) { + + var participant store.Participant + var err error + + numberString := string(c.checker.Find([]byte(c.args))) + number, err := strconv.Atoi(numberString) + if err != nil { + h.sendMessageToChat(c.chatId, "Wrong parameter") + return + } + + participant, err = h.Storage.FindByNumber(number, c.chatId) + if err != nil { + h.sendMessageToChat(c.chatId, store.Escape(err.Error())) + return + } + + h.Storage.Delete(participant) + h.sendMessageToChat(c.chatId, fmt.Sprintf("Removed %s", store.Escape(participant.Link()))) + + text := h.participantsText(c.chatId) + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) removeByLink(c conversation) { + + var participant store.Participant + var err error + + linkString := string(c.checker.Find([]byte(c.args))) + participant, err = h.Storage.FindByLink(linkString, c.chatId) + if err != nil { + h.sendMessageToChat(c.chatId, store.Escape(err.Error())) + return + } + + h.Storage.Delete(participant) + h.sendMessageToChat(c.chatId, fmt.Sprintf("Removed %s", store.Escape(participant.Link()))) + + text := h.participantsText(c.chatId) + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) removeByName(c conversation) { + + var participant store.Participant + var err error + + participant, err = h.Storage.FindByName(c.args, c.chatId) + if err != nil { + h.sendMessageToChat(c.chatId, store.Escape(err.Error())) + return + } + + h.Storage.Delete(participant) + h.sendMessageToChat(c.chatId, fmt.Sprintf("Removed %s", store.Escape(participant.Link()))) + + text := h.participantsText(c.chatId) + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) reset(c conversation) { + err := h.Storage.DeleteAll(c.chatId) + if err != nil { + h.sendMessageToChat(c.chatId, store.Escape(err.Error())) + return + } + + h.sendMessageToChat(c.chatId, "All participants was deleted") +} + +func (h *MessageHandler) help(c conversation) { + text := "*Help:*\n" + + "/list - participants list\n" + + "/add - add yourself or someone\n" + + "/rm - remove yourself or someone\n" + + "/reset - remove all\n" + + //"/ping - turn to non-participants\n" + + "/help - help\n" + + "\n" + + "*Examples:*\n" + + "``` /add @smith\n" + + " /add My brother John\n" + + " /rm @smith\n" + + " /rm My brother John\n" + + " /rm 3\n" + + "```\n" + + "The last example is the removal of the third participant\n\n" + + "_Version: " + h.Version + "_" + h.sendMessageToChat(c.chatId, text) +} + +func (h *MessageHandler) ping(c conversation) { + h.sendMessageToChat(c.chatId, "Turn to non-participants... *Not implemented*.\nWelcome to https://github.com/taras-by/tbot") +} + +func (h *MessageHandler) participantsText(chatId int64) (text string) { + participants := h.Storage.FindByChatId(chatId) + if len(participants) == 0 { + return "No participants" + } + text = "List of participants:\n" + for i, p := range participants { + text = text + fmt.Sprintf(" *%v)* %v\n", i+1, store.Escape(p.Name())) + } + return text +} + +func (h *MessageHandler) sendMessageToChat(chatId int64, text string) { + msg := tgbotapi.NewMessage(chatId, text) + msg.ParseMode = "markdown" + _, err := h.Bot.Send(msg) + if err != nil { + log.Print(err) + log.Print(text) + } +} diff --git a/telegram/telegram.go b/telegram/telegram.go new file mode 100644 index 0000000..3d04192 --- /dev/null +++ b/telegram/telegram.go @@ -0,0 +1,31 @@ +package telegram + +import ( + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" +) + +type BotService struct { + Bot *tgbotapi.BotAPI + Handler *MessageHandler +} + +func (s *BotService) Run() error { + + updates, err := s.chatUpdates() + if err != nil { + return err + } + + for update := range updates { + s.Handler.handle(update.Message) + } + return nil +} + +func (s *BotService) chatUpdates() (tgbotapi.UpdatesChannel, error) { + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates, err := s.Bot.GetUpdatesChan(u) + return updates, err +}