diff --git a/internal/server/feedGenerator.go b/internal/server/feedGenerator.go index 0450ff6..f154c2c 100644 --- a/internal/server/feedGenerator.go +++ b/internal/server/feedGenerator.go @@ -8,7 +8,6 @@ import ( "math/rand" "sort" "strconv" - "time" ) const ( @@ -88,7 +87,8 @@ func (f *FeedGenerator) rankAndSortPosts( authCredentials string, posts []*pbcommon.File, ) error { - // fisher-yates shuffle the array to try and break up posts by a given user + // fisher-yates shuffle the array to try and break up posts by a given user, since we are using + // stable sorts later on for i := range posts { j := rand.Intn(i + 1) posts[i], posts[j] = posts[j], posts[i] @@ -96,27 +96,32 @@ func (f *FeedGenerator) rankAndSortPosts( // we want chronological order to be the principle ordering, so the first index will be the most recent post sort.SliceStable(posts, func(i, j int) bool { - if posts[i].DateStored.Year > posts[j].DateStored.Year { + if posts[i].DateStored.Year < posts[j].DateStored.Year { + return false + } else if posts[i].DateStored.Year > posts[j].DateStored.Year { return true } + if posts[i].DateStored.Month > posts[j].DateStored.Month { return true - } - if posts[i].DateStored.Day > posts[j].DateStored.Day { + } else if posts[i].DateStored.Month < posts[j].DateStored.Month { + return false + } else if posts[i].DateStored.Day > posts[j].DateStored.Day { return true + } else if posts[i].DateStored.Day < posts[j].DateStored.Day { + return false } - return false - }) - - // since we do not distinguish between times in a given day, we sort today further by friend strength - dateToday := time.Now() - day := dateToday.Day() + return true + }) + // since we do not distinguish between times in a given day, we sort the latest day further by friend strength endFriendIndex := 0 + day := posts[endFriendIndex].DateStored.Day + month := posts[endFriendIndex].DateStored.Month for { - if int(posts[endFriendIndex].DateStored.Day) != day { + if posts[endFriendIndex].DateStored.Day != day || posts[endFriendIndex].DateStored.Month != month { break } endFriendIndex += 1 @@ -125,7 +130,7 @@ func (f *FeedGenerator) rankAndSortPosts( // we have multiple posts for today, do the sort if endFriendIndex > 2 { f.logger.Debug("Sorting by connection strength") - todaySlice := posts[:endFriendIndex+1] + todaySlice := posts[:endFriendIndex] strengthMap := make(map[int]float32) for i := 0; i < endFriendIndex; i++ { @@ -152,24 +157,51 @@ func (f *FeedGenerator) rankAndSortPosts( return nil } +func min(a, b int) int { + if a < b { + return a + } + return b +} + func (f *FeedGenerator) injectMentalHealthPosts( ctx context.Context, userID int64, authCredentials string, posts []*pbcommon.File, -) error { +) ([]*pbcommon.File, error) { score, err := f.healthClient.GetMentalHealthScoreForUser(ctx, userID, authCredentials) if err != nil { f.logger.Errorf("Failed to get health score for user: %v", err) - return err + return posts, err } if score < healthThreshold { + healthPosts, err := f.mediaClient.GetFilesForUser(ctx, -1, authCredentials) + if err != nil { + f.logger.Errorf("Failed to get mental health posts for user: %v", err) + return posts, err + } // inject posts here + numInject := min(len(healthPosts), len(posts) % 5) + healthPostIndex := 0 + returnSize := numInject + len(posts) + toReturn := make([]*pbcommon.File, returnSize) + + for i := 0; i < returnSize; i++ { + // every 5th post we make a mental health post + if i % 5 == 0 { + toReturn[i] = healthPosts[healthPostIndex] + healthPostIndex++ + } else { + toReturn[i] = posts[i-healthPostIndex] + } + } + return toReturn, nil } - return nil + return posts, nil } func (f *FeedGenerator) GenerateFeedForUser( @@ -218,7 +250,7 @@ func (f *FeedGenerator) GenerateFeedForUser( // Finally inject mental health posts if needed - err = f.injectMentalHealthPosts(ctx, userID, authCredentials, allPosts) + allPosts, err = f.injectMentalHealthPosts(ctx, userID, authCredentials, allPosts) if err != nil { f.logger.Debugf("Failed to inject posts for uid %v, err: %v", userID, err) diff --git a/internal/server/feedGenerator_test.go b/internal/server/feedGenerator_test.go new file mode 100644 index 0000000..6c8e5fb --- /dev/null +++ b/internal/server/feedGenerator_test.go @@ -0,0 +1,276 @@ +package server + +import ( + "context" + "fmt" + pbcommon "github.com/kic/feed/pkg/proto/common" + "testing" + + "go.uber.org/zap" + + "github.com/kic/feed/pkg/logging" +) + +var gen *FeedGenerator + +func configureFriends() FriendServicer { + + friendsList := map[int64][]uint64{ + 1: {2, 3, 5}, + 2: {1, 4, 5}, + 3: {1, 4}, + 4: {2, 3, 5}, + 5: {1, 2, 4}, + } + connections := map[int64][]float32{ + 1: {1.0, 1.5, 0.5}, + 2: {1.0, 1.0, 2.0}, + 3: {1.5, 1.5}, + 4: {1.0, 1.5, 0.5}, + 5: {0.5, 2.0, 0.5}, + } + + return NewMockFriendClient(friendsList, connections) +} + +func configureUsers() UserServicer { + + usernames := map[int64]string{ + 1: "user1", + 2: "user2", + 3: "user3", + 4: "user4", + 5: "user5", + } + + return NewMockUserClient(usernames) +} + +func configureMedia() MediaServicer { + + files := map[int64][]*pbcommon.File{ + 2: { + &pbcommon.File{ + FileName: "u2post1", + FileLocation: "", + Metadata: map[string]string{ + "userID": "2", + }, + DateStored: &pbcommon.Date{ + Year: 2021, + Month: 2, + Day: 2, + }, + }, + &pbcommon.File{ + FileName: "u2post2", + FileLocation: "", + Metadata: map[string]string{ + "userID": "2", + }, + DateStored: &pbcommon.Date{ + Year: 2021, + Month: 1, + Day: 2, + }, + }, + &pbcommon.File{ + FileName: "u2post3", + FileLocation: "", + Metadata: map[string]string{ + "userID": "2", + }, + DateStored: &pbcommon.Date{ + Year: 2020, + Month: 1, + Day: 1, + }, + }, + }, + 3: { + &pbcommon.File{ + FileName: "u3post1", + FileLocation: "", + Metadata: map[string]string{ + "userID": "3", + }, + DateStored: &pbcommon.Date{ + Year: 2021, + Month: 2, + Day: 2, + }, + }, + &pbcommon.File{ + FileName: "u3post2", + FileLocation: "", + Metadata: map[string]string{ + "userID": "3", + }, + DateStored: &pbcommon.Date{ + Year: 2021, + Month: 1, + Day: 2, + }, + }, + &pbcommon.File{ + FileName: "u3post3", + FileLocation: "", + Metadata: map[string]string{ + "userID": "3", + }, + DateStored: &pbcommon.Date{ + Year: 2020, + Month: 1, + Day: 1, + }, + }, + }, + 5: { + &pbcommon.File{ + FileName: "u5post1", + FileLocation: "", + Metadata: map[string]string{ + "userID": "5", + }, + DateStored: &pbcommon.Date{ + Year: 2021, + Month: 2, + Day: 2, + }, + }, + &pbcommon.File{ + FileName: "u5post2", + FileLocation: "", + Metadata: map[string]string{ + "userID": "5", + }, + DateStored: &pbcommon.Date{ + Year: 2021, + Month: 1, + Day: 2, + }, + }, + &pbcommon.File{ + FileName: "u5post3", + FileLocation: "", + Metadata: map[string]string{ + "userID": "5", + }, + DateStored: &pbcommon.Date{ + Year: 2020, + Month: 3, + Day: 1, + }, + }, + }, + -1: { + &pbcommon.File{ + FileName: "mentalHealth1", + FileLocation: "", + Metadata: map[string]string{ + "userID": "-1", + }, + DateStored: &pbcommon.Date{ + Year: 2021, + Month: 2, + Day: 2, + }, + }, + &pbcommon.File{ + FileName: "mentalHealth2", + FileLocation: "", + Metadata: map[string]string{ + "userID": "-1", + }, + DateStored: &pbcommon.Date{ + Year: 2021, + Month: 1, + Day: 2, + }, + }, + &pbcommon.File{ + FileName: "mentalHealth3", + FileLocation: "", + Metadata: map[string]string{ + "userID": "-1", + }, + DateStored: &pbcommon.Date{ + Year: 2020, + Month: 3, + Day: 1, + }, + }, + }, + } + + return NewMockMediaClient(files) +} + +func configureHealth() HealthServicer { + + health := map[int64]int32{ + 1: 2, + 2: -12, + 3: 1, + 4: -20, + 5: 5, + } + + return NewMockHealthClient(health) +} + +func TestMain(m *testing.M) { + gen = NewFeedGenerator( + logging.CreateLogger(zap.DebugLevel), + configureFriends(), + configureUsers(), + configureMedia(), + configureHealth(), + ) + m.Run() +} + +func TestFeedGenerator_GenerateFeedForUser(t *testing.T) { + posts, err := gen.GenerateFeedForUser(context.Background(), 1, "") + + if err != nil { + t.Errorf("Failed to generate feed with error: %v", err) + } + + // first there are three posts on the same day, assert that they are in order of closest friends + if posts[0].Metadata["userID"] != "5" { + t.Errorf("Expected post to be by user 5, got: %v", posts[0].Metadata["userID"]) + } + if posts[1].Metadata["userID"] != "2" { + t.Errorf("Expected post to be by user 2, got: %v", posts[1].Metadata["userID"]) + } + if posts[2].Metadata["userID"] != "3" { + t.Errorf("Expected post to be by user 3, got: %v", posts[2].Metadata["userID"]) + } + if posts[3].Metadata["userID"] != "5" { + t.Errorf("Expected post to be by user 5, got: %v", posts[3].Metadata["userID"]) + } +} + +func TestFeedGenerator_GenerateFeedForNegativeUser(t *testing.T) { + posts, err := gen.GenerateFeedForUser(context.Background(), 4, "") + + if err != nil { + t.Errorf("Failed to generate feed with error: %v", err) + } + + // this user is quite negative, the first post should be mental health based, and then every 5 after that + if posts[0].Metadata["userID"] != "-1" { + t.Errorf("Expected first post to be by user -1, got: %v", posts[0].Metadata["userID"]) + } + if posts[5].Metadata["userID"] != "-1" { + t.Errorf("Expected first post to be by user -1, got: %v", posts[5].Metadata["userID"]) + } + if posts[10].Metadata["userID"] != "-1" { + t.Errorf("Expected first post to be by user -1, got: %v", posts[10].Metadata["userID"]) + } + + for _, post := range posts { + fmt.Println(post) + } +}