Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ wgpu.workspace = true
bytemuck = "1.23.1"
kameo = "0.17.2"
tauri-plugin-sentry = "0.5.0"
thiserror.workspace = true

[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.24.0"
Expand Down
24 changes: 13 additions & 11 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ use tauri_specta::Event;
use tokio::sync::{RwLock, oneshot};
use tracing::{error, trace};
use upload::{S3UploadMeta, create_or_get_video, upload_image, upload_video};
use web_api::AuthedApiError;
use web_api::ManagerExt as WebManagerExt;
use windows::{CapWindowId, EditorWindowIds, ShowCapWindow, set_window_transparent};

Expand Down Expand Up @@ -1077,7 +1078,7 @@ async fn upload_exported_video(

channel.send(UploadProgress { progress: 0.0 }).ok();

let s3_config = async {
let s3_config = match async {
let video_id = match mode {
UploadMode::Initial { pre_created_video } => {
if let Some(pre_created) = pre_created_video {
Expand All @@ -1087,7 +1088,7 @@ async fn upload_exported_video(
}
UploadMode::Reupload => {
let Some(sharing) = meta.sharing.clone() else {
return Err("No sharing metadata found".to_string());
return Err("No sharing metadata found".into());
};

Some(sharing.id)
Expand All @@ -1103,7 +1104,13 @@ async fn upload_exported_video(
)
.await
}
.await?;
.await
{
Ok(data) => data,
Err(AuthedApiError::InvalidAuthentication) => return Ok(UploadResult::NotAuthenticated),
Err(AuthedApiError::UpgradeRequired) => return Ok(UploadResult::UpgradeRequired),
Err(err) => return Err(err.to_string()),
};

let upload_id = s3_config.id().to_string();

Expand Down Expand Up @@ -1136,11 +1143,12 @@ async fn upload_exported_video(
NotificationType::ShareableLinkCopied.send(&app);
Ok(UploadResult::Success(uploaded_video.link))
}
Err(AuthedApiError::UpgradeRequired) => Ok(UploadResult::UpgradeRequired),
Err(e) => {
error!("Failed to upload video: {e}");

NotificationType::UploadFailed.send(&app);
Err(e)
Err(e.to_string().into())
}
}
}
Expand Down Expand Up @@ -1547,16 +1555,10 @@ async fn check_upgraded_and_update(app: AppHandle) -> Result<bool, String> {
.await
.map_err(|e| {
println!("Failed to fetch plan: {e}");
format!("Failed to fetch plan: {e}")
e.to_string()
})?;

println!("Plan fetch response status: {}", response.status());
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
println!("Unauthorized response, clearing auth store");
AuthStore::set(&app, None).map_err(|e| e.to_string())?;
return Ok(false);
}

let plan_data = response.json::<serde_json::Value>().await.map_err(|e| {
println!("Failed to parse plan response: {e}");
format!("Failed to parse plan response: {e}")
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ fn normalize_thumbnail_dimensions(image: &image::RgbaImage) -> image::RgbaImage
async fn capture_thumbnail_from_filter(filter: &cidre::sc::ContentFilter) -> Option<String> {
use cidre::{cv, sc};
use image::{ImageEncoder, RgbaImage, codecs::png::PngEncoder};
use std::{io::Cursor, slice};
use std::io::Cursor;

let mut config = sc::StreamCfg::new();
config.set_width(THUMBNAIL_WIDTH as usize);
Expand Down
58 changes: 22 additions & 36 deletions apps/desktop/src-tauri/src/upload.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// credit @filleduchaos

use crate::web_api::ManagerExt;
use crate::web_api::{AuthedApiError, ManagerExt};
use crate::{UploadProgress, VideoUploadInfo};
use cap_utils::spawn_actor;
use ffmpeg::ffi::AV_TIME_BASE;
Expand Down Expand Up @@ -216,7 +216,7 @@ pub async fn upload_video(
screenshot_path: Option<PathBuf>,
meta: Option<S3VideoMeta>,
channel: Option<Channel<UploadProgress>>,
) -> Result<UploadedVideo, String> {
) -> Result<UploadedVideo, AuthedApiError> {
println!("Uploading video {video_id}...");

let client = reqwest::Client::new();
Expand Down Expand Up @@ -284,7 +284,7 @@ pub async fn upload_video(

let (video_upload, screenshot_result): (
Result<reqwest::Response, reqwest::Error>,
Option<Result<reqwest::Response, String>>,
Option<Result<reqwest::Response, AuthedApiError>>,
) = tokio::join!(video_upload.send(), async {
if let Some(screenshot_req) = screenshot_upload {
Some(screenshot_req.await)
Expand Down Expand Up @@ -326,12 +326,13 @@ pub async fn upload_video(
status,
error_body
);
Err(format!(
"Failed to upload file. Status: {status}. Body: {error_body}"
))
Err(format!("Failed to upload file. Status: {status}. Body: {error_body}").into())
}

pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result<UploadedImage, String> {
pub async fn upload_image(
app: &AppHandle,
file_path: PathBuf,
) -> Result<UploadedImage, AuthedApiError> {
let file_name = file_path
.file_name()
.and_then(|name| name.to_str())
Expand Down Expand Up @@ -382,9 +383,7 @@ pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result<Uploade
status,
error_body
);
Err(format!(
"Failed to upload file. Status: {status}. Body: {error_body}"
))
Err(format!("Failed to upload file. Status: {status}. Body: {error_body}").into())
}

pub async fn create_or_get_video(
Expand All @@ -393,7 +392,9 @@ pub async fn create_or_get_video(
video_id: Option<String>,
name: Option<String>,
meta: Option<S3VideoMeta>,
) -> Result<S3UploadMeta, String> {
) -> Result<S3UploadMeta, AuthedApiError> {
return Err(AuthedApiError::InvalidAuthentication); // TODO

let mut s3_config_url = if let Some(id) = video_id {
format!("/api/desktop/video/create?recordingMode=desktopMP4&videoId={id}")
} else if is_screenshot {
Expand All @@ -417,23 +418,13 @@ pub async fn create_or_get_video(

let response = app
.authed_api_request(s3_config_url, |client, url| client.get(url))
.await
.map_err(|e| format!("Failed to send request to Next.js handler: {e}"))?;

if response.status() == StatusCode::UNAUTHORIZED {
return Err("Failed to authenticate request; please log in again".into());
}
.await?;

if response.status() != StatusCode::OK {
if let Ok(error) = response.json::<CreateErrorResponse>().await {
if error.error == "upgrade_required" {
return Err(
"You must upgrade to Cap Pro to upload recordings over 5 minutes in length"
.into(),
);
return Err(AuthedApiError::UpgradeRequired);
}

return Err(format!("server error: {}", error.error));
}

return Err("Unknown error uploading video".into());
Expand Down Expand Up @@ -469,7 +460,10 @@ pub enum PresignedS3PutRequestMethod {
Put,
}

async fn presigned_s3_put(app: &AppHandle, body: PresignedS3PutRequest) -> Result<String, String> {
async fn presigned_s3_put(
app: &AppHandle,
body: PresignedS3PutRequest,
) -> Result<String, AuthedApiError> {
#[derive(Deserialize, Debug)]
struct Data {
url: String,
Expand All @@ -485,17 +479,9 @@ async fn presigned_s3_put(app: &AppHandle, body: PresignedS3PutRequest) -> Resul
.authed_api_request("/api/upload/signed", |client, url| {
client.post(url).json(&body)
})
.await
.map_err(|e| format!("Failed to send request to Next.js handler: {e}"))?;
.await?;

if response.status() == StatusCode::UNAUTHORIZED {
return Err("Failed to authenticate request; please log in again".into());
}

let Wrapper { presigned_put_data } = response
.json::<Wrapper>()
.await
.map_err(|e| format!("Failed to deserialize server response: {e}"))?;
let Wrapper { presigned_put_data } = response.json::<Wrapper>().await?;

Ok(presigned_put_data.url)
}
Expand Down Expand Up @@ -556,7 +542,7 @@ pub async fn prepare_screenshot_upload(
app: &AppHandle,
s3_config: &S3UploadMeta,
screenshot_path: PathBuf,
) -> Result<reqwest::Response, String> {
) -> Result<reqwest::Response, AuthedApiError> {
let presigned_put = presigned_s3_put(
app,
PresignedS3PutRequest {
Expand All @@ -576,7 +562,7 @@ pub async fn prepare_screenshot_upload(
.body(compressed_image)
.send()
.await
.map_err(|e| format!("Error uploading screenshot: {e}"))
.map_err(Into::into)
}

async fn compress_image(path: PathBuf) -> Result<Vec<u8>, String> {
Expand Down
54 changes: 39 additions & 15 deletions apps/desktop/src-tauri/src/web_api.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
use reqwest::StatusCode;
use tauri::{Emitter, Manager, Runtime};
use tauri_specta::Event;
use tracing::error;
use thiserror::Error;
use tracing::{error, warn};

use crate::{
ArcLock,
auth::{AuthSecret, AuthStore, AuthenticationInvalid},
};

#[derive(Error, Debug)]
pub enum AuthedApiError {
#[error("User is not authenticated or credentials have expired!")]
InvalidAuthentication,
#[error("User needs to upgrade their account to use this feature!")]
UpgradeRequired,
#[error("AuthedApiError/AuthStore: {0}")]
AuthStore(String),
#[error("AuthedApiError/Request: {0}")]
Request(#[from] reqwest::Error),
#[error("AuthedApiError/Deserialization: {0}")]
Deserialization(#[from] serde_json::Error),
#[error("AuthedApiError/Other: {0}")]
Other(String),
}

impl From<&'static str> for AuthedApiError {
fn from(value: &'static str) -> Self {
AuthedApiError::Other(value.into())
}
}

impl From<String> for AuthedApiError {
fn from(value: String) -> Self {
AuthedApiError::Other(value)
}
}

async fn do_authed_request(
auth: &AuthStore,
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
Expand All @@ -28,7 +57,7 @@ async fn do_authed_request(
)
.header("X-Desktop-Version", env!("CARGO_PKG_VERSION"));

if let Some(s) = std::option_env!("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") {
if let Ok(s) = std::env::var("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") {
req = req.header("x-vercel-protection-bypass", s);
}

Expand All @@ -40,7 +69,7 @@ pub trait ManagerExt<R: Runtime>: Manager<R> {
&self,
path: impl Into<String>,
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
) -> Result<reqwest::Response, String>;
) -> Result<reqwest::Response, AuthedApiError>;

async fn make_app_url(&self, pathname: impl AsRef<str>) -> String;
}
Expand All @@ -50,26 +79,21 @@ impl<T: Manager<R> + Emitter<R>, R: Runtime> ManagerExt<R> for T {
&self,
path: impl Into<String>,
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
) -> Result<reqwest::Response, String> {
let Some(auth) = AuthStore::get(self.app_handle())? else {
println!("Not logged in");

) -> Result<reqwest::Response, AuthedApiError> {
let Some(auth) = AuthStore::get(self.app_handle()).map_err(AuthedApiError::AuthStore)?
else {
warn!("Not logged in");
AuthenticationInvalid.emit(self).ok();

return Err("Unauthorized".to_string());
return Err(AuthedApiError::InvalidAuthentication);
};

let url = self.make_app_url(path.into()).await;
let response = do_authed_request(&auth, build, url)
.await
.map_err(|e| e.to_string())?;
let response = do_authed_request(&auth, build, url).await?;

if response.status() == StatusCode::UNAUTHORIZED {
error!("Authentication expired. Please log in again.");

AuthenticationInvalid.emit(self).ok();

return Err("Unauthorized".to_string());
return Err(AuthedApiError::InvalidAuthentication);
}

Ok(response)
Expand Down
Loading