diff --git a/AGENTS.md b/AGENTS.md index 77b951b..ac21e96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,7 @@ Two tables, `dailyQuestion` and `users`, are expected to exist (see README.md). - All outgoing messages use `parse_mode: HTML`, so content must be HTML-safe and use only Telegram's supported tag subset — go through `common.RemoveUnsupportedTags` / `FixTagsAndImages` rather than emitting tags directly. - User-visible commands are exact string matches. `/Subscribe 7` is not supported; `/Subscribe` only opens the hour keyboard, and the subsequent `H:00` text triggers subscription. - Main keyboard replies are JSON-encoded into `TelegramResponse.ReplyMarkup`; task hint/difficulty buttons use Telegram inline keyboards. -- Callback payloads are JSON-encoded `common.CallbackData`; the keyboard builder in `common.GetInlineKeyboard` and the callback router in `Application.processCallback` must stay in sync on the `CallbackType` values (`HintReuqest`, `DifficultyRequest`). +- Callback payloads are JSON-encoded `common.CallbackData`; the keyboard builder in `common.GetInlineKeyboard` and the callback router in `Application.processCallback` must stay in sync on the `CallbackType` values (`HintRequest`, `DifficultyRequest`). - Callback handling intentionally reads only from storage and does not fall back to the LeetCode API. That prevents arbitrary callback payloads from making the app fetch many historical tasks. - `SendDailyTaskToSubscribedUsers` selects users by the current UTC hour and sends messages concurrently. Individual send failures are logged after three attempts and do not fail the whole reminder run. diff --git a/internal/bot/application.go b/internal/bot/application.go index a0fe284..c1fdb6e 100644 --- a/internal/bot/application.go +++ b/internal/bot/application.go @@ -29,7 +29,7 @@ List of available commands: If you've found this bot useless and have ideas of possible improvements, please, add them to https://github.com/dartkron/leetcodeBot/issues` alreadyUnsubscribedMessage = "%s, you were not subscribed. No additional actions required." - subcribedMessage = "%s, you have successfully subscribed. You'll automatically receive daily tasks every day at %d:00 UTC ." + subscribedMessage = "%s, you have successfully subscribed. You'll automatically receive daily tasks every day at %d:00 UTC ." alreadySubscribedMessage = "%s, you have already subscribed for daily updates at the same time, nothing to do." getActualDailyTaskCommand = "Get actual daily task" getActualDailyTaskCommandSlash = "/getDailyTask" @@ -40,7 +40,7 @@ If you've found this bot useless and have ideas of possible improvements, please telegramAPIURL = "https://api.telegram.org/bot%s/sendMessage" ) -// TelegramResponse is a short representation of fileds supported by Telegram +// TelegramResponse is a short representation of fields supported by Telegram. type TelegramResponse struct { Method string `json:"method"` ParseMode string `json:"parse_mode"` @@ -72,7 +72,7 @@ type TelegramRequest struct { } // KeyboardDef is a representation of Telegram keyboard, don't confuse it with inline keyboard -// Two dimensonal slice of Key: first dimension levels of keys and second is particular keys. +// Two dimensional slice of Key: first dimension levels of keys and second is particular keys. // So you can have different amount of keys on every level type KeyboardDef struct { Keyboard [][]Key `json:"keyboard"` @@ -85,7 +85,7 @@ type Key struct { Text string `json:"text"` } -// NewTelegramResponse TelegramResponse constructor with filling basic fileds +// NewTelegramResponse is a TelegramResponse constructor with basic fields. func NewTelegramResponse() *TelegramResponse { return &TelegramResponse{ Method: "sendMessage", @@ -93,7 +93,7 @@ func NewTelegramResponse() *TelegramResponse { } } -// Application holder for dependicies for easy injection +// Application holds dependencies for easy injection. type Application struct { storageController storage.Controller leetcodeAPIClient leetcodeclient.LeetcodeClient @@ -139,7 +139,7 @@ func (app *Application) processCallback(ctx context.Context, request TelegramReq } return response, nil } - if callback.Type == common.HintReuqest { + if callback.Type == common.HintRequest { if callback.Hint > len(task.Hints)-1 { response.Text = fmt.Sprintf("There is no such hint for task %d", callback.DateID) return response, nil @@ -225,7 +225,7 @@ func (app *Application) subscribeAction(ctx context.Context, request *TelegramRe } else if err != nil { return err } else { - response.Text = fmt.Sprintf(subcribedMessage, user.FirstName, sendingHour) + response.Text = fmt.Sprintf(subscribedMessage, user.FirstName, sendingHour) } return nil } diff --git a/internal/bot/application_test.go b/internal/bot/application_test.go index 13b6d1d..8801ca4 100644 --- a/internal/bot/application_test.go +++ b/internal/bot/application_test.go @@ -125,8 +125,8 @@ func getTodaySendMessageString(chatID uint64) string { return string(bytes) } -func getTestApp() (*mocks.MockHTTPTRansport, *MockStorageController, *lcclientmocks.MockLeetcodeClient, *Application) { - httpTransportMock := &mocks.MockHTTPTRansport{} +func getTestApp() (*mocks.MockHTTPTransport, *MockStorageController, *lcclientmocks.MockLeetcodeClient, *Application) { + httpTransportMock := &mocks.MockHTTPTransport{} leetcodeClient := &lcclientmocks.MockLeetcodeClient{} storageController := &MockStorageController{ tasks: map[uint64]*common.BotLeetCodeTask{}, @@ -417,7 +417,7 @@ func TestNewApplication(t *testing.T) { func TestSubscribeAction(t *testing.T) { _, storageController, _, app := getTestApp() userBeforeRequest := *storageController.users[1124] - assert.False(t, userBeforeRequest.Subscribed, "Before request user shouldn't be scubscribed") + assert.False(t, userBeforeRequest.Subscribed, "Before request user shouldn't be subscribed") request := TelegramRequest{} request.Message.From.ID = 1124 request.Message.Chat.ID = 1124 @@ -430,13 +430,13 @@ func TestSubscribeAction(t *testing.T) { userBeforeRequest.Subscribed = true userBeforeRequest.SendingHour += 2 assert.Equal(t, userBeforeRequest, *storageController.users[1124], "Unexpected changes in stored user after subscribe action") - assert.Equal(t, fmt.Sprintf(subcribedMessage, request.Message.From.FirstName, userBeforeRequest.SendingHour), response.Text, "Unexpected response text") + assert.Equal(t, fmt.Sprintf(subscribedMessage, request.Message.From.FirstName, userBeforeRequest.SendingHour), response.Text, "Unexpected response text") } func TestSubscribeActionAlreadySubscribed(t *testing.T) { _, storageController, _, app := getTestApp() userBeforeRequest := *storageController.users[1126] - assert.True(t, userBeforeRequest.Subscribed, "Before request user should be scubscribed") + assert.True(t, userBeforeRequest.Subscribed, "Before request user should be subscribed") request := TelegramRequest{} request.Message.From.ID = 1126 request.Message.Chat.ID = 1126 @@ -574,7 +574,7 @@ func TestProcessRequestBodyHelp(t *testing.T) { assert.Equal(t, responseBytes, []byte(expectedResponse), "Unexprected response bytes") } -func TestProcessSubscriveKeyboard(t *testing.T) { +func TestProcessSubscribeKeyboard(t *testing.T) { _, _, _, app := getTestApp() request := TelegramRequest{} request.Message.From.ID = 6667 @@ -718,7 +718,7 @@ func TestProcessRequestTaskHint(t *testing.T) { } request := TelegramRequest{} request.CallbackQuery.From.ID = 1126 - data, err := common.GetMarshalledCallbackData(todayTaskID, 1, common.HintReuqest) + data, err := common.GetMarshalledCallbackData(todayTaskID, 1, common.HintRequest) assert.Nil(t, err, "Unexpected GetMarshalledCallbackData error") request.CallbackQuery.Data = data requestbytes, err := json.Marshal(request) @@ -745,7 +745,7 @@ func TestProcessRequestTaskHintMoreThenExists(t *testing.T) { } request := TelegramRequest{} request.CallbackQuery.From.ID = 1126 - data, err := common.GetMarshalledCallbackData(taskID, 2, common.HintReuqest) + data, err := common.GetMarshalledCallbackData(taskID, 2, common.HintRequest) assert.Nil(t, err, "Unexpected GetMarshalledCallbackData error") request.CallbackQuery.Data = data requestbytes, err := json.Marshal(request) @@ -762,7 +762,7 @@ func TestProcessRequestTaskHintError(t *testing.T) { storageController.failedTaskID = todayTaskID request := TelegramRequest{} request.CallbackQuery.From.ID = 1126 - data, err := common.GetMarshalledCallbackData(todayTaskID, 2, common.HintReuqest) + data, err := common.GetMarshalledCallbackData(todayTaskID, 2, common.HintRequest) assert.Nil(t, err, "Unexpected GetMarshalledCallbackData error") request.CallbackQuery.Data = data requestbytes, err := json.Marshal(request) @@ -779,7 +779,7 @@ func TestProcessRequestTaskHintNoTask(t *testing.T) { delete(storageController.tasks, todayTaskID) request := TelegramRequest{} request.CallbackQuery.From.ID = 1126 - data, err := common.GetMarshalledCallbackData(todayTaskID, 2, common.HintReuqest) + data, err := common.GetMarshalledCallbackData(todayTaskID, 2, common.HintRequest) assert.Nil(t, err, "Unexpected GetMarshalledCallbackData error") request.CallbackQuery.Data = data requestbytes, err := json.Marshal(request) diff --git a/internal/common/common.go b/internal/common/common.go index 021c5a8..291c365 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -31,9 +31,9 @@ type inlineButton struct { type CallbackType uint8 const ( - // HintReuqest means callback is about hint, not difficulty - HintReuqest CallbackType = iota - // DifficultyRequest means that callback require only difficulty + // HintRequest means callback is about hint, not difficulty. + HintRequest CallbackType = iota + // DifficultyRequest means that callback requires only difficulty. DifficultyRequest // TopicTagsRequest means that callback requires task topic tags. TopicTagsRequest @@ -42,14 +42,14 @@ const ( // ErrClosedContext universal error about closed context var ErrClosedContext error = errors.New("context closed during execution") -// CallbackData stuct for unmarshal JSON in callbackRequests and marshal in inlineKeyboard building +// CallbackData is used to unmarshal callback request JSON and marshal inline keyboard data. type CallbackData struct { DateID uint64 `json:"dateID,string,omitempty"` Type CallbackType `json:"callback_type"` Hint int `json:"hint"` } -// BotLeetCodeTask is iternal LeetcodeTask representation with bot-related info: DateID +// BotLeetCodeTask is internal LeetCodeTask representation with bot-related info: DateID. type BotLeetCodeTask struct { leetcodeclient.LeetCodeTask DateID uint64 `json:"dateID,string"` @@ -66,15 +66,15 @@ type User struct { SendingHour uint8 } -// GetTaskText returns task text representation to couple login into class(struct, of course struct) +// GetTaskText returns task text representation. func (task *BotLeetCodeTask) GetTaskText() string { return fmt.Sprintf("%s\n\n%s", task.Title, task.Content) } -// GetMarshalledCallbackData shotcut to get ready data for callback +// GetMarshalledCallbackData returns serialized callback data. func GetMarshalledCallbackData(dateID uint64, hintID int, dataType CallbackType) (string, error) { callbackData := CallbackData{DateID: dateID, Type: dataType} - if dataType == HintReuqest { + if dataType == HintRequest { callbackData.Hint = int(hintID) } callbackDataMarshaled, err := json.Marshal(callbackData) @@ -86,7 +86,7 @@ func (task *BotLeetCodeTask) GetInlineKeyboard() string { listOfHints := [][]inlineButton{{}} level := 0 for i := range task.Hints { - callbackData, err := GetMarshalledCallbackData(task.DateID, i, HintReuqest) + callbackData, err := GetMarshalledCallbackData(task.DateID, i, HintRequest) if err != nil { fmt.Println(callbackDataMarshalErrorMessage, err) } @@ -144,7 +144,7 @@ func (task *BotLeetCodeTask) GetInlineKeyboard() string { inlineKeyboard, err := json.Marshal(map[string][][]inlineButton{"inline_keyboard": listOfHints}) if err != nil { - fmt.Println("Error during marshall inlineKeyboard:", err) + fmt.Println("Error during marshal inlineKeyboard:", err) } return string(inlineKeyboard) } @@ -172,9 +172,9 @@ func (task *BotLeetCodeTask) GetDifficultyNum() uint8 { } } -// SetDifficultyFromNum set task difficulty from uint8 value -func (task *BotLeetCodeTask) SetDifficultyFromNum(difficuty uint8) { - switch difficuty { +// SetDifficultyFromNum sets task difficulty from uint8 value. +func (task *BotLeetCodeTask) SetDifficultyFromNum(difficulty uint8) { + switch difficulty { case easy: task.Difficulty = "Easy" case medium: @@ -186,7 +186,7 @@ func (task *BotLeetCodeTask) SetDifficultyFromNum(difficuty uint8) { } } -// GetDateInRightTimeZone returns time in corrent time zone for Leetcode +// GetDateInRightTimeZone returns time in the correct time zone for LeetCode. func GetDateInRightTimeZone() time.Time { return time.Now().UTC() } diff --git a/internal/common/common_test.go b/internal/common/common_test.go index 134d367..5cc03e2 100644 --- a/internal/common/common_test.go +++ b/internal/common/common_test.go @@ -37,7 +37,7 @@ func TestGetDateId(t *testing.T) { } } -func TestRemoveSimpleUnsuppotedTags(t *testing.T) { +func TestRemoveSimpleUnsupportedTags(t *testing.T) { cases := map[string]string{ "test": "test", "te

