Skip to content

Commit

Permalink
Scheduler refactoring (#136)
Browse files Browse the repository at this point in the history
* clean up

* split into small chunks

* tests

* code review

* refactoring + ignore

* refactoring

* clean up

* tests

* unit tests

* more tests

* more tests

---------

Co-authored-by: Maksym Bilan <>
  • Loading branch information
maximbilan authored Dec 30, 2024
1 parent 01afb7d commit b6a45f0
Show file tree
Hide file tree
Showing 12 changed files with 528 additions and 145 deletions.
19 changes: 19 additions & 0 deletions internal/scheduler/admin_stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package scheduler

import (
"context"

"github.com/capymind/internal/database"
"github.com/capymind/internal/helpers"
"github.com/capymind/internal/translator"
)

func prepareAdminStats(ctx *context.Context, locale translator.Locale, adminStorage database.AdminStorage, feedbackStorage database.FeedbackStorage) *string {
stats := helpers.GetStats(ctx, locale, adminStorage, feedbackStorage)

var finalString string
for _, stat := range stats {
finalString += stat + "\n"
}
return &finalString
}
22 changes: 22 additions & 0 deletions internal/scheduler/admin_stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package scheduler

import (
"context"
"testing"

"github.com/capymind/internal/mocks"
"github.com/capymind/internal/translator"
)

func TestAdminStats(t *testing.T) {
context := context.Background()
locale := translator.EN
adminStorage := mocks.AdminStorageMock{}
feedbackStorage := mocks.FeedbackStorageMock{}

response := prepareAdminStats(&context, locale, adminStorage, feedbackStorage)

if *response != "The total number of users is 100\nThe total number of active users is 75\nThe total number of notes is 999\n\nFeedback from last week 📈\n\nJohn \nDoe\n:\n\nTest feedback\n\nJohn \nDoe\n:\n\nTest feedback 2\n\n" {
t.Errorf("Expected valid response, got %s", *response)
}
}
113 changes: 113 additions & 0 deletions internal/scheduler/messaging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package scheduler

import (
"context"
"encoding/json"
"log"
"net/http"
"time"

"github.com/capymind/internal/botservice"
"github.com/capymind/internal/database"
"github.com/capymind/internal/taskservice"
"github.com/capymind/internal/translator"
)

func prepareMessage(user *database.User, ctx *context.Context, offset int, messageType taskservice.MessageType, message string, isCloud bool) {
//coverage:ignore
defer wg.Done()

log.Printf("[Scheduler] Schedule a message for user: %s", user.ID)

userLocale := translator.Locale(*user.Locale)

var localizedMessage *string
if messageType == taskservice.WeeklyAnalysis {
localizedMessage = prepareWeeklyAnalysis(user, ctx, userLocale, noteStorage, aiService)
} else if messageType == taskservice.UserStats {
// Send only to active users
if !user.IsActive() {
return
}
localizedMessage = prepareUserStats(user, ctx, userLocale, noteStorage)
} else if messageType == taskservice.AdminStats {
// Send only to admins
if !database.IsAdmin(user.Role) {
return
}
localizedMessage = prepareAdminStats(ctx, userLocale, adminStorage, feedbackStorage)
} else {
msg := translator.Translate(userLocale, message)
localizedMessage = &msg
}

if localizedMessage == nil {
return
}

var scheduledTime time.Time
if isCloud {
scheduledTime = time.Now().Add(time.Duration(offset) * time.Hour)
scheduledTime = scheduledTime.Add(-time.Duration(*user.SecondsFromUTC) * time.Second)
} else {
// For local testing, schedule the message in 10 seconds
scheduledTime = time.Now().Add(10 * time.Second)
}

scheduledMessage := taskservice.ScheduledTask{
ChatID: user.ChatID,
Text: *localizedMessage,
Type: messageType,
Locale: userLocale,
}

tasks.Schedule(ctx, scheduledMessage, scheduledTime)
}

// Send a message to a user
func SendMessage(w http.ResponseWriter, r *http.Request) {
//coverage:ignore
var msg taskservice.ScheduledTask
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
log.Printf("[Scheduler] Could not parse message %s", err.Error())
return
}

result := prepareBotResult(msg)
bot.SendResult(msg.ChatID, result)
log.Printf("[Scheduler] Message sent to user: %d", msg.ChatID)
}

