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
32 changes: 32 additions & 0 deletions scripts/seed_test_relevant_tweets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash
set -e

# CONFIG ------------------------------------------
CONTAINER_NAME="task_master_test_db" # Change if your container is named differently
DB_USER="postgres"
DB_NAME="task_master"
SQL_FILE="seed_relevant_tweets.sql"
# --------------------------------------------------

echo "🔧 Generating seed SQL..."

cat << 'EOF' > $SQL_FILE
INSERT INTO relevant_tweets (
id, author_id, text, created_at, fetched_at
)
VALUES
('2037267283798593848', '420308365', 'Test Tweet', '2026-03-26 20:35:45+00', '2026-03-29 12:38:29.171782+00');
EOF

echo "📦 Copying SQL file into container ($CONTAINER_NAME)..."
podman cp "$SQL_FILE" "$CONTAINER_NAME":/"$SQL_FILE"

echo "🚀 Running seed script inside Postgres..."
podman exec -it "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -f "/$SQL_FILE"

echo "🔍 Verifying result..."
podman exec -it "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -c "SELECT * FROM relevant_tweets WHERE id = '2037267283798593848';"

rm -rf "$SQL_FILE"

echo "✅ Seeding complete!"
20 changes: 19 additions & 1 deletion scripts/seed_test_tweet_authors.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ INSERT INTO tweet_authors (
)
VALUES
('1862779229277954048', 'Yuvi Lightman', 'YuviLightman', 0, 0, 0, 0, 0, 0, NOW(), false)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
username = EXCLUDED.username,
followers_count = EXCLUDED.followers_count,
following_count = EXCLUDED.following_count,
tweet_count = EXCLUDED.tweet_count,
listed_count = EXCLUDED.listed_count,
like_count = EXCLUDED.like_count,
media_count = EXCLUDED.media_count,
fetched_at = NOW(),
is_ignored = EXCLUDED.is_ignored;

INSERT INTO tweet_authors (
id, name, username, followers_count, following_count,
tweet_count, listed_count, like_count, media_count, fetched_at, is_ignored
)
VALUES
('420308365', 'nic carter', 'nic_carter', 0, 0, 0, 0, 0, 0, '2026-03-29 12:38:29.166611+00', false)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
username = EXCLUDED.username,
Expand All @@ -37,7 +55,7 @@ echo "🚀 Running seed script inside Postgres..."
podman exec -it "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -f "/$SQL_FILE"

echo "🔍 Verifying result..."
podman exec -it "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -c "SELECT * FROM tweet_authors WHERE id = '1862779229277954048';"
podman exec -it "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -c "SELECT * FROM tweet_authors;"

rm -rf "$SQL_FILE"

Expand Down
4 changes: 2 additions & 2 deletions src/repositories/tweet_author.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ impl TweetAuthorRepository {
}

pub async fn get_whitelist(&self) -> Result<Vec<String>, DbError> {
let ids = sqlx::query_scalar::<_, String>("SELECT id FROM tweet_authors WHERE is_ignored = false")
let usernames = sqlx::query_scalar::<_, String>("SELECT username FROM tweet_authors WHERE is_ignored = false")
.fetch_all(&self.pool)
.await?;

Ok(ids)
Ok(usernames)
}

pub async fn set_ignore_status(&self, id: &str, status: bool) -> Result<(), DbError> {
Expand Down
92 changes: 90 additions & 2 deletions src/services/tweet_synchronizer_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,14 @@ impl TweetSynchronizerService {
// Track metrics for tweets pulled
track_tweets_pulled("search_recent", tweets_pulled);

self.process_sending_raid_targets(&tweet_authors, &relevant_tweets)
.await?;
// We might get duplicate because X will return the tweet including the since_id not just the new ones.
let new_tweets: Vec<NewTweetPayload> = relevant_tweets
.iter()
.filter(|t| Some(&t.id) != last_id.as_ref())
.cloned()
.collect();

self.process_sending_raid_targets(&tweet_authors, &new_tweets).await?;
}

Ok(())
Expand All @@ -240,6 +246,7 @@ impl TweetSynchronizerService {

#[cfg(test)]
mod tests {
// NOTE: These tests share the test DB; run with `cargo test ... -- --test-threads=1` (see scripts/run_non_chain_tests.sh).
use super::*;
use crate::config::{Config, TelegramBotConfig};
use crate::models::raid_quest::CreateRaidQuest;
Expand Down Expand Up @@ -497,6 +504,87 @@ mod tests {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}

/// API may repeat the `since_id` tweet; it must not be forwarded to Telegram for raids.
#[tokio::test]
async fn test_sync_excludes_last_id_from_telegram_when_raid_active() {
let (db, mock_tg, telegram_service, alert_service, config) = setup_deps().await;

let dummy_author = crate::models::tweet_author::NewAuthorPayload {
id: "old_u".to_string(),
name: "Old".to_string(),
username: "old".to_string(),
followers_count: 0,
following_count: 0,
tweet_count: 0,
listed_count: 0,
like_count: 0,
media_count: 0,
is_ignored: Some(false),
};

db.tweet_authors.upsert(&dummy_author).await.unwrap();

let last_tweet_id = "since_id_dup";
db.relevant_tweets
.upsert_many(&vec![crate::models::relevant_tweet::NewTweetPayload {
id: last_tweet_id.to_string(),
author_id: dummy_author.id.clone(),
text: "Already seen".to_string(),
impression_count: 0,
reply_count: 0,
retweet_count: 0,
like_count: 0,
created_at: chrono::Utc::now(),
}])
.await
.unwrap();

db.raid_quests
.create(&CreateRaidQuest {
name: "Test Raid".to_string(),
})
.await
.unwrap();

Mock::given(method("POST"))
.and(path("/bot123456/sendMessage"))
.respond_with(ResponseTemplate::new(200))
.expect(0)
.mount(&mock_tg)
.await;

let mut mock_gateway = MockTwitterGateway::new();
let mut mock_search = MockSearchApi::new();

let author = dummy_author.clone();
mock_search.expect_recent().returning(move |_| {
Ok(TwitterApiResponse {
data: Some(vec![create_mock_tweet(last_tweet_id, &author.id)]),
includes: Some(Includes {
users: Some(vec![create_mock_user(&author.id, &author.username)]),
tweets: None,
}),
meta: None,
})
});

let search_api_arc: Arc<dyn SearchApi> = Arc::new(mock_search);

mock_gateway.expect_search().times(1).return_const(search_api_arc);

let service = TweetSynchronizerService::new(
db.clone(),
Arc::new(mock_gateway),
telegram_service,
alert_service,
config,
);

service.sync_relevant_tweets().await.unwrap();

tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}

#[tokio::test]
async fn test_pagination_logic_uses_last_id() {
let (db, _mock_tg, telegram_service, alert_service, config) = setup_deps().await;
Expand Down
Loading