use std::sync::Arc;
use anyhow::Context;
use async_trait::async_trait;
use mockall::automock;
use sqlx::{query, query_as};
use tracing::debug;
use crate::database::browser_profile::{Bookmark, Extension};
use crate::database::Database;
use crate::server::dtos::browser_profile_dto::MiniBrowserProfile;
use super::{
BrowserProfilePreliminary, BrowserProfileWithFingerprint, Homepage,
MainWebsite, Proxy,
};
pub type DynBrowserProfileRepository =
Arc<dyn BrowserProfileRepository + Send + Sync>;
pub type DynProxyRepository = Arc<dyn ProxyRepository + Send + Sync>;
#[automock]
#[async_trait]
pub trait BrowserProfileRepository {
async fn get_mini_by_id(&self, id: i64)
-> anyhow::Result<MiniBrowserProfile>;
async fn get_preliminary_by_id(
&self,
id: i64,
) -> anyhow::Result<BrowserProfilePreliminary>;
async fn get_by_id_with_fingerprint(
&self,
id: i64,
) -> anyhow::Result<BrowserProfileWithFingerprint>;
async fn get_homepages_by_browser_profile_id(
&self,
id: i64,
) -> anyhow::Result<Vec<Homepage>>;
async fn get_bookmarks(
&self,
user_id: i64,
team_id: i64,
main_website: MainWebsite,
) -> anyhow::Result<Vec<Bookmark>>;
async fn get_extensions(
&self,
user_id: i64,
team_id: i64,
) -> anyhow::Result<Vec<Extension>>;
async fn update_datadir_hash(
&self,
id: i64,
hash: String,
) -> anyhow::Result<()>;
async fn is_pending_transfer(
&self,
browser_profile_id: i64,
) -> anyhow::Result<bool>;
}
#[async_trait]
impl BrowserProfileRepository for Database {
async fn get_mini_by_id(
&self,
id: i64,
) -> anyhow::Result<MiniBrowserProfile> {
sqlx::query_as(
r#"
SELECT `browser_profiles`.id,
`browser_profiles`.userId as user_id,
`browser_profiles`.teamId as team_id,
`browser_profiles`.created_at,
`browser_profiles_hashes`.datadirHash as datadir_hash
FROM `browser_profiles`
LEFT JOIN `browser_profiles_hashes` ON `browser_profiles_hashes`.`browserProfileId` = `browser_profiles`.`id`
WHERE `browser_profiles`.`id` = ?
"#,
)
.bind(id)
.fetch_one(&self.pool)
.await
.context("browser profile was not found")
}
async fn get_preliminary_by_id(
&self,
id: i64,
) -> anyhow::Result<BrowserProfilePreliminary> {
let mut browser_profile: BrowserProfilePreliminary = sqlx::query_as(
r#"
SELECT `browser_profiles`.id,
`browser_profiles`.created_at,
`browser_profiles`.name,
`browser_profiles`.mainWebsite,
`browser_profiles`.proxyId,
`browser_profiles`.login,
`browser_profiles`.password
FROM `browser_profiles`
WHERE `browser_profiles`.`id` = ?
"#,
)
.bind(id)
.fetch_one(&self.pool)
.await
.context("browser profile was not found")?;
if browser_profile.proxy_id == Some(0) {
browser_profile.proxy_id = None;
}
Ok(browser_profile)
}
async fn get_by_id_with_fingerprint(
&self,
id: i64,
) -> anyhow::Result<BrowserProfileWithFingerprint> {
sqlx::query_as(
r#"
SELECT `browser_profiles`.id,
`browser_profiles`.userId,
`browser_profiles`.teamId,
`browser_profiles`.name,
`browser_profiles`.mainWebsite,
`browser_profiles`.platform,
`browser_profiles`.browserType,
`browser_profiles`.proxyId,
`browser_profiles`.useragent,
`browser_profiles`.webrtc,
`browser_profiles`.canvas,
`browser_profiles`.webgl,
`browser_profiles`.webglInfo,
`browser_profiles`.clientRect,
`browser_profiles`.notes,
`browser_profiles`.timezone,
`browser_profiles`.locale,
`browser_profiles`.userFields,
`browser_profiles`.geolocation,
`browser_profiles`.doNotTrack,
`browser_profiles`.args,
`browser_profiles`.cpu,
`browser_profiles`.memory,
`browser_profiles`.screen,
`browser_profiles`.ports,
`browser_profiles`.tabs,
`browser_profiles`.deleted_at,
`browser_profiles`.cpuArchitecture,
`browser_profiles`.osVersion,
`browser_profiles`.screenWidth,
`browser_profiles`.screenHeight,
`browser_profiles`.connectionDownlink,
`browser_profiles`.connectionEffectiveType,
`browser_profiles`.connectionRtt,
`browser_profiles`.connectionSaveData,
`browser_profiles`.vendorSub,
`browser_profiles`.productSub,
`browser_profiles`.vendor,
`browser_profiles`.product,
`browser_profiles`.appCodeName,
`browser_profiles`.mediaDevices,
`browser_profiles_hashes`.datadirHash,
`browser_profiles`.platformVersion,
`browser_profiles`.archived,
`browser_profiles`.webgl2Maximum,
`browser_profiles`.login,
`browser_profiles`.password,
`browser_profiles`.created_at,
`browser_profile_tabs`.tabs as `browser_profile_tabs`,
`browser_profiles_webgpu`.webgpu as `webgpu`,
`browser_profile_details`.isHiddenProfileName as `isHiddenProfileName`
FROM `browser_profiles`
LEFT JOIN `browser_profile_tabs` ON `browser_profile_tabs`.`browserProfileId` = `browser_profiles`.`id`
LEFT JOIN `browser_profile_storage_path` ON `browser_profile_storage_path`.`browserProfileId` = `browser_profiles`.`id`
LEFT JOIN `browser_profile_details` ON `browser_profile_details`.`browserProfileId` = `browser_profiles`.`id`
LEFT JOIN `browser_profiles_webgpu` ON `browser_profiles_webgpu`.`browserProfileId` = `browser_profiles`.`id`
LEFT JOIN `browser_profiles_hashes` ON `browser_profiles_hashes`.`browserProfileId` = `browser_profiles`.`id`
LEFT JOIN `settings` ON `settings`.`teamId` = `browser_profiles`.`teamId`
WHERE `browser_profiles`.`id` = ?
AND `browser_profiles`.`archived` <> 1
LIMIT 1;
"#,
)
.bind(id)
.fetch_one(&self.pool)
.await
.context("browser profile was not found")
}
async fn get_homepages_by_browser_profile_id(
&self,
id: i64,
) -> anyhow::Result<Vec<Homepage>> {
query_as!(
Homepage,
r#"
SELECT `name`, `url`, `sharedToEntireTeam`, `mainWebsite`, `isGlobal`
FROM homepages
WHERE id IN (
SELECT homepageId
FROM browser_profile_homepages
WHERE profileId = ?
);
"#,
id
)
.fetch_all(&self.pool)
.await
.context("homepages was not found")
}
async fn get_bookmarks(
&self,
user_id: i64,
team_id: i64,
main_website: MainWebsite,
) -> anyhow::Result<Vec<Bookmark>> {
let main_website: String = main_website.into();
debug!(
r#"
SELECT name, deletedUrl, mainWebsite, urlCrypt, cryptoKeyId
FROM bookmarks
WHERE (
({main_website} = '' AND (mainWebsite LIKE '%all%' OR mainWebsite like '%none%'))
OR
({main_website} != '' AND (mainWebsite LIKE CONCAT('%', {main_website}, '%') OR mainWebsite LIKE '%all%'))
)
AND (userId = {user_id} OR (teamId = {team_id} AND sharedToEntireTeam = 1))
AND deleted_at IS NULL
"#,
main_website = main_website,
user_id = user_id,
team_id = team_id
);
query_as!(
Bookmark,
r#"
SELECT name, deletedUrl, mainWebsite, urlCrypt, cryptoKeyId
FROM bookmarks
WHERE (
(? = '' AND (mainWebsite LIKE '%all%' OR mainWebsite like '%none%'))
OR
(? != '' AND (mainWebsite LIKE CONCAT('%', ?, '%') OR mainWebsite LIKE '%all%'))
)
AND (userId = ? OR (teamId = ? AND sharedToEntireTeam = 1))
AND deleted_at IS NULL
"#,
main_website,
main_website,
main_website,
user_id,
team_id
)
.fetch_all(&self.pool)
.await
.context("failed to fetch bookmarks")
}
async fn get_extensions(
&self,
user_id: i64,
team_id: i64,
) -> anyhow::Result<Vec<Extension>> {
query_as!(
Extension,
r#"SELECT extensionId as extension_id, url, name, type, hash FROM extensions WHERE (userId = ? or (teamId = ? and sharedToEntireTeam = 1)) and deleted_at IS NULL"#,
user_id,
team_id
)
.fetch_all(&self.pool)
.await
.context("failed to fetch extensions")
}
async fn update_datadir_hash(
&self,
id: i64,
hash: String,
) -> anyhow::Result<()> {
let update_result = sqlx::query!(
r#"
UPDATE `browser_profiles_hashes`
SET `datadirHash` = ?
WHERE `browserProfileId` = ?
"#,
hash,
id
)
.execute(&self.pool)
.await
.context("failed to update datadir hash")?;
if update_result.rows_affected() == 0 {
sqlx::query!(
r#"
INSERT INTO `browser_profiles_hashes` (`datadirHash`, `browserProfileId`)
VALUES (?, ?)
"#,
hash,
id
)
.execute(&self.pool)
.await
.context("failed to insert datadir hash")?;
}
Ok(())
}
async fn is_pending_transfer(&self, id: i64) -> anyhow::Result<bool> {
query!(
r#"SELECT `id` FROM `browser_profile_transfers` WHERE `browserProfileId` = ? and status = 'pending'"#,
id
)
.fetch_optional(&self.pool)
.await
.map(|transfer| transfer.is_some())
.context("failed to check if browser profile is pending transfer")
}
}
#[automock]
#[async_trait]
pub trait ProxyRepository {
async fn maybe_get_by_id(&self, id: i64) -> anyhow::Result<Option<Proxy>>;
}
#[async_trait]
impl ProxyRepository for Database {
async fn maybe_get_by_id(&self, id: i64) -> anyhow::Result<Option<Proxy>> {
query_as!(
Proxy,
r#"
SELECT `id`, `type`, `host`, `deletedPort`, `deletedLogin`, `password`, `loginCrypt`, `passwordCrypt`, `portCrypt`, `cryptoKeyId`, `changeIpUrl`, `changeIpUrlCrypt`
FROM proxy
WHERE id = ?
AND deleted_at IS NULL
"#,
id
)
.fetch_optional(&self.pool)
.await
.context("proxy was not found")
}
}