diff --git a/bot/src/events/actions/finalize.rs b/bot/src/events/actions/finalize.rs index 1ee5874..1e11e51 100644 --- a/bot/src/events/actions/finalize.rs +++ b/bot/src/events/actions/finalize.rs @@ -44,7 +44,7 @@ impl PullRequestFinalize { .await?; info.executed = true; - if !info.allowed_repo || info.paused { + if info.paused_repo || info.blocked_repo { return Ok(EventResult::success(false)); } diff --git a/bot/src/events/actions/merge.rs b/bot/src/events/actions/merge.rs index 3cfa175..8190782 100644 --- a/bot/src/events/actions/merge.rs +++ b/bot/src/events/actions/merge.rs @@ -27,7 +27,7 @@ impl PullRequestMerge { context.near.send_merge(pr).await?; info.merged = true; - if !info.allowed_repo || info.paused { + if info.paused_repo || info.blocked_repo { return Ok(EventResult::success(false)); } diff --git a/bot/src/events/actions/stale.rs b/bot/src/events/actions/stale.rs index 89b562b..15b4f69 100644 --- a/bot/src/events/actions/stale.rs +++ b/bot/src/events/actions/stale.rs @@ -31,7 +31,7 @@ impl PullRequestStale { ..*check_info }; - if !check_info.allowed_repo || check_info.paused { + if check_info.paused_repo || check_info.blocked_repo { return Ok(EventResult::success(false)); } diff --git a/bot/src/events/common.rs b/bot/src/events/common.rs index 0a3176d..3f9380d 100644 --- a/bot/src/events/common.rs +++ b/bot/src/events/common.rs @@ -15,6 +15,22 @@ impl Context { .await } + pub async fn add_repo(&self, repo_info: &RepoInfo) -> anyhow::Result<()> { + let result = self.near.add_repo(&repo_info.owner, &repo_info.repo).await; + info!("Added repo {repo_info:?} to near"); + let message = format!( + "New repo in the [{}](https://github.com/{}/{}/pull/{}) was {}", + repo_info.full_id, + repo_info.owner, + repo_info.repo, + repo_info.number, + result.as_ref().map(|_| "added").unwrap_or("failed") + ); + self.telegram.send_to_telegram(&message, &Level::INFO); + + result.map(|_| ()) + } + pub async fn reply_with_text( &self, repo_info: &RepoInfo, diff --git a/bot/src/events/issue_commands/mod.rs b/bot/src/events/issue_commands/mod.rs index 5a63ab1..934dabc 100644 --- a/bot/src/events/issue_commands/mod.rs +++ b/bot/src/events/issue_commands/mod.rs @@ -43,9 +43,9 @@ impl Command { first_reply: bool, sender: &User, ) -> anyhow::Result { - if !check_info.allowed_repo { + if check_info.blocked_repo { info!( - "Sloth called for a PR from not allowed org: {}. Skipping", + "Sloth called for a PR from blocked repo: {}. Skipping", repo_info.full_id ); if first_reply { @@ -53,7 +53,7 @@ impl Command { .reply_with_error( repo_info, None, - MsgCategory::ErrorOrgNotInAllowedListMessage, + MsgCategory::ErrorRepoIsBanned, vec![("pr_author_username", sender.login.clone())], ) .await?; diff --git a/bot/src/events/pr_commands/mod.rs b/bot/src/events/pr_commands/mod.rs index d0f3f7a..87edf28 100644 --- a/bot/src/events/pr_commands/mod.rs +++ b/bot/src/events/pr_commands/mod.rs @@ -79,9 +79,13 @@ impl Command { sender: &User, first_reply: bool, ) -> anyhow::Result { - if !check_info.allowed_repo { + if check_info.new_repo { + context.add_repo(&pr.repo_info).await?; + } + + if check_info.blocked_repo { info!( - "Sloth called for a PR from not allowed org: {}. Skipping", + "Sloth called for a PR from blocked repo: {}. Skipping", pr.repo_info.full_id ); if first_reply { @@ -89,7 +93,7 @@ impl Command { .reply_with_error( &pr.repo_info, None, - MsgCategory::ErrorOrgNotInAllowedListMessage, + MsgCategory::ErrorRepoIsBanned, vec![("pr_author_username", pr.author.login.clone())], ) .await?; @@ -99,7 +103,7 @@ impl Command { return Ok(EventResult::Skipped); } - if check_info.paused && !matches!(self, Command::Unpause(_) | Command::Pause(_)) { + if check_info.paused_repo && !matches!(self, Command::Unpause(_) | Command::Pause(_)) { info!( "Sloth called for a PR from paused repo: {}. Skipping", pr.repo_info.full_id diff --git a/bot/src/events/pr_commands/pause.rs b/bot/src/events/pr_commands/pause.rs index 2617993..8e1ca2b 100644 --- a/bot/src/events/pr_commands/pause.rs +++ b/bot/src/events/pr_commands/pause.rs @@ -21,7 +21,7 @@ impl BotPaused { check_info: &mut PRInfo, sender: &User, ) -> anyhow::Result { - if check_info.paused { + if check_info.paused_repo { info!( "Tried to pause a PR from paused repo: {}. Skipping", pr.repo_info.full_id @@ -58,7 +58,7 @@ impl BotPaused { .near .send_pause(&pr.repo_info.owner, &pr.repo_info.repo) .await?; - check_info.paused = true; + check_info.paused_repo = true; context .reply( &pr.repo_info, @@ -102,12 +102,12 @@ impl BotUnpaused { return Ok(EventResult::Skipped); } - if info.paused { + if info.paused_repo { context .near .send_unpause(&repo_info.owner, &repo_info.repo) .await?; - info.paused = false; + info.paused_repo = false; debug!("Unpaused PR {}", repo_info.full_id); let msg = if self.from_issue { MsgCategory::UnpauseIssueMessage diff --git a/bot/src/messages.rs b/bot/src/messages.rs index 7962f4f..d5fcdc6 100644 --- a/bot/src/messages.rs +++ b/bot/src/messages.rs @@ -36,7 +36,7 @@ pub enum MsgCategory { ErrorPausedMessage, ErrorLateScoringMessage, ErrorSelfScore, - ErrorOrgNotInAllowedListMessage, + ErrorRepoIsBanned, FirstTimeContribution, FirstWeekContribution, @@ -257,9 +257,7 @@ impl MessageLoader { MsgCategory::ErrorLateIncludeMessage => &self.error_late_include_messages, MsgCategory::ErrorLateScoringMessage => &self.error_late_scoring_messages, MsgCategory::ErrorSelfScore => &self.error_selfscore_messages, - MsgCategory::ErrorOrgNotInAllowedListMessage => { - &self.error_org_not_in_allowed_list_messages - } + MsgCategory::ErrorRepoIsBanned => &self.error_org_not_in_allowed_list_messages, MsgCategory::ErrorPausePausedMessage => &self.error_pause_paused_messages, MsgCategory::ErrorUnpauseUnpausedMessage => &self.error_unpause_unpaused_messages, MsgCategory::ErrorPausedMessage => &self.error_paused_messages, @@ -672,8 +670,9 @@ mod tests { let mut pr_info = shared::PRInfo { votes: vec![], - allowed_repo: true, - paused: false, + new_repo: false, + paused_repo: false, + blocked_repo: false, merged: false, executed: false, excluded: false, diff --git a/contract/src/lib.rs b/contract/src/lib.rs index e1acf3e..c916fc4 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -11,7 +11,7 @@ use shared::{ TimePeriodString, UserId, UserPeriodDataV2, VersionedAccount, VersionedPR, VersionedStreak, VersionedStreakUserData, VersionedUserPeriodData, }; -use types::{Repository, VersionedRepository}; +use types::{Repository, RepositoryStatus, RepositoryV2, VersionedRepository}; pub mod events; pub mod migrate; @@ -155,7 +155,7 @@ impl Contract { override_exclude: bool, ) { self.assert_sloth(); - self.assert_repo_allowed(&organization, &repo); + self.assert_repo_active(&organization, &repo); let (user_id, _) = self.get_or_create_account(&user); let pr_id = format!("{organization}/{repo}/{pr_number}"); @@ -245,23 +245,6 @@ impl Contract { self.excluded_prs.insert(pr_id); } - pub fn exclude_repo(&mut self, organization: String, repo: String) { - self.assert_sloth(); - - self.repos.remove(&(organization, repo)); - } - - pub fn bulk_remove_orgs(&mut self, allowed_orgs: Vec) { - self.assert_sloth(); - - for allowed_org in allowed_orgs { - for repo in allowed_org.repos { - self.repos - .remove(&(allowed_org.organization.to_string(), repo.login)); - } - } - } - pub fn bulk_include_orgs(&mut self, allowed_orgs: Vec) { self.assert_sloth(); @@ -282,45 +265,79 @@ impl Contract { } } + pub fn ban_repo(&mut self, organization: String, repo: String) { + self.assert_sloth(); + + self.repos.insert( + (organization, repo), + VersionedRepository::V2(RepositoryV2 { + status: RepositoryStatus::Blocked, + }), + ); + } + + pub fn unban_repo(&mut self, organization: String, repo: String) { + self.assert_sloth(); + + self.repos.insert( + (organization, repo), + VersionedRepository::V2(RepositoryV2 { + status: RepositoryStatus::Active, + }), + ); + } + pub fn include_repo(&mut self, organization: String, repo: String) { self.assert_sloth(); self.repos.insert( (organization, repo), - VersionedRepository::V1(Repository { paused: false }), + VersionedRepository::V2(RepositoryV2 { + status: RepositoryStatus::Active, + }), ); } pub fn pause_repo(&mut self, organization: String, repo: String) { self.assert_sloth(); - let repo = self.repos.get_mut(&(organization, repo)); - if repo.is_none() { - env::panic_str("Repository is not allowlisted") - } - - let repo = repo.unwrap(); - match repo { - VersionedRepository::V1(repo) => { - repo.paused = true; + let repository = self.repos.get(&(organization.clone(), repo.clone())); + let repository = repository.map(|r| { + let repository: RepositoryV2 = r.into(); + repository + }); + if let Some(repository) = repository { + if repository.status == RepositoryStatus::Blocked { + env::panic_str("Repository is blocked") } } + self.repos.insert( + (organization, repo), + VersionedRepository::V2(RepositoryV2 { + status: RepositoryStatus::Paused, + }), + ); } pub fn unpause_repo(&mut self, organization: String, repo: String) { self.assert_sloth(); - let repo = self.repos.get_mut(&(organization, repo)); - if repo.is_none() { - env::panic_str("Repository is not allowlisted") - } - - let repo = repo.unwrap(); - match repo { - VersionedRepository::V1(repo) => { - repo.paused = false; + let repository = self.repos.get(&(organization.clone(), repo.clone())); + let repository = repository.map(|r| { + let repository: RepositoryV2 = r.into(); + repository + }); + if let Some(repository) = repository { + if repository.status == RepositoryStatus::Blocked { + env::panic_str("Repository is blocked") } } + self.repos.insert( + (organization, repo), + VersionedRepository::V2(RepositoryV2 { + status: RepositoryStatus::Active, + }), + ); } pub fn sloth_stale(&mut self, pr_id: String) { @@ -575,14 +592,14 @@ impl Contract { } } - pub fn assert_repo_allowed(&self, organization: &str, repo: &str) { + pub fn assert_repo_active(&self, organization: &str, repo: &str) { let repo: Option<_> = self.repos.get(&(organization.to_owned(), repo.to_owned())); if let Some(repo) = repo { - if repo.is_paused() { - env::panic_str("Repo is paused") + if !repo.is_active() { + env::panic_str("Repo is not active") } } else { - env::panic_str("Repo is not allowlisted") + env::panic_str("Repo is not supported") } } } diff --git a/contract/src/migrate.rs b/contract/src/migrate.rs index 23b47a1..e7a09ae 100644 --- a/contract/src/migrate.rs +++ b/contract/src/migrate.rs @@ -4,18 +4,15 @@ use super::*; impl Contract { #[init(ignore_state)] #[private] - pub fn migrate() -> Self { + pub fn migrate(list: Vec) -> Self { let mut state: Self = env::state_read().unwrap(); - let users = state.users.len(); - for user_id in 0..users { - let period_data = state.period_data(user_id, &"102024".to_string()); - if let Some(period_data) = period_data { - state.sloths_per_period.insert( - (user_id, "rosctober2024".to_string()), - VersionedUserPeriodData::V2(period_data), - ); - } + for repo in list { + let mut split = repo.split('/'); + let owner = split.next().unwrap(); + let name = split.next().unwrap(); + + state.repos.remove(&(owner.to_string(), name.to_string())); } state diff --git a/contract/src/tests.rs b/contract/src/tests.rs index fef8334..5c8afa2 100644 --- a/contract/src/tests.rs +++ b/contract/src/tests.rs @@ -35,6 +35,7 @@ impl ContractExt { repos: vec![Repo { login: "devbot".to_owned(), paused: false, + blocked: false, }], }], ); diff --git a/contract/src/types.rs b/contract/src/types.rs index 2ac6966..8918ea8 100644 --- a/contract/src/types.rs +++ b/contract/src/types.rs @@ -4,24 +4,82 @@ use near_sdk::{ NearSchema, }; -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, NearSchema)] +#[derive( + Debug, Clone, Copy, BorshSerialize, BorshDeserialize, Serialize, Deserialize, NearSchema, +)] #[serde(crate = "near_sdk::serde")] #[borsh(crate = "near_sdk::borsh")] pub enum VersionedRepository { V1(Repository), + V2(RepositoryV2), +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + NearSchema, +)] +#[serde(crate = "near_sdk::serde")] +#[borsh(crate = "near_sdk::borsh")] +pub enum RepositoryStatus { + Paused, + Active, + Blocked, } impl VersionedRepository { + pub fn is_active(&self) -> bool { + let v2: RepositoryV2 = self.into(); + v2.status == RepositoryStatus::Active + } + pub fn is_paused(&self) -> bool { - match self { - VersionedRepository::V1(data) => data.paused, + let v2: RepositoryV2 = self.into(); + v2.status == RepositoryStatus::Paused + } + + pub fn is_blocked(&self) -> bool { + let v2: RepositoryV2 = self.into(); + v2.status == RepositoryStatus::Blocked + } +} + +impl From<&VersionedRepository> for RepositoryV2 { + fn from(value: &VersionedRepository) -> Self { + match value { + VersionedRepository::V1(data) => RepositoryV2 { + status: if data.paused { + RepositoryStatus::Paused + } else { + RepositoryStatus::Active + }, + }, + VersionedRepository::V2(data) => *data, } } } -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, NearSchema)] +#[derive( + Debug, Copy, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, NearSchema, +)] #[serde(crate = "near_sdk::serde")] #[borsh(crate = "near_sdk::borsh")] pub struct Repository { pub paused: bool, } + +#[derive( + Debug, Copy, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, NearSchema, +)] +#[serde(crate = "near_sdk::serde")] +#[borsh(crate = "near_sdk::borsh")] +pub struct RepositoryV2 { + pub status: RepositoryStatus, +} diff --git a/contract/src/views.rs b/contract/src/views.rs index e523530..d6ba430 100644 --- a/contract/src/views.rs +++ b/contract/src/views.rs @@ -15,10 +15,13 @@ impl Contract { let repo_allowed = self.repos.get(&(organization, repo)); PRInfo { - allowed_repo: repo_allowed.is_some(), - paused: repo_allowed + new_repo: repo_allowed.is_none(), + paused_repo: repo_allowed .map(|repo| repo.is_paused()) .unwrap_or_default(), + blocked_repo: repo_allowed + .map(|repo| repo.is_blocked()) + .unwrap_or_default(), exist: pr.is_some(), merged: pr .as_ref() @@ -140,6 +143,7 @@ impl Contract { .push(Repo { login: repo.clone(), paused: data.is_paused(), + blocked: data.is_blocked(), }); } diff --git a/server/.sqlx/query-59946ed7facd4066fab6d3ff721b2b9f74670b2f56cb42bbfd88553820c73581.json b/server/.sqlx/query-59946ed7facd4066fab6d3ff721b2b9f74670b2f56cb42bbfd88553820c73581.json new file mode 100644 index 0000000..2b70f34 --- /dev/null +++ b/server/.sqlx/query-59946ed7facd4066fab6d3ff721b2b9f74670b2f56cb42bbfd88553820c73581.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM repos\n WHERE (organization_id, name) NOT IN (\n SELECT o.id, r.name\n FROM unnest($1::text[], $2::text[]) AS p(org, repo)\n JOIN organizations o ON o.login = p.org\n JOIN repos r ON r.organization_id = o.id AND r.name = p.repo\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "TextArray", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "59946ed7facd4066fab6d3ff721b2b9f74670b2f56cb42bbfd88553820c73581" +} diff --git a/server/.sqlx/query-54f130bb95d418a0e2bf61b7709966efbd6d51c0d7763cf6a0a32b28a9c8affa.json b/server/.sqlx/query-a9146592da9133424ee9c022b7ff390ba82a1c72f3525f4ef85e61f4ab9f3955.json similarity index 86% rename from server/.sqlx/query-54f130bb95d418a0e2bf61b7709966efbd6d51c0d7763cf6a0a32b28a9c8affa.json rename to server/.sqlx/query-a9146592da9133424ee9c022b7ff390ba82a1c72f3525f4ef85e61f4ab9f3955.json index da0d676..e0f4bd7 100644 --- a/server/.sqlx/query-54f130bb95d418a0e2bf61b7709966efbd6d51c0d7763cf6a0a32b28a9c8affa.json +++ b/server/.sqlx/query-a9146592da9133424ee9c022b7ff390ba82a1c72f3525f4ef85e61f4ab9f3955.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH top_contributors AS (\n SELECT\n pr.repo_id,\n u.login AS contributor_login,\n u.full_name AS contributor_full_name,\n ROW_NUMBER() OVER (\n PARTITION BY pr.repo_id\n ORDER BY\n SUM(pr.rating) DESC\n ) AS rank\n FROM\n pull_requests pr\n JOIN users u ON pr.author_id = u.id\n WHERE\n pr.included_at >= $3\n GROUP BY\n pr.repo_id,\n u.id\n)\nSELECT\n o.login AS organization,\n o.full_name AS organization_full_name,\n r.name AS name,\n COALESCE(COUNT(pr.id), 0) AS total_prs,\n COALESCE(SUM(pr.score), 0) AS total_score,\n COALESCE(SUM(pr.rating), 0) AS total_rating,\n MAX(tc.contributor_full_name) AS contributor_full_name,\n MAX(tc.contributor_login) AS contributor_login,\n r.primary_language,\n r.open_issues,\n r.stars,\n r.forks\nFROM\n repos r\n JOIN organizations o ON r.organization_id = o.id\n LEFT JOIN pull_requests pr ON pr.repo_id = r.id\n LEFT JOIN top_contributors tc ON tc.repo_id = r.id\n AND tc.rank = 1\nWHERE\n r.paused = false\nGROUP BY\n o.login,\n o.full_name,\n r.name,\n r.primary_language,\n r.open_issues,\n r.stars,\n r.forks\nORDER BY\n total_prs DESC,\n open_issues DESC,\n organization ASC,\n name ASC\nLIMIT\n $1 OFFSET $2;\n", + "query": "WITH top_contributors AS (\n SELECT\n pr.repo_id,\n u.login AS contributor_login,\n u.full_name AS contributor_full_name,\n ROW_NUMBER() OVER (\n PARTITION BY pr.repo_id\n ORDER BY\n SUM(pr.rating) DESC\n ) AS rank\n FROM\n pull_requests pr\n JOIN users u ON pr.author_id = u.id\n WHERE\n pr.included_at >= $3\n GROUP BY\n pr.repo_id,\n u.id\n)\nSELECT\n o.login AS organization,\n o.full_name AS organization_full_name,\n r.name AS name,\n COALESCE(COUNT(pr.id), 0) AS total_prs,\n COALESCE(SUM(pr.score), 0) AS total_score,\n COALESCE(SUM(pr.rating), 0) AS total_rating,\n MAX(tc.contributor_full_name) AS contributor_full_name,\n MAX(tc.contributor_login) AS contributor_login,\n r.primary_language,\n r.open_issues,\n r.stars,\n r.forks\nFROM\n repos r\n JOIN organizations o ON r.organization_id = o.id\n LEFT JOIN pull_requests pr ON pr.repo_id = r.id\n LEFT JOIN top_contributors tc ON tc.repo_id = r.id\n AND tc.rank = 1\nWHERE\n r.paused = false\n AND r.banned = false\nGROUP BY\n o.login,\n o.full_name,\n r.name,\n r.primary_language,\n r.open_issues,\n r.stars,\n r.forks\nORDER BY\n total_prs DESC,\n open_issues DESC,\n organization ASC,\n name ASC\nLIMIT\n $1 OFFSET $2;\n", "describe": { "columns": [ { @@ -86,5 +86,5 @@ true ] }, - "hash": "54f130bb95d418a0e2bf61b7709966efbd6d51c0d7763cf6a0a32b28a9c8affa" + "hash": "a9146592da9133424ee9c022b7ff390ba82a1c72f3525f4ef85e61f4ab9f3955" } diff --git a/server/.sqlx/query-c4e0304c9ad167c4163358a5ecc9155ce08c075a4befe670334169368d653280.json b/server/.sqlx/query-fe6cdc0fa89e21b90cb5c4227e4bbca8d3c0c92b3030a9a43bd8a6995c7e4f63.json similarity index 62% rename from server/.sqlx/query-c4e0304c9ad167c4163358a5ecc9155ce08c075a4befe670334169368d653280.json rename to server/.sqlx/query-fe6cdc0fa89e21b90cb5c4227e4bbca8d3c0c92b3030a9a43bd8a6995c7e4f63.json index 9ba6be3..0e435d8 100644 --- a/server/.sqlx/query-c4e0304c9ad167c4163358a5ecc9155ce08c075a4befe670334169368d653280.json +++ b/server/.sqlx/query-fe6cdc0fa89e21b90cb5c4227e4bbca8d3c0c92b3030a9a43bd8a6995c7e4f63.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE repos\n SET name = $2, paused = $3\n WHERE organization_id = $1 AND name = $2\n RETURNING id\n ", + "query": "\n UPDATE repos\n SET name = $2, paused = $3, banned = $4\n WHERE organization_id = $1 AND name = $2\n RETURNING id\n ", "describe": { "columns": [ { @@ -13,6 +13,7 @@ "Left": [ "Int4", "Text", + "Bool", "Bool" ] }, @@ -20,5 +21,5 @@ false ] }, - "hash": "c4e0304c9ad167c4163358a5ecc9155ce08c075a4befe670334169368d653280" + "hash": "fe6cdc0fa89e21b90cb5c4227e4bbca8d3c0c92b3030a9a43bd8a6995c7e4f63" } diff --git a/server/migrations/20241105142153_ban_repo.sql b/server/migrations/20241105142153_ban_repo.sql new file mode 100644 index 0000000..4b53b4f --- /dev/null +++ b/server/migrations/20241105142153_ban_repo.sql @@ -0,0 +1,5 @@ +-- Add migration script here +ALTER TABLE + repos +ADD + COLUMN banned BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/server/sql/get_repo_leaderboard.sql b/server/sql/get_repo_leaderboard.sql index 01baa9d..31bec16 100644 --- a/server/sql/get_repo_leaderboard.sql +++ b/server/sql/get_repo_leaderboard.sql @@ -38,6 +38,7 @@ FROM AND tc.rank = 1 WHERE r.paused = false + AND r.banned = false GROUP BY o.login, o.full_name, diff --git a/server/src/contract_pull.rs b/server/src/contract_pull.rs index 65fba50..a1d2838 100644 --- a/server/src/contract_pull.rs +++ b/server/src/contract_pull.rs @@ -119,6 +119,7 @@ async fn fetch_and_store_repos( tx: &mut Transaction<'static, Postgres>, ) -> anyhow::Result<()> { let organizations = near_client.repos().await?; + DB::remove_non_existent_repos(tx, &organizations).await?; for org in organizations { let organization_id = DB::upsert_organization(tx, &org.organization) .await diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index 7d2635c..da576fb 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -3,7 +3,9 @@ use rocket::{ Build, Rocket, }; use rocket_db_pools::Database; -use shared::{PRv2, Repo, StreakUserData, TimePeriod, TimePeriodString, UserPeriodDataV2}; +use shared::{ + AllowedRepos, PRv2, Repo, StreakUserData, TimePeriod, TimePeriodString, UserPeriodDataV2, +}; use sqlx::{PgPool, Postgres, Transaction}; #[derive(Database, Clone, Debug)] @@ -166,13 +168,14 @@ impl DB { let rec = sqlx::query!( r#" UPDATE repos - SET name = $2, paused = $3 + SET name = $2, paused = $3, banned = $4 WHERE organization_id = $1 AND name = $2 RETURNING id "#, organization_id, repo.login, - repo.paused + repo.paused, + repo.blocked ) .fetch_optional(tx.as_mut()) .await?; @@ -237,6 +240,44 @@ impl DB { Ok(()) } + pub async fn remove_non_existent_repos( + tx: &mut Transaction<'static, Postgres>, + repos: &[AllowedRepos], + ) -> anyhow::Result<()> { + let repo_keys: Vec<(String, String)> = repos + .iter() + .flat_map(|allowed_repos| { + allowed_repos + .repos + .iter() + .map(|repo| (allowed_repos.organization.clone(), repo.login.clone())) + }) + .collect(); + + sqlx::query!( + r#" + DELETE FROM repos + WHERE (organization_id, name) NOT IN ( + SELECT o.id, r.name + FROM unnest($1::text[], $2::text[]) AS p(org, repo) + JOIN organizations o ON o.login = p.org + JOIN repos r ON r.organization_id = o.id AND r.name = p.repo + ) + "#, + &repo_keys + .iter() + .map(|(org, _)| org.clone()) + .collect::>(), + &repo_keys + .iter() + .map(|(_, repo)| repo.clone()) + .collect::>(), + ) + .execute(tx.as_mut()) + .await?; + Ok(()) + } + pub async fn upsert_pull_request( tx: &mut Transaction<'static, Postgres>, repo_id: i32, diff --git a/shared/src/github.rs b/shared/src/github.rs index 4ad3128..21c1110 100644 --- a/shared/src/github.rs +++ b/shared/src/github.rs @@ -8,7 +8,7 @@ pub struct User { } impl User { - pub fn new(login: String, contributor_type: AuthorAssociation) -> Self { + pub const fn new(login: String, contributor_type: AuthorAssociation) -> Self { Self { login, contributor_type, diff --git a/shared/src/lib.rs b/shared/src/lib.rs index af84ef6..7a37edf 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -79,7 +79,7 @@ pub struct AccountWithPermanentPercentageBonus { } impl AccountWithPermanentPercentageBonus { - pub fn new(github_handle: GithubHandle) -> Self { + pub const fn new(github_handle: GithubHandle) -> Self { Self { github_handle, permanent_percentage_bonus: Vec::new(), @@ -173,27 +173,27 @@ impl VersionedUserPeriodData { pub fn pr_opened(&mut self) { let mut data: UserPeriodDataV2 = self.clone().into(); data.prs_opened += 1; - *self = VersionedUserPeriodData::V2(data); + *self = Self::V2(data); } pub fn reward_for_scoring(&mut self) { let mut data: UserPeriodDataV2 = self.clone().into(); data.prs_scored += 1; data.total_rating += 25; - *self = VersionedUserPeriodData::V2(data); + *self = Self::V2(data); } pub fn remove_reward_for_scoring(&mut self) { let mut data: UserPeriodDataV2 = self.clone().into(); data.prs_scored -= 1; data.total_rating -= 25; - *self = VersionedUserPeriodData::V2(data); + *self = Self::V2(data); } pub fn pr_merged(&mut self) { let mut data: UserPeriodDataV2 = self.clone().into(); data.prs_merged += 1; - *self = VersionedUserPeriodData::V2(data); + *self = Self::V2(data); } pub fn pr_scored(&mut self, old_score: u32, new_score: u32) { @@ -213,14 +213,14 @@ impl VersionedUserPeriodData { data.largest_rating_per_pr = rating; } - *self = VersionedUserPeriodData::V2(data); + *self = Self::V2(data); } pub fn pr_executed(&mut self) { let mut data: UserPeriodDataV2 = self.clone().into(); data.executed_prs += 1; - *self = VersionedUserPeriodData::V2(data); + *self = Self::V2(data); } pub fn pr_bonus_rating(&mut self, total_rating: u32, old_rating: u32) { @@ -232,7 +232,7 @@ impl VersionedUserPeriodData { data.largest_rating_per_pr = total_rating; } - *self = VersionedUserPeriodData::V2(data); + *self = Self::V2(data); } pub fn pr_closed(&mut self, score: u32) { @@ -240,14 +240,14 @@ impl VersionedUserPeriodData { data.prs_opened -= 1; data.total_score -= score; data.total_rating -= score * 10; - *self = VersionedUserPeriodData::V2(data); + *self = Self::V2(data); } } impl From for UserPeriodDataV2 { fn from(message: VersionedUserPeriodData) -> Self { match message { - VersionedUserPeriodData::V1(x) => UserPeriodDataV2 { + VersionedUserPeriodData::V1(x) => Self { total_score: x.total_score, executed_prs: x.executed_prs, largest_score: x.largest_score, @@ -270,6 +270,7 @@ impl From for UserPeriodDataV2 { Serialize, Deserialize, NearSchema, + Eq, PartialEq, Default, )] @@ -293,6 +294,7 @@ pub struct UserPeriodData { Serialize, Deserialize, NearSchema, + Eq, PartialEq, Default, )] @@ -335,6 +337,7 @@ impl User { pub struct Repo { pub login: String, pub paused: bool, + pub blocked: bool, } #[derive(Serialize, Deserialize, BorshDeserialize, BorshSerialize, NearSchema)] diff --git a/shared/src/near.rs b/shared/src/near.rs index 175bd4d..90c030e 100644 --- a/shared/src/near.rs +++ b/shared/src/near.rs @@ -306,6 +306,22 @@ impl NearClient { process_execution_final_result(result) } + #[instrument(skip(self))] + pub async fn add_repo(&self, organization: &str, repo: &str) -> anyhow::Result> { + let result = self + .contract + .call_function( + "include_repo", + json!({ "organization": organization, "repo": repo }), + )? + .transaction() + .with_signer(self.contract.0.clone(), self.signer.clone()) + .send_to(&self.network) + .await + .map_err(|e| anyhow::anyhow!("Failed to call add_repo: {:?}", e))?; + process_execution_final_result(result) + } + #[instrument(skip(self))] pub async fn user_info( &self, diff --git a/shared/src/pr.rs b/shared/src/pr.rs index e958e6d..c7b0dc5 100644 --- a/shared/src/pr.rs +++ b/shared/src/pr.rs @@ -8,7 +8,15 @@ pub const SCORE_TIMEOUT_IN_SECONDS: Timestamp = 24 * 60 * 60; pub const SCORE_TIMEOUT_IN_NANOSECONDS: Timestamp = SCORE_TIMEOUT_IN_SECONDS * 1_000_000_000; #[derive( - Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema, PartialEq, + Debug, + Clone, + BorshDeserialize, + BorshSerialize, + Serialize, + Deserialize, + NearSchema, + Eq, + PartialEq, )] #[serde(crate = "near_sdk::serde")] #[borsh(crate = "near_sdk::borsh")] @@ -21,8 +29,9 @@ pub struct Score { #[serde(crate = "near_sdk::serde")] pub struct PRInfo { pub votes: Vec, - pub allowed_repo: bool, - pub paused: bool, + pub new_repo: bool, + pub paused_repo: bool, + pub blocked_repo: bool, pub exist: bool, pub merged: bool, pub executed: bool, @@ -41,7 +50,15 @@ impl PRInfo { } #[derive( - Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema, PartialEq, + Debug, + Clone, + BorshDeserialize, + BorshSerialize, + Serialize, + Deserialize, + NearSchema, + Eq, + PartialEq, )] #[serde(crate = "near_sdk::serde")] #[borsh(crate = "near_sdk::borsh")] @@ -83,7 +100,7 @@ impl VersionedPR { impl From for PRv2 { fn from(message: VersionedPR) -> Self { match message { - VersionedPR::V1(x) => PRv2 { + VersionedPR::V1(x) => Self { organization: x.organization, repo: x.repo, number: x.number, @@ -101,7 +118,15 @@ impl From for PRv2 { } #[derive( - Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema, PartialEq, + Debug, + Clone, + BorshDeserialize, + BorshSerialize, + Serialize, + Deserialize, + NearSchema, + Eq, + PartialEq, )] #[serde(crate = "near_sdk::serde")] #[borsh(crate = "near_sdk::borsh")] @@ -118,7 +143,7 @@ pub struct PRWithRating { } impl PRv2 { - pub fn new( + pub const fn new( organization: String, repo: String, number: u64, diff --git a/shared/src/streak.rs b/shared/src/streak.rs index 29574fe..8e69561 100644 --- a/shared/src/streak.rs +++ b/shared/src/streak.rs @@ -24,15 +24,15 @@ pub enum StreakType { } impl StreakType { - pub fn from_prs_opened(value: u32) -> Self { + pub const fn from_prs_opened(value: u32) -> Self { Self::PRsOpened(value) } - pub fn from_prs_merged(value: u32) -> Self { + pub const fn from_prs_merged(value: u32) -> Self { Self::PRsMerged(value) } - pub fn is_streak_achieved(&self, user_period_data: &UserPeriodDataV2) -> bool { + pub const fn is_streak_achieved(&self, user_period_data: &UserPeriodDataV2) -> bool { match self { Self::PRsOpened(value) => user_period_data.prs_opened >= *value, Self::PRsMerged(value) => user_period_data.prs_merged >= *value, @@ -55,15 +55,15 @@ pub enum VersionedStreak { } impl VersionedStreak { - pub fn is_active(&self) -> bool { + pub const fn is_active(&self) -> bool { match self { - VersionedStreak::V1(streak) => streak.is_active, + Self::V1(streak) => streak.is_active, } } - pub fn id(&self) -> StreakId { + pub const fn id(&self) -> StreakId { match self { - VersionedStreak::V1(streak) => streak.id, + Self::V1(streak) => streak.id, } } } diff --git a/shared/src/telegram.rs b/shared/src/telegram.rs index fb68e14..0d4192c 100644 --- a/shared/src/telegram.rs +++ b/shared/src/telegram.rs @@ -112,12 +112,7 @@ async fn sender_task( impl TelegramSubscriber { pub async fn new(bot_token: String, chat_id: String) -> Self { let (sender, receiver) = mpsc::unbounded_channel(); - tokio::spawn(sender_task( - receiver, - Client::new(), - bot_token.clone(), - chat_id.clone(), - )); + tokio::spawn(sender_task(receiver, Client::new(), bot_token, chat_id)); Self { sender } } diff --git a/shared/src/timeperiod.rs b/shared/src/timeperiod.rs index 0479bef..433748b 100644 --- a/shared/src/timeperiod.rs +++ b/shared/src/timeperiod.rs @@ -16,6 +16,7 @@ pub type TimePeriodString = String; NearSchema, Debug, PartialEq, + Eq, Clone, Copy, EnumIter, @@ -46,36 +47,36 @@ impl TimePeriod { pub fn time_string(&self, timestamp: Timestamp) -> TimePeriodString { match self { - TimePeriod::Day => timestamp_to_day_string(timestamp), - TimePeriod::Week => timestamp_to_week_string(timestamp), - TimePeriod::Month => timestamp_to_month_string(timestamp), - TimePeriod::Quarter => timestamp_to_quarter_string(timestamp), - TimePeriod::Year => DateTime::from_timestamp_nanos(timestamp as i64) + Self::Day => timestamp_to_day_string(timestamp), + Self::Week => timestamp_to_week_string(timestamp), + Self::Month => timestamp_to_month_string(timestamp), + Self::Quarter => timestamp_to_quarter_string(timestamp), + Self::Year => DateTime::from_timestamp_nanos(timestamp as i64) .year() .to_string(), - TimePeriod::AllTime => "all-time".to_string(), + Self::AllTime => "all-time".to_string(), } } pub fn previous_period(&self, timestamp: Timestamp) -> Option { let timestamp = DateTime::from_timestamp_nanos(timestamp as i64); let result = match self { - TimePeriod::Day => timestamp + Self::Day => timestamp .checked_sub_days(Days::new(1))? .timestamp_nanos_opt()? as Timestamp, - TimePeriod::Week => timestamp + Self::Week => timestamp .checked_sub_days(Days::new(7))? .timestamp_nanos_opt()? as Timestamp, - TimePeriod::Month => timestamp + Self::Month => timestamp .checked_sub_months(Months::new(1))? .timestamp_nanos_opt()? as Timestamp, - TimePeriod::Quarter => timestamp + Self::Quarter => timestamp .checked_sub_months(Months::new(3))? .timestamp_nanos_opt()? as Timestamp, - TimePeriod::Year => timestamp + Self::Year => timestamp .checked_sub_months(Months::new(12))? .timestamp_nanos_opt()? as Timestamp, - TimePeriod::AllTime => return None, + Self::AllTime => return None, }; Some(result) @@ -85,10 +86,10 @@ impl TimePeriod { let date_time = DateTime::::from_timestamp_nanos(timestamp as i64).date_naive(); match self { - TimePeriod::Day => (date_time + chrono::Duration::days(1)) + Self::Day => (date_time + chrono::Duration::days(1)) .and_hms_opt(0, 0, 0) .map(|d| d.and_utc()), - TimePeriod::Week => { + Self::Week => { let iso_week = date_time.iso_week(); NaiveDate::from_isoywd_opt( iso_week.year(), @@ -97,13 +98,13 @@ impl TimePeriod { ) .and_then(|d| d.and_hms_opt(0, 0, 0).map(|d| d.and_utc())) } - TimePeriod::Month => if date_time.month() == 12 { + Self::Month => if date_time.month() == 12 { Utc.with_ymd_and_hms(date_time.year() + 1, 1, 1, 0, 0, 0) } else { Utc.with_ymd_and_hms(date_time.year(), date_time.month() + 1, 1, 0, 0, 0) } .earliest(), - TimePeriod::Quarter => { + Self::Quarter => { let current_quarter = (date_time.month() - 1) / 3 + 1; if current_quarter == 4 { Utc.with_ymd_and_hms(date_time.year() + 1, 1, 1, 0, 0, 0) @@ -112,10 +113,10 @@ impl TimePeriod { } .earliest() } - TimePeriod::Year => Utc + Self::Year => Utc .with_ymd_and_hms(date_time.year() + 1, 1, 1, 0, 0, 0) .earliest(), - TimePeriod::AllTime => None, + Self::AllTime => None, } } @@ -123,24 +124,24 @@ impl TimePeriod { let date_time = DateTime::::from_timestamp_nanos(timestamp as i64).date_naive(); match self { - TimePeriod::Day => date_time.and_hms_opt(0, 0, 0).map(|d| d.and_utc()), - TimePeriod::Week => { + Self::Day => date_time.and_hms_opt(0, 0, 0).map(|d| d.and_utc()), + Self::Week => { let iso_week = date_time.iso_week(); NaiveDate::from_isoywd_opt(iso_week.year(), iso_week.week(), chrono::Weekday::Mon) .and_then(|d| d.and_hms_opt(0, 0, 0).map(|d| d.and_utc())) } - TimePeriod::Month => Utc + Self::Month => Utc .with_ymd_and_hms(date_time.year(), date_time.month(), 1, 0, 0, 0) .earliest(), - TimePeriod::Quarter => { + Self::Quarter => { let current_quarter = (date_time.month() - 1) / 3 + 1; Utc.with_ymd_and_hms(date_time.year(), current_quarter * 3 - 2, 1, 0, 0, 0) .earliest() } - TimePeriod::Year => Utc + Self::Year => Utc .with_ymd_and_hms(date_time.year(), 1, 1, 0, 0, 0) .earliest(), - TimePeriod::AllTime => None, + Self::AllTime => None, } } }