func prepareBotResult(scheduledTask taskservice.ScheduledTask) botservice.BotResult {
var result botservice.BotResult
switch scheduledTask.Type {
case taskservice.Morning, taskservice.Evening:
var button botservice.BotResultTextButton = botservice.BotResultTextButton{
TextID: "make_record_to_journal",
Locale: scheduledTask.Locale,
Callback: "/note",
}
result = botservice.BotResult{
TextID: scheduledTask.Text,
Locale: scheduledTask.Locale,
Buttons: []botservice.BotResultTextButton{button},
}
case taskservice.Feedback:
var button botservice.BotResultTextButton = botservice.BotResultTextButton{
TextID: "feedback_button",
Locale: scheduledTask.Locale,
Callback: "/support",
}
result = botservice.BotResult{
TextID: scheduledTask.Text,
Locale: scheduledTask.Locale,
Buttons: []botservice.BotResultTextButton{button},
}
default:
result = botservice.BotResult{
TextID: scheduledTask.Text,
Locale: scheduledTask.Locale,
}
}
return result
}
79 changes: 79 additions & 0 deletions internal/scheduler/messaging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package scheduler

import (
"testing"

"github.com/capymind/internal/taskservice"
)

func TestPrepareMorningMessage(t *testing.T) {
scheduledMessage := taskservice.ScheduledTask{
ChatID: 1,
Text: "Bot result",
Type: taskservice.Morning,
Locale: "en",
}

result := prepareBotResult(scheduledMessage)
if result.TextID != "Bot result" {
t.Error("Expected Bot result, got nil")
}
if result.Locale != "en" {
t.Error("Expected en, got nil")
}
if result.Buttons[0].TextID != "make_record_to_journal" {
t.Error("Expected make_record_to_journal, got nil")
}
if result.Buttons[0].Locale != "en" {
t.Error("Expected en, got nil")
}
if result.Buttons[0].Callback != "/note" {
t.Error("Expected /note, got nil")
}
}

func TestPrepareFeedbackMessage(t *testing.T) {
scheduledMessage := taskservice.ScheduledTask{
ChatID: 1,
Text: "Bot result",
Type: taskservice.Feedback,
Locale: "en",
}

result := prepareBotResult(scheduledMessage)
if result.TextID != "Bot result" {
t.Error("Expected Bot result, got nil")
}
if result.Locale != "en" {
t.Error("Expected en, got nil")
}
if result.Buttons[0].TextID != "feedback_button" {
t.Error("Expected feedback_button, got nil")
}
if result.Buttons[0].Locale != "en" {
t.Error("Expected en, got nil")
}
if result.Buttons[0].Callback != "/support" {
t.Error("Expected /support, got nil")
}
}

func TestPrepareRegularMessage(t *testing.T) {
scheduledMessage := taskservice.ScheduledTask{
ChatID: 1,
Text: "Bot result",
Type: taskservice.Regular,
Locale: "en",
}

result := prepareBotResult(scheduledMessage)
if result.TextID != "Bot result" {
t.Error("Expected Bot result, got nil")
}
if result.Locale != "en" {
t.Error("Expected en, got nil")
}
if len(result.Buttons) != 0 {
t.Error("Expected no buttons, got some")
}
}
41 changes: 41 additions & 0 deletions internal/scheduler/parsing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package scheduler

import (
"fmt"
"log"
"net/url"
"time"

"github.com/capymind/internal/taskservice"
)

