diff --git a/vibi-dpu/src/bitbucket/auth.rs b/vibi-dpu/src/bitbucket/auth.rs new file mode 100644 index 00000000..a9bdf094 --- /dev/null +++ b/vibi-dpu/src/bitbucket/auth.rs @@ -0,0 +1,50 @@ +use std::env; +use std::str; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; +use reqwest::Client; +use crate::db::auth::{save_auth_info_to_db, auth_info}; +use crate::utils::auth::AuthInfo; + +pub async fn get_access_token_from_bitbucket(code: &str) -> Option { + let client = Client::new(); + let bitbucket_client_id = env::var("BITBUCKET_CLIENT_ID").unwrap(); + let bitbucket_client_secret = env::var("BITBUCKET_CLIENT_SECRET").unwrap(); + let mut params = std::collections::HashMap::new(); + let redirect_uri = format!("{}/api/bitbucket/callbacks/install", + env::var("SERVER_URL").expect("SERVER_URL must be set")); + params.insert("client_id", bitbucket_client_id); + params.insert("client_secret", bitbucket_client_secret); + params.insert("code", code.to_owned()); + params.insert("grant_type", "authorization_code".to_owned()); + params.insert("redirect_uri", redirect_uri); + println!("params = {:?}", ¶ms); + let response = client + .post("https://bitbucket.org/site/oauth2/access_token") + .form(¶ms) + .send() + .await; + if response.is_err() { + let response_err = response.expect_err("No error in access token response"); + eprintln!("error in calling api : {:?}", &response_err); + return None; + } + let res = response.expect("Uncaught error in response"); + if !res.status().is_success() { + eprintln!( + "Failed to exchange code for access token. Status code: {}, Response content: {}", + res.status(), + res.text().await.expect("No text in response") + ); + return None; + } + let json_res = res.json::().await; + if json_res.is_err() { + let json_error = json_res.expect_err("Error not found in json"); + eprintln!("error deserializing : {:?}", json_error); + return None; + } + let mut response_json = json_res.expect("Uncaught error in deserializing response json"); + save_auth_info_to_db(&mut response_json); + return Some(response_json); +} \ No newline at end of file diff --git a/vibi-dpu/src/bitbucket/config.rs b/vibi-dpu/src/bitbucket/config.rs new file mode 100644 index 00000000..8c487514 --- /dev/null +++ b/vibi-dpu/src/bitbucket/config.rs @@ -0,0 +1,106 @@ +use std::{env, collections::HashMap}; + +use reqwest::{Response, header::{HeaderMap, HeaderValue}}; +use serde_json::Value; + +pub fn bitbucket_base_url() -> String { + env::var("BITBUCKET_BASE_URL").expect("BITBUCKET_BASE_URL must be set") +} + +pub async fn get_api(url: &str, access_token: &str, params: Option> ) -> Vec { + let response_opt = call_get_api(url, access_token, ¶ms).await; + println!("response of get_api = {:?}", &response_opt); + let (mut response_values, next_url) = deserialize_response(response_opt).await; + if next_url.is_some() { + let mut page_values = get_all_pages(next_url, access_token, ¶ms).await; + response_values.append(&mut page_values); + } + return response_values; +} + +pub async fn call_get_api(url: &str, access_token: &str, params: &Option> ) -> Option{ + println!("GET api url = {}", url); + let client = reqwest::Client::new(); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( reqwest::header::AUTHORIZATION, + format!("Bearer {}", access_token).parse().expect("Invalid auth header"), ); + headers.insert("Accept", + "application/json".parse().expect("Invalid Accept header")); + match params { + Some(params) => { + match client.get(url).headers(headers).query(params).send().await { + Ok(response) => { + if response.status().is_success() { + return Some(response); + } + else { eprintln!("Failed to call API {}, status: {}", url, response.status()); } + }, + Err(e) => { eprintln!("Error sending GET request to {}, error: {}", url, e); }, + }; + }, + None => { + match client.get(url).headers(headers).send().await { + Ok(response) => { + if response.status().is_success() { + return Some(response); + } + else { eprintln!("Failed to call API {}, status: {}", url, response.status()); } + }, + Err(e) => { eprintln!("Error sending GET request to {}, error: {}", url, e); }, + }; + } + }; + return None; +} + +async fn deserialize_response(response_opt: Option) -> (Vec, Option) { + let values_vec = Vec::new(); + match response_opt { + Some(response) => { + match response.json::().await { + Ok(response_json) => { + let mut values_vec = Vec::new(); + if let Some(values) = response_json["values"].as_array() { + for value in values { + values_vec.push(value.to_owned()); + } + return (values_vec, Some(response_json["next"].to_string())); + } + } + Err(e) => { eprintln!("Unable to deserialize response: {}", e); } + }; + }, + None => { eprintln!("Response is None");} + }; + return (values_vec, None); +} + +async fn get_all_pages(next_url: Option, access_token: &str, params: &Option>) -> Vec{ + let mut values_vec = Vec::new(); + let mut next_url = next_url; + while next_url.is_some() { + let url = next_url.as_ref().expect("next_url is none").trim_matches('"'); + if url != "null" { + let response_opt = call_get_api(url, access_token, params).await; + let (mut response_values, url_opt) = deserialize_response(response_opt).await; + next_url = url_opt.clone(); + values_vec.append(&mut response_values); + } else { + break; + } + } + return values_vec; +} + +pub fn prepare_auth_headers(access_token: &str) -> HeaderMap{ + let mut headers_map = HeaderMap::new(); + let auth_header = format!("Bearer {}", access_token); + let headervalres = HeaderValue::from_str(&auth_header); + match headervalres { + Ok(headerval) => { + headers_map.insert("Authorization", headerval); + }, + Err(e) => panic!("Could not parse header value: {}", e), + }; + return headers_map; +} \ No newline at end of file diff --git a/vibi-dpu/src/bitbucket/mod.rs b/vibi-dpu/src/bitbucket/mod.rs new file mode 100644 index 00000000..f0db7d02 --- /dev/null +++ b/vibi-dpu/src/bitbucket/mod.rs @@ -0,0 +1,6 @@ +pub mod auth; +pub mod workspace; +pub mod repo; +mod config; +pub mod webhook; +pub mod user; \ No newline at end of file diff --git a/vibi-dpu/src/bitbucket/repo.rs b/vibi-dpu/src/bitbucket/repo.rs new file mode 100644 index 00000000..8dda05ed --- /dev/null +++ b/vibi-dpu/src/bitbucket/repo.rs @@ -0,0 +1,30 @@ +use serde_json::Value; + +use crate::db::repo::save_repo_to_db; +use crate::utils::repo::Repository; +use super::config::{bitbucket_base_url, get_api}; + +pub async fn get_workspace_repos(workspace: &str, access_token: &str) -> Option> { + let repos_url = format!("{}/repositories/{}", bitbucket_base_url(), workspace); + let response_json = get_api(&repos_url, access_token, None).await; + let mut repos_data = Vec::new(); + for repo_json in response_json { + let val = Repository::new( + repo_json["name"].to_string().trim_matches('"').to_string(), + repo_json["uuid"].to_string().trim_matches('"').to_string(), + repo_json["owner"]["username"].to_string().trim_matches('"').to_string(), + repo_json["is_private"].as_bool().unwrap_or(false), + repo_json["links"]["clone"].as_array() + .expect("Unable to convert clone to array").iter().filter(|clone_val| { + clone_val["name".to_string()].as_str() == Some("ssh") + }).collect::>()[0]["href"].to_string().replace('\"',""), + repo_json["project"]["name"].to_string().trim_matches('"').to_string(), + repo_json["workspace"]["slug"].to_string().trim_matches('"').to_string(), + None, + "bitbucket".to_string(), + ); + save_repo_to_db(&val); + repos_data.push(val); + } + Some(repos_data) +} \ No newline at end of file diff --git a/vibi-dpu/src/bitbucket/user.rs b/vibi-dpu/src/bitbucket/user.rs new file mode 100644 index 00000000..5e649954 --- /dev/null +++ b/vibi-dpu/src/bitbucket/user.rs @@ -0,0 +1,23 @@ +use chrono::{DateTime, Utc, FixedOffset}; +use crate::db::auth::auth_info; +use crate::db::user::{save_user_to_db, user_from_db}; +use crate::utils::auth::AuthInfo; +use crate::utils::user::{User, Provider, ProviderEnum}; +use super::config::{bitbucket_base_url, get_api, call_get_api}; + +pub async fn get_and_save_workspace_users(workspace_id: &str, access_token: &str) { + let base_url = bitbucket_base_url(); + let members_url = format!("{}/workspaces/{}/members", &base_url, workspace_id); + let response_json = get_api(&members_url, access_token, None).await; + for user_json in response_json { + let provider_id = user_json["user"]["uuid"].to_string().replace('"', ""); + let user = User::new( + Provider::new( + provider_id, + ProviderEnum::Bitbucket), + user_json["user"]["display_name"].to_string().replace('"', ""), + user_json["workspace"]["slug"].to_string().replace('"', ""), + None); + save_user_to_db(&user); + } +} \ No newline at end of file diff --git a/vibi-dpu/src/bitbucket/webhook.rs b/vibi-dpu/src/bitbucket/webhook.rs new file mode 100644 index 00000000..67a9498a --- /dev/null +++ b/vibi-dpu/src/bitbucket/webhook.rs @@ -0,0 +1,86 @@ +use std::env; + +use reqwest::{header::HeaderValue, Response, Error}; +use serde_json::json; + +use crate::{db::webhook::save_webhook_to_db, utils::webhook::{Webhook, WebhookResponse}, bitbucket::config::{bitbucket_base_url, get_api}}; + +use super::config::prepare_auth_headers; + + +pub async fn get_webhooks_in_repo(workspace_slug: &str, repo_slug: &str, access_token: &str) -> Vec { + let url = format!("{}/repositories/{}/{}/hooks", bitbucket_base_url(), workspace_slug, repo_slug); + println!("Getting webhooks from {}", url); + let response_json = get_api(&url, access_token, None).await; + let mut webhooks = Vec::new(); + for webhook_json in response_json { + let active = matches!(webhook_json["active"].to_string().trim_matches('"'), "true" | "false"); + let webhook = Webhook::new( + webhook_json["uuid"].to_string(), + active, + webhook_json["created_at"].to_string().replace('"', ""), + webhook_json["events"].as_array().expect("Unable to deserialize events").into_iter() + .map(|events| events.as_str().expect("Unable to convert event").to_string()).collect(), + webhook_json["links"]["self"]["href"].to_string().replace('"', ""), + webhook_json["url"].to_string().replace('"', ""), + ); + webhooks.push(webhook); + } + return webhooks; +} + +pub async fn add_webhook(workspace_slug: &str, repo_slug: &str, access_token: &str) { + let url = format!( + "{}/repositories/{}/{}/hooks", + bitbucket_base_url(), workspace_slug, repo_slug + ); + + let mut headers_map = prepare_auth_headers(&access_token); + headers_map.insert("Accept", HeaderValue::from_static("application/vnd.github+json")); + let callback_url = format!("{}/api/bitbucket/callbacks/webhook", + env::var("SERVER_URL").expect("SERVER_URL must be set")); + let payload = json!({ + "description": "Webhook for PRs when raised and when something is pushed to the open PRs", + "url": callback_url, + "active": true, + "events": ["pullrequest:created", "pullrequest:updated"] + }); + + let response = reqwest::Client::new() + .post(&url) + .headers(headers_map) + .json(&payload) + .send() + .await; + process_add_webhook_response(response).await; +} + +async fn process_add_webhook_response(response: Result){ + if response.is_err() { + let err = response.expect_err("No error in response"); + eprintln!("Error in api call: {:?}", err); + return; + } + let res = response.expect("Uncaught error in response"); + if !res.status().is_success() { + println!("Failed to add webhook. Status code: {}, Text: {:?}", + res.status(), res.text().await); + return; + } + let webhook_res = res.json::().await; + if webhook_res.is_err() { + let err = webhook_res.expect_err("No error in webhook response"); + return; + } + + let webhook = webhook_res.expect("Uncaught error in webhook response"); + let webhook_data = Webhook::new( + webhook.uuid().to_string(), + webhook.active(), + webhook.created_at().to_string(), + webhook.events().to_owned(), + webhook.links()["self"]["href"].clone(), + webhook.url().to_string(), + ); + save_webhook_to_db(&webhook_data); +} \ No newline at end of file diff --git a/vibi-dpu/src/bitbucket/workspace.rs b/vibi-dpu/src/bitbucket/workspace.rs new file mode 100644 index 00000000..045e37e2 --- /dev/null +++ b/vibi-dpu/src/bitbucket/workspace.rs @@ -0,0 +1,14 @@ +use super::config::{bitbucket_base_url, get_api}; +use crate::db::owner::save_workspace_to_db; +use crate::utils::owner::Workspace; +pub async fn get_bitbucket_workspaces(access_token: &str) -> Vec { + let user_url = format!("{}/workspaces", bitbucket_base_url()); + let response = get_api(&user_url, access_token, None).await; + let mut workspace_vec = Vec::new(); + for workspace_json in response { + let val = serde_json::from_value::(workspace_json.clone()).expect("Unable to deserialize workspace"); + save_workspace_to_db(&val); + workspace_vec.push(val); + } + return workspace_vec; +} \ No newline at end of file diff --git a/vibi-dpu/src/core/mod.rs b/vibi-dpu/src/core/mod.rs new file mode 100644 index 00000000..7633fd0b --- /dev/null +++ b/vibi-dpu/src/core/mod.rs @@ -0,0 +1 @@ +pub mod setup; \ No newline at end of file diff --git a/vibi-dpu/src/core/setup.rs b/vibi-dpu/src/core/setup.rs new file mode 100644 index 00000000..10bb4312 --- /dev/null +++ b/vibi-dpu/src/core/setup.rs @@ -0,0 +1,161 @@ +use std::env; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tokio::{task, fs}; + +use crate::bitbucket::auth::get_access_token_from_bitbucket; +use crate::bitbucket::repo::get_workspace_repos; +use crate::bitbucket::workspace::get_bitbucket_workspaces; +use crate::bitbucket::webhook::{get_webhooks_in_repo, add_webhook}; +use crate::bitbucket::user::get_and_save_workspace_users; +use crate::db::repo::save_repo_to_db; +use crate::db::webhook::save_webhook_to_db; +use crate::utils::repo::Repository; + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct SetupInfo { + provider: String, + owner: String, + repos: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct PublishRequest { + installationId: String, + info: Vec, +} + +pub async fn handle_install_bitbucket(installation_code: &str) { + // get access token from installation code by calling relevant repo provider's api + // out of github, bitbucket, gitlab + + let authinfo = get_access_token_from_bitbucket(installation_code).await.expect("Unable to get access token"); + println!("AuthInfo: {:?}", authinfo); + // let auth_info = { "access_token": access_token, "expires_in": expires_in_formatted, "refresh_token": auth_info["refresh_token"] }; db.insert("auth_info", serde_json::to_string(&auth_info).unwrap()); + let access_token = authinfo.access_token().clone(); + let user_workspaces = get_bitbucket_workspaces(&access_token).await; + let mut pubreqs: Vec = Vec::new(); + for workspace in user_workspaces { + let workspace_slug = workspace.slug(); + println!("=========<{:?}>=======", workspace_slug); + + let repos = get_workspace_repos(workspace.uuid(), + &access_token).await; + get_and_save_workspace_users(workspace.uuid(), &access_token).await; + let mut reponames: Vec = Vec::new(); + for repo in repos.expect("repos is None") { + let token_copy = access_token.clone(); + let mut repo_copy = repo.clone(); + clone_git_repo(&mut repo_copy, &token_copy).await; + let repo_name = repo.name(); + reponames.push(repo_name.clone()); + println!("Repo url git = {:?}", &repo.clone_ssh_url()); + println!("Repo name = {:?}", repo_name); + process_webhooks(workspace_slug.to_string(), + repo_name.to_string(), + access_token.to_string()).await; + let repo_name_async = repo_name.clone(); + let workspace_slug_async = workspace_slug.clone(); + let access_token_async = access_token.clone(); + // task::spawn(async move { + // get_prs(&workspace_slug_async, + // &repo_name_async, + // &access_token_async, + // "OPEN").await; + // }); + } + pubreqs.push(SetupInfo { + provider: "bitbucket".to_owned(), + owner: workspace_slug.clone(), + repos: reponames }); + } + send_setup_info(&pubreqs).await; +} + +async fn send_setup_info(setup_info: &Vec) { + let installation_id = env::var("INSTALL_ID") + .expect("INSTALL_ID must be set"); + println!("install_id = {:?}", &installation_id); + let base_url = env::var("SERVER_URL") + .expect("SERVER_URL must be set"); + let body = PublishRequest { + installationId: installation_id, + info: setup_info.to_vec(), + }; + println!("body = {:?}", &body); + let client = Client::new(); + let resp = client + .post(format!("{base_url}/api/rustApp/setup")) + .json(&body) + .send() + .await + .unwrap(); + + println!("Response: {}", resp.text().await.unwrap()); +} + +async fn clone_git_repo(repo: &mut Repository, access_token: &str) { + let git_url = repo.clone_ssh_url(); + let clone_url = git_url.to_string() + .replace("git@", format!("https://x-token-auth:{{{access_token}}}@").as_str()) + .replace("bitbucket.org:", "bitbucket.org/"); + let random_string: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); + let mut directory = format!("/tmp/{}/{}/{}", repo.provider(), repo.workspace(), random_string); + // Check if directory exists + if fs::metadata(&directory).await.is_ok() { + fs::remove_dir_all(&directory).await.expect("Unable to remove pre-existing directory components"); + } + fs::create_dir_all(&directory).await.expect("Unable to create directory"); + println!("directory exists? {}", fs::metadata(&directory).await.is_ok()); + let mut cmd = std::process::Command::new("git"); + cmd.arg("clone").arg(clone_url).current_dir(&directory); + let output = cmd.output().expect("Failed to clone git repo"); + println!("Git clone output: {:?}", output); + directory = format!("{}/{}", &directory, repo.name()); + repo.set_local_dir(directory); + save_repo_to_db(repo); +} + +async fn process_webhooks(workspace_slug: String, repo_name: String, access_token: String) { + let webhooks_data = get_webhooks_in_repo( + &workspace_slug, &repo_name, &access_token).await; + let webhook_callback_url = format!("{}/api/bitbucket/callbacks/webhook", + env::var("SERVER_URL").expect("SERVER_URL must be set")); + if webhooks_data.is_empty() { + let repo_name_async = repo_name.clone(); + let workspace_slug_async = workspace_slug.clone(); + let access_token_async = access_token.clone(); + task::spawn(async move { + add_webhook( + &workspace_slug_async, + &repo_name_async, + &access_token_async).await; + }); + } + else { + let matching_webhook = webhooks_data.into_iter() + .find(|w| w.url().to_string() == webhook_callback_url); + if matching_webhook.is_some() { + let webhook = matching_webhook.expect("no matching webhook"); + println!("Webhook already exists: {:?}", &webhook); + save_webhook_to_db(&webhook); + } else { + println!("Adding new webhook..."); + let repo_name_async = repo_name.clone(); + let workspace_slug_async = workspace_slug.clone(); + let access_token_async = access_token.clone(); + task::spawn(async move { + add_webhook( + &workspace_slug_async, + &repo_name_async, + &access_token_async).await; + }); + } + } +} \ No newline at end of file diff --git a/vibi-dpu/src/db/auth.rs b/vibi-dpu/src/db/auth.rs new file mode 100644 index 00000000..c9408e19 --- /dev/null +++ b/vibi-dpu/src/db/auth.rs @@ -0,0 +1,35 @@ +use std::time::SystemTime; +use std::time::UNIX_EPOCH; +use sled::IVec; + +use crate::db::config::get_db; +use crate::utils::auth::AuthInfo; + +pub fn save_auth_info_to_db(auth_info: &mut AuthInfo) { + let db = get_db(); + let now = SystemTime::now(); + let since_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards"); + auth_info.set_timestamp(since_epoch.as_secs()); + println!("auth info = {:?}", &auth_info); + let json = serde_json::to_string(&auth_info).expect("Failed to serialize auth info"); + // Convert JSON string to bytes + let bytes = json.as_bytes(); + + // Create IVec from bytes + let ivec = IVec::from(bytes); + + // Insert into sled DB + db.insert("bitbucket_auth_info", ivec).expect("Failed to insert auth info in sled database"); +} + +pub fn auth_info() -> AuthInfo { + let db = get_db(); + let authinfo_key = "bitbucket_auth_info"; + let authinfo_ivec = db.get(IVec::from(authinfo_key.as_bytes())) + .expect("Unable to get bb authinfo from db") + .expect("Empty bitbucket authinfo in db"); + let authinfo: AuthInfo = + serde_json::from_slice(&authinfo_ivec) + .expect("Unable to deserialize authinfo"); + return authinfo; +} \ No newline at end of file diff --git a/vibi-dpu/src/db/config.rs b/vibi-dpu/src/db/config.rs new file mode 100644 index 00000000..9c545353 --- /dev/null +++ b/vibi-dpu/src/db/config.rs @@ -0,0 +1,14 @@ +use std::sync::Mutex; + +static mut DB: Option = None; +static DB_MUTEX: Mutex<()> = Mutex::new(()); + +pub fn get_db() -> &'static sled::Db { + let _lock = DB_MUTEX.lock().unwrap(); + unsafe { + if DB.is_none() { + DB = Some(sled::open("/tmp/db").unwrap()); + } + DB.as_ref().unwrap() + } +} \ No newline at end of file diff --git a/vibi-dpu/src/db/mod.rs b/vibi-dpu/src/db/mod.rs new file mode 100644 index 00000000..792d190b --- /dev/null +++ b/vibi-dpu/src/db/mod.rs @@ -0,0 +1,6 @@ +pub mod auth; +pub mod owner; +pub mod repo; +mod config; +pub mod webhook; +pub mod user; \ No newline at end of file diff --git a/vibi-dpu/src/db/owner.rs b/vibi-dpu/src/db/owner.rs new file mode 100644 index 00000000..2651be3c --- /dev/null +++ b/vibi-dpu/src/db/owner.rs @@ -0,0 +1,15 @@ +use sled::IVec; + +use crate::db::config::get_db; +use crate::utils::owner::Workspace; + +pub fn save_workspace_to_db(workspace: &Workspace) { + let uuid = workspace.uuid().clone(); + let db = get_db(); + let json = serde_json::to_string(&workspace).expect("Failed to serialize workspace"); + // Convert JSON string to bytes + let bytes = json.as_bytes(); + // Create IVec from bytes + let ivec = IVec::from(bytes); + db.insert(format!("owners:{}", uuid), ivec).expect("Unable to save workspace in db"); +} \ No newline at end of file diff --git a/vibi-dpu/src/db/repo.rs b/vibi-dpu/src/db/repo.rs new file mode 100644 index 00000000..a7eae087 --- /dev/null +++ b/vibi-dpu/src/db/repo.rs @@ -0,0 +1,28 @@ +use sled::IVec; + +use crate::db::config::get_db; +use crate::utils::repo::Repository; + +pub fn save_repo_to_db(repo: &Repository) { + let db = get_db(); + let repo_key = format!("{}/{}/{}", repo.provider(), repo.workspace(), repo.name()); + println!("repo_key = {}", &repo_key); + + // Serialize repo struct to JSON + let json = serde_json::to_vec(repo).expect("Failed to serialize repo"); + + // Insert JSON into sled DB + db.insert(IVec::from(repo_key.as_bytes()), json).expect("Failed to upsert repo into sled DB"); +} + +pub fn get_clone_url_clone_dir(repo_provider: &str, workspace_name: &str, repo_name: &str) -> (String, String) { + let db = get_db(); + let key = format!("{}/{}/{}", repo_provider, workspace_name, repo_name); + let repo_opt = db.get(IVec::from(key.as_bytes())).expect("Unable to get repo from db"); + let repo_ivec = repo_opt.expect("Empty value"); + let repo: Repository = serde_json::from_slice::(&repo_ivec).unwrap(); + println!("repo = {:?}", &repo); + let clone_dir = repo.local_dir().to_owned().expect("No local dir for repo set in db"); + let clone_url = repo.clone_ssh_url().to_string(); + return (clone_url, clone_dir); +} \ No newline at end of file diff --git a/vibi-dpu/src/db/user.rs b/vibi-dpu/src/db/user.rs new file mode 100644 index 00000000..ba21956f --- /dev/null +++ b/vibi-dpu/src/db/user.rs @@ -0,0 +1,32 @@ +use sled::IVec; + +use crate::db::config::get_db; +use crate::utils::user::User; + +pub fn save_user_to_db(user: &User) { + let db = get_db(); + let provider_obj = user.provider(); + let user_key = format!("{}/{}/{}", + provider_obj.provider_type().to_string(), user.workspace(), provider_obj.id()); + println!("user_key = {}", &user_key); + + // Serialize repo struct to JSON + let json = serde_json::to_vec(user).expect("Failed to serialize user"); + + // Insert JSON into sled DB + db.insert(IVec::from(user_key.as_bytes()), json).expect("Failed to upsert user into sled DB"); +} + +pub fn user_from_db(repo_provider: &str, workspace: &str, user_id: &str, ) -> Option { + let db = get_db(); + let user_key = format!("{}/{}/{}", + repo_provider, workspace, user_id); + let user_opt = db.get(IVec::from(user_key.as_bytes())).expect("Unable to get repo from db"); + if user_opt.is_none() { + return None; + } + let user_ivec = user_opt.expect("Empty value"); + let user: User = serde_json::from_slice::(&user_ivec).unwrap(); + println!("user from db = {:?}", &user); + return Some(user); +} \ No newline at end of file diff --git a/vibi-dpu/src/db/webhook.rs b/vibi-dpu/src/db/webhook.rs new file mode 100644 index 00000000..a49797fe --- /dev/null +++ b/vibi-dpu/src/db/webhook.rs @@ -0,0 +1,15 @@ +use sled::IVec; +use uuid::Uuid; + +use crate::db::config::get_db; +use crate::utils::webhook::Webhook; +pub fn save_webhook_to_db(webhook: &Webhook) { + let db = get_db(); + // Generate unique ID + let uuid = Uuid::new_v4(); + let id = uuid.as_bytes(); + // Serialize webhook struct to JSON + let json = serde_json::to_vec(webhook).expect("Failed to serialize webhook"); + // Insert JSON into sled DB + db.insert(IVec::from(id), json).expect("Failed to insert webhook into sled DB"); +} \ No newline at end of file diff --git a/vibi-dpu/src/main.rs b/vibi-dpu/src/main.rs index 31646959..665b5a75 100644 --- a/vibi-dpu/src/main.rs +++ b/vibi-dpu/src/main.rs @@ -1,9 +1,9 @@ use std::env; mod pubsub; -// mod db; -// mod core; -// mod bitbucket; -// mod utils; +mod db; +mod core; +mod bitbucket; +mod utils; #[tokio::main] async fn main() { diff --git a/vibi-dpu/src/pubsub/listener.rs b/vibi-dpu/src/pubsub/listener.rs index aac98777..c4aea21b 100644 --- a/vibi-dpu/src/pubsub/listener.rs +++ b/vibi-dpu/src/pubsub/listener.rs @@ -12,6 +12,7 @@ use tokio::task; use std::collections::VecDeque; use sha256::digest; use tonic::Code; +use crate::core::setup::handle_install_bitbucket; // use crate::core::{setup::handle_install_bitbucket, review::process_review}; // To be added in future PR #[derive(Debug, Deserialize)] @@ -39,7 +40,7 @@ async fn process_message(attributes: &HashMap, data_bytes: &Vec< let data = msg_data_res.expect("msg_data not found"); let code_async = data.installation_code.clone(); task::spawn(async move { - // handle_install_bitbucket(&code_async).await; + handle_install_bitbucket(&code_async).await; println!("Processed install callback message"); }); }, @@ -85,7 +86,11 @@ async fn setup_subscription(keypath: &str, topicname: &str) -> Subscription{ ..Default::default() }; let subscription_name = format!("{topicname}-sub"); - let subscription = client.subscription(&subscriptionname); + let subscription = client.subscription(&subscription_name); + let subconfig = SubscriptionConfig { + enable_message_ordering: true, + ..Default::default() + }; if !subscription.exists(None).await.expect("Unable to get subscription information") { subscription.create( topic.fully_qualified_name(), subconfig, None) diff --git a/vibi-dpu/src/utils/auth.rs b/vibi-dpu/src/utils/auth.rs new file mode 100644 index 00000000..c5d473bc --- /dev/null +++ b/vibi-dpu/src/utils/auth.rs @@ -0,0 +1,44 @@ +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Deserialize, Serialize)] +pub struct AuthInfo { + access_token: String, + refresh_token: String, + expires_in: u64, + timestamp: Option, +} + +impl AuthInfo { + // Constructor + pub fn new(access_token: String, refresh_token: String, expires_in: u64, timestamp: Option) -> Self { + Self { + access_token, + refresh_token, + expires_in, + timestamp, + } + } + + // Public getter methods + pub fn access_token(&self) -> &String { + &self.access_token + } + + pub fn refresh_token(&self) -> &String { + &self.refresh_token + } + + pub fn expires_in(&self) -> u64 { + self.expires_in + } + + pub fn timestamp(&self) -> &Option { + &self.timestamp + } + + // Public setters + pub fn set_timestamp(&mut self, timestamp: u64) { + self.timestamp = Some(timestamp); + } +} diff --git a/vibi-dpu/src/utils/mod.rs b/vibi-dpu/src/utils/mod.rs new file mode 100644 index 00000000..d8ed4b7e --- /dev/null +++ b/vibi-dpu/src/utils/mod.rs @@ -0,0 +1,5 @@ +pub mod repo; +pub mod owner; +pub mod auth; +pub mod webhook; +pub mod user; \ No newline at end of file diff --git a/vibi-dpu/src/utils/owner.rs b/vibi-dpu/src/utils/owner.rs new file mode 100644 index 00000000..4f74f9c4 --- /dev/null +++ b/vibi-dpu/src/utils/owner.rs @@ -0,0 +1,33 @@ +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Workspace { + name: String, + uuid: String, + slug: String, +} + +impl Workspace { + // Constructor + pub fn new(name: String, uuid: String, slug: String) -> Self { + Self { + name, + uuid, + slug, + } + } + + // Public getter methods + pub fn name(&self) -> &String { + &self.name + } + + pub fn uuid(&self) -> &String { + &self.uuid + } + + pub fn slug(&self) -> &String { + &self.slug + } +} diff --git a/vibi-dpu/src/utils/repo.rs b/vibi-dpu/src/utils/repo.rs new file mode 100644 index 00000000..e68ae16b --- /dev/null +++ b/vibi-dpu/src/utils/repo.rs @@ -0,0 +1,84 @@ +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Repository { + name: String, + uuid: String, + owner: String, + is_private: bool, + clone_ssh_url: String, + project: String, + workspace: String, + local_dir: Option, + provider: String, +} + +impl Repository { + // Constructor + pub fn new( + name: String, + uuid: String, + owner: String, + is_private: bool, + clone_ssh_url: String, + project: String, + workspace: String, + local_dir: Option, + provider: String, + ) -> Self { + Self { + name, + uuid, + owner, + is_private, + clone_ssh_url, + project, + workspace, + local_dir, + provider, + } + } + + // Public getter methods + pub fn name(&self) -> &String { + &self.name + } + + pub fn uuid(&self) -> &String { + &self.uuid + } + + pub fn owner(&self) -> &String { + &self.owner + } + + pub fn is_private(&self) -> bool { + self.is_private + } + + pub fn clone_ssh_url(&self) -> &String { + &self.clone_ssh_url + } + + pub fn project(&self) -> &String { + &self.project + } + + pub fn workspace(&self) -> &String { + &self.workspace + } + + pub fn local_dir(&self) -> &Option { + &self.local_dir + } + + pub fn provider(&self) -> &String { + &self.provider + } + + //Public Setters + pub fn set_local_dir(&mut self, local_dir: String) { + self.local_dir = Some(local_dir); + } +} diff --git a/vibi-dpu/src/utils/user.rs b/vibi-dpu/src/utils/user.rs new file mode 100644 index 00000000..e3b07eb8 --- /dev/null +++ b/vibi-dpu/src/utils/user.rs @@ -0,0 +1,90 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum ProviderEnum { + Bitbucket, + Github, +} + +impl fmt::Display for ProviderEnum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ProviderEnum::Bitbucket => write!(f, "bitbucket"), + ProviderEnum::Github => write!(f, "github"), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Provider { + id: String, + provider_type: ProviderEnum, +} + +impl Provider { + // Constructor + pub fn new(id: String, provider_type: ProviderEnum) -> Self { + Self { id, provider_type } + } + + // Public getter methods + pub fn id(&self) -> &String { + &self.id + } + + pub fn provider_type(&self) -> &ProviderEnum { + &self.provider_type + } + + // Public setter methods + pub fn set_id(&mut self, id: String) { + self.id = id; + } + + pub fn set_provider_type(&mut self, provider_type: ProviderEnum) { + self.provider_type = provider_type; + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct User { + provider: Provider, + name: String, + workspace: String, + aliases: Option>, +} + +impl User { + // Constructor + pub fn new(provider: Provider, name: String, workspace: String, aliases: Option>) -> Self { + Self { + provider, + name, + workspace, + aliases, + } + } + + // Public getter methods + pub fn provider(&self) -> &Provider { + &self.provider + } + + pub fn name(&self) -> &String { + &self.name + } + + pub fn workspace(&self) -> &String { + &self.workspace + } + + pub fn aliases(&self) -> &Option> { + &self.aliases + } + + pub fn set_aliases(&mut self, aliases: Option>) { + self.aliases = aliases; + } +} diff --git a/vibi-dpu/src/utils/webhook.rs b/vibi-dpu/src/utils/webhook.rs new file mode 100644 index 00000000..ff10542a --- /dev/null +++ b/vibi-dpu/src/utils/webhook.rs @@ -0,0 +1,116 @@ +use std::collections::HashMap; + +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Webhook { + uuid: String, + active: bool, + created_at: String, + events: Vec, + ping_url: String, + url: String, +} + +impl Webhook { + // Constructor + pub fn new( + uuid: String, + active: bool, + created_at: String, + events: Vec, + ping_url: String, + url: String, + ) -> Self { + Self { + uuid, + active, + created_at, + events, + ping_url, + url, + } + } + + // Public getter methods + pub fn uuid(&self) -> &String { + &self.uuid + } + + pub fn active(&self) -> bool { + self.active + } + + pub fn created_at(&self) -> &String { + &self.created_at + } + + pub fn events(&self) -> &Vec { + &self.events + } + + pub fn ping_url(&self) -> &String { + &self.ping_url + } + + pub fn url(&self) -> &String { + &self.url + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WebhookResponse { + uuid: String, + active: bool, + url: String, + created_at: String, + events: Vec, + links: HashMap>, +} + +impl WebhookResponse { + // Constructor + pub fn new( + uuid: String, + active: bool, + url: String, + created_at: String, + events: Vec, + links: HashMap>, + ) -> Self { + Self { + uuid, + active, + url, + created_at, + events, + links, + } + } + + // Public getter methods + pub fn uuid(&self) -> &String { + &self.uuid + } + + pub fn active(&self) -> bool { + self.active + } + + pub fn url(&self) -> &String { + &self.url + } + + pub fn created_at(&self) -> &String { + &self.created_at + } + + pub fn events(&self) -> &Vec { + &self.events + } + + pub fn links(&self) -> &HashMap> { + &self.links + } +}