st": "test", @@ -74,7 +74,7 @@ func TestRemoveSimpleUnsuppotedTags(t *testing.T) { } } -func TestRemoveUnsuppotedTags(t *testing.T) { +func TestRemoveUnsupportedTags(t *testing.T) { cases := map[string]string{ "test": "test", "notvery

complextestswithmessageInSpan": "notverycomplextests(withmessageInSpan\n", @@ -124,8 +124,8 @@ type callbackTestCase struct { func TestGetMarshalledCallbackData(t *testing.T) { testCases := []callbackTestCase{ - {dateID: 10, hintID: 22, dataType: HintReuqest, awaitingResult: "{\"dateID\":\"10\",\"callback_type\":0,\"hint\":22}"}, - {dateID: 10, hintID: 0, dataType: HintReuqest, awaitingResult: "{\"dateID\":\"10\",\"callback_type\":0,\"hint\":0}"}, + {dateID: 10, hintID: 22, dataType: HintRequest, awaitingResult: "{\"dateID\":\"10\",\"callback_type\":0,\"hint\":22}"}, + {dateID: 10, hintID: 0, dataType: HintRequest, awaitingResult: "{\"dateID\":\"10\",\"callback_type\":0,\"hint\":0}"}, {dateID: 10, hintID: 22, dataType: DifficultyRequest, awaitingResult: "{\"dateID\":\"10\",\"callback_type\":1,\"hint\":0}"}, {dateID: 10, hintID: 22, dataType: TopicTagsRequest, awaitingResult: "{\"dateID\":\"10\",\"callback_type\":2,\"hint\":0}"}, } diff --git a/pkg/leetcodeclient/http_test.go b/pkg/leetcodeclient/http_test.go index 0af1840..aee5457 100644 --- a/pkg/leetcodeclient/http_test.go +++ b/pkg/leetcodeclient/http_test.go @@ -20,7 +20,7 @@ type testRequest struct { } func TestRequestGraphQl(t *testing.T) { - httpTransportMock := &mocks.MockHTTPTRansport{} + httpTransportMock := &mocks.MockHTTPTransport{} requester := newHTTPGraphQlRequester(&http.Client{Transport: httpTransportMock}) expected_headers := http.Header{ diff --git a/tests/mocks/http.go b/tests/mocks/http.go index 9df9334..3be19a6 100644 --- a/tests/mocks/http.go +++ b/tests/mocks/http.go @@ -7,13 +7,13 @@ import ( "github.com/stretchr/testify/mock" ) -// MockHTTPTRansport provides universal mock for http.Post function -type MockHTTPTRansport struct { +// MockHTTPTransport provides universal mock for http.Post function. +type MockHTTPTransport struct { mock.Mock } -// RoundTrip doing fake transport -func (m *MockHTTPTRansport) RoundTrip(r *http.Request) (*http.Response, error) { +// RoundTrip provides fake transport behavior. +func (m *MockHTTPTransport) RoundTrip(r *http.Request) (*http.Response, error) { request, err := io.ReadAll(r.Body) url := r.URL.String() headers := r.Header.Clone()