Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 7 additions & 7 deletions internal/bot/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <strong>not subscribed</strong>. No additional actions required."
subcribedMessage = "%s, you have <strong>successfully subscribed</strong>. You'll automatically receive daily tasks every day at %d:00 UTC ."
subscribedMessage = "%s, you have <strong>successfully subscribed</strong>. You'll automatically receive daily tasks every day at %d:00 UTC ."
alreadySubscribedMessage = "%s, you have <strong>already subscribed</strong> for daily updates at the same time, nothing to do."
getActualDailyTaskCommand = "Get actual daily task"
getActualDailyTaskCommandSlash = "/getDailyTask"
Expand All @@ -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"`
Expand Down Expand Up @@ -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"`
Expand All @@ -85,15 +85,15 @@ 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",
ParseMode: "HTML",
}
}

// Application holder for dependicies for easy injection
// Application holds dependencies for easy injection.
type Application struct {
storageController storage.Controller
leetcodeAPIClient leetcodeclient.LeetcodeClient
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
20 changes: 10 additions & 10 deletions internal/bot/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
28 changes: 14 additions & 14 deletions internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
Expand All @@ -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("<strong>%s</strong>\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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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:
Expand All @@ -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()
}
Expand Down
8 changes: 4 additions & 4 deletions internal/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<p>st": "test",
Expand Down Expand Up @@ -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<p>complex</u>tests<sub>with<span type=\"test\">messageInSpan</span>": "notverycomplex</u>tests(withmessageInSpan\n",
Expand Down Expand Up @@ -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}"},
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/leetcodeclient/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
8 changes: 4 additions & 4 deletions tests/mocks/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading