From 1e229af8bc1d18bb58b1ae6cf3daa6a3f80adc92 Mon Sep 17 00:00:00 2001 From: xoti$ Date: Thu, 2 Jan 2025 01:50:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B8,=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=20=D0=B8=20=D0=B4=D1=80=D1=83=D0=B3=D0=BE=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 3 +- go.sum | 10 +- internal/bot/app/app.go | 13 +- internal/bot/handlers/handlers.go | 248 ++++++++++++++++++++++---- internal/bot/middleware/middleware.go | 34 +++- internal/bot/models/buttons.go | 34 +++- internal/bot/models/responses.go | 28 ++- internal/db/db.go | 44 +++-- internal/logger/logger.go | 128 +++++++++++++ internal/server/app/app.go | 6 +- internal/server/handlers/handlers.go | 25 ++- 11 files changed, 508 insertions(+), 65 deletions(-) create mode 100644 internal/logger/logger.go diff --git a/go.mod b/go.mod index ff114e8..be22215 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.5 require ( github.com/gofiber/fiber/v2 v2.52.5 github.com/joho/godotenv v1.5.1 + github.com/rs/zerolog v1.33.0 gopkg.in/telebot.v4 v4.0.0-beta.4 gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.10 @@ -29,6 +30,6 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index aba031f..4ecb807 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,7 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -304,6 +305,7 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= @@ -360,6 +362,9 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -622,8 +627,9 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/bot/app/app.go b/internal/bot/app/app.go index 7f04d62..c105f6f 100644 --- a/internal/bot/app/app.go +++ b/internal/bot/app/app.go @@ -11,6 +11,7 @@ import ( "github.com/xoticdsign/shortybot/internal/bot/middleware" "github.com/xoticdsign/shortybot/internal/bot/models" "github.com/xoticdsign/shortybot/internal/db" + "github.com/xoticdsign/shortybot/internal/logger" ) // Инициализирует бота, возвращает структуру *telebot.Bot или одну из возможных ошибок. @@ -20,8 +21,11 @@ func InitApp() (*telebot.Bot, error) { return nil, err } + logger := logger.InitLogger() + handlers := &handlers.Dependencies{ DB: db, + Logger: logger, Helpers: helpers.Helpers{}, } @@ -37,7 +41,14 @@ func InitApp() (*telebot.Bot, error) { return nil, err } - bot.Use(middleware.GetUserDetails) + bot.Use(middleware.GetSenderDetails) + bot.Use(middleware.AdminValidation) + bot.Use(middleware.SpeedCounter) + + admin := bot.Group() + + admin.Handle(&models.BtnReturnToAdminPanel, handlers.AdminPanel) + admin.Handle(&models.BtnAdminUsersAndShorties, handlers.AdminUsersAndShorties) unsupported := bot.Group() diff --git a/internal/bot/handlers/handlers.go b/internal/bot/handlers/handlers.go index 7b25abc..81593a9 100644 --- a/internal/bot/handlers/handlers.go +++ b/internal/bot/handlers/handlers.go @@ -2,7 +2,9 @@ package handlers import ( "os" + "strconv" "strings" + "time" "gopkg.in/telebot.v4" @@ -11,36 +13,115 @@ import ( "github.com/xoticdsign/shortybot/internal/bot/helpers" "github.com/xoticdsign/shortybot/internal/bot/models" "github.com/xoticdsign/shortybot/internal/db" + "github.com/xoticdsign/shortybot/internal/logger" ) // Структура, хранящая все необходимые хендлерам бота зависимости. type Dependencies struct { DB db.Querier + Logger logger.Loggier Helpers helpers.Helpers } -// Отлавливает ошибки, обрабатывает и направляет соответствующее переменную в контексте в хенделер Menu. +// Отлавливает ошибки и логгирует. func (d *Dependencies) OnError(err error, c telebot.Context) { - c.Set("error", models.MsgOnError+err.Error()) + d.Logger.ErrorBot( + err.Error(), + logger.OriginBot, + ) +} + +// Админский хендлер. Отправляет админскую панель. +func (d *Dependencies) AdminPanel(c telebot.Context) error { + var data string + + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext + } + + if c.Callback() != nil { + data = c.Callback().Data + } + + d.Logger.WarnBot( + logger.WarnAdminAccess, + logger.OriginBot, + logger.FromAdminPanel, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) + + switch { + case data == "send": + if user.FirstName != "" { + return c.Send(models.MsgAdminPanel1+user.FirstName+models.MsgAdminPanel2, models.ReplyAdminPanel) + } + return c.Send(models.MsgAdminPanel, models.ReplyAdminPanel) + + default: + if user.FirstName != "" { + return c.EditOrSend(models.MsgAdminPanel1+user.FirstName+models.MsgAdminPanel2, models.ReplyAdminPanel) + } + return c.EditOrSend(models.MsgAdminPanel, models.ReplyAdminPanel) + } +} - d.Menu(c) +// Админский хендлер. Отправляет количество уникальных пользователей и сокращенных ссылок. +func (d *Dependencies) AdminUsersAndShorties(c telebot.Context) error { + usersCount, shortiesCount, err := d.DB.UsersAndShorties() + if err != nil { + return c.EditOrSend(models.MsgAdminSuccess+"\n"+err.Error(), models.ReplyReturnToAdminPanel) + } + result := []string{ + models.MsgAdminSuccess, + "· Пользователей:\n" + strconv.Itoa(usersCount), + "· Ссылок:\n" + strconv.Itoa(int(shortiesCount)), + } + + resultFmt := strings.Join(result, "\n\n") + + return c.EditOrSend(resultFmt, models.ReplyReturnToAdminPanel) } // Обрабатывает обновления, неподдерживаемые ботом. func (d *Dependencies) Unsupported(c telebot.Context) error { + isAdmin := false + + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext + } + + _, ok = c.Get("admin").(string) + if ok { + isAdmin = true + } + switch { case c.Text() == "/start": return d.Menu(c) + case c.Text() == "/admin" && isAdmin: + return d.AdminPanel(c) + case strings.Contains(c.Text(), "https://") || strings.Contains(c.Text(), "http://"): c.Set("url", c.Text()) return d.New(c) default: - c.Set("msg", models.FailedGlobalUnsupportedCmd+"\n\n") - - return d.Menu(c) + d.Logger.InfoBot( + logger.InfoUpdateFulfilled, + logger.OriginBot, + logger.FromUnsupported, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) + + return c.Send(models.FailedGlobalUnsupportedCmd, models.ReplyReturnToMenuWithSend) } } @@ -48,46 +129,64 @@ func (d *Dependencies) Unsupported(c telebot.Context) error { func (d *Dependencies) New(c telebot.Context) error { publicAdr := os.Getenv("SERVER_PUBLIC_ADR") - user := c.Get("user").(*telebot.User) - url := c.Get("url").(string) + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext + } + + url, ok := c.Get("url").(string) + if !ok { + return telebot.ErrBadContext + } - ok := strings.Contains(url, publicAdr) + ok = strings.Contains(url, publicAdr) if ok { - return c.Send(models.FailedNewCantShortShorty, models.ReplyReturnToMenuWithError) + return c.Send(models.FailedNewCantShortShorty, models.ReplyReturnToMenuWithSend) } ok = d.Helpers.CheckURL(url) if !ok { - return c.Send(models.FailedNewIncorrectURL, models.ReplyReturnToMenuWithError) + return c.Send(models.FailedNewIncorrectURL, models.ReplyReturnToMenuWithSend) } shorty := d.Helpers.ShortyGenerator(7) - err := d.DB.New(user.Username, url, shorty) + err := d.DB.New(user.ID, url, shorty) if err != nil { switch { case err == gorm.ErrDuplicatedKey: - return c.Send(models.FailedNewDuplicate, models.ReplyReturnToMenuWithError) + return c.Send(models.FailedNewDuplicate, models.ReplyReturnToMenuWithSend) case err == gorm.ErrCheckConstraintViolated: - return c.Send(models.FailedNewLimitExceeded, models.ReplyReturnToMenuWithError) + return c.Send(models.FailedNewLimitExceeded, models.ReplyReturnToMenuWithSend) default: return err } } - c.Set("msg", models.SuccessNew+publicAdr+shorty+"\n\n") - return d.Menu(c) + d.Logger.InfoBot( + logger.InfoUpdateFulfilled, + logger.OriginBot, + logger.FromNew, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) + + return c.Send(models.SuccessNew+"\n\n"+publicAdr+shorty, models.ReplyReturnToMenuWithSend) } // Отвечает за работу кнопки "Мои Shorties". func (d *Dependencies) ListShorties(c telebot.Context) error { publicAdr := os.Getenv("SERVER_PUBLIC_ADR") - user := c.Get("user").(*telebot.User) + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext + } - shorties, err := d.DB.ListShorties(user.Username) + shorties, err := d.DB.ListShorties(user.ID) if err != nil { switch { case err == gorm.ErrRecordNotFound: @@ -117,6 +216,15 @@ func (d *Dependencies) ListShorties(c telebot.Context) error { btnReturnToMenu = append(btnReturnToMenu, models.BtnReturnToMenu) btns = append(btns, btnReturnToMenu) + d.Logger.InfoBot( + logger.InfoUpdateFulfilled, + logger.OriginBot, + logger.FromListShorties, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) + return c.EditOrSend(models.MsgListShorties, &telebot.ReplyMarkup{ InlineKeyboard: btns, }) @@ -126,6 +234,11 @@ func (d *Dependencies) ListShorties(c telebot.Context) error { func (d *Dependencies) ShortyInfo(c telebot.Context) error { publicAdr := os.Getenv("SERVER_PUBLIC_ADR") + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext + } + shortyURL := c.Data() shorty, err := d.DB.ShortyInfo(shortyURL) @@ -147,6 +260,15 @@ func (d *Dependencies) ShortyInfo(c telebot.Context) error { shortyInfoFmt := strings.Join(shortyInfo, "\n\n") + d.Logger.InfoBot( + logger.InfoUpdateFulfilled, + logger.OriginBot, + logger.FromShortyInfo, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) + return c.EditOrSend(shortyInfoFmt, models.ReplyReturnToListShorties) } @@ -154,9 +276,12 @@ func (d *Dependencies) ShortyInfo(c telebot.Context) error { func (d *Dependencies) DeleteShorty(c telebot.Context) error { publicAdr := os.Getenv("SERVER_PUBLIC_ADR") - user := c.Get("user").(*telebot.User) + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext + } - shorties, err := d.DB.ListShorties(user.Username) + shorties, err := d.DB.ListShorties(user.ID) if err != nil { switch { case err == gorm.ErrRecordNotFound: @@ -186,6 +311,15 @@ func (d *Dependencies) DeleteShorty(c telebot.Context) error { btnReturnToMenu = append(btnReturnToMenu, models.BtnReturnToMenu) btns = append(btns, btnReturnToMenu) + d.Logger.InfoBot( + logger.InfoUpdateFulfilled, + logger.OriginBot, + logger.FromDeleteShorty, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) + return c.EditOrSend(models.MsgDeleteShorty, &telebot.ReplyMarkup{ InlineKeyboard: btns, }) @@ -193,8 +327,22 @@ func (d *Dependencies) DeleteShorty(c telebot.Context) error { // Отвечает за работу кнопок "Да" и "Нет" при действии промпта на удаление ссылки. func (d *Dependencies) DeleteShortyPrompt(c telebot.Context) error { + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext + } + shortyURL := c.Data() + d.Logger.InfoBot( + logger.InfoUpdateFulfilled, + logger.OriginBot, + logger.FromDeleteShortyPrompt, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) + return c.EditOrSend(models.MsgDeleteShortyPrompt, &telebot.ReplyMarkup{ InlineKeyboard: [][]telebot.InlineButton{ { @@ -214,6 +362,11 @@ func (d *Dependencies) DeleteShortyPrompt(c telebot.Context) error { // Удаляет сокращенную ссылку из БД. func (d *Dependencies) DeleteSelectedShorty(c telebot.Context) error { + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext + } + shortyURL := c.Data() err := d.DB.DeleteShorty(shortyURL) @@ -226,7 +379,16 @@ func (d *Dependencies) DeleteSelectedShorty(c telebot.Context) error { return err } } - c.Set("msg", models.SuccessDelete+"\n\n") + c.Set("msg", models.SuccessDelete) + + d.Logger.InfoBot( + logger.InfoUpdateFulfilled, + logger.OriginBot, + logger.FromDeleteSelectedShorty, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) return d.Menu(c) } @@ -234,23 +396,45 @@ func (d *Dependencies) DeleteSelectedShorty(c telebot.Context) error { // Отвечает за работу главного меню. func (d *Dependencies) Menu(c telebot.Context) error { var data string + var msgText string - if c.Callback() != nil { - data = c.Callback().Data + user, ok := c.Get("user").(*telebot.User) + if !ok { + return telebot.ErrBadContext } - if data == "failed" { - return c.Send(models.MsgMenuGreeting+models.MsgMenuDescrtiption, models.ReplyMenu) + msg, ok := c.Get("msg").(string) + if ok { + msgText = msg } - msgErr, ok := c.Get("error").(string) - if ok { - return c.Send(msgErr+models.MsgMenuDescrtiption, models.ReplyMenu) + if c.Callback() != nil { + data = c.Callback().Data } - msg, ok := c.Get("msg").(string) - if ok { - return c.EditOrSend(msg+models.MsgMenuDescrtiption, models.ReplyMenu) + d.Logger.InfoBot( + logger.InfoUpdateFulfilled, + logger.OriginBot, + logger.FromMenu, + user.ID, + user.Username, + c.Get("start").(time.Time), + ) + + switch { + case data == "send": + if user.FirstName != "" { + return c.Send(models.MsgMenuGreeting1+user.FirstName+models.MsgMenuGreeting2+"\n\n"+models.MsgMenuDescrtiption, models.ReplyMenu) + } + return c.Send(models.MsgMenuGreeting+"\n\n"+models.MsgMenuDescrtiption, models.ReplyMenu) + + case msgText != "": + return c.EditOrSend(msgText+"\n\n"+models.MsgMenuDescrtiption, models.ReplyMenu) + + default: + if user.FirstName != "" { + return c.EditOrSend(models.MsgMenuGreeting1+user.FirstName+models.MsgMenuGreeting2+"\n\n"+models.MsgMenuDescrtiption, models.ReplyMenu) + } + return c.EditOrSend(models.MsgMenuGreeting+"\n\n"+models.MsgMenuDescrtiption, models.ReplyMenu) } - return c.EditOrSend(models.MsgMenuGreeting+models.MsgMenuDescrtiption, models.ReplyMenu) } diff --git a/internal/bot/middleware/middleware.go b/internal/bot/middleware/middleware.go index 24d14e2..3969a1a 100644 --- a/internal/bot/middleware/middleware.go +++ b/internal/bot/middleware/middleware.go @@ -1,19 +1,39 @@ package middleware import ( - "gopkg.in/telebot.v4" + "os" + "strconv" + "strings" + "time" - "github.com/xoticdsign/shortybot/internal/bot/models" + "gopkg.in/telebot.v4" ) -// Перехватывает все необходимые данные пользователя для последующего использования в хендлерах. -func GetUserDetails(next telebot.HandlerFunc) telebot.HandlerFunc { +// Перехватывает все необходимые данные пользователя для последующего использования в хендлерах, а также проверяет, является ли пользователем админом. +func GetSenderDetails(next telebot.HandlerFunc) telebot.HandlerFunc { return func(c telebot.Context) error { - if c.Sender().Username == "" { - return c.Send(models.FailedGlobalUsernameAbsent, models.ReplyReturnToMenuWithError) - } c.Set("user", c.Sender()) return next(c) } } + +func AdminValidation(next telebot.HandlerFunc) telebot.HandlerFunc { + return func(c telebot.Context) error { + admins := os.Getenv("BOT_ADMINS") + if !strings.Contains(admins, strconv.Itoa(int(c.Sender().ID))) { + return next(c) + } + c.Set("admin", "") + + return next(c) + } +} + +func SpeedCounter(next telebot.HandlerFunc) telebot.HandlerFunc { + return func(c telebot.Context) error { + c.Set("start", time.Now()) + + return next(c) + } +} diff --git a/internal/bot/models/buttons.go b/internal/bot/models/buttons.go index 74c95ef..ad74b06 100644 --- a/internal/bot/models/buttons.go +++ b/internal/bot/models/buttons.go @@ -2,6 +2,20 @@ package models import "gopkg.in/telebot.v4" +var ( + // Кнопка "Вернуться к панели". Появляется в действиях из админской панели. + BtnReturnToAdminPanel = telebot.InlineButton{ + Unique: "adminPanel", + Text: "Вернуться к панели", + } + + // Кнопка "Счетчик пользователей и ссылок". Появляется в админской панели. + BtnAdminUsersAndShorties = telebot.InlineButton{ + Unique: "adminUsersAndShorties", + Text: "Счетчик пользователей и ссылок", + } +) + var ( // Кнопка "Мои Shorties". Появляется в главном меню. BtnListShorties = telebot.InlineButton{ @@ -54,6 +68,22 @@ var ( } ) +var ( + // Набор кнопок, отображаемый в админской панели. + ReplyAdminPanel = &telebot.ReplyMarkup{ + InlineKeyboard: [][]telebot.InlineButton{ + {BtnAdminUsersAndShorties}, + }, + } + + // Набор кнопок, отображаемый, как предложение вернуться к админской панели. + ReplyReturnToAdminPanel = &telebot.ReplyMarkup{ + InlineKeyboard: [][]telebot.InlineButton{ + {BtnReturnToAdminPanel}, + }, + } +) + var ( // Набор кнопок, отображаемый, как предложение вернуться к списку сокращенных ссылок. ReplyReturnToListShorties = &telebot.ReplyMarkup{ @@ -80,9 +110,9 @@ var ( } // Набор кнопок, отображаемый, как предложение вернуться в главное меню. Дополнительно имеет при себе данные, которые свидетельствуют о том, что пользователь решил вернуться в главное меню после какого-то неудачного действия. - ReplyReturnToMenuWithError = &telebot.ReplyMarkup{ + ReplyReturnToMenuWithSend = &telebot.ReplyMarkup{ InlineKeyboard: [][]telebot.InlineButton{ - {*BtnReturnToMenu.With("failed")}, + {*BtnReturnToMenu.With("send")}, }, } ) diff --git a/internal/bot/models/responses.go b/internal/bot/models/responses.go index 2ae61d7..f3e77d9 100644 --- a/internal/bot/models/responses.go +++ b/internal/bot/models/responses.go @@ -1,13 +1,28 @@ package models var ( - // Отправляется при ошибке. - MsgOnError = "× Что-то пошло не так... Свяжись и поделись ошибкой с @xoticdsign - моим создателем!\n\nОшибка: " + // Отправляется в админской панели. + MsgAdminPanel = "< Добрый день! >\n\nВас приветствует местный Джарвис. Добро пожаловать в админскую панель!\n\nНиже представлены команды, которыми вы можете воспользоваться.\n\n/start" + + // Первая часть сообщения из админской панели. + MsgAdminPanel1 = "< Добрый день, " + + // Вторая часть сообщения из админской панели. + MsgAdminPanel2 = "! >\n\nВас приветствует местный Джарвис. Добро пожаловать в админскую панель!\n\nНиже представлены команды, которыми вы можете воспользоваться.\n\n/start" + + // Используется во всех командах, перечисленных в админской панели. + MsgAdminSuccess = "Ниже ответ на твой запрос!" ) var ( // Стандартная первая половина сообщения из главного меню. - MsgMenuGreeting = "< Привет, я помогу тебе сократить твои ссылки! >\n\n" + MsgMenuGreeting = "< Привет, я помогу тебе сократить твои ссылки! >" + + // Первая часть приветствия. + MsgMenuGreeting1 = "< Привет, " + + // Вторая часть приветствия. + MsgMenuGreeting2 = "! Я помогу тебе сократить твои ссылки. >" // Вторая половина сообщения из главного меню. MsgMenuDescrtiption = "| Мой создатель: @xoticdsign\n\nОтправь ссылку в чат со мной, чтобы я ее сократил. Также ты можешь воспользоваться одной из команд ниже, чтобы управлять своими Shorties!\n\nПример ссылки:\nhttps://trex-runner.com/night/" @@ -15,7 +30,7 @@ var ( var ( // Отправляется, если удалось создать сокращенную ссылку. Используется, как первая половина сообщения из главного меню. - SuccessNew = "+ Создал для тебя Shorty! Я прикреплю ее ниже.\n\n" + SuccessNew = "+ Создал для тебя Shorty! Я прикреплю ее ниже." // Отправляется, если не удалось создать сокращенную ссылку. Причина: пользователь отправил ссылку, сокращенную ботом. FailedNewCantShortShorty = "× Я не могу сократить Shorty!\n\nПопробуй отправить правильную ссылку или вернись в меню." @@ -51,11 +66,8 @@ var ( var ( // Отправляется, если пользователь использовал неподдерживаемую команду. - FailedGlobalUnsupportedCmd = "× Такой команды я не знаю!" + FailedGlobalUnsupportedCmd = "× Такой команды я не знаю!\n\nВернись в главное меню, там описаны все доступные действия." // Отправляется, если у пользователя нет сокращенных ссылок. FailedGlobalNoShorties = "× У тебя нет доступных Shorties!\n\nПопробуй создать парочку. Для того, чтобы узнать, как это сделать - вернись в главное меню." - - // Отправляется, если пользователь не установил Username в настройках Телеграмма. - FailedGlobalUsernameAbsent = "× У тебя не установлено имя пользователя, я не смогу тебя запомнить!\n\nУстанови имя пользователя в настройках Telegram." ) diff --git a/internal/db/db.go b/internal/db/db.go index b90da83..50dbb48 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -16,16 +16,17 @@ type DB struct { // Интерфейс, содержащий методы для работы с БД. type Querier interface { - New(username string, url string, shorty string) error - ListShorties(username string) ([]Shorties, error) + UsersAndShorties() (int, int, error) + New(userID int64, url string, shorty string) error + ListShorties(userID int64) ([]Shorties, error) ShortyInfo(shortyURL string) (Shorties, error) DeleteShorty(shortyURL string) error } // Модель для работы с БД. type Shorties struct { - ID uint `gorm:"primaryKey;not null;autoIncrement"` - Username string `gorm:"not null"` + ID int64 `gorm:"primaryKey;not null;autoIncrement"` + UserID int64 `gorm:"not null"` OriginalURL string `gorm:"not null"` ShortyURL string `gorm:"not null;unique"` DateCreated time.Time `gorm:"not null"` @@ -57,11 +58,34 @@ func InitDB() (*DB, error) { return &DB{db: db}, nil } +func (d *DB) UsersAndShorties() (int, int, error) { + var usersCount int64 + var shortiesCount int64 + + t := d.db.Table("shorties").Distinct("user_id").Count(&usersCount) + if usersCount == 0 { + return 0, 0, gorm.ErrRecordNotFound + } + if t.Error != nil && t.Error != gorm.ErrRecordNotFound { + return 0, 0, t.Error + } + + t = d.db.Table("shorties").Count(&shortiesCount) + if shortiesCount == 0 { + return 0, 0, gorm.ErrRecordNotFound + } + if t.Error != nil && t.Error != gorm.ErrRecordNotFound { + return 0, 0, t.Error + } + + return int(usersCount), int(shortiesCount), nil +} + // Создает новую запись в БД. Используется в функции handlers.New. -func (d *DB) New(username string, url string, shorty string) error { +func (d *DB) New(userID int64, url string, shorty string) error { var shortiesCount int64 - t := d.db.Table("shorties").Where("username = ?", username).Where("original_url = ?", url).Count(&shortiesCount) + t := d.db.Table("shorties").Where("user_id = ?", userID).Where("original_url = ?", url).Count(&shortiesCount) if shortiesCount != 0 { return gorm.ErrDuplicatedKey } @@ -69,7 +93,7 @@ func (d *DB) New(username string, url string, shorty string) error { return t.Error } - t = d.db.Table("shorties").Where("username = ?", username).Count(&shortiesCount) + t = d.db.Table("shorties").Where("user_id = ?", userID).Count(&shortiesCount) if shortiesCount >= 5 { return gorm.ErrCheckConstraintViolated } @@ -78,7 +102,7 @@ func (d *DB) New(username string, url string, shorty string) error { } t = d.db.Table("shorties").Create(&Shorties{ - Username: username, + UserID: userID, OriginalURL: url, ShortyURL: shorty, DateCreated: time.Now().UTC(), @@ -91,10 +115,10 @@ func (d *DB) New(username string, url string, shorty string) error { } // Возвращает все доступные цитаты по имени пользователя. Используется в функциях handlers.ListShorties и handlers.DeleteShorty. -func (d *DB) ListShorties(username string) ([]Shorties, error) { +func (d *DB) ListShorties(userID int64) ([]Shorties, error) { var shorties []Shorties - t := d.db.Table("shorties").Where("username = ?", username).Find(&shorties) + t := d.db.Table("shorties").Where("user_id = ?", userID).Find(&shorties) if len(shorties) == 0 { return []Shorties{}, gorm.ErrRecordNotFound } diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..93b3cec --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,128 @@ +package logger + +import ( + "os" + "time" + + "github.com/rs/zerolog" +) + +var ( + WarnAdminAccess = "ВХОД В АДМИНКУ" + WarnTooLong = "СЛИШКОМ ДОЛГАЯ ОБРАБОТКА" + InfoUpdateFulfilled = "Обновление обработано" + InfoRequestFulfilled = "Обработан запрос" +) + +var ( + FromAdminPanel = "AdminPanel" + FromUnsupported = "Unsupported" + FromNew = "New" + FromListShorties = "ListShorties" + FromShortyInfo = "ShortyInfo" + FromDeleteShorty = "DeleteShorty" + FromDeleteShortyPrompt = "DeleteShortyPrompt" + FromDeleteSelectedShorty = "DeleteSelectedShorty" + FromMenu = "Menu" +) + +var ( + OriginBot = "Bot" + OriginServer = "Server" +) + +// Структура, хранящая переменную для доступа к логгеру. Также реализует Loggier. +type Logger struct { + logger *zerolog.Logger +} + +// Интерфейс, содержащий методы для работы с логгером. +type Loggier interface { + InfoBot(msg string, origin string, from string, userID int64, username string, start time.Time) + WarnBot(msg string, origin string, from string, userID int64, username string, start time.Time) + ErrorBot(msg string, origin string) + InfoServer(msg string, shortyURL string, originalURL string, start time.Time) + WarnServer(msg string, shortyURL string, originalURL string, start time.Time) + ErrorServer(msg string, code int) +} + +// Инициализирует логгер, возвращает структуру *Logger. +func InitLogger() *Logger { + logger := zerolog.New(&zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: "02.01.2006 | 15:04:05", + TimeLocation: time.UTC, + }).With().Timestamp().Logger() + + return &Logger{logger: &logger} +} + +// Логгирует Info в боте. +func (l *Logger) InfoBot(msg string, origin string, from string, userID int64, username string, start time.Time) { + if username == "" { + username = "отсутствует" + } + + if time.Since(start) >= time.Duration(time.Millisecond*10) { + l.WarnBot(WarnTooLong, origin, from, userID, username, start) + } + + l.logger.Info(). + Str("ORIGIN", origin). + Str("FROM", from). + Int64("USER_ID", userID). + Str("USERNAME", username). + TimeDiff("SPEED", time.Now(), start). + Msg(msg) +} + +// Логгирует Warning в боте. +func (l *Logger) WarnBot(msg string, origin string, from string, userID int64, username string, start time.Time) { + if username == "" { + username = "отсутствует" + } + + l.logger.Warn(). + Str("ORIGIN", origin). + Str("FROM", from). + Int64("USER_ID", userID). + Str("USERNAME", username). + TimeDiff("SPEED", time.Now(), start). + Msg(msg) +} + +// Логгирует Error в боте. +func (l *Logger) ErrorBot(msg string, origin string) { + l.logger.Error(). + Str("ORIGIN", origin). + Msg(msg) +} + +// Логгирует Info на сервере. +func (l *Logger) InfoServer(msg string, shortyURL string, originalURL string, start time.Time) { + if time.Since(start) >= time.Duration(time.Second*2) { + l.WarnServer(WarnTooLong, shortyURL, originalURL, start) + } + + l.logger.Info(). + Str("SHORTY", shortyURL). + Str("REDIRECT", originalURL). + TimeDiff("SPEED", time.Now(), start). + Msg(msg) +} + +// Логгирует Warning на сервере. +func (l *Logger) WarnServer(msg string, shortyURL string, originalURL string, start time.Time) { + l.logger.Warn(). + Str("SHORTY", shortyURL). + Str("REDIRECT", originalURL). + TimeDiff("SPEED", time.Now(), start). + Msg(msg) +} + +// Логгирует Error на сервере. +func (l *Logger) ErrorServer(msg string, code int) { + l.logger.Warn(). + Int("CODE", code). + Msg(msg) +} diff --git a/internal/server/app/app.go b/internal/server/app/app.go index 5a62f1a..710abc1 100644 --- a/internal/server/app/app.go +++ b/internal/server/app/app.go @@ -6,6 +6,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/xoticdsign/shortybot/internal/db" + "github.com/xoticdsign/shortybot/internal/logger" "github.com/xoticdsign/shortybot/internal/server/handlers" ) @@ -16,8 +17,11 @@ func InitApp() (*fiber.App, error) { return nil, err } + logger := logger.InitLogger() + handlers := &handlers.Dependencies{ - DB: db, + DB: db, + Logger: logger, } app := fiber.New(fiber.Config{ diff --git a/internal/server/handlers/handlers.go b/internal/server/handlers/handlers.go index 55ae935..b7be952 100644 --- a/internal/server/handlers/handlers.go +++ b/internal/server/handlers/handlers.go @@ -2,18 +2,22 @@ package handlers import ( "errors" + "os" + "time" "gorm.io/gorm" "github.com/gofiber/fiber/v2" "github.com/xoticdsign/shortybot/internal/db" + "github.com/xoticdsign/shortybot/internal/logger" "github.com/xoticdsign/shortybot/internal/server/models" ) // Структура, хранящая все необходимые хендлерам сервера зависимости. type Dependencies struct { - DB db.Querier + DB db.Querier + Logger logger.Loggier } // Отлавливает ошибки и обрабатывает. @@ -21,8 +25,18 @@ func (d *Dependencies) OnError(c *fiber.Ctx, err error) error { var e *fiber.Error if errors.As(err, &e) { + d.Logger.ErrorServer( + e.Error(), + e.Code, + ) + return c.JSON(e) } + d.Logger.ErrorServer( + err.Error(), + 0, + ) + return c.JSON(models.Error{ Code: 0, Message: err.Error(), @@ -31,6 +45,8 @@ func (d *Dependencies) OnError(c *fiber.Ctx, err error) error { // Достает оригинальную ссылку из БД и редиректит запросы. func (d *Dependencies) Redirect(c *fiber.Ctx) error { + start := time.Now() + shortyURL := c.Params("shortyURL") shorty, err := d.DB.ShortyInfo(shortyURL) @@ -43,5 +59,12 @@ func (d *Dependencies) Redirect(c *fiber.Ctx) error { return err } } + d.Logger.InfoServer( + logger.InfoRequestFulfilled, + os.Getenv("SERVER_PUBLIC_ADR")+shorty.ShortyURL, + shorty.OriginalURL, + start, + ) + return c.Redirect(shorty.OriginalURL, fiber.StatusSeeOther) }