@@ -4,14 +4,14 @@ use std::{
44} ;
55
66use rusx:: {
7- resources:: { tweet:: TweetParams , TweetField } ,
7+ resources:: { tweet:: TweetParams , TweetExpansion , TweetField } ,
88 TwitterGateway ,
99} ;
1010
1111use crate :: {
1212 db_persistence:: DbPersistence ,
1313 metrics:: { track_tweets_pulled, track_twitter_api_call} ,
14- models:: raid_submission:: { RaidSubmission , UpdateRaidSubmissionStats } ,
14+ models:: raid_submission:: { UpdateRaidSubmissionStats , ValidRaidSubmissionWithRaiderUsername } ,
1515 services:: alert_service:: AlertService ,
1616 AppError , AppResult , Config ,
1717} ;
@@ -25,13 +25,13 @@ pub struct RaidLeaderboardService {
2525}
2626
2727impl RaidLeaderboardService {
28- fn build_batched_tweet_queries ( submissions : & [ RaidSubmission ] ) -> Vec < Vec < String > > {
28+ fn build_batched_tweet_queries ( submissions : & [ ValidRaidSubmissionWithRaiderUsername ] ) -> Vec < Vec < String > > {
2929 // Twitter's limit for the get ids result
3030 const TWEET_GET_MAX_IDS : usize = 100 ;
3131
3232 submissions
3333 . chunks ( TWEET_GET_MAX_IDS )
34- . map ( |chunk| chunk. iter ( ) . map ( |s| s. id . clone ( ) ) . collect ( ) )
34+ . map ( |chunk| chunk. iter ( ) . map ( |s| s. raid_submission_id . clone ( ) ) . collect ( ) )
3535 . collect ( )
3636 }
3737
@@ -88,7 +88,10 @@ impl RaidLeaderboardService {
8888 } ;
8989
9090 let queries = RaidLeaderboardService :: build_batched_tweet_queries ( & raid_submissions) ;
91- let raider_map: HashMap < String , String > = raid_submissions. into_iter ( ) . map ( |s| ( s. id , s. raider_id ) ) . collect ( ) ;
91+ let submission_to_x_username_map: HashMap < String , String > = raid_submissions
92+ . into_iter ( )
93+ . map ( |s| ( s. raid_submission_id , s. raider_username ) )
94+ . collect ( ) ;
9295
9396 let mut params = TweetParams :: new ( ) ;
9497 params. tweet_fields = Some ( vec ! [
@@ -98,6 +101,7 @@ impl RaidLeaderboardService {
98101 TweetField :: InReplyToUserId ,
99102 TweetField :: ReferencedTweets ,
100103 ] ) ;
104+ params. expansions = Some ( vec ! [ TweetExpansion :: AuthorId ] ) ;
101105
102106 // X Api Request Limit: 15 requests / 15 mins.
103107 // We set interval to 1 min (~1 req/min) to be safe.
@@ -119,6 +123,17 @@ impl RaidLeaderboardService {
119123 tracing:: info!( "No tweets found!." ) ;
120124 continue ;
121125 } ;
126+ let Some ( includes) = & response. includes else {
127+ tracing:: info!( "No includes found!." ) ;
128+ continue ;
129+ } ;
130+ let Some ( users) = & includes. users else {
131+ tracing:: info!( "No users found!." ) ;
132+ continue ;
133+ } ;
134+
135+ let user_id_to_username_map: HashMap < String , String > =
136+ users. iter ( ) . map ( |u| ( u. id . clone ( ) , u. username . clone ( ) ) ) . collect ( ) ;
122137
123138 // Track Twitter API usage (for alerting)
124139 let tweets_pulled = tweets. len ( ) ;
@@ -150,7 +165,13 @@ impl RaidLeaderboardService {
150165 // Check if ANY of the referenced IDs exist in our valid set
151166 refs. iter ( ) . any ( |r| valid_raid_ids. contains ( & r. id ) )
152167 } ) ;
153- let is_eligible_owner = raider_map. get ( & tweet. id ) == tweet. author_id . as_ref ( ) ;
168+ let is_eligible_owner = match (
169+ tweet. author_id . as_ref ( ) . and_then ( |id| user_id_to_username_map. get ( id) ) ,
170+ submission_to_x_username_map. get ( & tweet. id ) ,
171+ ) {
172+ ( Some ( author) , Some ( expected) ) => author. eq_ignore_ascii_case ( expected) ,
173+ _ => false ,
174+ } ;
154175
155176 if is_valid_reply && is_eligible_owner {
156177 valid_tweets. push ( tweet) ;
@@ -193,7 +214,8 @@ mod tests {
193214 use rusx:: {
194215 resources:: {
195216 tweet:: { ReferenceType , ReferencedTweet , Tweet , TweetApi , TweetPublicMetrics } ,
196- TwitterApiResponse ,
217+ user:: User ,
218+ Includes , TwitterApiResponse ,
197219 } ,
198220 MockTweetApi , MockTwitterGateway ,
199221 } ;
@@ -242,6 +264,8 @@ mod tests {
242264 raid_id : i32 ,
243265 target_id : & str ,
244266 submission_id : & str ,
267+ x_username : & str ,
268+ x_user_id : & str ,
245269 ) {
246270 // 1. Seed Raider (Address)
247271 // Handle constraint if address already exists from previous calls in same test
@@ -252,22 +276,34 @@ mod tests {
252276 . execute ( & db. pool )
253277 . await ;
254278
255- // 2. Seed Tweet Author (Foreign Key for RelevantTweet)
279+ // 2. Seed X Association (Required for raider_id to X user ID mapping)
280+ let _ = sqlx:: query (
281+ "INSERT INTO x_associations (quan_address, username) VALUES ($1, $2) ON CONFLICT (quan_address) DO UPDATE SET username = EXCLUDED.username" ,
282+ )
283+ . bind ( raider_id)
284+ . bind ( x_username)
285+ . execute ( & db. pool )
286+ . await ;
287+
288+ // 3. Seed Tweet Author (Foreign Key for RelevantTweet)
256289 let _ = sqlx:: query (
257- "INSERT INTO tweet_authors (id, name, username) VALUES ('auth_1' , 'Auth', 'auth' ) ON CONFLICT DO NOTHING" ,
290+ "INSERT INTO tweet_authors (id, name, username) VALUES ($1 , 'Auth', $2 ) ON CONFLICT DO NOTHING" ,
258291 )
292+ . bind ( x_user_id)
293+ . bind ( x_username)
259294 . execute ( & db. pool )
260295 . await ;
261296
262- // 3 . Seed Relevant Tweet (Target)
297+ // 4 . Seed Relevant Tweet (Target)
263298 let _ = sqlx:: query (
264- "INSERT INTO relevant_tweets (id, author_id, text, created_at) VALUES ($1, 'auth_1' , 'Target', NOW())" ,
299+ "INSERT INTO relevant_tweets (id, author_id, text, created_at) VALUES ($1, $2 , 'Target', NOW())" ,
265300 )
266301 . bind ( target_id)
302+ . bind ( x_user_id)
267303 . execute ( & db. pool )
268304 . await ;
269305
270- // 4 . Create Submission
306+ // 5 . Create Submission
271307 let _ = sqlx:: query (
272308 "INSERT INTO raid_submissions (id, raid_id, raider_id, impression_count, like_count)
273309 VALUES ($1, $2, $3, 0, 0)" ,
@@ -337,28 +373,42 @@ mod tests {
337373 let raider_id = "0xRaider" ;
338374 let sub_id = "12345_submission" ;
339375 let target_id = "target_12345_submission" ;
340- seed_submission ( & db, raider_id, raid_id, target_id, sub_id) . await ;
376+ let x_username = "test_raider" ;
377+ let x_user_id = "1234567890" ; // X user ID
378+ seed_submission ( & db, raider_id, raid_id, target_id, sub_id, x_username, x_user_id) . await ;
341379
342380 // 3. Setup Mocks
343381 let mut mock_gateway = MockTwitterGateway :: new ( ) ;
344382 let mut mock_tweet_api = MockTweetApi :: new ( ) ;
345383
346384 // Expect get_many to be called with the submission ID
385+ let x_user_id_clone = x_user_id. to_string ( ) ;
386+ let target_id_clone = target_id. to_string ( ) ;
387+ let sub_id_clone = sub_id. to_string ( ) ;
347388 mock_tweet_api
348389 . expect_get_many ( )
349390 . with ( predicate:: eq ( vec ! [ sub_id. to_string( ) ] ) , predicate:: always ( ) )
350391 . times ( 1 )
351- . returning ( |_, _| {
392+ . returning ( move |_, _| {
352393 Ok ( TwitterApiResponse {
353394 // Return UPDATED stats (100 impressions, 50 likes)
354395 data : Some ( vec ! [ create_mock_tweet(
355- sub_id ,
356- target_id . to_string ( ) ,
357- raider_id . to_string ( ) ,
396+ & sub_id_clone ,
397+ target_id_clone . clone ( ) ,
398+ x_user_id_clone . clone ( ) , // Use X user ID, not raider_id
358399 100 ,
359400 50 ,
360401 ) ] ) ,
361- includes : None ,
402+ includes : Some ( Includes {
403+ users : Some ( vec ! [ User {
404+ id: x_user_id_clone. clone( ) ,
405+ username: x_username. to_string( ) ,
406+ name: "Test User" . to_string( ) ,
407+ description: None ,
408+ public_metrics: None ,
409+ } ] ) ,
410+ tweets : None ,
411+ } ) ,
362412 meta : None ,
363413 } )
364414 } ) ;
@@ -398,28 +448,41 @@ mod tests {
398448 let raider_id = "0xRaider" ;
399449 let sub_id = "12345_submission" ;
400450 let target_id = "target_12345_submission" ;
401- seed_submission ( & db, raider_id, raid_id, target_id, sub_id) . await ;
451+ let x_username = "test_raider" ;
452+ let x_user_id = "1234567890" ; // X user ID
453+ seed_submission ( & db, raider_id, raid_id, target_id, sub_id, x_username, x_user_id) . await ;
402454
403455 // 3. Setup Mocks
404456 let mut mock_gateway = MockTwitterGateway :: new ( ) ;
405457 let mut mock_tweet_api = MockTweetApi :: new ( ) ;
406458
407459 // Expect get_many to be called with the submission ID
460+ let x_user_id_clone = x_user_id. to_string ( ) ;
461+ let sub_id_clone = sub_id. to_string ( ) ;
408462 mock_tweet_api
409463 . expect_get_many ( )
410464 . with ( predicate:: eq ( vec ! [ sub_id. to_string( ) ] ) , predicate:: always ( ) )
411465 . times ( 1 )
412- . returning ( |_, _| {
466+ . returning ( move |_, _| {
413467 Ok ( TwitterApiResponse {
414468 // Return UPDATED stats (100 impressions, 50 likes)
415469 data : Some ( vec ! [ create_mock_tweet(
416- sub_id ,
470+ & sub_id_clone ,
417471 "invalid_id" . to_string( ) ,
418- raider_id . to_string ( ) ,
472+ x_user_id_clone . clone ( ) , // Use X user ID, not raider_id
419473 100 ,
420474 50 ,
421475 ) ] ) ,
422- includes : None ,
476+ includes : Some ( Includes {
477+ users : Some ( vec ! [ User {
478+ id: x_user_id_clone. clone( ) ,
479+ username: "invalid_username" . to_string( ) ,
480+ name: "Test User" . to_string( ) ,
481+ description: None ,
482+ public_metrics: None ,
483+ } ] ) ,
484+ tweets : None ,
485+ } ) ,
423486 meta : None ,
424487 } )
425488 } ) ;
@@ -460,9 +523,20 @@ mod tests {
460523 // We just need unique IDs.
461524 let mut all_ids = Vec :: new ( ) ;
462525 let raider_id = "0xRaider" ;
526+ let x_username = "test_raider" ;
527+ let x_user_id = "1234567890" ; // X user ID
463528 for i in 0 ..150 {
464529 let id = format ! ( "sub_{}" , i) ;
465- seed_submission ( & db, raider_id, raid_id, & format ! ( "target_{}" , id) , & id) . await ;
530+ seed_submission (
531+ & db,
532+ raider_id,
533+ raid_id,
534+ & format ! ( "target_{}" , id) ,
535+ & id,
536+ x_username,
537+ x_user_id,
538+ )
539+ . await ;
466540 all_ids. push ( id) ;
467541 }
468542
@@ -473,11 +547,12 @@ mod tests {
473547 // We expect `get_many` to be called 2 times.
474548 // 1st time: 100 IDs
475549 // 2nd time: 50 IDs
476- mock_tweet_api. expect_get_many ( ) . times ( 2 ) . returning ( |ids, _| {
550+ let x_user_id_clone = x_user_id. to_string ( ) ;
551+ mock_tweet_api. expect_get_many ( ) . times ( 2 ) . returning ( move |ids, _| {
477552 // Return valid responses for whatever IDs were requested
478553 let tweets = ids
479554 . iter ( )
480- . map ( |id| create_mock_tweet ( id, format ! ( "target_{}" , id) , raider_id . to_string ( ) , 10 , 1 ) )
555+ . map ( |id| create_mock_tweet ( id, format ! ( "target_{}" , id) , x_user_id_clone . clone ( ) , 10 , 1 ) )
481556 . collect ( ) ;
482557 Ok ( TwitterApiResponse {
483558 data : Some ( tweets) ,
0 commit comments