diff --git a/common/commonconfig/InitConfig.go b/common/commonconfig/InitConfig.go index bc8ac2e..4bb11fe 100644 --- a/common/commonconfig/InitConfig.go +++ b/common/commonconfig/InitConfig.go @@ -35,6 +35,16 @@ func NewServerConfig() *ServerConfig { return conf } +func NewLoggingConfig() *LoggingConfig { + conf := new(LoggingConfig) + viper.UnmarshalKey("log", conf) + return conf +} + type ServerConfig struct { ListenAddress string } + +type LoggingConfig struct { + Level string +} diff --git a/common/commonconfig/initLogs.go b/common/commonconfig/initLogs.go new file mode 100644 index 0000000..ba906e6 --- /dev/null +++ b/common/commonconfig/initLogs.go @@ -0,0 +1,25 @@ +package commonconfig + +import ( + "log/slog" + "os" +) + +func InitLogs() { + conf := NewLoggingConfig() + + handler := slog.NewTextHandler(os.Stdout, nil) + logger := slog.New(handler) + switch conf.Level { + case "debug": + slog.SetLogLoggerLevel(slog.LevelDebug) + case "info": + slog.SetLogLoggerLevel(slog.LevelInfo) + case "error": + slog.SetLogLoggerLevel(slog.LevelError) + default: + slog.SetLogLoggerLevel(slog.LevelDebug) + } + + slog.SetDefault(logger) +} diff --git a/common/commonconst/consts.go b/common/commonconst/consts.go index d2c22b0..16e4aff 100644 --- a/common/commonconst/consts.go +++ b/common/commonconst/consts.go @@ -4,3 +4,4 @@ const ERR_LOG = "error" const ACCOUNTID_LOG = "accountId" const QUERY_LOG = "query" const DATA_LOG = "data" +const OFFSET_LOG = "offset" diff --git a/ocr-service/config.yaml b/ocr-service/config.yaml index 1dc206a..c943af5 100644 --- a/ocr-service/config.yaml +++ b/ocr-service/config.yaml @@ -1,3 +1,7 @@ +log: + Level: info + + server: ListenAddress: :7002 diff --git a/ocr-service/src/main.go b/ocr-service/src/main.go index 786f4e1..fffdaff 100644 --- a/ocr-service/src/main.go +++ b/ocr-service/src/main.go @@ -14,6 +14,7 @@ import ( func main() { commonconfig.InitConfig() + commonconfig.InitLogs() fx.New( fx.Provide(commonconfig.NewServerConfig), diff --git a/storage-service/config.yaml b/storage-service/config.yaml index 1411d50..8eab0b6 100644 --- a/storage-service/config.yaml +++ b/storage-service/config.yaml @@ -1,3 +1,6 @@ +log: + Level: info + server: ListenAddress: :7001 diff --git a/storage-service/src/main.go b/storage-service/src/main.go index efda25e..6abd813 100644 --- a/storage-service/src/main.go +++ b/storage-service/src/main.go @@ -14,6 +14,7 @@ import ( func main() { commonconfig.InitConfig() + commonconfig.InitLogs() fx.New( fx.Provide(NewValidator), diff --git a/storage-service/src/service/ApiHandlerService.go b/storage-service/src/service/ApiHandlerService.go index 6f5c03d..4355d92 100644 --- a/storage-service/src/service/ApiHandlerService.go +++ b/storage-service/src/service/ApiHandlerService.go @@ -3,14 +3,17 @@ package service import ( "context" "errors" - "log" + "fmt" + "log/slog" "strings" "time" + "github.com/gdexlab/go-render/render" "github.com/go-playground/validator/v10" "github.com/google/uuid" "github.com/labstack/echo/v4" "mine.local/ocr-gallery/apispec/meme-storage/server" + "mine.local/ocr-gallery/common/commonconst" "mine.local/ocr-gallery/storage-service/entity" "mine.local/ocr-gallery/storage-service/helper" ) @@ -29,6 +32,9 @@ func (a *ApiHandler) CreateMeme( ) (server.CreateMemeResponseObject, error) { image := request.Body + slog.Info("CreateMeme", + commonconst.ACCOUNTID_LOG, request.AccountId) + idUuid, _ := uuid.NewRandom() if len(*image.ImageBase64) == 0 { return nil, errors.New("image is empty") @@ -37,7 +43,7 @@ func (a *ApiHandler) CreateMeme( hash := helper.CalcHash(request.Body.ImageBase64) hashDuplicate, err := a.findHashDuplicates(ctx, hash) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to find hash duplicates : %w", err) } if hashDuplicate != nil { @@ -47,9 +53,14 @@ func (a *ApiHandler) CreateMeme( reqImage := helper.ImageToEntity2(request.Body) ocrResult, err := a.ocr.DoOcr(ctx, idUuid, reqImage) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to do ocr : %w", err) } + slog.Info("CreateMeme: ocr result", + commonconst.ACCOUNTID_LOG, request.AccountId, + "id", idUuid, + "ocrText", ocrResult.OcrText) + contentDuplicate, err := a.findContentDuplicates(ctx, ocrResult) if err != nil { return nil, err @@ -65,7 +76,7 @@ func (a *ApiHandler) CreateMeme( err = a.imageStorage.Save(ctx, idUuid, ocrResult.Image, ocrResult.Thumbnail.Image) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to save image metadata : %w", err) } elasticMetaData := OcrResultToElastic( @@ -97,7 +108,7 @@ func (a *ApiHandler) CreateMeme( func (a *ApiHandler) SearchMeme(ctx context.Context, request server.SearchMemeRequestObject) (server.SearchMemeResponseObject, error) { query := request.Params.MemeQuery - log.Printf("SearchMeme: query=%s", query) + slog.Info("SearchMeme", "query", query) matchedMetadata, err := a.metaStorage.Search( ctx, @@ -106,21 +117,31 @@ func (a *ApiHandler) SearchMeme(ctx context.Context, request server.SearchMemeRe request.Params.SearchAfterSortId, request.Params.PageSize, ) - if err != nil { - return nil, err + return nil, fmt.Errorf("failed to search memes : %w", err) } + slog.Info("SearchMeme results", + commonconst.ACCOUNTID_LOG, request.AccountId, + "query", query, + "resultListSize", len(matchedMetadata)) + response := make(server.SearchMeme200JSONResponse, len(matchedMetadata)) for index, metadataItem := range matchedMetadata { + slog.Debug("Search meme result item", + commonconst.ACCOUNTID_LOG, metadataItem.Metadata.AccountId, + "id", metadataItem.Metadata.ImageId, + "s3id", metadataItem.Metadata.S3Id, + "index", index, + "matched", render.Render(metadataItem.ResultMatched)) imageThumbUrl, err := a.imageStorage.GetUrlThumb(ctx, metadataItem.Metadata.S3Id) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to obtain thumb url for image id=%s : %w", metadataItem.Metadata.ImageId, err) } imageUrl, err := a.imageStorage.GetUrl(ctx, metadataItem.Metadata.S3Id) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to obtain image url for image id=%s : %w", metadataItem.Metadata.ImageId, err) } dto := server.SearchMemeDto{} @@ -143,7 +164,8 @@ func (a *ApiHandler) CheckDuplicates(ctx context.Context, request server.CheckDu ctx, request.AccountId, func(ctx2 context.Context, emc *entity.ElasticMatchedContent) error { - return a.internalCheckDuplicate(ctx2, emc.Metadata) + a.internalCheckDuplicate(ctx2, emc.Metadata) + return nil }) } @@ -155,7 +177,8 @@ func (a *ApiHandler) UpdateOcr(ctx context.Context, request server.UpdateOcrRequ ctx, request.AccountId, func(ctx context.Context, emc *entity.ElasticMatchedContent) error { - return a.internalUpdateOcr(ctx, emc.Metadata) + a.internalUpdateOcr(ctx, emc.Metadata) + return nil }) } @@ -212,24 +235,29 @@ func (a *ApiHandler) UpdateOcrOne(ctx context.Context, request server.UpdateOcrO return nil, echo.ErrNotFound } - err = a.internalUpdateOcr(ctx, memeMetadata) - return server.UpdateOcrOne200Response{}, err + a.internalUpdateOcr(ctx, memeMetadata) + return server.UpdateOcrOne200Response{}, nil } -func (a *ApiHandler) internalCheckDuplicate(ctx context.Context, emc *entity.ElasticImageMetaData) error { +func (a *ApiHandler) internalCheckDuplicate(ctx context.Context, emc *entity.ElasticImageMetaData) { id := emc.ImageId embedding := emc.EmbeddingV1 - log.Printf("Check-duplicate: imageId: %s", id.String()) + slog.Info("Check-duplicate", + "id", id.String()) if embedding == nil { - log.Printf("Check-duplicate: NO EMBEDDING: imageId: %s", id.String()) - return nil + slog.Info("Check-duplicate: NO EMBEDDING:", + "id", id.String()) + return } embeddingFoundImage, err := a.metaStorage.GetByEmbeddingV1(ctx, embedding, 10) if err != nil { - log.Printf("Check-duplicate: failed to search image embedding duplicates : id=%s, err=%v", id, err) - return nil + slog.Error("Check-duplicate: failed to search image embedding duplicates ", + "id", id.String(), + commonconst.ERR_LOG, err) + + return } for i, item := range embeddingFoundImage { @@ -239,10 +267,9 @@ func (a *ApiHandler) internalCheckDuplicate(ctx context.Context, emc *entity.Ela a.metaStorage.Delete(ctx, item.ImageId) } - return nil } -func (a *ApiHandler) internalUpdateOcr(ctx context.Context, emc *entity.ElasticImageMetaData) error { +func (a *ApiHandler) internalUpdateOcr(ctx context.Context, emc *entity.ElasticImageMetaData) { id := emc.ImageId accountId := emc.AccountId @@ -250,27 +277,34 @@ func (a *ApiHandler) internalUpdateOcr(ctx context.Context, emc *entity.ElasticI s3id := emc.S3Id created := emc.Created - log.Printf("UpdateOcr: checking image id=%s", id) + slog.Info("UpdateOcr: checking image", + "id", id) img, err := a.imageStorage.GetImage(ctx, s3id) if err != nil { - log.Printf("Failed to read image from storage : id=%s, err=%v", id, err) - return nil + slog.Info("Failed to read image from storage", + "id", id, + commonconst.ERR_LOG, err) + + return } ocrResult, err := a.ocr.DoOcr(ctx, id, img) if err != nil { - log.Printf("Failed to doOcr for image id=%s, err=%v", id, err) - return nil + slog.Info("Failed to ocr image", + "id", id, + commonconst.ERR_LOG, err) + return } elasticObject := OcrResultToElastic(id, accountId, hash, created, ocrResult) err = a.metaStorage.Save(ctx, elasticObject) if err != nil { - log.Printf("Failed to save new metadata for image id=%s, err=%v", id, err) - return nil + slog.Info("Failed to save new image metadata", + "id", id, + commonconst.ERR_LOG, err) + return } - return nil } func (a *ApiHandler) iterateDocuments(ctx context.Context, accountId uuid.UUID, callback func(context.Context, *entity.ElasticMatchedContent) error) error { @@ -325,7 +359,7 @@ func (a *ApiHandler) HandleDuplicate( response := server.CreateMeme200JSONResponse{} if duplicate.AccountId != request.AccountId { copyMetadata := *duplicate - log.Printf("Found meme duplicate in another account: id=%s", duplicate.ImageId) + slog.Info("Found meme duplicate in another account", "id", duplicate.ImageId) copyMetadata.ImageId, _ = uuid.NewRandom() copyMetadata.AccountId = request.AccountId err := a.metaStorage.Save(ctx, ©Metadata) @@ -334,7 +368,7 @@ func (a *ApiHandler) HandleDuplicate( } helper.ElasticToCreateResponse(©Metadata, status, &response) } else { - log.Printf("Found meme duplicate in this account: id=%s", duplicate.ImageId) + slog.Info("Found meme duplicate in this account", "id", duplicate.ImageId) helper.ElasticToCreateResponse(duplicate, status, &response) } diff --git a/storage-service/src/service/ElasticMetadataStorageServiceImpl.go b/storage-service/src/service/ElasticMetadataStorageServiceImpl.go index 596fdd0..3cbd0fd 100644 --- a/storage-service/src/service/ElasticMetadataStorageServiceImpl.go +++ b/storage-service/src/service/ElasticMetadataStorageServiceImpl.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - "log" + "log/slog" "time" elasticsearch8 "github.com/elastic/go-elasticsearch/v8" @@ -16,11 +16,13 @@ import ( "github.com/gdexlab/go-render/render" "github.com/go-playground/validator/v10" "github.com/google/uuid" + "mine.local/ocr-gallery/common/commonconst" "mine.local/ocr-gallery/storage-service/conf" "mine.local/ocr-gallery/storage-service/entity" ) const INDEX_NAME = "image-metadata" +const MAX_FUZZY = 10 type ElasticMetadataStorageServiceImpl struct { client *elasticsearch8.TypedClient @@ -113,7 +115,10 @@ func (e *ElasticMetadataStorageServiceImpl) Delete(ctx context.Context, id uuid. return fmt.Errorf("elastic failed to delete: %w", err) } - log.Printf("Delete metadata document: elastic, id=%s response=%s", id, render.Render(response)) + slog.Info("Delete metadata document ", + "id", id, + "response", render.Render(response)) + return err } @@ -133,7 +138,11 @@ func (e *ElasticMetadataStorageServiceImpl) processKnn( TrackScores(true) knn, err := search.Do(ctx) - return errWrap(knn, "elastic failed to knn: %w", err) + if err != nil { + return nil, fmt.Errorf("elastic: failed to knn-search: response=%s : %w", render.Render(knn), err) + } + + return knn, nil } // Search implements MetadataStorageService. @@ -172,7 +181,11 @@ func (e *ElasticMetadataStorageServiceImpl) runSearchQuery( } resp, err := search.Do(ctx) - return errWrap(resp, "elastic failed to search: %w", err) + if err != nil { + return nil, fmt.Errorf("elastic: failed to search: response=%s : %w", render.Render(resp), err) + } + + return resp, nil } func (e *ElasticMetadataStorageServiceImpl) searchQueryInternal( @@ -187,29 +200,55 @@ func (e *ElasticMetadataStorageServiceImpl) searchQueryInternal( } if queryString == "" { - log.Printf("Search using ALL query: query: '%s' account: '%s' after: %v", - queryString, accountId, sortIdAfter) + slog.Info("Search ALL (no query string)", + commonconst.ACCOUNTID_LOG, accountId, + commonconst.OFFSET_LOG, sortIdAfter, + "pageSize", pageSize, + ) + q := e.allQuery(accountId) - return e.runSearchQuery(ctx, q, sortIdAfter, pageSize) + result, err := e.runSearchQuery(ctx, q, sortIdAfter, pageSize) + if err != nil { + return nil, fmt.Errorf("failed to search all query : %w", err) + } + + slog.Info("Search ALL result", "count", len(result.Hits.Hits)) + return result, nil } - log.Printf("Search using simple query: query: '%s' account: '%s' after: %v", - queryString, accountId, sortIdAfter) + slog.Info("Search SIMPLE", + commonconst.ACCOUNTID_LOG, accountId, + commonconst.OFFSET_LOG, sortIdAfter, + commonconst.QUERY_LOG, queryString, + "pageSize", pageSize, + ) q := e.simpleQuery(accountId, queryString) - res, err := e.runSearchQuery(ctx, q, sortIdAfter, pageSize) + result, err := e.runSearchQuery(ctx, q, sortIdAfter, pageSize) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to search SIMPLE query : %w", err) } - resultsSize := len(res.Hits.Hits) + slog.Info("Search SIMPLE result", "count", len(result.Hits.Hits)) + + resultsSize := len(result.Hits.Hits) if resultsSize > 0 || sortIdAfter != nil { - return res, nil + return result, nil } - log.Printf("Search using fuzzy query: query: '%s' account: '%s'", queryString, accountId) + slog.Info("Search FUZZY", + commonconst.ACCOUNTID_LOG, accountId, + commonconst.OFFSET_LOG, sortIdAfter, + commonconst.QUERY_LOG, queryString, + "maxCount", MAX_FUZZY, + ) + q = e.fuzzySearchQuery(accountId, queryString) - return e.runSearchQuery(ctx, q, sortIdAfter, pageSize) + result, err = e.runSearchQuery(ctx, q, sortIdAfter, addr(MAX_FUZZY)) + if err != nil { + return nil, fmt.Errorf("failed to search FUZZY query : %w", err) + } + return result, nil } // Search implements MetadataStorageService. @@ -254,7 +293,8 @@ func (e *ElasticMetadataStorageServiceImpl) Search( // GetById implements MetadataStorageService. func (e *ElasticMetadataStorageServiceImpl) GetById(ctx context.Context, id uuid.UUID) (*entity.ElasticImageMetaData, error) { - log.Printf("GetById: call id: %s", id.String()) + slog.Info("GetById: call", + "id", id.String()) query := types.NewQuery() query.Ids = types.NewIdsQuery() @@ -285,7 +325,8 @@ func (e *ElasticMetadataStorageServiceImpl) GetByHash( ctx context.Context, hash string, ) (*entity.ElasticImageMetaData, error) { - log.Printf("GetByHash: call hash: %s", hash) + slog.Info("GetByHash: call", + "hash", hash) query := types.NewQuery() query.QueryString = types.NewQueryStringQuery() @@ -318,7 +359,7 @@ func (e *ElasticMetadataStorageServiceImpl) GetByEmbeddingV1( img *entity.ElasticEmbeddingV1, count int, ) ([]*entity.ElasticImageMetaData, error) { - log.Printf("GetByEmbeddingV1: call") + slog.Info("GetByEmbeddingV1: call") query := e.embeddingV1KnnQuery(img, count) result, err := e.processKnn(ctx, *query) @@ -393,11 +434,15 @@ func (e *ElasticMetadataStorageServiceImpl) Save(ctx context.Context, file *enti Do(ctx) if err != nil { - log.Printf("Save metadata document error: id=%s error=%s", file.ImageId, err.Error()) - return err + return fmt.Errorf("Save metadata document error: id=%s : %w", file.ImageId, err) } - log.Printf("Save metadata document: elastic, id=%s response=%s", file.ImageId, render.Render(response)) + slog.Info("Save metadata document", + "id", file.ImageId) + + slog.Debug("Save metadata document details", + "id", file.ImageId, + "response", render.Render(response)) return err } @@ -442,13 +487,6 @@ func unmarhalSourceDocument(result json.RawMessage) (*entity.ElasticImageMetaDat func addr[T any](v T) *T { return &v } -func errWrap[T any](v *T, msg string, err error, args ...any) (*T, error) { - if err != nil { - return nil, fmt.Errorf(msg, err, args) - } - return v, err -} - func NewElasticMetadataStorage( config *conf.MetadataStorageConfig, validate *validator.Validate, @@ -458,7 +496,11 @@ func NewElasticMetadataStorage( responseCreate, err := es8.Indices. Create(config.Index). Do(context.Background()) - log.Printf("Elastic create index response: %s error: %v", render.Render(responseCreate), err) + + slog.Info("Elastic create index", + "index", config.Index, + "response", render.Render(responseCreate), + commonconst.ERR_LOG, err) indexTypeMapping := types.NewTypeMapping() indexTypeMapping.Properties["Created"] = types.NewLongNumberProperty() @@ -476,7 +518,9 @@ func NewElasticMetadataStorage( Properties(indexTypeMapping.Properties). Do(context.Background()) - log.Printf("Elastic mapping index response: %s error: %v", render.Render(responseMapping), err) + slog.Info("Elastic create mapping index", + "response", render.Render(responseMapping), + commonconst.ERR_LOG, err) return &ElasticMetadataStorageServiceImpl{ client: es8, diff --git a/storage-service/src/service/OcrService.go b/storage-service/src/service/OcrService.go index 4036a07..c223705 100644 --- a/storage-service/src/service/OcrService.go +++ b/storage-service/src/service/OcrService.go @@ -3,7 +3,7 @@ package service import ( "context" "errors" - "log" + "log/slog" "strings" "github.com/go-playground/validator/v10" @@ -107,7 +107,8 @@ func textVariantsToString(textVariants *[]client.OcrResponseItem) string { func NewOcrService(conf *conf.OcrConfig, validate *validator.Validate) (OcrSerivce, error) { ocrServiceUrl := conf.Uri - log.Printf("Creating ocr service url=%s\n", ocrServiceUrl) + slog.Info("Creating ocr service", + "url", ocrServiceUrl) client, err := client.NewClientWithResponses(ocrServiceUrl) if err != nil { diff --git a/telegram-service/config.yaml b/telegram-service/config.yaml index 009a257..fd88864 100644 --- a/telegram-service/config.yaml +++ b/telegram-service/config.yaml @@ -1,5 +1,8 @@ +log: + Level: info + telegram: - Token: + Token: mongo: Uri: "mongodb://mongo:mongo@localhost:27017/" diff --git a/telegram-service/src/main.go b/telegram-service/src/main.go index 174af9c..0165c96 100644 --- a/telegram-service/src/main.go +++ b/telegram-service/src/main.go @@ -1,9 +1,6 @@ package main import ( - "log/slog" - "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "go.uber.org/fx" "mine.local/ocr-gallery/common/commonconfig" @@ -16,12 +13,8 @@ func Statup(serv service.TelegramBotService) { } func main() { - handler := slog.NewTextHandler(os.Stdout, nil) - logger := slog.New(handler) - slog.SetLogLoggerLevel(slog.LevelDebug) - slog.SetDefault(logger) - commonconfig.InitConfig() + commonconfig.InitLogs() fx.New( fx.Provide(conf.NewTelegramConfig), diff --git a/telegram-service/src/service/InlineHandlerService.go b/telegram-service/src/service/InlineHandlerService.go index 4984d4e..01ab1d6 100644 --- a/telegram-service/src/service/InlineHandlerService.go +++ b/telegram-service/src/service/InlineHandlerService.go @@ -69,7 +69,7 @@ func (i *InineHandlerServiceImpl) ProcessQuery( if results == nil { retval := tgbotapi.InlineConfig{ InlineQueryID: request.ID, - CacheTime: 5, + CacheTime: 120, IsPersonal: true, } return &retval, nil @@ -77,7 +77,7 @@ func (i *InineHandlerServiceImpl) ProcessQuery( photos := make([]interface{}, len(results)) for index, item := range results { - slog.Info("SearchResultItem ", + slog.Debug("SearchResultItem ", "userId", userId, "requestId", request.ID, "index", index, @@ -85,6 +85,7 @@ func (i *InineHandlerServiceImpl) ProcessQuery( "sortId", item.SortId, "url", item.ImageUrl, ) + inlineChoice := tgbotapi.NewInlineQueryResultPhotoWithThumb( item.Id.String(), item.ImageUrl, diff --git a/telegram-service/src/service/Telegram.go b/telegram-service/src/service/Telegram.go index 70e3716..2f8a69a 100644 --- a/telegram-service/src/service/Telegram.go +++ b/telegram-service/src/service/Telegram.go @@ -38,7 +38,10 @@ func (srv *TelegramBotServiceImpl) StartBot() { } func (srv *TelegramBotServiceImpl) HandleMessage(update *tgbotapi.Update) { - slog.Info("Bot message request", "request", update.Message) + slog.Info("Bot message request") + slog.Debug("Bot message request details", + "request", update.Message) + answer, err := srv.message.ProcessMessage(update.Message) if err != nil { slog.Error("Failed to process message", commonconst.ERR_LOG, err) @@ -58,13 +61,18 @@ func (srv *TelegramBotServiceImpl) HandleMessage(update *tgbotapi.Update) { } } func (srv *TelegramBotServiceImpl) HandleInlineRequest(update *tgbotapi.Update) { - slog.Debug("Bot inline request:", commonconst.DATA_LOG, update.InlineQuery) + slog.Info("Bot inline request:", + "query", update.InlineQuery.Query) + + slog.Debug("Bot inline request details:", + commonconst.DATA_LOG, update.InlineQuery) inlineResponse, err := srv.inline.ProcessQuery(context.Background(), update.InlineQuery) if err != nil { slog.Error("failed to process inline query:", commonconst.ERR_LOG, err) return } - slog.Debug("Bot inline response:", commonconst.DATA_LOG, inlineResponse) + slog.Debug("Bot inline response details:", + commonconst.DATA_LOG, inlineResponse) _, err = srv.bot.Request(inlineResponse) if err != nil {