Skip to content

Commit 374eac4

Browse files
authored
Hotfix/who to follow (#228)
* fix(explore): remove followed users from who to follow * refactor(explore): remove unnecessary logs * fix(media): add heic extension
1 parent 0a72e64 commit 374eac4

File tree

4 files changed

+104
-31
lines changed

4 files changed

+104
-31
lines changed

src/constants/variables.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const ALLOWED_IMAGE_MIME_TYPES = [
2020
'image/tiff',
2121
'image/svg+xml',
2222
'image/x-icon',
23+
'image/heic',
2324
] as const;
2425
export const MAX_IMAGE_FILE_SIZE = 5 * 1024 * 1024; // 5MB
2526

src/explore/who-to-follow.service.spec.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('WhoToFollowService', () => {
6060
const mock_query_builder = {
6161
select: jest.fn().mockReturnThis(),
6262
where: jest.fn().mockReturnThis(),
63+
andWhere: jest.fn().mockReturnThis(),
6364
orderBy: jest.fn().mockReturnThis(),
6465
addOrderBy: jest.fn().mockReturnThis(),
6566
limit: jest.fn().mockReturnThis(),
@@ -97,6 +98,7 @@ describe('WhoToFollowService', () => {
9798
const mock_query_builder = {
9899
select: jest.fn().mockReturnThis(),
99100
where: jest.fn().mockReturnThis(),
101+
andWhere: jest.fn().mockReturnThis(),
100102
orderBy: jest.fn().mockReturnThis(),
101103
addOrderBy: jest.fn().mockReturnThis(),
102104
limit: jest.fn().mockReturnThis(),
@@ -187,6 +189,89 @@ describe('WhoToFollowService', () => {
187189
expect(user_repository.query).toHaveBeenCalledTimes(5); // 5 sources
188190
});
189191

192+
it('should exclude followed users from popular users backfill', async () => {
193+
const user_id = 'current-user-123';
194+
195+
// Mock minimal responses from all sources (only 1 user)
196+
jest.spyOn(user_repository, 'query')
197+
.mockResolvedValueOnce([{ user_id: 'user-1', mutual_count: 1 }])
198+
.mockResolvedValueOnce([])
199+
.mockResolvedValueOnce([])
200+
.mockResolvedValueOnce([])
201+
.mockResolvedValueOnce([]);
202+
203+
const mock_recommended_users = [
204+
{
205+
user_id: 'user-1',
206+
user_username: 'user1',
207+
user_name: 'User 1',
208+
user_bio: '',
209+
user_avatar_url: '',
210+
user_verified: false,
211+
user_followers: 10,
212+
user_following: 5,
213+
is_following: false,
214+
is_followed: false,
215+
},
216+
];
217+
218+
const mock_popular_users = [
219+
{
220+
id: 'popular-1',
221+
username: 'popular1',
222+
name: 'Popular User 1',
223+
bio: '',
224+
avatar_url: '',
225+
verified: true,
226+
followers: 10000,
227+
following: 100,
228+
},
229+
{
230+
id: 'popular-2',
231+
username: 'popular2',
232+
name: 'Popular User 2',
233+
bio: '',
234+
avatar_url: '',
235+
verified: false,
236+
followers: 5000,
237+
following: 200,
238+
},
239+
];
240+
241+
const mock_query_builder = {
242+
select: jest.fn().mockReturnThis(),
243+
where: jest.fn().mockReturnThis(),
244+
andWhere: jest.fn().mockReturnThis(),
245+
addSelect: jest.fn().mockReturnThis(),
246+
orderBy: jest.fn().mockReturnThis(),
247+
addOrderBy: jest.fn().mockReturnThis(),
248+
setParameter: jest.fn().mockReturnThis(),
249+
limit: jest.fn().mockReturnThis(),
250+
getRawMany: jest.fn().mockResolvedValue(mock_recommended_users),
251+
getMany: jest.fn().mockResolvedValue(mock_popular_users),
252+
};
253+
254+
jest.spyOn(user_repository, 'createQueryBuilder').mockReturnValue(
255+
mock_query_builder as any
256+
);
257+
258+
const result = await service.getWhoToFollow(user_id, 5);
259+
260+
// Verify that andWhere was called to filter out followed users
261+
expect(mock_query_builder.andWhere).toHaveBeenCalledWith(
262+
'user.id != :current_user_id',
263+
{ current_user_id: user_id }
264+
);
265+
266+
// Verify that andWhere was called to exclude followed users
267+
const and_where_calls = mock_query_builder.andWhere.mock.calls;
268+
expect(and_where_calls.length).toBeGreaterThan(1);
269+
const follows_filter_call = and_where_calls.find((call: any[]) =>
270+
call[0].includes('user_follows')
271+
);
272+
expect(follows_filter_call).toBeDefined();
273+
});
274+
190275
it('should backfill with popular users if recommendations are insufficient', async () => {
191276
const user_id = 'current-user-123';
192277

src/explore/who-to-follow.service.ts

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ import { UserRepository } from '../user/user.repository';
44
@Injectable()
55
export class WhoToFollowService {
66
private readonly CONFIG = {
7-
// thresholds
87
MAX_MUTUAL_CONNECTIONS_THRESHOLD: 10,
98
MAX_LIKES_THRESHOLD: 10,
109
MAX_REPLIES_THRESHOLD: 10,
1110
MAX_COMMON_CATEGORIES_THRESHOLD: 2,
1211

13-
// Distribution percentages
1412
DISTRIBUTION: {
1513
FRIENDS_OF_FRIENDS: 40,
1614
LIKES: 25,
@@ -23,9 +21,7 @@ export class WhoToFollowService {
2321
CANDIDATE_MULTIPLIER: 3,
2422
};
2523

26-
/* istanbul ignore start */
2724
constructor(private readonly user_repository: UserRepository) {}
28-
/* istanbul ignore stop */
2925

3026
async getWhoToFollow(current_user_id?: string, limit: number = 30) {
3127
if (!current_user_id) {
@@ -34,12 +30,11 @@ export class WhoToFollowService {
3430

3531
const recommendations = await this.getPersonalizedRecommendations(current_user_id, limit);
3632

37-
// If we don't have enough recommendations, fill with popular users
3833
if (recommendations.length < limit) {
3934
const needed = limit - recommendations.length;
4035
const existing_ids = new Set(recommendations.map((r) => r.id));
4136

42-
const additional_users = await this.getPopularUsers(needed * 2); // Get extra to filter
37+
const additional_users = await this.getPopularUsers(needed * 2, current_user_id);
4338
const filtered_additional = additional_users
4439
.filter((user) => !existing_ids.has(user.id))
4540
.slice(0, needed);
@@ -50,8 +45,8 @@ export class WhoToFollowService {
5045
return recommendations;
5146
}
5247

53-
private async getPopularUsers(limit: number) {
54-
const users = await this.user_repository
48+
private async getPopularUsers(limit: number, current_user_id?: string) {
49+
let query = this.user_repository
5550
.createQueryBuilder('user')
5651
.select([
5752
'user.id',
@@ -63,7 +58,18 @@ export class WhoToFollowService {
6358
'user.followers',
6459
'user.following',
6560
])
66-
.where('user.deleted_at IS NULL')
61+
.where('user.deleted_at IS NULL');
62+
63+
if (current_user_id) {
64+
query = query.andWhere('user.id != :current_user_id', { current_user_id }).andWhere(
65+
`user.id NOT IN (
66+
SELECT followed_id FROM user_follows WHERE follower_id = :current_user_id
67+
)`,
68+
{ current_user_id }
69+
);
70+
}
71+
72+
const users = await query
6773
.orderBy('user.followers', 'DESC')
6874
.addOrderBy('user.verified', 'DESC')
6975
.limit(limit)
@@ -97,7 +103,6 @@ export class WhoToFollowService {
97103
candidate_multiplier,
98104
};
99105

100-
//queries in parallel
101106
const [
102107
friends_of_friends,
103108
interest_based,
@@ -112,14 +117,6 @@ export class WhoToFollowService {
112117
this.getFollowersNotFollowed(current_user_id, limits.followers),
113118
]);
114119

115-
// console.log('\n=== WHO TO FOLLOW DEBUG ===');
116-
// console.log(`Friends of Friends: ${friends_of_friends.length} users`);
117-
// console.log(`Interest-Based: ${interest_based.length} users`);
118-
// console.log(`Liked Users: ${liked_users.length} users`);
119-
// console.log(`Replied Users: ${replied_users.length} users`);
120-
// console.log(`Followers Not Followed: ${followers_not_followed.length} users`);
121-
122-
// Combine users from different sources with distribution-based approach
123120
const combined_users_with_metadata = this.combineByDistribution(
124121
friends_of_friends,
125122
interest_based,
@@ -168,7 +165,6 @@ export class WhoToFollowService {
168165

169166
const user_map = new Map(users.map((u) => [u.user_id, u]));
170167

171-
// Map with metadata and filter out missing users
172168
const users_with_scores = combined_users_with_metadata
173169
.map((metadata) => {
174170
const user = user_map.get(metadata.user_id);
@@ -182,15 +178,6 @@ export class WhoToFollowService {
182178
})
183179
.filter((u) => u !== null);
184180

185-
// console.log('\n=== FINAL RECOMMENDATIONS (ordered by score) ===');
186-
// users_with_scores.forEach((item, index) => {
187-
// console.log(
188-
// `${index + 1}. @${item.user.user_username} - Score: ${item.score.toFixed(2)} - Source: ${item.source} - Data:`,
189-
// item.source_data
190-
// );
191-
// });
192-
// console.log('=========================\n');
193-
194181
return users_with_scores.map((item) => ({
195182
id: item.user.user_id,
196183
username: item.user.user_username,
@@ -325,7 +312,6 @@ export class WhoToFollowService {
325312
[];
326313
const seen = new Set<string>();
327314

328-
// Take top users from each source according to distribution
329315
const add_from_source = (users: any[], count: number) => {
330316
let added = 0;
331317
for (const user of users) {

src/tweets/utils/file-upload.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { BadRequestException } from '@nestjs/common';
22
import { ERROR_MESSAGES } from '../../constants/swagger-messages';
3+
import { ALLOWED_IMAGE_MIME_TYPES } from 'src/constants/variables';
34

45
// Image configuration
56
export const image_file_filter = (req: any, file: any, callback: any) => {
6-
const allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
7+
// const allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
78

8-
if (!allowed_mime_types.includes(file.mimetype)) {
9+
if (!ALLOWED_IMAGE_MIME_TYPES.includes(file.mimetype)) {
910
return callback(new BadRequestException(ERROR_MESSAGES.INVALID_FILE_TYPE), false);
1011
}
1112
callback(null, true);

0 commit comments

Comments
 (0)