// Returns the type and offset parameters from the URL
func parse(url *url.URL) (*string, int) {
typeStr := url.Query().Get("type")
offsetStr := url.Query().Get("offset") // hours (from UTC 0)
var offset int = 0
if offsetStr != "" {
_, err := fmt.Sscanf(offsetStr, "%d", &offset)
if err != nil {
log.Printf("[Scheduler] Error getting offset parameter, %s", err.Error())
}
}
return &typeStr, offset
}

func getTextMessage(messageType taskservice.MessageType) *string {
var message string
switch messageType {
case taskservice.Morning, taskservice.Evening:
message = taskservice.GetMessage(messageType, time.Now().Weekday())
case taskservice.Feedback:
message = "ask_write_review_about_bot"
case taskservice.WeeklyAnalysis, taskservice.UserStats, taskservice.AdminStats:
// Personalized for each user
message = ""
default:
log.Println("Missing message type parameter")
return nil
}
return &message
}
107 changes: 107 additions & 0 deletions internal/scheduler/parsing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package scheduler

import (
"net/url"
"testing"

"github.com/capymind/internal/taskservice"
)

func TestParse(t *testing.T) {
url1 := url.URL{
RawQuery: "type=morning&offset=2",
}

typeStr, offset := parse(&url1)
if *typeStr != "morning" {
t.Errorf("Expected type=morning, got %s", *typeStr)
}
if offset != 2 {
t.Errorf("Expected offset=2, got %d", offset)
}

url2 := url.URL{
RawQuery: "type=evening&offset=0",
}

typeStr, offset = parse(&url2)
if *typeStr != "evening" {
t.Errorf("Expected type=evening, got %s", *typeStr)
}
if offset != 0 {
t.Errorf("Expected offset=0, got %d", offset)
}

url3 := url.URL{
RawQuery: "type=weekly_analysis&offset=-1",
}

typeStr, offset = parse(&url3)
if *typeStr != "weekly_analysis" {
t.Errorf("Expected type=weekly_analysis, got %s", *typeStr)
}
if offset != -1 {
t.Errorf("Expected offset=-1, got %d", offset)
}

url4 := url.URL{
RawQuery: "type=aaa&",
}

typeStr, offset = parse(&url4)
if *typeStr != "aaa" {
t.Errorf("Expected nil, got %s", *typeStr)
}
if offset != 0 {
t.Errorf("Expected offset=0, got %d", offset)
}

url5 := url.URL{
RawQuery: "offset=2",
}

typeStr, offset = parse(&url5)
if *typeStr != "" {
t.Errorf("Expected nil, got %s", *typeStr)
}
if offset != 2 {
t.Errorf("Expected offset=2, got %d", offset)
}
}

func TestGetTextMessage(t *testing.T) {
message := getTextMessage(taskservice.Morning)
if *message != "how_are_you_morning_monday" {
t.Errorf("Expected how_are_you_morning_monday, got %s", *message)
}

message = getTextMessage(taskservice.Evening)
if *message != "how_are_you_evening_monday" {
t.Errorf("Expected how_are_you_evening_monday, got %s", *message)
}

message = getTextMessage(taskservice.Feedback)
if *message != "ask_write_review_about_bot" {
t.Errorf("Expected ask_write_review_about_bot, got %s", *message)
}

message = getTextMessage(taskservice.WeeklyAnalysis)
if *message != "" {
t.Errorf("Expected empty string, got %s", *message)
}

message = getTextMessage(taskservice.UserStats)
if *message != "" {
t.Errorf("Expected empty string, got %s", *message)
}

message = getTextMessage(taskservice.AdminStats)
if *message != "" {
t.Errorf("Expected empty string, got %s", *message)
}

message = getTextMessage("")
if message != nil {
t.Errorf("Expected nil, got %s", *message)
}
}
Loading

0 comments on commit b6a45f0

Please sign in to comment.